dsh-offwork 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +50 -32
- package/assets/animations/NOTICE.md +15 -0
- package/assets/animations/alpha-probe.webm +0 -0
- package/assets/animations/ornate-drink.webm +0 -0
- package/assets/animations/ornate-greet.webm +0 -0
- package/assets/animations/ornate-idle.webm +0 -0
- package/assets/animations/whale-drink.webm +0 -0
- package/assets/animations/whale-greet.webm +0 -0
- package/assets/characters/NOTICE.md +4 -0
- package/assets/characters/whale-actions.webp +0 -0
- package/assets/characters/whale-ornate-actions.webp +0 -0
- package/client.js +2970 -770
- package/client.source.js +1668 -444
- package/companion.js +6 -9
- package/countdown.js +83 -0
- package/hydration-store.js +55 -16
- package/hydration.js +231 -250
- package/index.js +2 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/media-manifest.js +29 -0
- package/media-server.js +123 -0
- package/package.json +9 -2
- package/pet-interactions.js +78 -7
- package/pet-motion.js +111 -0
- package/pet-reminder.js +20 -0
- package/pet-surfaces.js +130 -6
- package/pet-video.js +380 -0
- package/pet-view.js +129 -13
- package/tests/classic-overview.test.mjs +77 -0
- package/tests/companion.test.mjs +12 -12
- package/tests/countdown.test.mjs +115 -0
- package/tests/host.test.mjs +6 -0
- package/tests/hydration-extras.test.mjs +168 -204
- package/tests/hydration-store.test.mjs +199 -213
- package/tests/hydration.test.mjs +116 -186
- package/tests/media-server.test.mjs +179 -0
- package/tests/pet-character.test.mjs +8 -2
- package/tests/pet-motion.test.mjs +139 -0
- package/tests/pet-reminder.test.mjs +71 -0
- package/tests/pet-video.test.mjs +503 -0
- package/tools/build.mjs +62 -13
- package/tools/check-package.mjs +19 -0
- package/ui-utils.js +51 -1
package/hydration.js
CHANGED
|
@@ -1,200 +1,203 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { calculate } from './clock.js';
|
|
2
|
+
|
|
3
|
+
// Hydration uses wall-clock deadlines, independently of salary and work schedules.
|
|
4
|
+
export const HYDRATION_SCHEMA = 3;
|
|
3
5
|
export const HYDRATION_ACTION_LIMIT = 64;
|
|
4
6
|
export const HYDRATION_LEASE_MS = 8000;
|
|
7
|
+
const hydrationInteger = (number, max = Number.MAX_SAFE_INTEGER) =>
|
|
8
|
+
Number.isSafeInteger(number) && number >= 0 && number <= max;
|
|
9
|
+
const hydrationText = (value, max = 1024) =>
|
|
10
|
+
typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
11
|
+
const hydrationKeys = (value, keys) =>
|
|
12
|
+
value &&
|
|
13
|
+
typeof value === 'object' &&
|
|
14
|
+
!Array.isArray(value) &&
|
|
15
|
+
Object.keys(value).every((key) => keys.includes(key));
|
|
16
|
+
const hydrationClaim = (claim) =>
|
|
17
|
+
claim === null ||
|
|
18
|
+
(hydrationKeys(claim, ['owner', 'until', 'sequence']) &&
|
|
19
|
+
hydrationText(claim.owner) &&
|
|
20
|
+
hydrationInteger(claim.until) &&
|
|
21
|
+
hydrationInteger(claim.sequence));
|
|
22
|
+
const hydrationActions = (actions) =>
|
|
23
|
+
Array.isArray(actions) &&
|
|
24
|
+
actions.length <= HYDRATION_ACTION_LIMIT &&
|
|
25
|
+
actions.every((id) => hydrationText(id, 128)) &&
|
|
26
|
+
new Set(actions).size === actions.length;
|
|
5
27
|
|
|
6
28
|
export function defaultHydration() {
|
|
7
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
schema: HYDRATION_SCHEMA,
|
|
31
|
+
revision: 0,
|
|
32
|
+
enabled: false,
|
|
33
|
+
interval: 60,
|
|
34
|
+
count: 0,
|
|
35
|
+
paused: false,
|
|
36
|
+
dueAt: 0,
|
|
37
|
+
sequence: 0,
|
|
38
|
+
claim: null,
|
|
39
|
+
quietUntil: 0,
|
|
40
|
+
recentActions: [],
|
|
41
|
+
};
|
|
8
42
|
}
|
|
9
|
-
|
|
10
43
|
export function validateHydration(value) {
|
|
11
|
-
const integer = (number, max = Number.MAX_SAFE_INTEGER) =>
|
|
12
|
-
Number.isSafeInteger(number) && number >= 0 && number <= max;
|
|
13
|
-
const text = (string) => typeof string === 'string' && string.length > 0 && string.length <= 1024;
|
|
14
|
-
const keys = (object, allowed) =>
|
|
15
|
-
object &&
|
|
16
|
-
typeof object === 'object' &&
|
|
17
|
-
!Array.isArray(object) &&
|
|
18
|
-
Object.keys(object).every((key) => allowed.includes(key));
|
|
19
44
|
if (
|
|
20
|
-
!
|
|
21
|
-
|
|
22
|
-
|
|
45
|
+
!hydrationKeys(value, [
|
|
46
|
+
'schema',
|
|
47
|
+
'revision',
|
|
48
|
+
'enabled',
|
|
49
|
+
'interval',
|
|
50
|
+
'count',
|
|
51
|
+
'paused',
|
|
52
|
+
'dueAt',
|
|
53
|
+
'sequence',
|
|
54
|
+
'claim',
|
|
55
|
+
'quietUntil',
|
|
56
|
+
'recentActions',
|
|
57
|
+
]) ||
|
|
58
|
+
value.schema !== HYDRATION_SCHEMA ||
|
|
59
|
+
!hydrationInteger(value.revision) ||
|
|
23
60
|
typeof value.enabled !== 'boolean' ||
|
|
24
|
-
!
|
|
25
|
-
value.interval < 5
|
|
26
|
-
|
|
61
|
+
!hydrationInteger(value.interval, 240) ||
|
|
62
|
+
value.interval < 5 ||
|
|
63
|
+
!hydrationInteger(value.count, 1000000) ||
|
|
64
|
+
typeof value.paused !== 'boolean' ||
|
|
65
|
+
!hydrationInteger(value.dueAt) ||
|
|
66
|
+
!hydrationInteger(value.sequence) ||
|
|
67
|
+
!hydrationInteger(value.quietUntil) ||
|
|
68
|
+
!hydrationClaim(value.claim) ||
|
|
69
|
+
!hydrationActions(value.recentActions)
|
|
70
|
+
)
|
|
27
71
|
throw new Error('Invalid hydration data');
|
|
28
|
-
|
|
72
|
+
return structuredClone(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateLegacyHydration(value) {
|
|
76
|
+
if (
|
|
77
|
+
!hydrationKeys(value, ['schema', 'revision', 'enabled', 'interval', 'shift']) ||
|
|
78
|
+
![1, 2].includes(value.schema) ||
|
|
79
|
+
!hydrationInteger(value.revision) ||
|
|
80
|
+
typeof value.enabled !== 'boolean' ||
|
|
81
|
+
!hydrationInteger(value.interval, 240) ||
|
|
82
|
+
value.interval < 5
|
|
83
|
+
)
|
|
84
|
+
throw new Error('Invalid legacy hydration data');
|
|
29
85
|
const shift = value.shift;
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
!
|
|
44
|
-
!
|
|
45
|
-
!
|
|
46
|
-
!
|
|
86
|
+
if (
|
|
87
|
+
shift !== null &&
|
|
88
|
+
(!hydrationKeys(shift, [
|
|
89
|
+
'id',
|
|
90
|
+
'schedule',
|
|
91
|
+
'config',
|
|
92
|
+
'count',
|
|
93
|
+
'paused',
|
|
94
|
+
'due',
|
|
95
|
+
'sequence',
|
|
96
|
+
'claim',
|
|
97
|
+
...(value.schema === 2 ? ['quietUntil', 'recentActions'] : []),
|
|
98
|
+
]) ||
|
|
99
|
+
!hydrationText(shift.id) ||
|
|
100
|
+
!hydrationText(shift.schedule) ||
|
|
101
|
+
!hydrationText(shift.config) ||
|
|
102
|
+
!hydrationInteger(shift.count, 1000000) ||
|
|
47
103
|
typeof shift.paused !== 'boolean' ||
|
|
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
|
-
|
|
104
|
+
!hydrationInteger(shift.due, 172800000) ||
|
|
105
|
+
!hydrationInteger(shift.sequence) ||
|
|
106
|
+
!hydrationClaim(shift.claim) ||
|
|
107
|
+
(value.schema === 2 &&
|
|
108
|
+
(!hydrationInteger(shift.quietUntil) || !hydrationActions(shift.recentActions))))
|
|
109
|
+
)
|
|
110
|
+
throw new Error('Invalid legacy hydration record');
|
|
111
|
+
return structuredClone(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Invoked only inside the one-time, two-store migration transaction. The original
|
|
115
|
+
// work-based record stays untouched in its old object store for preservation.
|
|
116
|
+
export function migrateHydration(value, now) {
|
|
117
|
+
if (!hydrationInteger(now)) throw new Error('Invalid hydration migration time');
|
|
118
|
+
if (value === undefined) return defaultHydration();
|
|
119
|
+
const old = validateLegacyHydration(value),
|
|
120
|
+
next = defaultHydration(),
|
|
121
|
+
shift = old.shift;
|
|
122
|
+
next.revision = old.revision;
|
|
123
|
+
next.enabled = old.enabled;
|
|
124
|
+
next.interval = old.interval;
|
|
125
|
+
let remaining = next.interval * 60000;
|
|
126
|
+
if (shift) {
|
|
127
|
+
next.count = shift.count;
|
|
128
|
+
next.paused = shift.paused;
|
|
129
|
+
next.sequence = shift.sequence + 1;
|
|
130
|
+
next.quietUntil = old.schema === 2 && shift.quietUntil > now ? shift.quietUntil : 0;
|
|
131
|
+
next.recentActions = old.schema === 2 ? shift.recentActions : [];
|
|
132
|
+
// A matching retained shift supplies the remaining paid minutes. Convert
|
|
133
|
+
// that remainder once; never consult work settings again after migration.
|
|
134
|
+
try {
|
|
135
|
+
const [timezone, start, end, breakStart, breakEnd, weekdays] = JSON.parse(shift.config);
|
|
136
|
+
const clock = calculate(
|
|
137
|
+
{ timezone, start, end, breakStart, breakEnd, weekdays, payMode: 'daily', dailySalary: 1 },
|
|
138
|
+
now,
|
|
139
|
+
);
|
|
140
|
+
const id = JSON.stringify([timezone, clock.shiftDate]);
|
|
141
|
+
const schedule = JSON.stringify([
|
|
142
|
+
start,
|
|
143
|
+
end,
|
|
144
|
+
breakStart,
|
|
145
|
+
breakEnd,
|
|
146
|
+
[...weekdays].sort(),
|
|
147
|
+
clock.shiftStart,
|
|
148
|
+
clock.shiftEnd,
|
|
149
|
+
]);
|
|
150
|
+
if (id === shift.id && schedule === shift.schedule && !shift.paused)
|
|
151
|
+
remaining = Math.max(0, shift.due - Math.floor(clock.elapsed * 1000));
|
|
152
|
+
} catch {
|
|
153
|
+
/* Older/unknown schedule text: start one full configured interval. */
|
|
82
154
|
}
|
|
83
155
|
}
|
|
84
|
-
|
|
156
|
+
next.dueAt = next.enabled ? now + remaining : 0;
|
|
157
|
+
return validateHydration(next);
|
|
85
158
|
}
|
|
86
159
|
|
|
87
|
-
export function
|
|
88
|
-
|
|
89
|
-
|
|
160
|
+
export function reconcileHydration(state, now) {
|
|
161
|
+
const next = validateHydration(state);
|
|
162
|
+
if (!hydrationInteger(now)) throw new Error('Invalid reminder time');
|
|
163
|
+
if (next.enabled && next.dueAt === 0) next.dueAt = now + next.interval * 60000;
|
|
164
|
+
if (next.quietUntil <= now) next.quietUntil = 0;
|
|
165
|
+
if (!next.enabled || next.paused || next.quietUntil > now) next.claim = null;
|
|
166
|
+
return validateHydration(next);
|
|
167
|
+
}
|
|
168
|
+
export function hydrationView(state, now) {
|
|
169
|
+
const remaining = state.dueAt ? Math.max(0, state.dueAt - now) : state.interval * 60000;
|
|
170
|
+
const quietRemaining = state.enabled && !state.paused ? Math.max(0, state.quietUntil - now) : 0;
|
|
171
|
+
const phase = !state.enabled
|
|
172
|
+
? 'off'
|
|
173
|
+
: state.paused
|
|
174
|
+
? 'paused'
|
|
175
|
+
: quietRemaining > 0
|
|
176
|
+
? 'quiet'
|
|
177
|
+
: remaining === 0
|
|
178
|
+
? 'due'
|
|
179
|
+
: 'countdown';
|
|
90
180
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
]),
|
|
100
|
-
schedule: JSON.stringify([
|
|
101
|
-
settings.start,
|
|
102
|
-
settings.end,
|
|
103
|
-
settings.breakStart,
|
|
104
|
-
settings.breakEnd,
|
|
105
|
-
[...settings.weekdays].sort(),
|
|
106
|
-
clock.shiftStart,
|
|
107
|
-
clock.shiftEnd,
|
|
108
|
-
]),
|
|
109
|
-
status: clock.status,
|
|
110
|
-
end: clock.shiftEnd,
|
|
111
|
-
elapsed: Math.max(0, Math.floor(clock.elapsed * 1000)),
|
|
112
|
-
total: Math.max(0, Math.floor(clock.total * 1000)),
|
|
181
|
+
phase,
|
|
182
|
+
count: state.count,
|
|
183
|
+
remaining,
|
|
184
|
+
quietRemaining,
|
|
185
|
+
eventId:
|
|
186
|
+
state.enabled && state.dueAt
|
|
187
|
+
? JSON.stringify(['wall-clock', state.sequence, state.dueAt])
|
|
188
|
+
: null,
|
|
113
189
|
};
|
|
114
190
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
if (next.shift?.id !== context.id) {
|
|
124
|
-
// Do not discard the last shift's count on a day off. New working shifts
|
|
125
|
-
// are initialized once the user opens the page, never backfilled.
|
|
126
|
-
if (!['before', 'working', 'lunch'].includes(context.status)) return next;
|
|
127
|
-
next.shift = {
|
|
128
|
-
id: context.id,
|
|
129
|
-
schedule: context.schedule,
|
|
130
|
-
config: context.config,
|
|
131
|
-
count: 0,
|
|
132
|
-
paused: false,
|
|
133
|
-
due: context.elapsed + next.interval * 60000,
|
|
134
|
-
sequence: 0,
|
|
135
|
-
claim: null,
|
|
136
|
-
quietUntil: 0,
|
|
137
|
-
recentActions: [],
|
|
138
|
-
};
|
|
139
|
-
} else if (next.shift.schedule !== context.schedule) {
|
|
140
|
-
next.shift.schedule = context.schedule;
|
|
141
|
-
next.shift.config = context.config;
|
|
142
|
-
next.shift.due = context.elapsed + next.interval * 60000;
|
|
143
|
-
next.shift.sequence++;
|
|
144
|
-
next.shift.claim = null;
|
|
145
|
-
next.shift.quietUntil = Math.min(next.shift.quietUntil, context.end);
|
|
146
|
-
}
|
|
147
|
-
if (next.shift && (!next.enabled || context.status !== 'working')) next.shift.claim = null;
|
|
148
|
-
return next;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function hydrationView(state, context, now = 0) {
|
|
152
|
-
const same =
|
|
153
|
-
context && state.shift?.id === context.id && state.shift.schedule === context.schedule;
|
|
154
|
-
const shift = same ? state.shift : null;
|
|
155
|
-
const count = shift?.count ?? 0;
|
|
156
|
-
const remaining = shift ? Math.max(0, shift.due - context.elapsed) : state.interval * 60000;
|
|
157
|
-
const eventId = shift
|
|
158
|
-
? JSON.stringify([shift.id, shift.schedule, shift.sequence, shift.due])
|
|
159
|
-
: null;
|
|
160
|
-
const quietRemaining =
|
|
161
|
-
shift && ['working', 'lunch'].includes(context.status)
|
|
162
|
-
? Math.max(0, Math.min(shift.quietUntil, context.end) - now)
|
|
163
|
-
: 0;
|
|
164
|
-
let phase = 'waiting';
|
|
165
|
-
if (!state.enabled) phase = 'off';
|
|
166
|
-
else if (shift?.paused) phase = 'paused';
|
|
167
|
-
else if (quietRemaining > 0) phase = 'quiet';
|
|
168
|
-
else if (context?.status === 'lunch') phase = 'break';
|
|
169
|
-
else if (shift && context.status === 'working') {
|
|
170
|
-
phase = remaining === 0 ? 'due' : shift.due >= context.total ? 'finished' : 'countdown';
|
|
171
|
-
}
|
|
172
|
-
return { phase, count, remaining, eventId, quietRemaining };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function transitionHydration(state, context, action, now, owner) {
|
|
176
|
-
if (!Number.isSafeInteger(now) || now < 0) throw new Error('Invalid reminder time');
|
|
177
|
-
// Reconfiguration requires an explicit compare-and-set grant obtained before
|
|
178
|
-
// reading/saving Host settings. A stale tab may release its lease or disable
|
|
179
|
-
// reminders, but cannot silently restore its previous schedule.
|
|
180
|
-
if (
|
|
181
|
-
context &&
|
|
182
|
-
state.shift &&
|
|
183
|
-
(state.shift.config !== context.config ||
|
|
184
|
-
(state.shift.id === context.id && state.shift.schedule !== context.schedule)) &&
|
|
185
|
-
action.baseSchedule !== state.shift.schedule
|
|
186
|
-
)
|
|
187
|
-
context = null;
|
|
188
|
-
const manual =
|
|
189
|
-
action.type === 'manual-drink' &&
|
|
190
|
-
action.shiftId === context?.id &&
|
|
191
|
-
['working', 'lunch'].includes(context?.status);
|
|
192
|
-
let next = reconcileHydration(state, context, manual);
|
|
191
|
+
export function transitionHydration(state, action, now, owner) {
|
|
192
|
+
let next = reconcileHydration(state, now);
|
|
193
|
+
const finish = () => validateHydration(next);
|
|
194
|
+
const invalidate = () => {
|
|
195
|
+
next.sequence++;
|
|
196
|
+
next.claim = null;
|
|
197
|
+
};
|
|
193
198
|
const restart = () => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
next.shift.sequence++;
|
|
197
|
-
next.shift.claim = null;
|
|
199
|
+
next.dueAt = now + next.interval * 60000;
|
|
200
|
+
invalidate();
|
|
198
201
|
};
|
|
199
202
|
if (action.type === 'reset') return defaultHydration();
|
|
200
203
|
if (action.type === 'preferences') {
|
|
@@ -207,86 +210,65 @@ export function transitionHydration(state, context, action, now, owner) {
|
|
|
207
210
|
interval > 240
|
|
208
211
|
)
|
|
209
212
|
throw new Error('Invalid reminder preferences');
|
|
210
|
-
const changed =
|
|
213
|
+
const changed = enabled !== next.enabled || interval !== next.interval;
|
|
211
214
|
next.enabled = enabled;
|
|
212
215
|
next.interval = interval;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
next.
|
|
216
|
-
next.shift.quietUntil = 0;
|
|
216
|
+
if (!enabled) {
|
|
217
|
+
next.claim = null;
|
|
218
|
+
next.quietUntil = 0;
|
|
217
219
|
}
|
|
218
|
-
if (changed)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (shift?.claim?.owner === owner) shift.claim = null;
|
|
224
|
-
return next;
|
|
220
|
+
if (changed) {
|
|
221
|
+
if (enabled) restart();
|
|
222
|
+
else invalidate();
|
|
223
|
+
}
|
|
224
|
+
return finish();
|
|
225
225
|
}
|
|
226
|
-
|
|
227
|
-
if (shift.quietUntil && (now >= shift.quietUntil || now >= context.end)) shift.quietUntil = 0;
|
|
228
|
-
const view = hydrationView(next, context, now);
|
|
226
|
+
const view = hydrationView(next, now);
|
|
229
227
|
const validClaim =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (action.type === '
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
action.actionId.length > 128
|
|
240
|
-
)
|
|
241
|
-
throw new Error('Invalid hydration action id');
|
|
228
|
+
next.claim &&
|
|
229
|
+
next.claim.owner === owner &&
|
|
230
|
+
next.claim.sequence === next.sequence &&
|
|
231
|
+
next.claim.until > now &&
|
|
232
|
+
next.claim.until <= now + HYDRATION_LEASE_MS;
|
|
233
|
+
if (action.type === 'release') {
|
|
234
|
+
if (next.claim?.owner === owner) next.claim = null;
|
|
235
|
+
} else if (action.type === 'manual-drink') {
|
|
236
|
+
if (!hydrationText(action.actionId, 128)) throw new Error('Invalid hydration action id');
|
|
242
237
|
if (
|
|
243
|
-
|
|
238
|
+
next.recentActions.includes(action.actionId) ||
|
|
244
239
|
(action.eventId !== undefined && action.eventId !== view.eventId)
|
|
245
240
|
)
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
...
|
|
241
|
+
return finish();
|
|
242
|
+
next.recentActions = [
|
|
243
|
+
...next.recentActions.slice(-(HYDRATION_ACTION_LIMIT - 1)),
|
|
249
244
|
action.actionId,
|
|
250
245
|
];
|
|
251
|
-
|
|
252
|
-
if (next.enabled && !
|
|
253
|
-
else
|
|
254
|
-
|
|
255
|
-
shift.claim = null;
|
|
256
|
-
}
|
|
257
|
-
} else if (
|
|
258
|
-
action.type === 'quiet' &&
|
|
259
|
-
next.enabled &&
|
|
260
|
-
!shift.paused &&
|
|
261
|
-
['working', 'lunch'].includes(context.status)
|
|
262
|
-
) {
|
|
246
|
+
next.count = Math.min(1000000, next.count + 1);
|
|
247
|
+
if (next.enabled && !next.paused) restart();
|
|
248
|
+
else invalidate();
|
|
249
|
+
} else if (action.type === 'quiet' && next.enabled && !next.paused) {
|
|
263
250
|
if (![30, 60].includes(action.minutes)) throw new Error('Invalid quiet duration');
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
shift.claim = null;
|
|
270
|
-
shift.sequence++;
|
|
251
|
+
next.quietUntil = now + action.minutes * 60000;
|
|
252
|
+
invalidate();
|
|
253
|
+
} else if (action.type === 'end-quiet' && next.quietUntil) {
|
|
254
|
+
next.quietUntil = 0;
|
|
255
|
+
invalidate();
|
|
271
256
|
} else if (action.type === 'claim' && view.phase === 'due') {
|
|
272
|
-
|
|
257
|
+
if (!hydrationText(owner)) throw new Error('Invalid reminder owner');
|
|
273
258
|
if (
|
|
274
|
-
!claim ||
|
|
275
|
-
claim.owner === owner ||
|
|
276
|
-
claim.until <= now ||
|
|
277
|
-
claim.until > now + HYDRATION_LEASE_MS
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
shift.sequence++;
|
|
288
|
-
} else if (action.type === 'resume' && next.enabled && shift.paused) {
|
|
289
|
-
shift.paused = false;
|
|
259
|
+
!next.claim ||
|
|
260
|
+
next.claim.owner === owner ||
|
|
261
|
+
next.claim.until <= now ||
|
|
262
|
+
next.claim.until > now + HYDRATION_LEASE_MS
|
|
263
|
+
)
|
|
264
|
+
next.claim = { owner, until: now + HYDRATION_LEASE_MS, sequence: next.sequence };
|
|
265
|
+
} else if (action.type === 'clear-count') next.count = 0;
|
|
266
|
+
else if (action.type === 'pause' && next.enabled && !next.paused) {
|
|
267
|
+
next.paused = true;
|
|
268
|
+
next.quietUntil = 0;
|
|
269
|
+
invalidate();
|
|
270
|
+
} else if (action.type === 'resume' && next.enabled && next.paused) {
|
|
271
|
+
next.paused = false;
|
|
290
272
|
restart();
|
|
291
273
|
} else if (
|
|
292
274
|
['drink', 'snooze'].includes(action.type) &&
|
|
@@ -294,10 +276,9 @@ export function transitionHydration(state, context, action, now, owner) {
|
|
|
294
276
|
validClaim &&
|
|
295
277
|
action.eventId === view.eventId
|
|
296
278
|
) {
|
|
297
|
-
if (action.type === 'drink')
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
shift.claim = null;
|
|
279
|
+
if (action.type === 'drink') next.count = Math.min(1000000, next.count + 1);
|
|
280
|
+
next.dueAt = now + (action.type === 'snooze' ? 10 : next.interval) * 60000;
|
|
281
|
+
invalidate();
|
|
301
282
|
}
|
|
302
|
-
return
|
|
283
|
+
return finish();
|
|
303
284
|
}
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, writeFile, rename, unlink, chmod } from 'node:fs/promises';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { registerMediaRoute } from './media-server.js';
|
|
5
6
|
|
|
6
7
|
export const inject = ['commands', 'tools'];
|
|
7
8
|
|
|
@@ -200,6 +201,7 @@ const settingsSchema = {
|
|
|
200
201
|
};
|
|
201
202
|
|
|
202
203
|
export function apply(ctx) {
|
|
204
|
+
registerMediaRoute(ctx);
|
|
203
205
|
const operate = createOperations();
|
|
204
206
|
ctx.effect(() =>
|
|
205
207
|
ctx.commands.register({
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Offwork Clock",
|
|
4
|
-
"description": "A Harness character companion with
|
|
4
|
+
"description": "A Harness character companion and compact widget with clock-out, all-day hydration and independent smoking timers, earnings, action previews and privacy controls."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Generated by tools/build.mjs; edit the source modules, not this file.
|
|
2
|
+
export const MEDIA_ROUTE = "/offwork-clock/media";
|
|
3
|
+
export const MEDIA_ASSETS = Object.freeze({
|
|
4
|
+
"2b98a0d8f5ab3569254c57185f712ceae7081d081f3879a3bb48f62ae59ea616/ornate-greet.webm": {
|
|
5
|
+
"file": "assets/animations/ornate-greet.webm",
|
|
6
|
+
"size": 1419513,
|
|
7
|
+
"sha256": "2b98a0d8f5ab3569254c57185f712ceae7081d081f3879a3bb48f62ae59ea616"
|
|
8
|
+
},
|
|
9
|
+
"6149773dbf3b89062f649d54d990230caacdea084d22f93bd4867b0c25344b59/whale-greet.webm": {
|
|
10
|
+
"file": "assets/animations/whale-greet.webm",
|
|
11
|
+
"size": 1726801,
|
|
12
|
+
"sha256": "6149773dbf3b89062f649d54d990230caacdea084d22f93bd4867b0c25344b59"
|
|
13
|
+
},
|
|
14
|
+
"ac0e8be1731b24dfbc232114d08a771ea87512cef2c6f3949154a90c6413f1a7/ornate-drink.webm": {
|
|
15
|
+
"file": "assets/animations/ornate-drink.webm",
|
|
16
|
+
"size": 1746731,
|
|
17
|
+
"sha256": "ac0e8be1731b24dfbc232114d08a771ea87512cef2c6f3949154a90c6413f1a7"
|
|
18
|
+
},
|
|
19
|
+
"e6befff2ade25304f13c03e3ba30a19148cd7155c862f5a59cfdb31c8669e519/ornate-idle.webm": {
|
|
20
|
+
"file": "assets/animations/ornate-idle.webm",
|
|
21
|
+
"size": 890695,
|
|
22
|
+
"sha256": "e6befff2ade25304f13c03e3ba30a19148cd7155c862f5a59cfdb31c8669e519"
|
|
23
|
+
},
|
|
24
|
+
"f993c90acb9c4c6b9c4363986792790d4a38b2f5e3e4e589b0972d62d5a326f8/whale-drink.webm": {
|
|
25
|
+
"file": "assets/animations/whale-drink.webm",
|
|
26
|
+
"size": 1598173,
|
|
27
|
+
"sha256": "f993c90acb9c4c6b9c4363986792790d4a38b2f5e3e4e589b0972d62d5a326f8"
|
|
28
|
+
}
|
|
29
|
+
});
|