dsh-offwork 1.1.1 → 1.2.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 +39 -0
- package/README.md +48 -26
- package/ROADMAP.md +70 -0
- package/client.js +5066 -1099
- package/client.source.js +1988 -541
- package/companion-app.js +1431 -0
- package/companion-image.js +59 -0
- package/companion-state.js +424 -0
- package/companion.js +6 -9
- package/countdown.js +83 -0
- package/docs/RELEASE-1.2.2.md +20 -0
- package/hydration-store.js +55 -16
- package/hydration.js +231 -250
- package/icon.svg +28 -1
- package/index.js +59 -6
- 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 +13 -3
- package/pet-interactions.js +121 -17
- package/pet-motion.js +2 -1
- package/pet-reminder.js +20 -0
- package/pet-surfaces.js +130 -6
- package/pet-video.js +221 -72
- package/pet-view.js +71 -4
- package/settings-api.js +126 -0
- package/tests/classic-overview.test.mjs +77 -0
- package/tests/companion-state.test.mjs +267 -0
- package/tests/companion.test.mjs +12 -12
- package/tests/countdown.test.mjs +115 -0
- package/tests/host.test.mjs +26 -13
- 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-interactions.test.mjs +19 -0
- package/tests/pet-reminder.test.mjs +71 -0
- package/tests/pet-video.test.mjs +171 -5
- package/tests/settings-api.test.mjs +130 -0
- package/tests/ui.test.mjs +4 -1
- package/tools/build.mjs +45 -16
- package/tools/check-package.mjs +12 -0
- package/ui-utils.js +44 -0
package/client.source.js
CHANGED
|
@@ -13,7 +13,42 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
...PET_MESSAGES.zh,
|
|
14
14
|
...PET_INTERACTION_MESSAGES.zh,
|
|
15
15
|
...PET_SURFACE_MESSAGES.zh,
|
|
16
|
+
detailHelp: '使用说明',
|
|
17
|
+
detailApply: '应用',
|
|
18
|
+
detailShift: '班次详情',
|
|
19
|
+
detailWaterRule: '全天按自然时间提醒,需保持页面打开。',
|
|
20
|
+
detailSavedInterval: '已生效间隔',
|
|
21
|
+
detailDraftHint: '保存后生效',
|
|
22
|
+
detailNextInterval: '用于下一轮计时',
|
|
16
23
|
title: '下班计时器',
|
|
24
|
+
classicMiniWorking: '距下班',
|
|
25
|
+
classicMiniBefore: '距开工',
|
|
26
|
+
classicMiniLunch: '午休中',
|
|
27
|
+
classicMiniStatus: '本班状态',
|
|
28
|
+
classicMiniWaiting: '待开启',
|
|
29
|
+
classicMiniFinished: '已结束',
|
|
30
|
+
classicOverview: '今日概览',
|
|
31
|
+
classicReturn: '返回概览',
|
|
32
|
+
classicMore: '展开作息详情',
|
|
33
|
+
classicLess: '收起作息详情',
|
|
34
|
+
classicNoSession: '等待会话',
|
|
35
|
+
classicUnavailable: '暂时无法计时',
|
|
36
|
+
classicRetryHint: '重新读取作息后恢复计时。',
|
|
37
|
+
classicLoading: '正在准备计时',
|
|
38
|
+
classicLoadingHint: '正在读取已保存的作息。',
|
|
39
|
+
classicBeforeHint: '班次尚未开始,显示距离开工的时间。',
|
|
40
|
+
classicWorkingHint: '本班工作进行中。',
|
|
41
|
+
classicLunchRemaining: '距离午休结束',
|
|
42
|
+
classicLunchHint: '午休结束后继续工作;喝水提醒照常计时。',
|
|
43
|
+
classicDoneHint: '本班计时已结束,下一班开始后继续。',
|
|
44
|
+
classicRestHint: '今天不计工作时长,计时会在工作日恢复。',
|
|
45
|
+
classicWaterHint: '自然时间计时',
|
|
46
|
+
classicSmokeHint: '自然时间计时',
|
|
47
|
+
classicConfigure: '设置作息与薪资',
|
|
48
|
+
classicIncome: '本班收入',
|
|
49
|
+
classicIncomeEntry: '查看收入详情',
|
|
50
|
+
classicShift: '本班作息',
|
|
51
|
+
classicEnd: '下班时间',
|
|
17
52
|
working: '距离下班',
|
|
18
53
|
before: '距离开工',
|
|
19
54
|
lunch: '午休中',
|
|
@@ -48,7 +83,7 @@ window.__ModuleLoader__.load({
|
|
|
48
83
|
error: '操作失败,请重试。',
|
|
49
84
|
saved: '设置已保存',
|
|
50
85
|
days: '日一二三四五六',
|
|
51
|
-
privacy: '配置保存在 Harness
|
|
86
|
+
privacy: '配置保存在 Harness 所在机器;界面直接保存,不写入会话。代理访问默认关闭。',
|
|
52
87
|
drag: '按住可拖动;方向键微调,Home 复位',
|
|
53
88
|
setup: '先设好你的作息',
|
|
54
89
|
setupHint: '示例数值不是你的收入,保存后才开始计时。',
|
|
@@ -74,7 +109,7 @@ window.__ModuleLoader__.load({
|
|
|
74
109
|
opacityHint: '只影响背景,不影响文字。',
|
|
75
110
|
waterTitle: '喝水提醒',
|
|
76
111
|
waterEnable: '开启喝水提醒',
|
|
77
|
-
waterInterval: '
|
|
112
|
+
waterInterval: '提醒间隔(分钟)',
|
|
78
113
|
waterSave: '保存提醒设置',
|
|
79
114
|
waterEdit: '设置喝水提醒',
|
|
80
115
|
waterBack: '返回',
|
|
@@ -84,21 +119,21 @@ window.__ModuleLoader__.load({
|
|
|
84
119
|
waterEffective: '当前间隔',
|
|
85
120
|
waterManage: '数据管理',
|
|
86
121
|
waterSaveHint: '修改后点击保存,按新间隔重新计时。',
|
|
87
|
-
waterBrief: '
|
|
122
|
+
waterBrief: '按自然时间计时,午休、下班和休息日均可提醒。需要保持页面打开。',
|
|
88
123
|
waterApply: '应用间隔',
|
|
89
124
|
waterInvalid: '请输入 5–240 的整数。',
|
|
90
125
|
waterNext: '下次喝水',
|
|
91
126
|
waterDue: '该喝水啦',
|
|
92
127
|
waterDrink: '喝了',
|
|
93
|
-
waterSnooze: '
|
|
94
|
-
waterPause: '
|
|
95
|
-
waterResume: '
|
|
96
|
-
waterCount: '
|
|
128
|
+
waterSnooze: '10 分钟后',
|
|
129
|
+
waterPause: '暂停提醒',
|
|
130
|
+
waterResume: '恢复提醒',
|
|
131
|
+
waterCount: '饮水计数',
|
|
97
132
|
waterManual: '喝了一杯',
|
|
98
133
|
waterQuick: '喝水快捷操作',
|
|
99
134
|
waterQuickClose: '收起喝水快捷操作',
|
|
100
135
|
waterRecorded: '这杯记下啦。',
|
|
101
|
-
waterRecordHint: '
|
|
136
|
+
waterRecordHint: '随时可以打卡,关闭提醒也能记录;计数可手动清零。',
|
|
102
137
|
waterActionStale: '状态已变化,请再试一次。',
|
|
103
138
|
waterQuiet: '免打扰',
|
|
104
139
|
waterQuietAdjust: '调整时长',
|
|
@@ -109,32 +144,68 @@ window.__ModuleLoader__.load({
|
|
|
109
144
|
waterQuietEnded: '免打扰已结束。',
|
|
110
145
|
waterQuietHint: '按自然时间,到期自动恢复;已到期的提醒只提示一次。',
|
|
111
146
|
waterOff: '提醒已关闭',
|
|
112
|
-
waterWaiting: '
|
|
113
|
-
waterBreak: '
|
|
114
|
-
waterPaused: '
|
|
115
|
-
waterFinished: '
|
|
147
|
+
waterWaiting: '等待开启提醒',
|
|
148
|
+
waterBreak: '提醒已暂停',
|
|
149
|
+
waterPaused: '提醒已暂停',
|
|
150
|
+
waterFinished: '提醒已结束',
|
|
116
151
|
waterOther: '提醒已到期,等待可见标签页处理',
|
|
117
|
-
waterClear: '
|
|
152
|
+
waterClear: '清除饮水计数',
|
|
118
153
|
waterReset: '重置喝水提醒',
|
|
119
154
|
waterConfirm: '确认操作',
|
|
120
|
-
waterClearHint: '
|
|
121
|
-
waterResetHint: '
|
|
155
|
+
waterClearHint: '仅清零饮水计数,保留下一次提醒和暂停状态。',
|
|
156
|
+
waterResetHint: '清除当前喝水提醒设置和计数,恢复默认关闭。',
|
|
122
157
|
waterLocal: '提醒设置和打卡仅保存在当前浏览器,不进入会话。与薪资配置分别保存。',
|
|
123
|
-
waterRules:
|
|
158
|
+
waterRules:
|
|
159
|
+
'按自然分钟计时,与作息、薪资和会话无关。开启、修改间隔或手动恢复后重新计时;关闭页面后不提醒。',
|
|
124
160
|
waterUnavailable:
|
|
125
|
-
'
|
|
161
|
+
'喝水提醒存储不可用或数据无效,自动提醒已停止。可重试;重置将清除当前提醒设置和计数。',
|
|
126
162
|
waterRetry: '重试读取',
|
|
127
|
-
waterSetup: '
|
|
163
|
+
waterSetup: '开启后按所选间隔提醒。',
|
|
128
164
|
waterLoading: '正在读取喝水提醒…',
|
|
129
|
-
waterChanged: '
|
|
130
|
-
waterStale: '
|
|
165
|
+
waterChanged: '提醒设置已保存',
|
|
166
|
+
waterStale: '记录已更新,请重试读取。',
|
|
131
167
|
},
|
|
132
168
|
en: {
|
|
133
169
|
...COMPANION_MESSAGES.en,
|
|
134
170
|
...PET_MESSAGES.en,
|
|
135
171
|
...PET_INTERACTION_MESSAGES.en,
|
|
136
172
|
...PET_SURFACE_MESSAGES.en,
|
|
173
|
+
detailHelp: 'How it works',
|
|
174
|
+
detailApply: 'Apply',
|
|
175
|
+
detailShift: 'Shift details',
|
|
176
|
+
detailWaterRule: 'Reminds you throughout the day. Keep the page open.',
|
|
177
|
+
detailSavedInterval: 'Saved interval',
|
|
178
|
+
detailDraftHint: 'Applies when saved',
|
|
179
|
+
detailNextInterval: 'Applies to the next timer',
|
|
137
180
|
title: 'Offwork Clock',
|
|
181
|
+
classicMiniWorking: 'To end',
|
|
182
|
+
classicMiniBefore: 'To start',
|
|
183
|
+
classicMiniLunch: 'Break',
|
|
184
|
+
classicMiniStatus: 'Status',
|
|
185
|
+
classicMiniWaiting: 'Waiting',
|
|
186
|
+
classicMiniFinished: 'Done',
|
|
187
|
+
classicOverview: 'Today at a glance',
|
|
188
|
+
classicReturn: 'Back to overview',
|
|
189
|
+
classicMore: 'Show schedule details',
|
|
190
|
+
classicLess: 'Hide schedule details',
|
|
191
|
+
classicNoSession: 'Waiting for a session',
|
|
192
|
+
classicUnavailable: 'Timer unavailable',
|
|
193
|
+
classicRetryHint: 'Reload your schedule to restore the timer.',
|
|
194
|
+
classicLoading: 'Preparing your timer',
|
|
195
|
+
classicLoadingHint: 'Loading your saved schedule.',
|
|
196
|
+
classicBeforeHint: 'Your shift has not started. This is the time until work begins.',
|
|
197
|
+
classicWorkingHint: 'Your shift is in progress.',
|
|
198
|
+
classicLunchRemaining: 'Until lunch ends',
|
|
199
|
+
classicLunchHint: 'Work resumes after lunch. Water reminders continue as usual.',
|
|
200
|
+
classicDoneHint: 'This shift has ended. Timing resumes with your next shift.',
|
|
201
|
+
classicRestHint: 'No work time is counted today. Timing resumes on a scheduled workday.',
|
|
202
|
+
classicWaterHint: 'Clock time',
|
|
203
|
+
classicSmokeHint: 'Clock time',
|
|
204
|
+
classicConfigure: 'Set schedule and pay',
|
|
205
|
+
classicIncome: 'Shift earnings',
|
|
206
|
+
classicIncomeEntry: 'View earnings',
|
|
207
|
+
classicShift: 'Shift schedule',
|
|
208
|
+
classicEnd: 'Shift ends',
|
|
138
209
|
working: 'Until clock-out',
|
|
139
210
|
before: 'Until work starts',
|
|
140
211
|
lunch: 'On break',
|
|
@@ -169,7 +240,8 @@ window.__ModuleLoader__.load({
|
|
|
169
240
|
error: 'Operation failed. Please retry.',
|
|
170
241
|
saved: 'Settings saved',
|
|
171
242
|
days: 'SMTWTFS',
|
|
172
|
-
privacy:
|
|
243
|
+
privacy:
|
|
244
|
+
'Settings stay on the Harness host. UI saves directly without conversation logs; agent access is off by default.',
|
|
173
245
|
drag: 'Drag to move; arrow keys to adjust; Home resets',
|
|
174
246
|
setup: 'Set up your schedule',
|
|
175
247
|
setupHint: 'These are example values, not your income. Save to start.',
|
|
@@ -196,7 +268,7 @@ window.__ModuleLoader__.load({
|
|
|
196
268
|
opacityHint: 'Affects the background only.',
|
|
197
269
|
waterTitle: 'Water reminder',
|
|
198
270
|
waterEnable: 'Enable water reminders',
|
|
199
|
-
waterInterval: '
|
|
271
|
+
waterInterval: 'Interval (minutes)',
|
|
200
272
|
waterSave: 'Save reminder settings',
|
|
201
273
|
waterEdit: 'Reminder settings',
|
|
202
274
|
waterBack: 'Back',
|
|
@@ -206,21 +278,23 @@ window.__ModuleLoader__.load({
|
|
|
206
278
|
waterEffective: 'Current interval',
|
|
207
279
|
waterManage: 'Manage data',
|
|
208
280
|
waterSaveHint: 'Save changes to restart with the new interval.',
|
|
209
|
-
waterBrief:
|
|
281
|
+
waterBrief:
|
|
282
|
+
'Uses clock time, including breaks, off-hours and days off. Keep the page open.',
|
|
210
283
|
waterApply: 'Apply interval',
|
|
211
284
|
waterInvalid: 'Enter a whole number from 5 to 240.',
|
|
212
285
|
waterNext: 'Next water break',
|
|
213
286
|
waterDue: 'Time for some water',
|
|
214
287
|
waterDrink: 'Drank water',
|
|
215
|
-
waterSnooze: 'In 10
|
|
216
|
-
waterPause: 'Pause
|
|
217
|
-
waterResume: 'Resume
|
|
218
|
-
waterCount: 'Drinks
|
|
288
|
+
waterSnooze: 'In 10 minutes',
|
|
289
|
+
waterPause: 'Pause reminders',
|
|
290
|
+
waterResume: 'Resume reminders',
|
|
291
|
+
waterCount: 'Drinks recorded',
|
|
219
292
|
waterManual: 'Had a glass',
|
|
220
293
|
waterQuick: 'Quick water actions',
|
|
221
294
|
waterQuickClose: 'Close quick water actions',
|
|
222
295
|
waterRecorded: 'Got it, your drink is recorded.',
|
|
223
|
-
waterRecordHint:
|
|
296
|
+
waterRecordHint:
|
|
297
|
+
'Check in at any time, even with reminders off. Clear the count manually when needed.',
|
|
224
298
|
waterActionStale: 'The state changed. Please try again.',
|
|
225
299
|
waterQuiet: 'Quiet time',
|
|
226
300
|
waterQuietAdjust: 'Change duration',
|
|
@@ -232,32 +306,32 @@ window.__ModuleLoader__.load({
|
|
|
232
306
|
waterQuietHint:
|
|
233
307
|
'Uses clock time. Resumes automatically, with just one reminder if overdue.',
|
|
234
308
|
waterOff: 'Reminders off',
|
|
235
|
-
waterWaiting: 'Waiting for
|
|
236
|
-
waterBreak: '
|
|
237
|
-
waterPaused: '
|
|
238
|
-
waterFinished: '
|
|
309
|
+
waterWaiting: 'Waiting for reminders to be enabled',
|
|
310
|
+
waterBreak: 'Reminders paused',
|
|
311
|
+
waterPaused: 'Reminders paused',
|
|
312
|
+
waterFinished: 'Reminders ended',
|
|
239
313
|
waterOther: 'Reminder due; waiting for a visible tab',
|
|
240
|
-
waterClear: 'Clear
|
|
314
|
+
waterClear: 'Clear drink count',
|
|
241
315
|
waterReset: 'Reset water reminders',
|
|
242
316
|
waterConfirm: 'Confirm',
|
|
243
317
|
waterClearHint: 'Clear the drink count only. Keep the next reminder and pause state.',
|
|
244
|
-
waterResetHint: '
|
|
318
|
+
waterResetHint: 'Clear current water preferences and count, then turn reminders off.',
|
|
245
319
|
waterLocal:
|
|
246
320
|
'Reminder preferences and check-ins stay in this browser, outside conversations. Saved separately from salary settings.',
|
|
247
321
|
waterRules:
|
|
248
|
-
'
|
|
322
|
+
'Uses clock time, independently of work, salary and sessions. Enabling, changing the interval or manually resuming starts a new interval. Keep the page open.',
|
|
249
323
|
waterUnavailable:
|
|
250
|
-
'Water storage is unavailable or invalid; automatic reminders stopped. Retry, or reset
|
|
324
|
+
'Water storage is unavailable or invalid; automatic reminders stopped. Retry, or reset current water preferences and count.',
|
|
251
325
|
waterRetry: 'Retry loading',
|
|
252
|
-
waterSetup: '
|
|
326
|
+
waterSetup: 'Enable reminders to start the selected interval.',
|
|
253
327
|
waterLoading: 'Loading water reminders…',
|
|
254
|
-
waterChanged: 'Reminder
|
|
255
|
-
waterStale: '
|
|
328
|
+
waterChanged: 'Reminder settings saved',
|
|
329
|
+
waterStale: 'Records changed. Retry loading to continue.',
|
|
256
330
|
},
|
|
257
331
|
};
|
|
258
332
|
const STYLE =
|
|
259
333
|
`
|
|
260
|
-
.owc-v2{--owc-accent:color-mix(in srgb,var(--dsw-alias-brand-primary) 65%,#2563eb);pointer-events:auto;position:fixed;top:96px;right:24px;z-index:40;max-width:calc(100vw - 24px);max-height:calc(100dvh - 24px);border-radius:20px;color:var(--dsw-alias-label-primary);font:13px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;cursor:grab;touch-action:manipulation;user-select:none;-webkit-user-select:none}
|
|
334
|
+
.owc-v2{--owc-accent:color-mix(in srgb,var(--dsw-alias-brand-primary) 65%,#2563eb);pointer-events:auto;position:fixed;top:96px;right:24px;z-index:40;max-width:calc(100vw - 24px);max-height:calc(100dvh - 24px);border-radius:20px;color:var(--dsw-alias-label-primary);font:var(--owc-font-size,13px)/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;cursor:grab;touch-action:manipulation;user-select:none;-webkit-user-select:none}
|
|
261
335
|
.owc-v2:active{cursor:grabbing}
|
|
262
336
|
.owc-v2.owc-compact{touch-action:none;border-radius:18px}
|
|
263
337
|
.owc-v2.owc-compact.owc-quick{width:296px;touch-action:manipulation}
|
|
@@ -384,6 +458,15 @@ window.__ModuleLoader__.load({
|
|
|
384
458
|
water: [
|
|
385
459
|
['path', { d: 'M8 1.5C6.6 4 3.5 7.1 3.5 10a4.5 4.5 0 0 0 9 0C12.5 7.1 9.4 4 8 1.5Z' }],
|
|
386
460
|
],
|
|
461
|
+
smoking: [
|
|
462
|
+
['rect', { x: 1.5, y: 10, width: 11, height: 3, rx: 0.5 }],
|
|
463
|
+
[
|
|
464
|
+
'path',
|
|
465
|
+
{
|
|
466
|
+
d: 'M9.5 10v3M14.5 10v3M10 7.5c1-1 .8-1.8 0-2.5s-1-1.5 0-2.5M13 6c1-1 .8-1.8 0-2.5s-1-1.5 0-2.5',
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
],
|
|
387
470
|
eye: [
|
|
388
471
|
['path', { d: 'M1.7 8S4.3 3.9 8 3.9 14.3 8 14.3 8 11.7 12.1 8 12.1 1.7 8 1.7 8Z' }],
|
|
389
472
|
['circle', { cx: 8, cy: 8, r: 2.05 }],
|
|
@@ -410,6 +493,19 @@ window.__ModuleLoader__.load({
|
|
|
410
493
|
['path', { d: 'M13.1 3v3.2H9.9' }],
|
|
411
494
|
],
|
|
412
495
|
};
|
|
496
|
+
const LOCAL_SCREENS = [
|
|
497
|
+
null,
|
|
498
|
+
'appearance',
|
|
499
|
+
'smoking',
|
|
500
|
+
'water-countdown',
|
|
501
|
+
'water',
|
|
502
|
+
'home',
|
|
503
|
+
'income',
|
|
504
|
+
'wrap',
|
|
505
|
+
'preferences',
|
|
506
|
+
'data',
|
|
507
|
+
'reminders',
|
|
508
|
+
];
|
|
413
509
|
const iconSvg = (name, size = 15) =>
|
|
414
510
|
h(
|
|
415
511
|
'svg',
|
|
@@ -457,12 +553,20 @@ window.__ModuleLoader__.load({
|
|
|
457
553
|
const waterEditing = ui.screen === 'water';
|
|
458
554
|
const [appearanceDraft, setAppearanceDraft] = React.useState(null);
|
|
459
555
|
const appearanceBase = React.useRef(null);
|
|
556
|
+
const previewBox = React.useRef(null);
|
|
557
|
+
const previewMotion = React.useRef(null);
|
|
558
|
+
const [previewState, setPreviewState] = React.useState({ phase: 'idle', action: null });
|
|
559
|
+
const [previewReduced, setPreviewReduced] = React.useState(
|
|
560
|
+
() => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false,
|
|
561
|
+
);
|
|
460
562
|
const appearanceWasPet = React.useRef(false);
|
|
461
563
|
const salaryBase = React.useRef(null);
|
|
462
564
|
const menuBox = React.useRef(null);
|
|
463
565
|
const menuTrigger = React.useRef(null);
|
|
464
566
|
const [dragPosition, setDragPosition] = React.useState(null);
|
|
465
|
-
const
|
|
567
|
+
const requestBusy = React.useRef(false);
|
|
568
|
+
const [hostBusy, setBusy] = React.useState(false);
|
|
569
|
+
const busy = hostBusy && (requestBusy.current || !LOCAL_SCREENS.includes(ui.screen));
|
|
466
570
|
const [error, setError] = React.useState('');
|
|
467
571
|
const [notice, setNotice] = React.useState('');
|
|
468
572
|
const [storageError, setStorageError] = React.useState(false);
|
|
@@ -488,8 +592,118 @@ window.__ModuleLoader__.load({
|
|
|
488
592
|
const dragState = React.useRef(null);
|
|
489
593
|
const lastPosition = React.useRef(null);
|
|
490
594
|
const generation = React.useRef(0);
|
|
491
|
-
const requestBusy = React.useRef(false);
|
|
492
595
|
const focusReturn = React.useRef(null);
|
|
596
|
+
const [smoking, setSmoking] = React.useState(defaultCountdown);
|
|
597
|
+
const [smokingReady, setSmokingReady] = React.useState(false);
|
|
598
|
+
const [smokingBusy, setSmokingBusy] = React.useState(false);
|
|
599
|
+
const [smokingError, setSmokingError] = React.useState('');
|
|
600
|
+
const smokingRuntime = React.useRef(null);
|
|
601
|
+
React.useEffect(() => {
|
|
602
|
+
let alive = true,
|
|
603
|
+
reading = false,
|
|
604
|
+
writing = false,
|
|
605
|
+
issued = 0,
|
|
606
|
+
accepted = 0,
|
|
607
|
+
channel;
|
|
608
|
+
const makeStore = () => {
|
|
609
|
+
try {
|
|
610
|
+
return createStateStore(
|
|
611
|
+
window.indexedDB,
|
|
612
|
+
SMOKING_TIMER_DB,
|
|
613
|
+
defaultCountdown,
|
|
614
|
+
validateCountdown,
|
|
615
|
+
);
|
|
616
|
+
} catch {
|
|
617
|
+
return createStateStore(
|
|
618
|
+
null,
|
|
619
|
+
SMOKING_TIMER_DB,
|
|
620
|
+
defaultCountdown,
|
|
621
|
+
validateCountdown,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
let store = makeStore();
|
|
626
|
+
const accept = (next, order) => {
|
|
627
|
+
if (!alive || order < accepted) return;
|
|
628
|
+
accepted = order;
|
|
629
|
+
setSmoking(next);
|
|
630
|
+
setNow(Date.now());
|
|
631
|
+
setSmokingReady(true);
|
|
632
|
+
};
|
|
633
|
+
const refresh = async () => {
|
|
634
|
+
if (!alive || reading || writing) return;
|
|
635
|
+
reading = true;
|
|
636
|
+
const source = store,
|
|
637
|
+
order = ++issued;
|
|
638
|
+
try {
|
|
639
|
+
const next = await source.update((current) => current);
|
|
640
|
+
if (source === store) accept(next, order);
|
|
641
|
+
} catch {
|
|
642
|
+
if (alive && source === store && order >= accepted)
|
|
643
|
+
setSmokingError('timerStorageError');
|
|
644
|
+
} finally {
|
|
645
|
+
reading = false;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
const run = async (action) => {
|
|
649
|
+
if (!alive || writing) return;
|
|
650
|
+
writing = true;
|
|
651
|
+
const order = ++issued;
|
|
652
|
+
setSmokingBusy(true);
|
|
653
|
+
try {
|
|
654
|
+
if (action.type === 'retry') {
|
|
655
|
+
store.close();
|
|
656
|
+
store = makeStore();
|
|
657
|
+
}
|
|
658
|
+
let changedElsewhere = false;
|
|
659
|
+
const next = await store.update((current) => {
|
|
660
|
+
if (action.type === 'retry') return current;
|
|
661
|
+
if (current.revision !== action.revision) {
|
|
662
|
+
changedElsewhere = true;
|
|
663
|
+
return current;
|
|
664
|
+
}
|
|
665
|
+
return transitionCountdown(current, action, Date.now());
|
|
666
|
+
});
|
|
667
|
+
accept(next, order);
|
|
668
|
+
if (alive) setSmokingError(changedElsewhere ? 'timerChanged' : '');
|
|
669
|
+
try {
|
|
670
|
+
channel?.postMessage(next.revision);
|
|
671
|
+
} catch {
|
|
672
|
+
/* Polling also synchronizes. */
|
|
673
|
+
}
|
|
674
|
+
return changedElsewhere ? null : next;
|
|
675
|
+
} catch {
|
|
676
|
+
if (alive) setSmokingError('timerStorageError');
|
|
677
|
+
} finally {
|
|
678
|
+
writing = false;
|
|
679
|
+
if (alive) setSmokingBusy(false);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
smokingRuntime.current = { run };
|
|
683
|
+
try {
|
|
684
|
+
channel = new BroadcastChannel(SMOKING_TIMER_DB);
|
|
685
|
+
channel.onmessage = refresh;
|
|
686
|
+
} catch {
|
|
687
|
+
/* Polling is sufficient. */
|
|
688
|
+
}
|
|
689
|
+
void refresh();
|
|
690
|
+
const timer = window.setInterval(refresh, 2000);
|
|
691
|
+
window.addEventListener('focus', refresh);
|
|
692
|
+
document.addEventListener('visibilitychange', refresh);
|
|
693
|
+
return () => {
|
|
694
|
+
alive = false;
|
|
695
|
+
smokingRuntime.current = null;
|
|
696
|
+
window.clearInterval(timer);
|
|
697
|
+
window.removeEventListener('focus', refresh);
|
|
698
|
+
document.removeEventListener('visibilitychange', refresh);
|
|
699
|
+
channel?.close();
|
|
700
|
+
store.close();
|
|
701
|
+
};
|
|
702
|
+
}, []);
|
|
703
|
+
const smokingState = countdownView(smoking, now);
|
|
704
|
+
const smokingUnavailable = !smokingReady || smokingError === 'timerStorageError';
|
|
705
|
+
const smokingAction = (action) =>
|
|
706
|
+
smokingRuntime.current?.run({ ...action, revision: smoking.revision });
|
|
493
707
|
const [water, setWater] = React.useState(defaultHydration);
|
|
494
708
|
const [waterReady, setWaterReady] = React.useState(false);
|
|
495
709
|
const [waterError, setWaterError] = React.useState(false);
|
|
@@ -507,6 +721,7 @@ window.__ModuleLoader__.load({
|
|
|
507
721
|
const [waterInvalid, setWaterInvalid] = React.useState(false);
|
|
508
722
|
const [waterConfirm, setWaterConfirm] = React.useState(null);
|
|
509
723
|
const [waterNotice, setWaterNotice] = React.useState(false);
|
|
724
|
+
const [reminderGate, setReminderGate] = React.useState(initialPetReminder);
|
|
510
725
|
const reactionState = React.useRef(null);
|
|
511
726
|
const petMotion = React.useRef(null);
|
|
512
727
|
const petReactionToken = React.useRef(null);
|
|
@@ -516,32 +731,90 @@ window.__ModuleLoader__.load({
|
|
|
516
731
|
waterOwner.current =
|
|
517
732
|
window.crypto?.randomUUID?.() || String(Math.random()) + Date.now();
|
|
518
733
|
const waterInputs = React.useRef(null);
|
|
519
|
-
|
|
520
|
-
waterInputs.current = {
|
|
521
|
-
settings: data?.configured && !error ? data.settings : null,
|
|
522
|
-
editing: editing || waterEditing,
|
|
523
|
-
};
|
|
734
|
+
waterInputs.current = { editing: editing || waterEditing };
|
|
524
735
|
|
|
525
736
|
const settings = data?.settings;
|
|
526
737
|
const workingDraft = draft || settings;
|
|
527
738
|
const state = settings ? safeCalculate(settings) : null;
|
|
528
739
|
const preview = workingDraft ? safeCalculate(workingDraft) : null;
|
|
529
740
|
const safeState = state && !state.error ? state : null;
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
!!
|
|
534
|
-
(
|
|
535
|
-
|
|
741
|
+
const features = useCompanionFeatures(React, {
|
|
742
|
+
language: locale.active,
|
|
743
|
+
screen: ui.screen,
|
|
744
|
+
menuOpen: !!ui.menu,
|
|
745
|
+
navigate: (screen, event) => navigateScreen(screen, event),
|
|
746
|
+
enableCharacter: () => updateAppearance({ companion: true }),
|
|
747
|
+
configured: !!data?.configured,
|
|
748
|
+
shift: data?.configured ? safeState : null,
|
|
749
|
+
timezone: settings?.timezone,
|
|
750
|
+
smoking,
|
|
751
|
+
smokingReady: smokingReady && !smokingError,
|
|
752
|
+
water,
|
|
753
|
+
waterReady: waterReady && !waterError,
|
|
754
|
+
waterOwner: waterOwner.current,
|
|
755
|
+
smokingLabel: t('smokingCountdown'),
|
|
756
|
+
waterLabel: t('waterDue'),
|
|
757
|
+
wrapLabel: t('companionDone'),
|
|
758
|
+
editing: editing || waterEditing || ui.screen === 'wrap' || !!ui.pending,
|
|
759
|
+
sessionIds,
|
|
760
|
+
sessionTitles: Object.fromEntries(
|
|
761
|
+
sessionIds.map((id) => [id, sessions?.byId?.[id]?.title]),
|
|
762
|
+
),
|
|
763
|
+
runningSessions: sessionIds.filter((id) => sessions?.byId?.[id]?.running === true),
|
|
764
|
+
view: savedView,
|
|
765
|
+
setView: (patch) => setPreference(patch),
|
|
766
|
+
openLegacy: (screen) => navigateScreen(screen),
|
|
767
|
+
reload,
|
|
768
|
+
refreshLegacy: () => {
|
|
769
|
+
waterRuntime.current?.tick();
|
|
770
|
+
smokingRuntime.current?.run({ type: 'retry' });
|
|
771
|
+
},
|
|
772
|
+
restartSmoking: () => smokingAction({ type: 'restart' }),
|
|
773
|
+
drink: () => waterAction({ type: 'manual-drink', actionId: crypto.randomUUID() }),
|
|
774
|
+
react: () => petMotion.current?.play('greet'),
|
|
775
|
+
});
|
|
776
|
+
const displayPreferences =
|
|
777
|
+
appearanceOnly && features.previewPreferences
|
|
778
|
+
? features.previewPreferences
|
|
779
|
+
: features.state.preferences;
|
|
780
|
+
const featureStyle = {
|
|
781
|
+
...(COMPANION_THEMES[displayPreferences.theme]
|
|
782
|
+
? { '--owc-accent': COMPANION_THEMES[displayPreferences.theme] }
|
|
783
|
+
: {}),
|
|
784
|
+
'--owc-font-size': 13 * displayPreferences.cardScale + 'px',
|
|
785
|
+
};
|
|
786
|
+
const petMode = ready && (appearanceOnly ? appearanceWasPet.current : view.companion);
|
|
787
|
+
const petClockStatus =
|
|
788
|
+
error || state?.error
|
|
789
|
+
? 'petLoadFailed'
|
|
790
|
+
: !data
|
|
791
|
+
? 'loading'
|
|
792
|
+
: !data.configured
|
|
793
|
+
? 'petNeedsSetup'
|
|
794
|
+
: safeState?.status || 'loading';
|
|
795
|
+
const stageModel = features.customImage
|
|
796
|
+
? 'custom-image'
|
|
797
|
+
: appearanceOnly
|
|
798
|
+
? savedView.petModel
|
|
799
|
+
: view.petModel;
|
|
536
800
|
const petGeometry = petLayout(
|
|
537
801
|
dragPosition ?? view.petPosition,
|
|
538
802
|
viewport,
|
|
539
803
|
appearanceOnly ? savedView.petSize : view.petSize,
|
|
540
804
|
420,
|
|
541
805
|
stageModel,
|
|
806
|
+
(window.matchMedia?.('(pointer:coarse)').matches ? 144 : 108) +
|
|
807
|
+
(features.activeTimer ? 30 : 0),
|
|
808
|
+
196,
|
|
542
809
|
);
|
|
543
810
|
const surfaceScreen = ui.screen || 'reminder';
|
|
544
|
-
const surfaceWidth =
|
|
811
|
+
const surfaceWidth = Math.max(
|
|
812
|
+
1,
|
|
813
|
+
Math.min(
|
|
814
|
+
petSurfaceWidth(surfaceScreen, viewport) * displayPreferences.cardScale,
|
|
815
|
+
viewport.width - 24,
|
|
816
|
+
),
|
|
817
|
+
);
|
|
545
818
|
const measureKey = [surfaceScreen, surfaceWidth, locale.active].join(':');
|
|
546
819
|
const measured = cardMeasure.key === measureKey;
|
|
547
820
|
const naturalHeight = measured ? cardMeasure.height : 320;
|
|
@@ -582,7 +855,7 @@ window.__ModuleLoader__.load({
|
|
|
582
855
|
);
|
|
583
856
|
const waterAlertLayout = placePetSurface(anchor, viewport, {
|
|
584
857
|
width: Math.min(140, Math.max(110, alertRoom)),
|
|
585
|
-
height:
|
|
858
|
+
height: 44,
|
|
586
859
|
kind: 'menu',
|
|
587
860
|
obstacles: [...(petOpen ? [floatingPanel] : []), ...(ui.menu ? [menuLayout] : [])],
|
|
588
861
|
});
|
|
@@ -591,14 +864,15 @@ window.__ModuleLoader__.load({
|
|
|
591
864
|
height: toastHeight,
|
|
592
865
|
kind: 'menu',
|
|
593
866
|
});
|
|
594
|
-
const collapsed =
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
867
|
+
const collapsed = !petMode && ui.screen === null && view.mini;
|
|
868
|
+
const classicClock = classicClockPresentation({
|
|
869
|
+
hasSession: true,
|
|
870
|
+
loaded: !!data,
|
|
871
|
+
configured: !!data?.configured,
|
|
872
|
+
failed: !!error || !!state?.error,
|
|
873
|
+
state: safeState,
|
|
874
|
+
now,
|
|
875
|
+
});
|
|
602
876
|
const seconds = safeState
|
|
603
877
|
? safeState.status === 'before'
|
|
604
878
|
? safeState.startRemaining
|
|
@@ -626,6 +900,16 @@ window.__ModuleLoader__.load({
|
|
|
626
900
|
classicSize,
|
|
627
901
|
);
|
|
628
902
|
const cardStyle = {
|
|
903
|
+
...featureStyle,
|
|
904
|
+
width: ui.screen
|
|
905
|
+
? Math.min(
|
|
906
|
+
petSurfaceWidth(ui.screen, viewport) * displayPreferences.cardScale,
|
|
907
|
+
viewport.width - 24,
|
|
908
|
+
)
|
|
909
|
+
: Math.min(
|
|
910
|
+
(collapsed ? 224 : 320) * displayPreferences.cardScale,
|
|
911
|
+
Math.max(1, viewport.width - 24),
|
|
912
|
+
),
|
|
629
913
|
'--owc-alpha': String(view.opacity),
|
|
630
914
|
'--owc-viewport-height': Math.max(1, viewport.height - 24) + 'px',
|
|
631
915
|
left: classicPosition.x,
|
|
@@ -708,17 +992,8 @@ window.__ModuleLoader__.load({
|
|
|
708
992
|
store = createHydrationStore(null);
|
|
709
993
|
}
|
|
710
994
|
try {
|
|
711
|
-
channel = new BroadcastChannel('dsh.offwork-clock.hydration.
|
|
995
|
+
channel = new BroadcastChannel('dsh.offwork-clock.hydration.wall-clock.v3');
|
|
712
996
|
} catch {}
|
|
713
|
-
const contextAt = (time) => {
|
|
714
|
-
const current = waterInputs.current.settings;
|
|
715
|
-
if (!current) return null;
|
|
716
|
-
try {
|
|
717
|
-
return hydrationContext(current, calculate(current, time));
|
|
718
|
-
} catch {
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
997
|
const update = async (action = { type: 'tick' }, automatic = false) => {
|
|
723
998
|
if (!active || (automatic && (running || failed))) return;
|
|
724
999
|
running++;
|
|
@@ -734,59 +1009,31 @@ window.__ModuleLoader__.load({
|
|
|
734
1009
|
let changed = false;
|
|
735
1010
|
let recorded = false;
|
|
736
1011
|
let quietChanged = false;
|
|
737
|
-
let usedPermit = null;
|
|
738
1012
|
const result = await store.update(
|
|
739
1013
|
(current) => {
|
|
740
1014
|
const time = Date.now();
|
|
741
|
-
const context = contextAt(time);
|
|
742
1015
|
if (action.type === 'sync') return current;
|
|
743
|
-
|
|
744
|
-
const operation =
|
|
745
|
-
permit && context && permit.schedule === context.schedule
|
|
746
|
-
? { ...action, baseSchedule: permit.baseSchedule }
|
|
747
|
-
: action;
|
|
748
|
-
let next = transitionHydration(
|
|
749
|
-
current,
|
|
750
|
-
context,
|
|
751
|
-
operation,
|
|
752
|
-
time,
|
|
753
|
-
waterOwner.current,
|
|
754
|
-
);
|
|
1016
|
+
let next = transitionHydration(current, action, time, waterOwner.current);
|
|
755
1017
|
recorded =
|
|
756
|
-
['drink', 'manual-drink'].includes(action.type) &&
|
|
757
|
-
next.shift?.count >
|
|
758
|
-
(current.shift?.id === next.shift?.id ? (current.shift?.count ?? 0) : 0);
|
|
1018
|
+
['drink', 'manual-drink'].includes(action.type) && next.count > current.count;
|
|
759
1019
|
quietChanged =
|
|
760
1020
|
['quiet', 'end-quiet'].includes(action.type) &&
|
|
761
|
-
next.
|
|
1021
|
+
next.quietUntil !== current.quietUntil;
|
|
762
1022
|
if (automatic) {
|
|
763
1023
|
const allow = !document.hidden && !waterInputs.current.editing;
|
|
764
1024
|
next = transitionHydration(
|
|
765
1025
|
next,
|
|
766
|
-
context,
|
|
767
1026
|
{ type: allow ? 'claim' : 'release' },
|
|
768
1027
|
time,
|
|
769
1028
|
waterOwner.current,
|
|
770
1029
|
);
|
|
771
1030
|
}
|
|
772
|
-
if (
|
|
773
|
-
permit &&
|
|
774
|
-
context &&
|
|
775
|
-
permit.schedule === context.schedule &&
|
|
776
|
-
next.shift?.schedule === context.schedule
|
|
777
|
-
)
|
|
778
|
-
usedPermit = permit;
|
|
779
1031
|
changed = JSON.stringify(next) !== JSON.stringify(current);
|
|
780
1032
|
return next;
|
|
781
1033
|
},
|
|
782
1034
|
{ reset: action.type === 'reset' },
|
|
783
1035
|
);
|
|
784
1036
|
if (!active) return;
|
|
785
|
-
if (
|
|
786
|
-
action.type === 'reset' ||
|
|
787
|
-
(usedPermit && waterScheduleChange.current === usedPermit)
|
|
788
|
-
)
|
|
789
|
-
waterScheduleChange.current = null;
|
|
790
1037
|
failed = false;
|
|
791
1038
|
setWater(result);
|
|
792
1039
|
setWaterReady(true);
|
|
@@ -818,11 +1065,14 @@ window.__ModuleLoader__.load({
|
|
|
818
1065
|
const visibility = () => {
|
|
819
1066
|
setWaterVisible(!document.hidden);
|
|
820
1067
|
setNow(Date.now());
|
|
821
|
-
if (document.hidden)
|
|
822
|
-
|
|
1068
|
+
if (document.hidden) {
|
|
1069
|
+
setReminderGate(initialPetReminder);
|
|
1070
|
+
void update({ type: 'release' });
|
|
1071
|
+
} else tick();
|
|
823
1072
|
};
|
|
824
1073
|
const pageHide = () => {
|
|
825
1074
|
setWaterVisible(false);
|
|
1075
|
+
setReminderGate(initialPetReminder);
|
|
826
1076
|
void update({ type: 'release' });
|
|
827
1077
|
};
|
|
828
1078
|
waterRuntime.current = { update, tick };
|
|
@@ -847,8 +1097,7 @@ window.__ModuleLoader__.load({
|
|
|
847
1097
|
channel?.close();
|
|
848
1098
|
void store
|
|
849
1099
|
.update((current) => {
|
|
850
|
-
if (current.
|
|
851
|
-
current.shift.claim = null;
|
|
1100
|
+
if (current.claim?.owner === waterOwner.current) current.claim = null;
|
|
852
1101
|
return current;
|
|
853
1102
|
})
|
|
854
1103
|
.catch(() => {})
|
|
@@ -858,7 +1107,7 @@ window.__ModuleLoader__.load({
|
|
|
858
1107
|
|
|
859
1108
|
React.useEffect(() => {
|
|
860
1109
|
waterRuntime.current?.tick();
|
|
861
|
-
}, [
|
|
1110
|
+
}, [editing, waterEditing]);
|
|
862
1111
|
|
|
863
1112
|
React.useEffect(() => {
|
|
864
1113
|
if (!waterNotice) return;
|
|
@@ -904,6 +1153,7 @@ window.__ModuleLoader__.load({
|
|
|
904
1153
|
const interval = Number(waterInterval);
|
|
905
1154
|
if (!Number.isInteger(interval) || interval < 5 || interval > 240) {
|
|
906
1155
|
setWaterInvalid(true);
|
|
1156
|
+
box.current?.querySelector('[name=waterInterval]')?.focus();
|
|
907
1157
|
return;
|
|
908
1158
|
}
|
|
909
1159
|
const result = await waterAction({
|
|
@@ -928,6 +1178,13 @@ window.__ModuleLoader__.load({
|
|
|
928
1178
|
const outcome = await waterRuntime.current?.update(action);
|
|
929
1179
|
const result = outcome?.state;
|
|
930
1180
|
if (result) {
|
|
1181
|
+
if (outcome.recorded)
|
|
1182
|
+
void features.record({
|
|
1183
|
+
id: 'water-record:' + result.revision,
|
|
1184
|
+
type: 'water',
|
|
1185
|
+
at: Date.now(),
|
|
1186
|
+
label: 'Water',
|
|
1187
|
+
});
|
|
931
1188
|
setWaterConfirm(null);
|
|
932
1189
|
setNow(Date.now());
|
|
933
1190
|
if (action.type === 'preferences')
|
|
@@ -936,7 +1193,7 @@ window.__ModuleLoader__.load({
|
|
|
936
1193
|
setWaterNotice({ key: 'waterRecorded', kind: 'drink', token: result.revision });
|
|
937
1194
|
if (action.type === 'manual-drink') {
|
|
938
1195
|
setWaterNotice({
|
|
939
|
-
key: result.
|
|
1196
|
+
key: result.recentActions.includes(action.actionId)
|
|
940
1197
|
? 'waterRecorded'
|
|
941
1198
|
: 'waterActionStale',
|
|
942
1199
|
kind: outcome.recorded ? 'drink' : 'notice',
|
|
@@ -947,7 +1204,7 @@ window.__ModuleLoader__.load({
|
|
|
947
1204
|
if (action.type === 'quiet')
|
|
948
1205
|
setWaterNotice({
|
|
949
1206
|
key:
|
|
950
|
-
outcome.quietChanged && result.
|
|
1207
|
+
outcome.quietChanged && result.quietUntil > Date.now()
|
|
951
1208
|
? 'waterQuietStarted'
|
|
952
1209
|
: 'waterActionStale',
|
|
953
1210
|
kind: 'notice',
|
|
@@ -975,13 +1232,9 @@ window.__ModuleLoader__.load({
|
|
|
975
1232
|
? '.owc-pet-handle'
|
|
976
1233
|
: '.owc-grip,.owc-title'
|
|
977
1234
|
: action.type === 'manual-drink'
|
|
978
|
-
?
|
|
979
|
-
? '.owc-pet-water-row .owc-primary'
|
|
980
|
-
: '.owc-water-actions .owc-primary'
|
|
1235
|
+
? '.owc-water-record,.owc-water-actions .owc-primary'
|
|
981
1236
|
: action.type === 'end-quiet'
|
|
982
|
-
?
|
|
983
|
-
? '.owc-pet-quiet-row button'
|
|
984
|
-
: '.owc-water-quiet summary'
|
|
1237
|
+
? '.owc-detail-more summary,.owc-water-quiet summary'
|
|
985
1238
|
: '.owc-water-form details > summary';
|
|
986
1239
|
box.current?.querySelector(target)?.focus();
|
|
987
1240
|
});
|
|
@@ -996,12 +1249,11 @@ window.__ModuleLoader__.load({
|
|
|
996
1249
|
}
|
|
997
1250
|
|
|
998
1251
|
React.useEffect(() => {
|
|
999
|
-
|
|
1000
|
-
else setError(t('noSession'));
|
|
1252
|
+
reload();
|
|
1001
1253
|
return () => {
|
|
1002
1254
|
generation.current++;
|
|
1003
1255
|
};
|
|
1004
|
-
}, [
|
|
1256
|
+
}, []);
|
|
1005
1257
|
|
|
1006
1258
|
React.useEffect(() => {
|
|
1007
1259
|
let timer;
|
|
@@ -1050,7 +1302,7 @@ window.__ModuleLoader__.load({
|
|
|
1050
1302
|
window.visualViewport?.removeEventListener('resize', resize);
|
|
1051
1303
|
window.visualViewport?.removeEventListener('scroll', resize);
|
|
1052
1304
|
};
|
|
1053
|
-
}, [clamp, petMode]);
|
|
1305
|
+
}, [clamp, petMode, ready]);
|
|
1054
1306
|
|
|
1055
1307
|
React.useEffect(() => {
|
|
1056
1308
|
if (ui.pending) {
|
|
@@ -1087,13 +1339,19 @@ window.__ModuleLoader__.load({
|
|
|
1087
1339
|
);
|
|
1088
1340
|
|
|
1089
1341
|
async function request(action, patch) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1342
|
+
const response = await fetch(new URL('offwork-clock/settings', document.baseURI), {
|
|
1343
|
+
method: action === 'get' ? 'GET' : 'POST',
|
|
1344
|
+
credentials: 'same-origin',
|
|
1345
|
+
cache: 'no-store',
|
|
1346
|
+
headers: {
|
|
1347
|
+
'X-Offwork-Client': '1',
|
|
1348
|
+
...(action === 'get' ? {} : { 'Content-Type': 'application/json' }),
|
|
1349
|
+
},
|
|
1350
|
+
...(action === 'get' ? {} : { body: JSON.stringify({ action, settings: patch }) }),
|
|
1351
|
+
});
|
|
1352
|
+
const result = await response.json();
|
|
1353
|
+
if (!response.ok || !result.ok) throw new Error(result.error || t('error'));
|
|
1354
|
+
return result;
|
|
1097
1355
|
}
|
|
1098
1356
|
|
|
1099
1357
|
async function reload(explicit = false) {
|
|
@@ -1102,21 +1360,11 @@ window.__ModuleLoader__.load({
|
|
|
1102
1360
|
setBusy(true);
|
|
1103
1361
|
setError('');
|
|
1104
1362
|
try {
|
|
1105
|
-
const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
|
|
1106
|
-
?.shift?.schedule;
|
|
1107
|
-
if (token !== generation.current) return;
|
|
1108
1363
|
const next = await request('get');
|
|
1109
1364
|
if (token !== generation.current) return;
|
|
1110
|
-
if (next.configured)
|
|
1111
|
-
waterScheduleChange.current = {
|
|
1112
|
-
baseSchedule,
|
|
1113
|
-
schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
|
|
1114
|
-
.schedule,
|
|
1115
|
-
};
|
|
1116
1365
|
setData(next);
|
|
1117
1366
|
if (!next.configured) {
|
|
1118
1367
|
setDraft({ ...next.settings });
|
|
1119
|
-
dispatchUI({ type: 'navigate', screen: 'schedule', discard: true });
|
|
1120
1368
|
salaryBase.current = next.settings;
|
|
1121
1369
|
}
|
|
1122
1370
|
if (explicit) setNotice(t('synced'));
|
|
@@ -1135,19 +1383,11 @@ window.__ModuleLoader__.load({
|
|
|
1135
1383
|
setError('');
|
|
1136
1384
|
const token = ++generation.current;
|
|
1137
1385
|
try {
|
|
1138
|
-
const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
|
|
1139
|
-
?.shift?.schedule;
|
|
1140
|
-
if (token !== generation.current) return;
|
|
1141
1386
|
const next = await request(
|
|
1142
1387
|
'set',
|
|
1143
1388
|
changedSettings(salaryBase.current ?? settings, draft),
|
|
1144
1389
|
);
|
|
1145
1390
|
if (token === generation.current) {
|
|
1146
|
-
waterScheduleChange.current = {
|
|
1147
|
-
baseSchedule,
|
|
1148
|
-
schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
|
|
1149
|
-
.schedule,
|
|
1150
|
-
};
|
|
1151
1391
|
setData(next);
|
|
1152
1392
|
dispatchUI({ type: 'navigate', screen: ui.back, discard: true });
|
|
1153
1393
|
setDraft(null);
|
|
@@ -1253,7 +1493,7 @@ window.__ModuleLoader__.load({
|
|
|
1253
1493
|
petDragged.current = false;
|
|
1254
1494
|
if (ui.pending) return true;
|
|
1255
1495
|
if (!petMode) {
|
|
1256
|
-
if (!
|
|
1496
|
+
if (!requestBusy.current && !waterBusy) navigateScreen('appearance', event);
|
|
1257
1497
|
return true;
|
|
1258
1498
|
}
|
|
1259
1499
|
const trigger =
|
|
@@ -1269,7 +1509,7 @@ window.__ModuleLoader__.load({
|
|
|
1269
1509
|
: trigger;
|
|
1270
1510
|
const rect = trigger?.getBoundingClientRect();
|
|
1271
1511
|
const point =
|
|
1272
|
-
event.type === 'keydown'
|
|
1512
|
+
event.type === 'keydown' || (event.type === 'click' && event.detail === 0)
|
|
1273
1513
|
? { x: rect?.right ?? petGeometry.x, y: rect?.top ?? petGeometry.y }
|
|
1274
1514
|
: { x: event.clientX, y: event.clientY };
|
|
1275
1515
|
dispatchUI({ type: 'menu', position: point });
|
|
@@ -1283,6 +1523,7 @@ window.__ModuleLoader__.load({
|
|
|
1283
1523
|
function performNavigation(screen, root = false) {
|
|
1284
1524
|
if (root) panelPlacement.current = null;
|
|
1285
1525
|
if (screen !== ui.screen) {
|
|
1526
|
+
features.discardDrafts();
|
|
1286
1527
|
if (screen === 'water') {
|
|
1287
1528
|
waterPreferencesBase.current = { enabled: water.enabled, interval: water.interval };
|
|
1288
1529
|
setWaterInterval(String(water.interval));
|
|
@@ -1296,6 +1537,7 @@ window.__ModuleLoader__.load({
|
|
|
1296
1537
|
setError('');
|
|
1297
1538
|
}
|
|
1298
1539
|
if (screen === 'appearance') {
|
|
1540
|
+
features.beginAppearance();
|
|
1299
1541
|
appearanceBase.current = appearanceValues(savedView);
|
|
1300
1542
|
appearanceWasPet.current = !!petMode;
|
|
1301
1543
|
setAppearanceDraft(appearanceValues(savedView));
|
|
@@ -1310,8 +1552,21 @@ window.__ModuleLoader__.load({
|
|
|
1310
1552
|
setWaterConfirm(null);
|
|
1311
1553
|
}
|
|
1312
1554
|
function navigateScreen(screen, event) {
|
|
1313
|
-
if (
|
|
1314
|
-
if (
|
|
1555
|
+
if (screen === 'water-countdown') screen = 'home';
|
|
1556
|
+
if (
|
|
1557
|
+
screen !== null &&
|
|
1558
|
+
!LOCAL_SCREENS.includes(screen) &&
|
|
1559
|
+
(!data || (!data.configured && screen !== 'schedule'))
|
|
1560
|
+
)
|
|
1561
|
+
return;
|
|
1562
|
+
if (
|
|
1563
|
+
ui.pending ||
|
|
1564
|
+
features.busy ||
|
|
1565
|
+
waterBusy ||
|
|
1566
|
+
requestBusy.current ||
|
|
1567
|
+
waterPending.current ||
|
|
1568
|
+
(hostBusy && !LOCAL_SCREENS.includes(screen))
|
|
1569
|
+
)
|
|
1315
1570
|
return;
|
|
1316
1571
|
const root = event?.currentTarget?.getAttribute?.('role') === 'menuitem';
|
|
1317
1572
|
if (event?.currentTarget) focusReturn.current = event.currentTarget;
|
|
@@ -1322,6 +1577,7 @@ window.__ModuleLoader__.load({
|
|
|
1322
1577
|
performNavigation(screen, root);
|
|
1323
1578
|
}
|
|
1324
1579
|
function draftChanged() {
|
|
1580
|
+
if (features.dirty) return true;
|
|
1325
1581
|
if (waterEditing && waterPreferencesBase.current)
|
|
1326
1582
|
return (
|
|
1327
1583
|
waterEnabledDraft !== waterPreferencesBase.current.enabled ||
|
|
@@ -1335,7 +1591,8 @@ window.__ModuleLoader__.load({
|
|
|
1335
1591
|
);
|
|
1336
1592
|
return false;
|
|
1337
1593
|
}
|
|
1338
|
-
function applyAppearance() {
|
|
1594
|
+
async function applyAppearance() {
|
|
1595
|
+
if (!(await features.applyAppearance())) return false;
|
|
1339
1596
|
panelPlacement.current = null;
|
|
1340
1597
|
if (appearanceDraft)
|
|
1341
1598
|
setPreference(changedSettings(appearanceBase.current, appearanceDraft));
|
|
@@ -1347,13 +1604,16 @@ window.__ModuleLoader__.load({
|
|
|
1347
1604
|
const destination = ui.pending.screen;
|
|
1348
1605
|
const result = waterEditing
|
|
1349
1606
|
? await saveWaterSettings({ preventDefault() {} })
|
|
1350
|
-
:
|
|
1351
|
-
?
|
|
1352
|
-
:
|
|
1607
|
+
: ui.screen === 'wrap'
|
|
1608
|
+
? await features.saveNotes(false)
|
|
1609
|
+
: appearanceOnly
|
|
1610
|
+
? await applyAppearance()
|
|
1611
|
+
: await save({ preventDefault() {} });
|
|
1353
1612
|
if (result) performNavigation(destination, ui.pending.root);
|
|
1354
1613
|
else dispatchUI({ type: 'stay' });
|
|
1355
1614
|
}
|
|
1356
1615
|
function updateAppearance(patch) {
|
|
1616
|
+
if (patch.petModel) features.useBuiltin();
|
|
1357
1617
|
setAppearanceDraft((current) => ({ ...current, ...patch }));
|
|
1358
1618
|
}
|
|
1359
1619
|
function openAppearance(event) {
|
|
@@ -1363,7 +1623,7 @@ window.__ModuleLoader__.load({
|
|
|
1363
1623
|
navigateScreen('schedule', event);
|
|
1364
1624
|
}
|
|
1365
1625
|
function closeEditor() {
|
|
1366
|
-
if (
|
|
1626
|
+
if (busy) return;
|
|
1367
1627
|
performNavigation(ui.back);
|
|
1368
1628
|
}
|
|
1369
1629
|
function update(key, value) {
|
|
@@ -1432,20 +1692,19 @@ window.__ModuleLoader__.load({
|
|
|
1432
1692
|
'button',
|
|
1433
1693
|
{
|
|
1434
1694
|
type: 'button',
|
|
1435
|
-
className: 'owc-
|
|
1695
|
+
className: 'owc-icon',
|
|
1436
1696
|
'aria-pressed': view.masked,
|
|
1697
|
+
'aria-label': t(view.masked ? 'show' : 'hide'),
|
|
1437
1698
|
title: t(view.masked ? 'show' : 'hide'),
|
|
1438
1699
|
onClick: () => setPreference({ masked: !view.masked }),
|
|
1439
1700
|
},
|
|
1440
1701
|
iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
|
|
1441
|
-
t(view.masked ? 'show' : 'hide'),
|
|
1442
1702
|
);
|
|
1443
1703
|
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
const waterState = hydrationView(water, waterContext, now);
|
|
1447
|
-
const waterClaim = water.shift?.claim;
|
|
1704
|
+
const waterState = hydrationView(water, now);
|
|
1705
|
+
const waterClaim = water.claim;
|
|
1448
1706
|
const waterPrompt =
|
|
1707
|
+
!features.ready &&
|
|
1449
1708
|
waterReady &&
|
|
1450
1709
|
!waterError &&
|
|
1451
1710
|
!editing &&
|
|
@@ -1454,9 +1713,59 @@ window.__ModuleLoader__.load({
|
|
|
1454
1713
|
!document.hidden &&
|
|
1455
1714
|
waterState.phase === 'due' &&
|
|
1456
1715
|
waterClaim?.owner === waterOwner.current &&
|
|
1457
|
-
waterClaim.sequence === water.
|
|
1716
|
+
waterClaim.sequence === water.sequence &&
|
|
1458
1717
|
waterClaim.until > Date.now() &&
|
|
1459
1718
|
waterClaim.until <= Date.now() + HYDRATION_LEASE_MS;
|
|
1719
|
+
const reminderReady = !!(petMode && waterReady && !waterError);
|
|
1720
|
+
const reminderVisible = waterVisible && !document.hidden;
|
|
1721
|
+
const reminderAvailable = !petOpen && !ui.menu && !ui.pending && !busy;
|
|
1722
|
+
React.useEffect(() => {
|
|
1723
|
+
setReminderGate((current) =>
|
|
1724
|
+
nextPetReminder(current, {
|
|
1725
|
+
ready: reminderReady,
|
|
1726
|
+
eventId: waterState.eventId,
|
|
1727
|
+
phase: waterState.phase,
|
|
1728
|
+
visible: reminderVisible,
|
|
1729
|
+
available: reminderAvailable,
|
|
1730
|
+
ownsPrompt: waterPrompt,
|
|
1731
|
+
}),
|
|
1732
|
+
);
|
|
1733
|
+
}, [
|
|
1734
|
+
reminderReady,
|
|
1735
|
+
waterState.eventId,
|
|
1736
|
+
waterState.phase,
|
|
1737
|
+
reminderVisible,
|
|
1738
|
+
reminderAvailable,
|
|
1739
|
+
waterPrompt,
|
|
1740
|
+
]);
|
|
1741
|
+
const autoWaterPrompt =
|
|
1742
|
+
reminderReady &&
|
|
1743
|
+
reminderVisible &&
|
|
1744
|
+
reminderAvailable &&
|
|
1745
|
+
waterPrompt &&
|
|
1746
|
+
reminderGate.open &&
|
|
1747
|
+
reminderGate.eventId === waterState.eventId;
|
|
1748
|
+
function openWaterPopup(event) {
|
|
1749
|
+
if (!waterPrompt || waterBusy) return;
|
|
1750
|
+
if (!reminderAvailable) {
|
|
1751
|
+
navigateScreen('home', event);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const trigger = event.currentTarget;
|
|
1755
|
+
setReminderGate({ eventId: waterState.eventId, armed: false, open: true });
|
|
1756
|
+
requestAnimationFrame(() => {
|
|
1757
|
+
const active = document.activeElement;
|
|
1758
|
+
if (active !== trigger && active?.tagName !== 'BODY') return;
|
|
1759
|
+
if (petPanel.current?.dataset.petScreen === 'reminder')
|
|
1760
|
+
petPanel.current.querySelector('.owc-water-actions button')?.focus();
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
function dismissWaterPopup() {
|
|
1764
|
+
const hadFocus = petPanel.current?.contains(document.activeElement);
|
|
1765
|
+
setReminderGate(dismissPetReminder);
|
|
1766
|
+
if (hadFocus)
|
|
1767
|
+
requestAnimationFrame(() => box.current?.querySelector('.owc-pet-handle')?.focus());
|
|
1768
|
+
}
|
|
1460
1769
|
const reactionSurface = [
|
|
1461
1770
|
collapsed,
|
|
1462
1771
|
editing,
|
|
@@ -1478,8 +1787,8 @@ window.__ModuleLoader__.load({
|
|
|
1478
1787
|
workStatus: safeState?.status,
|
|
1479
1788
|
phase: waterState.phase,
|
|
1480
1789
|
ownsPrompt: waterPrompt,
|
|
1481
|
-
ready: waterReady
|
|
1482
|
-
failed: waterError
|
|
1790
|
+
ready: waterReady,
|
|
1791
|
+
failed: waterError,
|
|
1483
1792
|
feedback: waterNotice || null,
|
|
1484
1793
|
visible: view.companion,
|
|
1485
1794
|
motion: view.companionMotion && !reactionState.current?.stopped,
|
|
@@ -1510,11 +1819,10 @@ window.__ModuleLoader__.load({
|
|
|
1510
1819
|
waterVisible &&
|
|
1511
1820
|
waterReady &&
|
|
1512
1821
|
!waterError &&
|
|
1513
|
-
!error &&
|
|
1514
1822
|
!editing &&
|
|
1515
1823
|
!waterEditing &&
|
|
1516
1824
|
!ui.menu,
|
|
1517
|
-
resting: ['quiet', '
|
|
1825
|
+
resting: ['quiet', 'offline'].includes(companion.pose),
|
|
1518
1826
|
});
|
|
1519
1827
|
}, [
|
|
1520
1828
|
petMode,
|
|
@@ -1535,6 +1843,60 @@ window.__ModuleLoader__.load({
|
|
|
1535
1843
|
petReactionToken.current = token;
|
|
1536
1844
|
if (petMode && companion.animate) petMotion.current?.play('celebrate');
|
|
1537
1845
|
}, [petMode, waterNotice?.token, companion.animate]);
|
|
1846
|
+
React.useEffect(() => {
|
|
1847
|
+
const media = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
1848
|
+
const update = () => setPreviewReduced(!!media?.matches);
|
|
1849
|
+
media?.addEventListener?.('change', update);
|
|
1850
|
+
return () => media?.removeEventListener?.('change', update);
|
|
1851
|
+
}, []);
|
|
1852
|
+
React.useLayoutEffect(() => {
|
|
1853
|
+
if (!appearanceOnly || ui.pending) return;
|
|
1854
|
+
setPreviewState({ phase: 'idle', action: null });
|
|
1855
|
+
if (!view.companion || petModel(view.petModel).renderer !== 'video') return;
|
|
1856
|
+
const artwork = previewBox.current?.querySelector('.owc-pet-art');
|
|
1857
|
+
if (!artwork) return;
|
|
1858
|
+
const base = createPetMotion(artwork, window, { enabled: false, ambient: false });
|
|
1859
|
+
const controller = createPetVideoMotion(
|
|
1860
|
+
artwork,
|
|
1861
|
+
window,
|
|
1862
|
+
base,
|
|
1863
|
+
PET_CLIPS.models[view.petModel],
|
|
1864
|
+
PET_CLIPS.probe,
|
|
1865
|
+
{
|
|
1866
|
+
autoIdle: false,
|
|
1867
|
+
fallback: false,
|
|
1868
|
+
interruptible: true,
|
|
1869
|
+
onStateChange: setPreviewState,
|
|
1870
|
+
},
|
|
1871
|
+
);
|
|
1872
|
+
previewMotion.current = controller;
|
|
1873
|
+
return () => {
|
|
1874
|
+
controller.destroy();
|
|
1875
|
+
previewMotion.current = null;
|
|
1876
|
+
};
|
|
1877
|
+
}, [appearanceOnly, view.companion, view.petModel, !!ui.pending]);
|
|
1878
|
+
React.useLayoutEffect(() => {
|
|
1879
|
+
previewMotion.current?.configure({
|
|
1880
|
+
enabled:
|
|
1881
|
+
appearanceOnly &&
|
|
1882
|
+
view.companion &&
|
|
1883
|
+
view.companionMotion &&
|
|
1884
|
+
waterVisible &&
|
|
1885
|
+
!previewReduced &&
|
|
1886
|
+
!ui.menu &&
|
|
1887
|
+
!ui.pending,
|
|
1888
|
+
resting: false,
|
|
1889
|
+
});
|
|
1890
|
+
}, [
|
|
1891
|
+
appearanceOnly,
|
|
1892
|
+
view.companion,
|
|
1893
|
+
view.petModel,
|
|
1894
|
+
view.companionMotion,
|
|
1895
|
+
waterVisible,
|
|
1896
|
+
previewReduced,
|
|
1897
|
+
!!ui.menu,
|
|
1898
|
+
!!ui.pending,
|
|
1899
|
+
]);
|
|
1538
1900
|
const companionCard = (compact = false) =>
|
|
1539
1901
|
companion.visible &&
|
|
1540
1902
|
h(
|
|
@@ -1565,20 +1927,13 @@ window.__ModuleLoader__.load({
|
|
|
1565
1927
|
const waterSummary = () => {
|
|
1566
1928
|
if (!waterReady) return t('waterLoading');
|
|
1567
1929
|
if (waterError) return t('waterUnavailable');
|
|
1568
|
-
if (
|
|
1569
|
-
water.enabled &&
|
|
1570
|
-
waterContext &&
|
|
1571
|
-
water.shift &&
|
|
1572
|
-
water.shift.config !== waterContext.config
|
|
1573
|
-
)
|
|
1574
|
-
return t('waterStale');
|
|
1575
1930
|
const labels = {
|
|
1576
1931
|
off: 'waterOff',
|
|
1577
1932
|
waiting: 'waterWaiting',
|
|
1578
1933
|
break: 'waterBreak',
|
|
1579
1934
|
paused: 'waterPaused',
|
|
1580
1935
|
finished: 'waterFinished',
|
|
1581
|
-
due:
|
|
1936
|
+
due: 'waterDue',
|
|
1582
1937
|
};
|
|
1583
1938
|
if (waterState.phase === 'quiet')
|
|
1584
1939
|
return t('waterQuiet') + ' · ' + formatDuration(waterState.quietRemaining / 1000);
|
|
@@ -1586,19 +1941,274 @@ window.__ModuleLoader__.load({
|
|
|
1586
1941
|
? t('waterNext') + ' · ' + formatDuration(waterState.remaining / 1000)
|
|
1587
1942
|
: t(labels[waterState.phase]);
|
|
1588
1943
|
};
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
!
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
(
|
|
1595
|
-
|
|
1944
|
+
const waterCountdownText = () => {
|
|
1945
|
+
if (waterError) return t('timerUnavailable');
|
|
1946
|
+
if (!waterReady) return t('timerLoading');
|
|
1947
|
+
if (waterState.phase === 'due' && features.waterSnoozedUntil > now)
|
|
1948
|
+
return formatDuration(Math.ceil((features.waterSnoozedUntil - now) / 1000));
|
|
1949
|
+
if (['countdown', 'due', 'break'].includes(waterState.phase))
|
|
1950
|
+
return formatDuration(Math.ceil(waterState.remaining / 1000));
|
|
1951
|
+
if (waterState.phase === 'quiet')
|
|
1952
|
+
return formatDuration(Math.ceil(waterState.quietRemaining / 1000));
|
|
1953
|
+
return t(
|
|
1954
|
+
{
|
|
1955
|
+
off: 'timerOff',
|
|
1956
|
+
waiting: 'timerWaiting',
|
|
1957
|
+
paused: 'timerPaused',
|
|
1958
|
+
finished: 'timerFinished',
|
|
1959
|
+
}[waterState.phase] || 'timerUnavailable',
|
|
1960
|
+
);
|
|
1961
|
+
};
|
|
1962
|
+
const smokingCountdownText = () =>
|
|
1963
|
+
smokingUnavailable
|
|
1964
|
+
? t(smokingError ? 'timerUnavailable' : 'timerLoading')
|
|
1965
|
+
: smokingState.phase === 'idle'
|
|
1966
|
+
? t('timerIdle')
|
|
1967
|
+
: smokingState.phase === 'due' && features.smokingSnoozedUntil > now
|
|
1968
|
+
? formatDuration(Math.ceil((features.smokingSnoozedUntil - now) / 1000))
|
|
1969
|
+
: formatDuration(Math.ceil(smokingState.remaining / 1000));
|
|
1970
|
+
const timerRow = (kind, value, due, title) => {
|
|
1971
|
+
let phase = kind === 'water' ? waterState.phase : smokingState.phase;
|
|
1972
|
+
if (
|
|
1973
|
+
phase === 'due' &&
|
|
1974
|
+
(kind === 'water' ? features.waterSnoozedUntil : features.smokingSnoozedUntil) > now
|
|
1975
|
+
)
|
|
1976
|
+
phase = 'snoozed';
|
|
1977
|
+
const extra = {
|
|
1978
|
+
snoozed: 'timerSnoozed',
|
|
1979
|
+
paused: 'timerPaused',
|
|
1980
|
+
break: 'timerBreak',
|
|
1981
|
+
quiet: 'timerQuiet',
|
|
1982
|
+
due: 'timerDue',
|
|
1983
|
+
}[phase];
|
|
1984
|
+
return h(
|
|
1985
|
+
'button',
|
|
1986
|
+
{
|
|
1987
|
+
type: 'button',
|
|
1988
|
+
onClick: (event) => navigateScreen(kind === 'water' ? 'home' : 'smoking', event),
|
|
1989
|
+
className: 'owc-pet-timer-row',
|
|
1990
|
+
'data-countdown': kind,
|
|
1991
|
+
'data-countdown-due': due && phase !== 'snoozed',
|
|
1992
|
+
'data-countdown-phase': phase,
|
|
1993
|
+
'aria-label': t(kind === 'water' ? 'waterCountdown' : 'smokingCountdown'),
|
|
1994
|
+
title: phase === 'snoozed' ? t('timerSnoozed') + ' · ' + value : title,
|
|
1995
|
+
},
|
|
1996
|
+
h(
|
|
1997
|
+
'span',
|
|
1998
|
+
{ className: 'owc-pet-timer-label' },
|
|
1999
|
+
iconSvg(kind, 14),
|
|
2000
|
+
extra && h('span', { className: 'owc-sr-only' }, t(extra)),
|
|
2001
|
+
),
|
|
2002
|
+
h(
|
|
2003
|
+
'strong',
|
|
2004
|
+
null,
|
|
2005
|
+
['paused', 'break'].includes(phase) &&
|
|
2006
|
+
/^\d/.test(value) &&
|
|
2007
|
+
h('span', { className: 'owc-timer-pause', 'aria-hidden': true }, 'Ⅱ'),
|
|
2008
|
+
phase === 'quiet' &&
|
|
2009
|
+
h('span', { className: 'owc-timer-pause', 'aria-hidden': true }, '☾'),
|
|
2010
|
+
value.startsWith('00:') ? value.slice(3) : value.replace(/^0(?=\d:)/, ''),
|
|
2011
|
+
),
|
|
2012
|
+
);
|
|
2013
|
+
};
|
|
2014
|
+
const waterHero = () => {
|
|
2015
|
+
const ticking =
|
|
2016
|
+
waterReady && !waterError && ['countdown', 'due', 'quiet'].includes(waterState.phase);
|
|
2017
|
+
return h(
|
|
2018
|
+
'div',
|
|
2019
|
+
{ className: 'owc-detail-hero' },
|
|
2020
|
+
h(
|
|
2021
|
+
'p',
|
|
2022
|
+
{ className: 'owc-detail-kicker', 'data-water-status': true },
|
|
2023
|
+
t(
|
|
2024
|
+
ticking
|
|
2025
|
+
? features.waterSnoozedUntil > now
|
|
2026
|
+
? 'timerSnoozed'
|
|
2027
|
+
: waterState.phase === 'quiet'
|
|
2028
|
+
? 'waterQuiet'
|
|
2029
|
+
: waterState.phase === 'due'
|
|
2030
|
+
? 'waterDue'
|
|
2031
|
+
: 'waterNext'
|
|
2032
|
+
: 'waterTitle',
|
|
2033
|
+
),
|
|
2034
|
+
),
|
|
2035
|
+
h(
|
|
2036
|
+
'div',
|
|
2037
|
+
{
|
|
2038
|
+
className: 'owc-detail-value' + (ticking ? '' : ' owc-detail-state'),
|
|
2039
|
+
role: 'timer',
|
|
2040
|
+
'aria-label': t('waterCountdown'),
|
|
2041
|
+
},
|
|
2042
|
+
waterCountdownText(),
|
|
2043
|
+
),
|
|
2044
|
+
h(
|
|
2045
|
+
'div',
|
|
2046
|
+
{ className: 'owc-detail-meta' },
|
|
2047
|
+
h(
|
|
2048
|
+
'span',
|
|
2049
|
+
{ 'data-water-count': true },
|
|
2050
|
+
t('waterCount') + ' · ' + (waterReady && !waterError ? waterState.count : '—'),
|
|
2051
|
+
),
|
|
2052
|
+
h(
|
|
2053
|
+
'span',
|
|
2054
|
+
{ className: 'owc-detail-feedback' },
|
|
2055
|
+
waterNotice ? t(waterNotice.key) : '',
|
|
2056
|
+
),
|
|
2057
|
+
),
|
|
2058
|
+
);
|
|
2059
|
+
};
|
|
2060
|
+
const waterCountdownCard = () =>
|
|
2061
|
+
h(
|
|
2062
|
+
'div',
|
|
2063
|
+
{ className: 'owc-countdown-card', 'data-water-countdown-card': true },
|
|
2064
|
+
waterHero(),
|
|
2065
|
+
h(
|
|
2066
|
+
'div',
|
|
2067
|
+
{ className: 'owc-countdown-actions' },
|
|
2068
|
+
h(
|
|
2069
|
+
'button',
|
|
2070
|
+
{
|
|
2071
|
+
type: 'button',
|
|
2072
|
+
className: 'owc-primary owc-water-record',
|
|
2073
|
+
disabled: !waterCanRecord || waterBusy,
|
|
2074
|
+
onClick: recordWater,
|
|
2075
|
+
},
|
|
2076
|
+
t('petRecord'),
|
|
2077
|
+
),
|
|
2078
|
+
h(
|
|
2079
|
+
'button',
|
|
2080
|
+
{
|
|
2081
|
+
type: 'button',
|
|
2082
|
+
className: 'owc-text',
|
|
2083
|
+
onClick: (event) => navigateScreen('home', event),
|
|
2084
|
+
},
|
|
2085
|
+
t('timerWaterDetails'),
|
|
2086
|
+
),
|
|
2087
|
+
),
|
|
2088
|
+
);
|
|
2089
|
+
const smokingCountdownCard = () =>
|
|
2090
|
+
h(
|
|
2091
|
+
'div',
|
|
2092
|
+
{ className: 'owc-countdown-card', 'data-smoking-countdown-card': true },
|
|
2093
|
+
h(
|
|
2094
|
+
'div',
|
|
2095
|
+
{
|
|
2096
|
+
className: 'owc-countdown-value',
|
|
2097
|
+
role: 'timer',
|
|
2098
|
+
'aria-label': t('smokingCountdown'),
|
|
2099
|
+
},
|
|
2100
|
+
!smokingUnavailable && smokingState.phase === 'idle'
|
|
2101
|
+
? formatDuration(smoking.minutes * 60)
|
|
2102
|
+
: smokingCountdownText(),
|
|
2103
|
+
),
|
|
2104
|
+
h(
|
|
2105
|
+
'p',
|
|
2106
|
+
{ role: 'status' },
|
|
2107
|
+
smokingUnavailable
|
|
2108
|
+
? t(smokingError ? 'timerUnavailable' : 'timerLoading')
|
|
2109
|
+
: smokingState.phase === 'due' && features.smokingSnoozedUntil > now
|
|
2110
|
+
? t('timerSnoozed')
|
|
2111
|
+
: t(
|
|
2112
|
+
{
|
|
2113
|
+
idle: 'timerIdle',
|
|
2114
|
+
running: 'timerRunning',
|
|
2115
|
+
paused: 'timerPaused',
|
|
2116
|
+
due: 'timerDue',
|
|
2117
|
+
}[smokingState.phase],
|
|
2118
|
+
),
|
|
2119
|
+
),
|
|
2120
|
+
h(
|
|
2121
|
+
'label',
|
|
2122
|
+
null,
|
|
2123
|
+
t('timerInterval'),
|
|
2124
|
+
h(
|
|
2125
|
+
'select',
|
|
2126
|
+
{
|
|
2127
|
+
value: smoking.minutes,
|
|
2128
|
+
disabled: smokingUnavailable || smokingBusy,
|
|
2129
|
+
onChange: (event) =>
|
|
2130
|
+
smokingAction({ type: 'interval', minutes: Number(event.target.value) }),
|
|
2131
|
+
},
|
|
2132
|
+
COUNTDOWN_INTERVALS.map((minutes) =>
|
|
2133
|
+
h('option', { key: minutes, value: minutes }, String(minutes)),
|
|
2134
|
+
),
|
|
2135
|
+
),
|
|
2136
|
+
),
|
|
2137
|
+
h(
|
|
2138
|
+
'div',
|
|
2139
|
+
{ className: 'owc-countdown-actions' },
|
|
2140
|
+
h(
|
|
2141
|
+
'button',
|
|
2142
|
+
{
|
|
2143
|
+
type: 'button',
|
|
2144
|
+
className: 'owc-primary',
|
|
2145
|
+
disabled: smokingUnavailable || smokingBusy,
|
|
2146
|
+
onClick: () =>
|
|
2147
|
+
smokingAction({
|
|
2148
|
+
type:
|
|
2149
|
+
smokingState.phase === 'running'
|
|
2150
|
+
? 'pause'
|
|
2151
|
+
: smokingState.phase === 'paused'
|
|
2152
|
+
? 'resume'
|
|
2153
|
+
: 'start',
|
|
2154
|
+
}),
|
|
2155
|
+
},
|
|
2156
|
+
t(
|
|
2157
|
+
smokingState.phase === 'running'
|
|
2158
|
+
? 'timerPause'
|
|
2159
|
+
: smokingState.phase === 'paused'
|
|
2160
|
+
? 'timerResume'
|
|
2161
|
+
: 'timerStart',
|
|
2162
|
+
),
|
|
2163
|
+
),
|
|
2164
|
+
smokingState.phase !== 'idle' &&
|
|
2165
|
+
h(
|
|
2166
|
+
'button',
|
|
2167
|
+
{
|
|
2168
|
+
type: 'button',
|
|
2169
|
+
disabled: smokingUnavailable || smokingBusy,
|
|
2170
|
+
onClick: () => smokingAction({ type: 'restart' }),
|
|
2171
|
+
},
|
|
2172
|
+
t('timerRestart'),
|
|
2173
|
+
),
|
|
2174
|
+
smokingState.phase !== 'idle' &&
|
|
2175
|
+
h(
|
|
2176
|
+
'button',
|
|
2177
|
+
{
|
|
2178
|
+
type: 'button',
|
|
2179
|
+
disabled: smokingUnavailable || smokingBusy,
|
|
2180
|
+
onClick: () => smokingAction({ type: 'stop' }),
|
|
2181
|
+
},
|
|
2182
|
+
t('timerStop'),
|
|
2183
|
+
),
|
|
2184
|
+
),
|
|
2185
|
+
smokingState.phase !== 'idle' &&
|
|
2186
|
+
h('p', { className: 'owc-hint' }, t('detailNextInterval')),
|
|
2187
|
+
h(
|
|
2188
|
+
'details',
|
|
2189
|
+
{ className: 'owc-detail-more' },
|
|
2190
|
+
h('summary', null, t('detailHelp')),
|
|
2191
|
+
h('p', null, t('timerHint')),
|
|
2192
|
+
),
|
|
2193
|
+
smokingError && h('p', { className: 'owc-error', role: 'alert' }, t(smokingError)),
|
|
2194
|
+
smokingError === 'timerStorageError' &&
|
|
2195
|
+
h(
|
|
2196
|
+
'button',
|
|
2197
|
+
{
|
|
2198
|
+
type: 'button',
|
|
2199
|
+
disabled: smokingBusy,
|
|
2200
|
+
onClick: () => smokingAction({ type: 'retry' }),
|
|
2201
|
+
},
|
|
2202
|
+
t('timerRetry'),
|
|
2203
|
+
),
|
|
2204
|
+
);
|
|
2205
|
+
const waterCanRecord = waterReady && !waterError;
|
|
2206
|
+
const waterCanQuiet = waterCanRecord && water.enabled && !water.paused;
|
|
1596
2207
|
const recordWater = (event) => {
|
|
1597
2208
|
if (event.detail > 1) return;
|
|
1598
2209
|
if (!manualAttempt.current)
|
|
1599
2210
|
manualAttempt.current = {
|
|
1600
2211
|
type: 'manual-drink',
|
|
1601
|
-
shiftId: waterContext.id,
|
|
1602
2212
|
actionId: window.crypto?.randomUUID?.() || String(Math.random()) + Date.now(),
|
|
1603
2213
|
...(waterState.phase === 'due' ? { eventId: waterState.eventId } : {}),
|
|
1604
2214
|
};
|
|
@@ -1667,14 +2277,10 @@ window.__ModuleLoader__.load({
|
|
|
1667
2277
|
'button',
|
|
1668
2278
|
{
|
|
1669
2279
|
type: 'button',
|
|
1670
|
-
disabled:
|
|
1671
|
-
|
|
1672
|
-
waterBusy ||
|
|
1673
|
-
!waterContext ||
|
|
1674
|
-
water.shift?.id !== waterContext.id,
|
|
1675
|
-
onClick: () => waterAction({ type: water.shift?.paused ? 'resume' : 'pause' }),
|
|
2280
|
+
disabled: waterError || waterBusy || !waterReady,
|
|
2281
|
+
onClick: () => waterAction({ type: water.paused ? 'resume' : 'pause' }),
|
|
1676
2282
|
},
|
|
1677
|
-
t(water.
|
|
2283
|
+
t(water.paused ? 'waterResume' : 'waterPause'),
|
|
1678
2284
|
),
|
|
1679
2285
|
waterError &&
|
|
1680
2286
|
h(
|
|
@@ -1724,7 +2330,7 @@ window.__ModuleLoader__.load({
|
|
|
1724
2330
|
className: 'owc-text',
|
|
1725
2331
|
style: { marginLeft: 'auto' },
|
|
1726
2332
|
'aria-label': t('waterEdit'),
|
|
1727
|
-
disabled: !waterReady
|
|
2333
|
+
disabled: !waterReady,
|
|
1728
2334
|
onClick: openWaterEditor,
|
|
1729
2335
|
},
|
|
1730
2336
|
t('settings'),
|
|
@@ -1757,16 +2363,59 @@ window.__ModuleLoader__.load({
|
|
|
1757
2363
|
{ 'data-water-effective': water.interval },
|
|
1758
2364
|
t('waterEvery') + ' ' + water.interval + ' ' + t('waterMinutes'),
|
|
1759
2365
|
),
|
|
1760
|
-
h(
|
|
2366
|
+
h(
|
|
2367
|
+
'span',
|
|
2368
|
+
{ 'data-water-count': true },
|
|
2369
|
+
t('waterCount') + ': ' + (waterReady && !waterError ? waterState.count : '—'),
|
|
2370
|
+
),
|
|
1761
2371
|
),
|
|
1762
2372
|
waterControls(),
|
|
1763
|
-
|
|
1764
|
-
h('p', { className: 'owc-hint' }, t('waterRecordHint')),
|
|
2373
|
+
h('p', { className: 'owc-hint' }, t('waterRecordHint')),
|
|
1765
2374
|
waterState.phase === 'quiet' &&
|
|
1766
2375
|
h('p', { className: 'owc-hint' }, t('waterQuietHint')),
|
|
1767
2376
|
waterNotice && !view.companion && h('p', null, t(waterNotice.key)),
|
|
1768
2377
|
);
|
|
1769
2378
|
|
|
2379
|
+
const waterIntervalField = () =>
|
|
2380
|
+
h(
|
|
2381
|
+
'label',
|
|
2382
|
+
{ className: 'owc-water-interval' },
|
|
2383
|
+
h('span', null, t('waterInterval')),
|
|
2384
|
+
h('input', {
|
|
2385
|
+
name: 'waterInterval',
|
|
2386
|
+
type: 'number',
|
|
2387
|
+
inputMode: 'numeric',
|
|
2388
|
+
min: '5',
|
|
2389
|
+
max: '240',
|
|
2390
|
+
step: '1',
|
|
2391
|
+
value: waterInterval,
|
|
2392
|
+
'aria-invalid': waterInvalid,
|
|
2393
|
+
onChange: (event) => {
|
|
2394
|
+
setWaterInterval(event.target.value);
|
|
2395
|
+
setWaterInvalid(false);
|
|
2396
|
+
},
|
|
2397
|
+
}),
|
|
2398
|
+
);
|
|
2399
|
+
const waterSettingsActions = () =>
|
|
2400
|
+
h(
|
|
2401
|
+
'div',
|
|
2402
|
+
{ className: 'owc-save' },
|
|
2403
|
+
h(
|
|
2404
|
+
'button',
|
|
2405
|
+
{ type: 'button', disabled: waterBusy, onClick: closeWaterEditor },
|
|
2406
|
+
t('cancel'),
|
|
2407
|
+
),
|
|
2408
|
+
h(
|
|
2409
|
+
'button',
|
|
2410
|
+
{
|
|
2411
|
+
type: 'submit',
|
|
2412
|
+
className: 'owc-primary',
|
|
2413
|
+
'aria-label': t('waterSave'),
|
|
2414
|
+
disabled: !waterReady || waterError || waterBusy,
|
|
2415
|
+
},
|
|
2416
|
+
t(waterBusy ? 'busy' : 'save'),
|
|
2417
|
+
),
|
|
2418
|
+
);
|
|
1770
2419
|
const waterSettings = () =>
|
|
1771
2420
|
h(
|
|
1772
2421
|
'form',
|
|
@@ -1789,7 +2438,7 @@ window.__ModuleLoader__.load({
|
|
|
1789
2438
|
h('input', {
|
|
1790
2439
|
type: 'checkbox',
|
|
1791
2440
|
checked: waterEnabledDraft,
|
|
1792
|
-
disabled: !waterReady || waterError || waterBusy
|
|
2441
|
+
disabled: !waterReady || waterError || waterBusy,
|
|
1793
2442
|
onChange: (event) => {
|
|
1794
2443
|
setWaterEnabledDraft(event.target.checked);
|
|
1795
2444
|
setWaterNotice(false);
|
|
@@ -1798,8 +2447,9 @@ window.__ModuleLoader__.load({
|
|
|
1798
2447
|
),
|
|
1799
2448
|
h(
|
|
1800
2449
|
'fieldset',
|
|
1801
|
-
{ disabled: !waterReady || waterError || waterBusy
|
|
1802
|
-
h('legend', { className: 'owc-
|
|
2450
|
+
{ disabled: !waterReady || waterError || waterBusy },
|
|
2451
|
+
h('legend', { className: 'owc-sr-only' }, t('waterPresets')),
|
|
2452
|
+
waterIntervalField(),
|
|
1803
2453
|
h(
|
|
1804
2454
|
'div',
|
|
1805
2455
|
{ className: 'owc-water-presets' },
|
|
@@ -1810,63 +2460,33 @@ window.__ModuleLoader__.load({
|
|
|
1810
2460
|
type: 'button',
|
|
1811
2461
|
key: minutes,
|
|
1812
2462
|
'aria-pressed': Number(waterInterval) === minutes,
|
|
2463
|
+
'aria-label': minutes + ' ' + t('waterMinutes'),
|
|
1813
2464
|
onClick: () => {
|
|
1814
2465
|
setWaterInterval(String(minutes));
|
|
1815
2466
|
setWaterInvalid(false);
|
|
1816
2467
|
},
|
|
1817
2468
|
},
|
|
1818
|
-
minutes
|
|
2469
|
+
String(minutes),
|
|
1819
2470
|
),
|
|
1820
2471
|
),
|
|
1821
2472
|
),
|
|
1822
|
-
h(
|
|
1823
|
-
'label',
|
|
1824
|
-
{ className: 'owc-field' },
|
|
1825
|
-
h('span', null, t('waterInterval')),
|
|
1826
|
-
h('input', {
|
|
1827
|
-
name: 'waterInterval',
|
|
1828
|
-
type: 'number',
|
|
1829
|
-
inputMode: 'numeric',
|
|
1830
|
-
min: '5',
|
|
1831
|
-
max: '240',
|
|
1832
|
-
step: '1',
|
|
1833
|
-
value: waterInterval,
|
|
1834
|
-
'aria-invalid': waterInvalid,
|
|
1835
|
-
onChange: (event) => {
|
|
1836
|
-
setWaterInterval(event.target.value);
|
|
1837
|
-
setWaterInvalid(false);
|
|
1838
|
-
},
|
|
1839
|
-
}),
|
|
1840
|
-
),
|
|
1841
2473
|
),
|
|
1842
2474
|
waterInvalid && h('p', { role: 'alert' }, t('waterInvalid')),
|
|
1843
|
-
|
|
1844
|
-
'p',
|
|
1845
|
-
{ 'data-water-saved-interval': water.interval },
|
|
1846
|
-
t('waterEffective') + ': ' + water.interval + ' ' + t('waterMinutes'),
|
|
1847
|
-
),
|
|
1848
|
-
h('p', { className: 'owc-hint' }, t('waterSaveHint')),
|
|
1849
|
-
h('p', { className: 'owc-hint' }, t('waterBrief')),
|
|
1850
|
-
waterError && h('p', { role: 'alert' }, t('waterUnavailable')),
|
|
1851
|
-
!data?.configured && h('p', null, t('waterSetup')),
|
|
1852
|
-
h(
|
|
1853
|
-
'div',
|
|
1854
|
-
{ className: 'owc-save' },
|
|
2475
|
+
(Number(waterInterval) !== water.interval || waterEnabledDraft !== water.enabled) &&
|
|
1855
2476
|
h(
|
|
1856
|
-
'
|
|
1857
|
-
{
|
|
1858
|
-
t('
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
disabled: !waterReady || waterError || waterBusy || !data?.configured,
|
|
1866
|
-
},
|
|
1867
|
-
t(waterBusy ? 'busy' : 'waterSave'),
|
|
2477
|
+
'p',
|
|
2478
|
+
{ className: 'owc-hint', 'data-water-saved-interval': water.interval },
|
|
2479
|
+
t('detailSavedInterval') +
|
|
2480
|
+
': ' +
|
|
2481
|
+
water.interval +
|
|
2482
|
+
' ' +
|
|
2483
|
+
t('waterMinutes') +
|
|
2484
|
+
' · ' +
|
|
2485
|
+
t('detailDraftHint'),
|
|
1868
2486
|
),
|
|
1869
|
-
),
|
|
2487
|
+
h('p', { className: 'owc-hint' }, t('detailWaterRule')),
|
|
2488
|
+
waterError && h('p', { role: 'alert' }, t('waterUnavailable')),
|
|
2489
|
+
|
|
1870
2490
|
waterError &&
|
|
1871
2491
|
h(
|
|
1872
2492
|
'button',
|
|
@@ -1929,6 +2549,7 @@ window.__ModuleLoader__.load({
|
|
|
1929
2549
|
),
|
|
1930
2550
|
),
|
|
1931
2551
|
),
|
|
2552
|
+
waterSettingsActions(),
|
|
1932
2553
|
);
|
|
1933
2554
|
const waterReminder = () =>
|
|
1934
2555
|
waterPrompt &&
|
|
@@ -1943,6 +2564,7 @@ window.__ModuleLoader__.load({
|
|
|
1943
2564
|
'button',
|
|
1944
2565
|
{
|
|
1945
2566
|
type: 'button',
|
|
2567
|
+
className: 'owc-primary',
|
|
1946
2568
|
disabled: waterBusy,
|
|
1947
2569
|
onClick: () => waterAction({ type: 'drink', eventId: waterState.eventId }),
|
|
1948
2570
|
},
|
|
@@ -1960,6 +2582,453 @@ window.__ModuleLoader__.load({
|
|
|
1960
2582
|
),
|
|
1961
2583
|
);
|
|
1962
2584
|
|
|
2585
|
+
const classicMini = () => {
|
|
2586
|
+
const countdown = classicClock.seconds !== null;
|
|
2587
|
+
const caption =
|
|
2588
|
+
{
|
|
2589
|
+
working: 'classicMiniWorking',
|
|
2590
|
+
before: 'classicMiniBefore',
|
|
2591
|
+
lunch: 'classicMiniLunch',
|
|
2592
|
+
}[classicClock.phase] || 'classicMiniStatus';
|
|
2593
|
+
const stateTitle =
|
|
2594
|
+
{ setup: 'petNeedsSetup', error: 'petLoadFailed', waiting: 'petNeedsSession' }[
|
|
2595
|
+
classicClock.phase
|
|
2596
|
+
] || classicClock.title;
|
|
2597
|
+
const valid = !!data?.configured && !!safeState && !error;
|
|
2598
|
+
const showIncome = valid && ['working', 'lunch', 'done'].includes(safeState.status);
|
|
2599
|
+
const compact = (value) => {
|
|
2600
|
+
if (value === t('timerUnavailable')) return '—';
|
|
2601
|
+
if (value === t('timerWaiting')) return t('classicMiniWaiting');
|
|
2602
|
+
if (value === t('timerFinished')) return t('classicMiniFinished');
|
|
2603
|
+
return value.startsWith('00:') ? value.slice(3) : value.replace(/^0(?=\d:)/, '');
|
|
2604
|
+
};
|
|
2605
|
+
const tile = (kind, screen, label, value, summary, due) =>
|
|
2606
|
+
h(
|
|
2607
|
+
'button',
|
|
2608
|
+
{
|
|
2609
|
+
type: 'button',
|
|
2610
|
+
className: 'owc-classic-mini-timer',
|
|
2611
|
+
'data-classic-timer': kind,
|
|
2612
|
+
'data-countdown-due': due,
|
|
2613
|
+
'aria-label': t(label),
|
|
2614
|
+
title: t(label) + ' · ' + summary,
|
|
2615
|
+
disabled: busy || waterBusy,
|
|
2616
|
+
onClick: (event) => navigateScreen(screen, event),
|
|
2617
|
+
},
|
|
2618
|
+
iconSvg(kind, 13),
|
|
2619
|
+
h('strong', null, compact(value)),
|
|
2620
|
+
);
|
|
2621
|
+
return h(
|
|
2622
|
+
'div',
|
|
2623
|
+
{
|
|
2624
|
+
className: 'owc-classic-mini',
|
|
2625
|
+
'data-classic-overview': true,
|
|
2626
|
+
'data-clock-phase': classicClock.phase,
|
|
2627
|
+
},
|
|
2628
|
+
h(
|
|
2629
|
+
'div',
|
|
2630
|
+
{ className: 'owc-classic-mini-top' },
|
|
2631
|
+
h(
|
|
2632
|
+
'div',
|
|
2633
|
+
{
|
|
2634
|
+
className: 'owc-grip',
|
|
2635
|
+
tabIndex: 0,
|
|
2636
|
+
title: t(classicClock.hint) + ' · ' + t('drag'),
|
|
2637
|
+
onKeyDown: gripKeys,
|
|
2638
|
+
},
|
|
2639
|
+
h('span', { className: 'owc-classic-mini-label' }, t(caption)),
|
|
2640
|
+
countdown
|
|
2641
|
+
? h(
|
|
2642
|
+
'button',
|
|
2643
|
+
{
|
|
2644
|
+
type: 'button',
|
|
2645
|
+
onClick: (event) => navigateScreen('income', event),
|
|
2646
|
+
className: 'owc-classic-mini-time',
|
|
2647
|
+
'aria-label': t('petCountdownCard'),
|
|
2648
|
+
},
|
|
2649
|
+
formatDuration(classicClock.seconds),
|
|
2650
|
+
)
|
|
2651
|
+
: ['setup', 'error'].includes(classicClock.phase)
|
|
2652
|
+
? h(
|
|
2653
|
+
'button',
|
|
2654
|
+
{
|
|
2655
|
+
type: 'button',
|
|
2656
|
+
className: 'owc-classic-mini-state',
|
|
2657
|
+
disabled: hostBusy,
|
|
2658
|
+
'aria-label': t(
|
|
2659
|
+
classicClock.phase === 'setup' ? 'classicConfigure' : 'retry',
|
|
2660
|
+
),
|
|
2661
|
+
onClick:
|
|
2662
|
+
classicClock.phase === 'setup' ? openEditor : () => reload(true),
|
|
2663
|
+
},
|
|
2664
|
+
t(stateTitle),
|
|
2665
|
+
)
|
|
2666
|
+
: h(
|
|
2667
|
+
'strong',
|
|
2668
|
+
{ className: 'owc-classic-mini-state', role: 'status' },
|
|
2669
|
+
t(stateTitle),
|
|
2670
|
+
),
|
|
2671
|
+
),
|
|
2672
|
+
h(
|
|
2673
|
+
'button',
|
|
2674
|
+
{
|
|
2675
|
+
type: 'button',
|
|
2676
|
+
className: 'owc-icon',
|
|
2677
|
+
title: t('expand'),
|
|
2678
|
+
'aria-label': t('expand'),
|
|
2679
|
+
onClick: () => setPreference({ mini: false }),
|
|
2680
|
+
},
|
|
2681
|
+
iconSvg('chevronDown', 14),
|
|
2682
|
+
),
|
|
2683
|
+
),
|
|
2684
|
+
h(
|
|
2685
|
+
'div',
|
|
2686
|
+
{ className: 'owc-classic-mini-timers' },
|
|
2687
|
+
tile(
|
|
2688
|
+
'water',
|
|
2689
|
+
'water-countdown',
|
|
2690
|
+
'waterCountdown',
|
|
2691
|
+
waterCountdownText(),
|
|
2692
|
+
waterSummary(),
|
|
2693
|
+
waterState.phase === 'due',
|
|
2694
|
+
),
|
|
2695
|
+
tile(
|
|
2696
|
+
'smoking',
|
|
2697
|
+
'smoking',
|
|
2698
|
+
'smokingCountdown',
|
|
2699
|
+
smokingCountdownText(),
|
|
2700
|
+
t(
|
|
2701
|
+
smokingState.phase === 'paused'
|
|
2702
|
+
? 'timerPaused'
|
|
2703
|
+
: smokingState.phase === 'due'
|
|
2704
|
+
? 'timerDue'
|
|
2705
|
+
: smokingState.phase === 'idle'
|
|
2706
|
+
? 'timerIdle'
|
|
2707
|
+
: 'timerRunning',
|
|
2708
|
+
),
|
|
2709
|
+
smokingState.phase === 'due',
|
|
2710
|
+
),
|
|
2711
|
+
h(
|
|
2712
|
+
'div',
|
|
2713
|
+
{ className: 'owc-classic-mini-income' },
|
|
2714
|
+
showIncome &&
|
|
2715
|
+
!view.masked &&
|
|
2716
|
+
h(
|
|
2717
|
+
'button',
|
|
2718
|
+
{
|
|
2719
|
+
type: 'button',
|
|
2720
|
+
className: 'owc-classic-mini-amount',
|
|
2721
|
+
'aria-label': t('classicIncomeEntry'),
|
|
2722
|
+
'aria-description': moneyText,
|
|
2723
|
+
title: moneyText,
|
|
2724
|
+
onClick: (event) => navigateScreen('earnings', event),
|
|
2725
|
+
},
|
|
2726
|
+
moneyText,
|
|
2727
|
+
),
|
|
2728
|
+
h(
|
|
2729
|
+
'button',
|
|
2730
|
+
{
|
|
2731
|
+
type: 'button',
|
|
2732
|
+
className: 'owc-icon',
|
|
2733
|
+
disabled: !valid,
|
|
2734
|
+
'aria-label': t(
|
|
2735
|
+
showIncome ? (view.masked ? 'show' : 'hide') : 'classicIncomeEntry',
|
|
2736
|
+
),
|
|
2737
|
+
title: t(showIncome ? (view.masked ? 'show' : 'hide') : 'classicIncomeEntry'),
|
|
2738
|
+
'aria-pressed': showIncome ? !view.masked : undefined,
|
|
2739
|
+
onClick: (event) =>
|
|
2740
|
+
showIncome
|
|
2741
|
+
? setPreference({ masked: !view.masked })
|
|
2742
|
+
: navigateScreen('earnings', event),
|
|
2743
|
+
},
|
|
2744
|
+
iconSvg(view.masked || !showIncome ? 'eyeOff' : 'eye', 13),
|
|
2745
|
+
),
|
|
2746
|
+
),
|
|
2747
|
+
),
|
|
2748
|
+
);
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2751
|
+
const classicHeader = () =>
|
|
2752
|
+
h(
|
|
2753
|
+
'div',
|
|
2754
|
+
{ className: 'owc-head owc-classic-head' },
|
|
2755
|
+
ui.screen &&
|
|
2756
|
+
h(
|
|
2757
|
+
'button',
|
|
2758
|
+
{
|
|
2759
|
+
type: 'button',
|
|
2760
|
+
className: 'owc-icon',
|
|
2761
|
+
'aria-label': t(
|
|
2762
|
+
PET_EDITOR_SCREENS.includes(ui.screen) && ui.back
|
|
2763
|
+
? 'petBack'
|
|
2764
|
+
: 'classicReturn',
|
|
2765
|
+
),
|
|
2766
|
+
title: t(
|
|
2767
|
+
PET_EDITOR_SCREENS.includes(ui.screen) && ui.back
|
|
2768
|
+
? 'petBack'
|
|
2769
|
+
: 'classicReturn',
|
|
2770
|
+
),
|
|
2771
|
+
disabled: busy || waterBusy || !!ui.pending,
|
|
2772
|
+
onClick: () =>
|
|
2773
|
+
navigateScreen(PET_EDITOR_SCREENS.includes(ui.screen) ? ui.back : null),
|
|
2774
|
+
},
|
|
2775
|
+
'←',
|
|
2776
|
+
),
|
|
2777
|
+
h(
|
|
2778
|
+
'strong',
|
|
2779
|
+
{ className: 'owc-title', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
|
|
2780
|
+
t(ui.screen ? pageTitle(ui.screen) : 'classicOverview'),
|
|
2781
|
+
),
|
|
2782
|
+
['home', 'water-countdown'].includes(ui.screen) &&
|
|
2783
|
+
h(
|
|
2784
|
+
'button',
|
|
2785
|
+
{
|
|
2786
|
+
type: 'button',
|
|
2787
|
+
className: 'owc-icon',
|
|
2788
|
+
'aria-label': t('petWaterSettings'),
|
|
2789
|
+
title: t('petWaterSettings'),
|
|
2790
|
+
disabled: busy || waterBusy || !!ui.pending,
|
|
2791
|
+
onClick: openWaterEditor,
|
|
2792
|
+
},
|
|
2793
|
+
iconSvg('sliders', 14),
|
|
2794
|
+
),
|
|
2795
|
+
waterPrompt &&
|
|
2796
|
+
ui.screen &&
|
|
2797
|
+
!['home', 'water-countdown'].includes(ui.screen) &&
|
|
2798
|
+
h(
|
|
2799
|
+
'button',
|
|
2800
|
+
{
|
|
2801
|
+
type: 'button',
|
|
2802
|
+
className: 'owc-icon',
|
|
2803
|
+
'aria-label': t('waterDue'),
|
|
2804
|
+
title: t('waterDue'),
|
|
2805
|
+
onClick: (event) => navigateScreen('home', event),
|
|
2806
|
+
},
|
|
2807
|
+
iconSvg('water'),
|
|
2808
|
+
),
|
|
2809
|
+
ui.screen &&
|
|
2810
|
+
h(
|
|
2811
|
+
'button',
|
|
2812
|
+
{
|
|
2813
|
+
type: 'button',
|
|
2814
|
+
className: 'owc-icon',
|
|
2815
|
+
'data-panel-more': true,
|
|
2816
|
+
'aria-label': t('quickMore'),
|
|
2817
|
+
onClick: openQuickMenu,
|
|
2818
|
+
},
|
|
2819
|
+
'⋯',
|
|
2820
|
+
),
|
|
2821
|
+
!ui.screen &&
|
|
2822
|
+
h(
|
|
2823
|
+
React.Fragment,
|
|
2824
|
+
null,
|
|
2825
|
+
h(
|
|
2826
|
+
'button',
|
|
2827
|
+
{ type: 'button', className: 'owc-text', onClick: openAppearance },
|
|
2828
|
+
t('appearanceGroup'),
|
|
2829
|
+
),
|
|
2830
|
+
h(
|
|
2831
|
+
'button',
|
|
2832
|
+
{
|
|
2833
|
+
type: 'button',
|
|
2834
|
+
className: 'owc-icon',
|
|
2835
|
+
'aria-label': t('petWorkSettings'),
|
|
2836
|
+
title: t('petWorkSettings'),
|
|
2837
|
+
disabled: hostBusy || !data,
|
|
2838
|
+
onClick: openEditor,
|
|
2839
|
+
},
|
|
2840
|
+
iconSvg('sliders'),
|
|
2841
|
+
),
|
|
2842
|
+
h(
|
|
2843
|
+
'button',
|
|
2844
|
+
{
|
|
2845
|
+
type: 'button',
|
|
2846
|
+
className: 'owc-icon',
|
|
2847
|
+
'aria-label': t(collapsed ? 'classicMore' : 'classicLess'),
|
|
2848
|
+
title: t(collapsed ? 'classicMore' : 'classicLess'),
|
|
2849
|
+
onClick: () => setPreference({ mini: !view.mini }),
|
|
2850
|
+
},
|
|
2851
|
+
iconSvg(collapsed ? 'chevronDown' : 'chevronUp'),
|
|
2852
|
+
),
|
|
2853
|
+
),
|
|
2854
|
+
);
|
|
2855
|
+
const classicOverview = () => {
|
|
2856
|
+
const countdown = classicClock.seconds !== null;
|
|
2857
|
+
const valid = !!data?.configured && !!safeState && !error;
|
|
2858
|
+
const showIncome = valid && ['working', 'lunch', 'done'].includes(safeState.status);
|
|
2859
|
+
const tile = (kind, screen, label, value, summary) =>
|
|
2860
|
+
h(
|
|
2861
|
+
'button',
|
|
2862
|
+
{
|
|
2863
|
+
type: 'button',
|
|
2864
|
+
className: 'owc-classic-tile',
|
|
2865
|
+
'data-classic-timer': kind,
|
|
2866
|
+
'aria-label': t(label),
|
|
2867
|
+
disabled: busy || waterBusy,
|
|
2868
|
+
onClick: (event) => navigateScreen(screen, event),
|
|
2869
|
+
},
|
|
2870
|
+
h(
|
|
2871
|
+
'span',
|
|
2872
|
+
{ className: 'owc-classic-tile-title' },
|
|
2873
|
+
iconSvg(kind, 14),
|
|
2874
|
+
t(kind === 'water' ? 'waterTimerShort' : 'smokingTimerShort'),
|
|
2875
|
+
),
|
|
2876
|
+
h('strong', null, value),
|
|
2877
|
+
h('span', { className: 'owc-classic-tile-hint' }, summary),
|
|
2878
|
+
);
|
|
2879
|
+
return h(
|
|
2880
|
+
'div',
|
|
2881
|
+
{
|
|
2882
|
+
className: 'owc-classic-overview',
|
|
2883
|
+
'data-classic-overview': true,
|
|
2884
|
+
'data-clock-phase': classicClock.phase,
|
|
2885
|
+
},
|
|
2886
|
+
h(
|
|
2887
|
+
'div',
|
|
2888
|
+
{ className: 'owc-classic-hero' },
|
|
2889
|
+
h(
|
|
2890
|
+
'div',
|
|
2891
|
+
{ className: 'owc-classic-kicker' },
|
|
2892
|
+
iconSvg('alarm', 14),
|
|
2893
|
+
t(countdown ? classicClock.title : 'title'),
|
|
2894
|
+
),
|
|
2895
|
+
countdown
|
|
2896
|
+
? h(
|
|
2897
|
+
'div',
|
|
2898
|
+
{
|
|
2899
|
+
className: 'owc-classic-time',
|
|
2900
|
+
role: 'timer',
|
|
2901
|
+
'aria-label': t(classicClock.title),
|
|
2902
|
+
},
|
|
2903
|
+
formatDuration(classicClock.seconds),
|
|
2904
|
+
)
|
|
2905
|
+
: h(
|
|
2906
|
+
'h3',
|
|
2907
|
+
{ className: 'owc-classic-state', role: 'status' },
|
|
2908
|
+
t(classicClock.title),
|
|
2909
|
+
),
|
|
2910
|
+
h('p', { className: 'owc-classic-hint' }, t(classicClock.hint)),
|
|
2911
|
+
classicClock.phase === 'setup' &&
|
|
2912
|
+
h(
|
|
2913
|
+
'button',
|
|
2914
|
+
{ type: 'button', className: 'owc-primary', onClick: openEditor },
|
|
2915
|
+
t('classicConfigure'),
|
|
2916
|
+
),
|
|
2917
|
+
classicClock.phase === 'error' &&
|
|
2918
|
+
h(
|
|
2919
|
+
'button',
|
|
2920
|
+
{
|
|
2921
|
+
type: 'button',
|
|
2922
|
+
disabled: hostBusy,
|
|
2923
|
+
onClick: () => reload(true),
|
|
2924
|
+
},
|
|
2925
|
+
iconSvg('refresh', 12),
|
|
2926
|
+
t('retry'),
|
|
2927
|
+
),
|
|
2928
|
+
valid &&
|
|
2929
|
+
h(
|
|
2930
|
+
'div',
|
|
2931
|
+
{ className: 'owc-classic-schedule' },
|
|
2932
|
+
h(
|
|
2933
|
+
'span',
|
|
2934
|
+
null,
|
|
2935
|
+
settings.start + ' — ' + settings.end + (safeState.overnight ? ' +1' : ''),
|
|
2936
|
+
),
|
|
2937
|
+
h('span', null, t('classicEnd') + ' ' + settings.end),
|
|
2938
|
+
),
|
|
2939
|
+
),
|
|
2940
|
+
h(
|
|
2941
|
+
'div',
|
|
2942
|
+
{ className: 'owc-classic-timers' },
|
|
2943
|
+
tile(
|
|
2944
|
+
'water',
|
|
2945
|
+
'water-countdown',
|
|
2946
|
+
'waterCountdown',
|
|
2947
|
+
waterCountdownText(),
|
|
2948
|
+
waterState.phase === 'countdown' ? t('classicWaterHint') : waterSummary(),
|
|
2949
|
+
),
|
|
2950
|
+
tile(
|
|
2951
|
+
'smoking',
|
|
2952
|
+
'smoking',
|
|
2953
|
+
'smokingCountdown',
|
|
2954
|
+
smokingCountdownText(),
|
|
2955
|
+
smokingUnavailable
|
|
2956
|
+
? t(smokingError ? 'timerUnavailable' : 'timerLoading')
|
|
2957
|
+
: t(
|
|
2958
|
+
{
|
|
2959
|
+
idle: 'classicSmokeHint',
|
|
2960
|
+
running: 'classicSmokeHint',
|
|
2961
|
+
paused: 'timerPaused',
|
|
2962
|
+
due: 'timerDue',
|
|
2963
|
+
}[smokingState.phase],
|
|
2964
|
+
),
|
|
2965
|
+
),
|
|
2966
|
+
),
|
|
2967
|
+
h(
|
|
2968
|
+
'div',
|
|
2969
|
+
{ className: 'owc-classic-income' },
|
|
2970
|
+
h(
|
|
2971
|
+
'button',
|
|
2972
|
+
{
|
|
2973
|
+
type: 'button',
|
|
2974
|
+
className: 'owc-classic-income-link',
|
|
2975
|
+
disabled: !valid,
|
|
2976
|
+
onClick: (event) => navigateScreen('earnings', event),
|
|
2977
|
+
},
|
|
2978
|
+
h('span', null, t(showIncome ? 'classicIncome' : 'classicIncomeEntry')),
|
|
2979
|
+
showIncome &&
|
|
2980
|
+
h('strong', { 'aria-label': view.masked ? t('masked') : moneyText }, moneyText),
|
|
2981
|
+
h('span', { 'aria-hidden': true }, '›'),
|
|
2982
|
+
),
|
|
2983
|
+
showIncome && salaryToggle(),
|
|
2984
|
+
),
|
|
2985
|
+
!collapsed &&
|
|
2986
|
+
h(
|
|
2987
|
+
'div',
|
|
2988
|
+
{ className: 'owc-classic-details' },
|
|
2989
|
+
valid
|
|
2990
|
+
? h(
|
|
2991
|
+
React.Fragment,
|
|
2992
|
+
null,
|
|
2993
|
+
h(
|
|
2994
|
+
'div',
|
|
2995
|
+
{ className: 'owc-classic-detail-title' },
|
|
2996
|
+
t('classicShift'),
|
|
2997
|
+
h('span', null, Math.floor(safeState.progress * 100) + '%'),
|
|
2998
|
+
),
|
|
2999
|
+
h(
|
|
3000
|
+
'div',
|
|
3001
|
+
{
|
|
3002
|
+
className: 'owc-progress',
|
|
3003
|
+
role: 'progressbar',
|
|
3004
|
+
'aria-label': t('progress'),
|
|
3005
|
+
'aria-valuenow': Math.floor(safeState.progress * 100),
|
|
3006
|
+
'aria-valuemin': 0,
|
|
3007
|
+
'aria-valuemax': 100,
|
|
3008
|
+
},
|
|
3009
|
+
h('div', { style: { width: safeState.progress * 100 + '%' } }),
|
|
3010
|
+
),
|
|
3011
|
+
h(
|
|
3012
|
+
'p',
|
|
3013
|
+
null,
|
|
3014
|
+
t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
|
|
3015
|
+
),
|
|
3016
|
+
h(
|
|
3017
|
+
'p',
|
|
3018
|
+
null,
|
|
3019
|
+
settings.breakStart === settings.breakEnd
|
|
3020
|
+
? t('noBreak')
|
|
3021
|
+
: t('lunch') + ' ' + settings.breakStart + '–' + settings.breakEnd,
|
|
3022
|
+
),
|
|
3023
|
+
h('button', { type: 'button', onClick: openEditor }, t('petWorkSettings')),
|
|
3024
|
+
)
|
|
3025
|
+
: h('p', null, t(classicClock.hint)),
|
|
3026
|
+
),
|
|
3027
|
+
waterNotice &&
|
|
3028
|
+
h('p', { className: 'owc-classic-feedback', role: 'status' }, t(waterNotice.key)),
|
|
3029
|
+
);
|
|
3030
|
+
};
|
|
3031
|
+
|
|
1963
3032
|
const strip = () =>
|
|
1964
3033
|
h(
|
|
1965
3034
|
'div',
|
|
@@ -2186,7 +3255,6 @@ window.__ModuleLoader__.load({
|
|
|
2186
3255
|
h('span', { className: 'owc-dot' }),
|
|
2187
3256
|
t('loading'),
|
|
2188
3257
|
),
|
|
2189
|
-
!sessionId && h('p', { className: 'owc-hint' }, t('noSession')),
|
|
2190
3258
|
h(
|
|
2191
3259
|
'div',
|
|
2192
3260
|
{ className: 'owc-row-actions' },
|
|
@@ -2196,14 +3264,26 @@ window.__ModuleLoader__.load({
|
|
|
2196
3264
|
type: 'button',
|
|
2197
3265
|
className: 'owc-text',
|
|
2198
3266
|
onClick: () => reload(true),
|
|
2199
|
-
disabled:
|
|
3267
|
+
disabled: hostBusy,
|
|
2200
3268
|
},
|
|
2201
3269
|
iconSvg('refresh', 12),
|
|
2202
3270
|
t('retry'),
|
|
2203
3271
|
),
|
|
2204
3272
|
),
|
|
2205
3273
|
);
|
|
2206
|
-
const
|
|
3274
|
+
const clockSeconds = ['done', 'rest'].includes(safeState.status) ? null : seconds;
|
|
3275
|
+
const clockTitle =
|
|
3276
|
+
safeState.status === 'before'
|
|
3277
|
+
? 'before'
|
|
3278
|
+
: ['working', 'lunch'].includes(safeState.status)
|
|
3279
|
+
? 'working'
|
|
3280
|
+
: safeState.status;
|
|
3281
|
+
const status =
|
|
3282
|
+
clockSeconds === null
|
|
3283
|
+
? ''
|
|
3284
|
+
: safeState.status === 'lunch'
|
|
3285
|
+
? t('lunch') + ' · ' + t('working')
|
|
3286
|
+
: t(clockTitle);
|
|
2207
3287
|
return h(
|
|
2208
3288
|
'div',
|
|
2209
3289
|
{ className: 'owc-main' },
|
|
@@ -2213,10 +3293,15 @@ window.__ModuleLoader__.load({
|
|
|
2213
3293
|
section !== 'earnings' &&
|
|
2214
3294
|
h(
|
|
2215
3295
|
'div',
|
|
2216
|
-
{
|
|
2217
|
-
|
|
3296
|
+
{
|
|
3297
|
+
className: 'owc-time' + (clockSeconds === null ? ' owc-detail-state' : ''),
|
|
3298
|
+
role: clockSeconds === null ? 'status' : 'timer',
|
|
3299
|
+
'aria-label': t(clockTitle),
|
|
3300
|
+
},
|
|
3301
|
+
clockSeconds === null ? t(clockTitle) : formatDuration(clockSeconds),
|
|
2218
3302
|
),
|
|
2219
3303
|
section !== 'earnings' &&
|
|
3304
|
+
['working', 'lunch', 'done'].includes(safeState.status) &&
|
|
2220
3305
|
h(
|
|
2221
3306
|
'div',
|
|
2222
3307
|
{
|
|
@@ -2260,57 +3345,30 @@ window.__ModuleLoader__.load({
|
|
|
2260
3345
|
),
|
|
2261
3346
|
),
|
|
2262
3347
|
h(
|
|
2263
|
-
'
|
|
2264
|
-
{ className: 'owc-
|
|
2265
|
-
h(
|
|
2266
|
-
'span',
|
|
2267
|
-
null,
|
|
2268
|
-
settings.start +
|
|
2269
|
-
' — ' +
|
|
2270
|
-
settings.end +
|
|
2271
|
-
(settings.end < settings.start ? ' +1' : '') +
|
|
2272
|
-
' · ' +
|
|
2273
|
-
(settings.breakStart === settings.breakEnd
|
|
2274
|
-
? t('noBreak')
|
|
2275
|
-
: settings.breakStart + '–' + settings.breakEnd),
|
|
2276
|
-
),
|
|
2277
|
-
h(
|
|
2278
|
-
'span',
|
|
2279
|
-
null,
|
|
2280
|
-
t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
|
|
2281
|
-
),
|
|
2282
|
-
),
|
|
2283
|
-
h(
|
|
2284
|
-
'div',
|
|
2285
|
-
{ className: 'owc-row-actions' },
|
|
3348
|
+
'details',
|
|
3349
|
+
{ className: 'owc-detail-more' },
|
|
3350
|
+
h('summary', null, t('detailShift')),
|
|
2286
3351
|
h(
|
|
2287
|
-
'
|
|
2288
|
-
{
|
|
2289
|
-
type: 'button',
|
|
2290
|
-
className: 'owc-text',
|
|
2291
|
-
onClick: () => reload(true),
|
|
2292
|
-
disabled: busy,
|
|
2293
|
-
},
|
|
2294
|
-
iconSvg('refresh', 12),
|
|
2295
|
-
t('refresh'),
|
|
2296
|
-
),
|
|
2297
|
-
petMode &&
|
|
3352
|
+
'div',
|
|
3353
|
+
{ className: 'owc-meta' },
|
|
2298
3354
|
h(
|
|
2299
|
-
'
|
|
2300
|
-
|
|
2301
|
-
|
|
3355
|
+
'span',
|
|
3356
|
+
null,
|
|
3357
|
+
settings.start +
|
|
3358
|
+
' — ' +
|
|
3359
|
+
settings.end +
|
|
3360
|
+
(settings.end < settings.start ? ' +1' : '') +
|
|
3361
|
+
' · ' +
|
|
3362
|
+
(settings.breakStart === settings.breakEnd
|
|
3363
|
+
? t('noBreak')
|
|
3364
|
+
: settings.breakStart + '–' + settings.breakEnd),
|
|
2302
3365
|
),
|
|
2303
|
-
!petMode &&
|
|
2304
3366
|
h(
|
|
2305
|
-
'
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
className: 'owc-text',
|
|
2309
|
-
onClick: () =>
|
|
2310
|
-
setPreference(petMode ? { petPosition: null } : { position: null }),
|
|
2311
|
-
},
|
|
2312
|
-
t('reset'),
|
|
3367
|
+
'span',
|
|
3368
|
+
null,
|
|
3369
|
+
t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
|
|
2313
3370
|
),
|
|
3371
|
+
),
|
|
2314
3372
|
),
|
|
2315
3373
|
);
|
|
2316
3374
|
};
|
|
@@ -2331,100 +3389,201 @@ window.__ModuleLoader__.load({
|
|
|
2331
3389
|
}),
|
|
2332
3390
|
t('petMode'),
|
|
2333
3391
|
),
|
|
2334
|
-
h(
|
|
2335
|
-
'label',
|
|
2336
|
-
{ className: 'owc-check' },
|
|
2337
|
-
h('input', {
|
|
2338
|
-
type: 'checkbox',
|
|
2339
|
-
checked: view.companionMotion,
|
|
2340
|
-
disabled: !view.companion,
|
|
2341
|
-
onChange: (event) => updateAppearance({ companionMotion: event.target.checked }),
|
|
2342
|
-
}),
|
|
2343
|
-
t('companionMotion'),
|
|
2344
|
-
),
|
|
2345
3392
|
label(
|
|
2346
3393
|
t('petCharacter'),
|
|
2347
3394
|
h(
|
|
2348
3395
|
'select',
|
|
2349
3396
|
{
|
|
2350
3397
|
'aria-label': t('petCharacter'),
|
|
2351
|
-
value: view.petModel,
|
|
3398
|
+
value: features.previewImage ? 'custom-image' : view.petModel,
|
|
2352
3399
|
disabled: !view.companion,
|
|
2353
|
-
onChange: (event) =>
|
|
3400
|
+
onChange: (event) =>
|
|
3401
|
+
event.target.value === 'custom-image'
|
|
3402
|
+
? features.selectCustom()
|
|
3403
|
+
: updateAppearance({ petModel: event.target.value }),
|
|
2354
3404
|
},
|
|
3405
|
+
features.hasCustom &&
|
|
3406
|
+
h(
|
|
3407
|
+
'option',
|
|
3408
|
+
{ value: 'custom-image' },
|
|
3409
|
+
locale.active.startsWith('en') ? 'Custom image' : '自定义图片',
|
|
3410
|
+
),
|
|
2355
3411
|
...Object.values(PET_MODELS).map((model) =>
|
|
2356
3412
|
h('option', { key: model.id, value: model.id }, t(model.label)),
|
|
2357
3413
|
),
|
|
2358
3414
|
),
|
|
2359
3415
|
),
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
type: 'range',
|
|
2364
|
-
min: '80',
|
|
2365
|
-
max: '180',
|
|
2366
|
-
step: '4',
|
|
2367
|
-
value: view.petSize,
|
|
2368
|
-
'aria-label': t('petSize'),
|
|
2369
|
-
disabled: !view.companion,
|
|
2370
|
-
onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
|
|
2371
|
-
}),
|
|
2372
|
-
),
|
|
2373
|
-
h('p', { className: 'owc-hint' }, t('petAppearanceHint')),
|
|
2374
|
-
label(
|
|
2375
|
-
t('opacity'),
|
|
3416
|
+
h(
|
|
3417
|
+
'details',
|
|
3418
|
+
{ className: 'owc-appearance-display' },
|
|
2376
3419
|
h(
|
|
2377
|
-
|
|
3420
|
+
'summary',
|
|
2378
3421
|
null,
|
|
3422
|
+
locale.active.startsWith('en') ? 'Display and motion' : '显示与动作',
|
|
3423
|
+
),
|
|
3424
|
+
h(
|
|
3425
|
+
'label',
|
|
3426
|
+
{ className: 'owc-check' },
|
|
2379
3427
|
h('input', {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
max: '100',
|
|
2384
|
-
step: '1',
|
|
2385
|
-
value: String(Math.round(view.opacity * 100)),
|
|
2386
|
-
disabled: busy,
|
|
2387
|
-
'aria-label': t('opacity'),
|
|
3428
|
+
type: 'checkbox',
|
|
3429
|
+
checked: view.companionMotion,
|
|
3430
|
+
disabled: !view.companion,
|
|
2388
3431
|
onChange: (event) =>
|
|
2389
|
-
updateAppearance({
|
|
3432
|
+
updateAppearance({ companionMotion: event.target.checked }),
|
|
2390
3433
|
}),
|
|
3434
|
+
t('companionMotion'),
|
|
3435
|
+
),
|
|
3436
|
+
label(
|
|
3437
|
+
t('petSize'),
|
|
3438
|
+
h('input', {
|
|
3439
|
+
type: 'range',
|
|
3440
|
+
min: '80',
|
|
3441
|
+
max: '180',
|
|
3442
|
+
step: '4',
|
|
3443
|
+
value: view.petSize,
|
|
3444
|
+
'aria-label': t('petSize'),
|
|
3445
|
+
disabled: !view.companion,
|
|
3446
|
+
onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
|
|
3447
|
+
}),
|
|
3448
|
+
),
|
|
3449
|
+
label(
|
|
3450
|
+
t('opacity'),
|
|
2391
3451
|
h(
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
3452
|
+
React.Fragment,
|
|
3453
|
+
null,
|
|
3454
|
+
h('input', {
|
|
3455
|
+
className: 'owc-range',
|
|
3456
|
+
type: 'range',
|
|
3457
|
+
min: '30',
|
|
3458
|
+
max: '100',
|
|
3459
|
+
step: '1',
|
|
3460
|
+
value: String(Math.round(view.opacity * 100)),
|
|
3461
|
+
disabled: busy,
|
|
3462
|
+
'aria-label': t('opacity'),
|
|
3463
|
+
onChange: (event) =>
|
|
3464
|
+
updateAppearance({ opacity: event.target.valueAsNumber / 100 }),
|
|
3465
|
+
}),
|
|
3466
|
+
h(
|
|
3467
|
+
'span',
|
|
3468
|
+
{ className: 'owc-range-value', title: t('opacityHint') },
|
|
3469
|
+
Math.round(view.opacity * 100) + '%',
|
|
3470
|
+
),
|
|
3471
|
+
),
|
|
3472
|
+
),
|
|
3473
|
+
),
|
|
3474
|
+
);
|
|
3475
|
+
const previewActions = () => {
|
|
3476
|
+
if (!view.companion || petModel(view.petModel).renderer !== 'video') return null;
|
|
3477
|
+
const labels = {
|
|
3478
|
+
idle: 'petPreviewIdle',
|
|
3479
|
+
greet: 'petPreviewGreet',
|
|
3480
|
+
celebrate: 'petPreviewDrink',
|
|
3481
|
+
};
|
|
3482
|
+
const names = { idle: 'petTryIdle', greet: 'petTryGreet', celebrate: 'petTryDrink' };
|
|
3483
|
+
const clips = PET_CLIPS.models[view.petModel] || {};
|
|
3484
|
+
const blocked =
|
|
3485
|
+
!view.companionMotion || previewReduced || previewState.phase === 'unsupported';
|
|
3486
|
+
const message = previewReduced
|
|
3487
|
+
? t('petPreviewReduced')
|
|
3488
|
+
: !view.companionMotion
|
|
3489
|
+
? t('petPreviewEnable')
|
|
3490
|
+
: previewState.phase === 'loading'
|
|
3491
|
+
? t('petPreviewLoading')
|
|
3492
|
+
: previewState.phase === 'playing'
|
|
3493
|
+
? t('petPreviewPlaying') + ' · ' + t(labels[previewState.action])
|
|
3494
|
+
: previewState.phase === 'unsupported'
|
|
3495
|
+
? t('petPreviewUnsupported')
|
|
3496
|
+
: previewState.phase === 'unavailable'
|
|
3497
|
+
? t('petPreviewUnavailable')
|
|
3498
|
+
: t('petPreviewHint');
|
|
3499
|
+
return h(
|
|
3500
|
+
'div',
|
|
3501
|
+
{ className: 'owc-preview-tools' },
|
|
3502
|
+
h(
|
|
3503
|
+
'div',
|
|
3504
|
+
{
|
|
3505
|
+
className: 'owc-preview-actions',
|
|
3506
|
+
role: 'group',
|
|
3507
|
+
'aria-label': t('petPreviewActions'),
|
|
3508
|
+
},
|
|
3509
|
+
...Object.keys(labels)
|
|
3510
|
+
.filter((action) => Object.hasOwn(clips, action))
|
|
3511
|
+
.map((action) =>
|
|
3512
|
+
h(
|
|
3513
|
+
'button',
|
|
3514
|
+
{
|
|
3515
|
+
type: 'button',
|
|
3516
|
+
key: action,
|
|
3517
|
+
'aria-label': t(names[action]),
|
|
3518
|
+
'aria-pressed':
|
|
3519
|
+
previewState.action === action &&
|
|
3520
|
+
['loading', 'playing'].includes(previewState.phase),
|
|
3521
|
+
disabled: blocked,
|
|
3522
|
+
onClick: () => previewMotion.current?.retry?.(action),
|
|
3523
|
+
},
|
|
3524
|
+
t(labels[action]),
|
|
3525
|
+
),
|
|
2395
3526
|
),
|
|
3527
|
+
h(
|
|
3528
|
+
'button',
|
|
3529
|
+
{
|
|
3530
|
+
type: 'button',
|
|
3531
|
+
'aria-label': t('petStopPreview'),
|
|
3532
|
+
disabled: !['loading', 'playing'].includes(previewState.phase),
|
|
3533
|
+
onClick: () => previewMotion.current?.stop?.(),
|
|
3534
|
+
},
|
|
3535
|
+
t('petPreviewStop'),
|
|
2396
3536
|
),
|
|
2397
3537
|
),
|
|
3538
|
+
h(
|
|
3539
|
+
'p',
|
|
3540
|
+
{ className: 'owc-hint owc-preview-status', role: 'status', 'aria-atomic': true },
|
|
3541
|
+
message,
|
|
3542
|
+
),
|
|
2398
3543
|
);
|
|
3544
|
+
};
|
|
2399
3545
|
const editor = () => {
|
|
2400
3546
|
if (appearanceOnly)
|
|
2401
3547
|
return h(
|
|
2402
3548
|
'div',
|
|
2403
3549
|
{ className: 'owc-form owc-appearance', 'data-appearance-settings': true },
|
|
2404
|
-
h('p', { className: 'owc-
|
|
3550
|
+
h('p', { className: 'owc-sr-only' }, t('petAppearancePreview')),
|
|
2405
3551
|
h(
|
|
2406
3552
|
'div',
|
|
2407
|
-
{
|
|
3553
|
+
{
|
|
3554
|
+
ref: previewBox,
|
|
3555
|
+
'data-appearance-preview': true,
|
|
3556
|
+
style: features.cropping ? { display: 'none' } : undefined,
|
|
3557
|
+
className: 'owc-appearance-preview',
|
|
3558
|
+
},
|
|
2408
3559
|
view.companion
|
|
2409
3560
|
? petArtwork(
|
|
2410
3561
|
h,
|
|
2411
|
-
|
|
3562
|
+
{ pose: 'calm', animate: false },
|
|
2412
3563
|
view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
|
|
2413
3564
|
'',
|
|
2414
|
-
view.petModel,
|
|
2415
|
-
PET_SPRITES,
|
|
3565
|
+
features.previewImage ? 'custom-image' : view.petModel,
|
|
3566
|
+
{ ...PET_SPRITES, 'custom-image': features.previewImage },
|
|
3567
|
+
!features.previewImage && petModel(view.petModel).renderer === 'video',
|
|
2416
3568
|
)
|
|
2417
3569
|
: h('p', null, t('petClassicPreview')),
|
|
2418
3570
|
),
|
|
3571
|
+
!features.previewImage && previewActions(),
|
|
2419
3572
|
appearanceFields(),
|
|
3573
|
+
features.render('appearance'),
|
|
2420
3574
|
h(
|
|
2421
3575
|
'div',
|
|
2422
3576
|
{ className: 'owc-save' },
|
|
2423
3577
|
h('button', { type: 'button', onClick: closeEditor }, t('cancel')),
|
|
2424
3578
|
h(
|
|
2425
3579
|
'button',
|
|
2426
|
-
{
|
|
2427
|
-
|
|
3580
|
+
{
|
|
3581
|
+
type: 'button',
|
|
3582
|
+
className: 'owc-primary',
|
|
3583
|
+
'aria-label': t('petApplyAppearance'),
|
|
3584
|
+
onClick: applyAppearance,
|
|
3585
|
+
},
|
|
3586
|
+
t('detailApply'),
|
|
2428
3587
|
),
|
|
2429
3588
|
),
|
|
2430
3589
|
);
|
|
@@ -2432,21 +3591,7 @@ window.__ModuleLoader__.load({
|
|
|
2432
3591
|
const payDaily = workingDraft.payMode === 'daily';
|
|
2433
3592
|
const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
|
|
2434
3593
|
const salaryControl = view.masked
|
|
2435
|
-
? h(
|
|
2436
|
-
'div',
|
|
2437
|
-
{ className: 'owc-private' },
|
|
2438
|
-
h('span', null, '••••'),
|
|
2439
|
-
h(
|
|
2440
|
-
'button',
|
|
2441
|
-
{
|
|
2442
|
-
type: 'button',
|
|
2443
|
-
className: 'owc-text',
|
|
2444
|
-
'aria-label': t('show'),
|
|
2445
|
-
onClick: () => setPreference({ masked: false }),
|
|
2446
|
-
},
|
|
2447
|
-
t('show'),
|
|
2448
|
-
),
|
|
2449
|
-
)
|
|
3594
|
+
? h('div', { className: 'owc-private', 'aria-label': t('masked') }, '••••')
|
|
2450
3595
|
: numberInput(salaryKey);
|
|
2451
3596
|
const breakOn = workingDraft.breakStart !== workingDraft.breakEnd;
|
|
2452
3597
|
const commonZones = [
|
|
@@ -2471,6 +3616,20 @@ window.__ModuleLoader__.load({
|
|
|
2471
3616
|
const zones = [
|
|
2472
3617
|
...new Set([workingDraft.timezone, ...commonZones, ...allZones].filter(Boolean)),
|
|
2473
3618
|
];
|
|
3619
|
+
const timezoneField = () =>
|
|
3620
|
+
label(
|
|
3621
|
+
t('timezone'),
|
|
3622
|
+
h(
|
|
3623
|
+
'select',
|
|
3624
|
+
{
|
|
3625
|
+
name: 'timezone',
|
|
3626
|
+
value: workingDraft.timezone ?? '',
|
|
3627
|
+
disabled: busy,
|
|
3628
|
+
onChange: (e) => update('timezone', e.target.value),
|
|
3629
|
+
},
|
|
3630
|
+
zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
|
|
3631
|
+
),
|
|
3632
|
+
);
|
|
2474
3633
|
return h(
|
|
2475
3634
|
'form',
|
|
2476
3635
|
{
|
|
@@ -2487,7 +3646,22 @@ window.__ModuleLoader__.load({
|
|
|
2487
3646
|
h(
|
|
2488
3647
|
'fieldset',
|
|
2489
3648
|
{ className: 'owc-setting-group' },
|
|
2490
|
-
h(
|
|
3649
|
+
h(
|
|
3650
|
+
'legend',
|
|
3651
|
+
{ className: 'owc-salary-legend' },
|
|
3652
|
+
h('span', null, t('salaryGroup')),
|
|
3653
|
+
h(
|
|
3654
|
+
'button',
|
|
3655
|
+
{
|
|
3656
|
+
type: 'button',
|
|
3657
|
+
className: 'owc-icon',
|
|
3658
|
+
'aria-label': t(view.masked ? 'show' : 'hide'),
|
|
3659
|
+
title: t(view.masked ? 'show' : 'hide'),
|
|
3660
|
+
onClick: () => setPreference({ masked: !view.masked }),
|
|
3661
|
+
},
|
|
3662
|
+
iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
|
|
3663
|
+
),
|
|
3664
|
+
),
|
|
2491
3665
|
label(
|
|
2492
3666
|
t('payMode'),
|
|
2493
3667
|
h(
|
|
@@ -2581,22 +3755,10 @@ window.__ModuleLoader__.load({
|
|
|
2581
3755
|
),
|
|
2582
3756
|
),
|
|
2583
3757
|
),
|
|
2584
|
-
label(
|
|
2585
|
-
t('timezone'),
|
|
2586
|
-
h(
|
|
2587
|
-
'select',
|
|
2588
|
-
{
|
|
2589
|
-
name: 'timezone',
|
|
2590
|
-
value: workingDraft.timezone ?? '',
|
|
2591
|
-
disabled: busy,
|
|
2592
|
-
onChange: (e) => update('timezone', e.target.value),
|
|
2593
|
-
},
|
|
2594
|
-
zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
|
|
2595
|
-
),
|
|
2596
|
-
),
|
|
2597
3758
|
),
|
|
2598
3759
|
preview &&
|
|
2599
3760
|
!preview.error &&
|
|
3761
|
+
!view.masked &&
|
|
2600
3762
|
h(
|
|
2601
3763
|
'div',
|
|
2602
3764
|
{ className: 'owc-preview' },
|
|
@@ -2620,8 +3782,7 @@ window.__ModuleLoader__.load({
|
|
|
2620
3782
|
'details',
|
|
2621
3783
|
{ className: 'owc-advanced' },
|
|
2622
3784
|
h('summary', null, t('advanced')),
|
|
2623
|
-
|
|
2624
|
-
label(t('session'), sessionSelect()),
|
|
3785
|
+
timezoneField(),
|
|
2625
3786
|
),
|
|
2626
3787
|
h('p', { className: 'owc-hint' }, t('privacy')),
|
|
2627
3788
|
h(
|
|
@@ -2642,12 +3803,18 @@ window.__ModuleLoader__.load({
|
|
|
2642
3803
|
);
|
|
2643
3804
|
};
|
|
2644
3805
|
|
|
2645
|
-
const notices = () =>
|
|
3806
|
+
const notices = (showBusinessError = true) =>
|
|
2646
3807
|
h(
|
|
2647
3808
|
React.Fragment,
|
|
2648
3809
|
null,
|
|
2649
|
-
|
|
2650
|
-
|
|
3810
|
+
showBusinessError &&
|
|
3811
|
+
!LOCAL_SCREENS.includes(ui.screen) &&
|
|
3812
|
+
error &&
|
|
3813
|
+
h('p', { className: 'owc-error', role: 'alert' }, error),
|
|
3814
|
+
showBusinessError &&
|
|
3815
|
+
!LOCAL_SCREENS.includes(ui.screen) &&
|
|
3816
|
+
state?.error &&
|
|
3817
|
+
h('p', { className: 'owc-error', role: 'alert' }, state.error),
|
|
2651
3818
|
notice && h('p', { className: 'owc-note', role: 'status' }, notice),
|
|
2652
3819
|
storageError && h('p', { className: 'owc-note' }, t('storage')),
|
|
2653
3820
|
);
|
|
@@ -2685,8 +3852,15 @@ window.__ModuleLoader__.load({
|
|
|
2685
3852
|
waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
|
|
2686
3853
|
),
|
|
2687
3854
|
);
|
|
3855
|
+
React.useLayoutEffect(() => {
|
|
3856
|
+
if (!ready) return;
|
|
3857
|
+
const scroll = petMode
|
|
3858
|
+
? petPanel.current?.querySelector('.owc-pet-scroll')
|
|
3859
|
+
: box.current?.querySelector('.owc-content');
|
|
3860
|
+
if (scroll) scroll.scrollTop = 0;
|
|
3861
|
+
}, [ready, petMode, ui.screen]);
|
|
2688
3862
|
const petShowing = petOpen;
|
|
2689
|
-
const petFloating = petShowing ||
|
|
3863
|
+
const petFloating = petShowing || autoWaterPrompt || features.alert;
|
|
2690
3864
|
React.useLayoutEffect(() => {
|
|
2691
3865
|
if (!petMode || !petFloating || !cardContent.current) return;
|
|
2692
3866
|
const content = cardContent.current,
|
|
@@ -2775,22 +3949,46 @@ window.__ModuleLoader__.load({
|
|
|
2775
3949
|
return () => window.removeEventListener('blur', blur);
|
|
2776
3950
|
}, [ui.menu]);
|
|
2777
3951
|
React.useEffect(() => {
|
|
2778
|
-
if (!
|
|
3952
|
+
if (!petShowing && !ui.menu) return;
|
|
2779
3953
|
const outside = (event) => {
|
|
3954
|
+
if (ui.menu && event.target.closest?.('[data-quick-more],[data-panel-more]')) return;
|
|
2780
3955
|
if (ui.menu && !menuBox.current?.contains(event.target)) {
|
|
2781
3956
|
dispatchUI({ type: 'dismiss-menu' });
|
|
2782
3957
|
return;
|
|
2783
3958
|
}
|
|
2784
3959
|
if (box.current?.contains(event.target)) return;
|
|
2785
3960
|
if (ui.menu) dispatchUI({ type: 'dismiss-menu' });
|
|
2786
|
-
else if (
|
|
2787
|
-
|
|
3961
|
+
else if (
|
|
3962
|
+
petMode &&
|
|
3963
|
+
!editing &&
|
|
3964
|
+
!waterEditing &&
|
|
3965
|
+
!waterBusy &&
|
|
3966
|
+
!features.dirty &&
|
|
3967
|
+
!features.busy
|
|
3968
|
+
)
|
|
3969
|
+
navigateScreen(null);
|
|
2788
3970
|
};
|
|
2789
3971
|
document.addEventListener('pointerdown', outside);
|
|
2790
3972
|
return () => document.removeEventListener('pointerdown', outside);
|
|
2791
|
-
}, [
|
|
3973
|
+
}, [
|
|
3974
|
+
petMode,
|
|
3975
|
+
petShowing,
|
|
3976
|
+
ui.menu,
|
|
3977
|
+
ui.screen,
|
|
3978
|
+
editing,
|
|
3979
|
+
waterEditing,
|
|
3980
|
+
waterBusy,
|
|
3981
|
+
features.dirty,
|
|
3982
|
+
features.busy,
|
|
3983
|
+
]);
|
|
2792
3984
|
React.useEffect(() => {
|
|
2793
|
-
if (ui.menu)
|
|
3985
|
+
if (!ui.menu) return;
|
|
3986
|
+
const waterEntry = menuTrigger.current?.classList.contains('owc-pet-water-alert')
|
|
3987
|
+
? menuBox.current?.querySelector('[data-pet-menu-screen="home"]:not(:disabled)')
|
|
3988
|
+
: null;
|
|
3989
|
+
(
|
|
3990
|
+
waterEntry || menuBox.current?.querySelector('[role=menuitem]:not(:disabled)')
|
|
3991
|
+
)?.focus();
|
|
2794
3992
|
}, [ui.menu]);
|
|
2795
3993
|
React.useEffect(() => {
|
|
2796
3994
|
if (!petMode || editing || waterEditing || ui.menu) return;
|
|
@@ -2806,6 +4004,32 @@ window.__ModuleLoader__.load({
|
|
|
2806
4004
|
if (ui.pending && (busy || waterBusy))
|
|
2807
4005
|
box.current?.querySelector('[data-nav-confirm]')?.focus();
|
|
2808
4006
|
}, [ui.pending, busy, waterBusy]);
|
|
4007
|
+
const classicNavigation = React.useRef({ screen: ui.screen, pending: !!ui.pending });
|
|
4008
|
+
React.useEffect(() => {
|
|
4009
|
+
const previous = classicNavigation.current;
|
|
4010
|
+
classicNavigation.current = { screen: ui.screen, pending: !!ui.pending };
|
|
4011
|
+
if (
|
|
4012
|
+
petMode ||
|
|
4013
|
+
!ready ||
|
|
4014
|
+
(previous.screen === ui.screen && previous.pending === !!ui.pending)
|
|
4015
|
+
)
|
|
4016
|
+
return;
|
|
4017
|
+
const frame = requestAnimationFrame(() => {
|
|
4018
|
+
if (ui.pending) box.current?.querySelector('[data-keep-editing]')?.focus();
|
|
4019
|
+
else if (!editing && !waterEditing) {
|
|
4020
|
+
const kind = focusReturn.current?.getAttribute?.('data-classic-timer');
|
|
4021
|
+
const target =
|
|
4022
|
+
!ui.screen && ['water', 'smoking'].includes(kind)
|
|
4023
|
+
? box.current?.querySelector(`[data-classic-timer="${kind}"]`)
|
|
4024
|
+
: box.current?.querySelector(
|
|
4025
|
+
'.owc-classic-head .owc-title,.owc-classic-mini .owc-grip',
|
|
4026
|
+
);
|
|
4027
|
+
target?.focus();
|
|
4028
|
+
} else if (previous.pending)
|
|
4029
|
+
box.current?.querySelector('form input,form select,form button')?.focus();
|
|
4030
|
+
});
|
|
4031
|
+
return () => cancelAnimationFrame(frame);
|
|
4032
|
+
}, [petMode, ready, ui.screen, !!ui.pending]);
|
|
2809
4033
|
const draftGuard = () =>
|
|
2810
4034
|
h(
|
|
2811
4035
|
'div',
|
|
@@ -2867,12 +4091,28 @@ window.__ModuleLoader__.load({
|
|
|
2867
4091
|
);
|
|
2868
4092
|
const petPageTitle = {
|
|
2869
4093
|
home: 'petWaterCard',
|
|
4094
|
+
'water-countdown': 'waterCountdown',
|
|
4095
|
+
smoking: 'smokingCountdown',
|
|
2870
4096
|
income: 'petCountdownCard',
|
|
2871
4097
|
earnings: 'petEarningsCard',
|
|
2872
4098
|
water: 'petWaterSettings',
|
|
2873
4099
|
appearance: 'petAppearance',
|
|
2874
4100
|
schedule: 'petWorkSettings',
|
|
4101
|
+
wrap: 'companionWrapTitle',
|
|
4102
|
+
preferences: 'companionPreferencesTitle',
|
|
4103
|
+
data: 'companionDataTitle',
|
|
4104
|
+
reminders: 'companionRemindersTitle',
|
|
2875
4105
|
};
|
|
4106
|
+
const pageTitle = (screen) =>
|
|
4107
|
+
screen === 'income' && safeState
|
|
4108
|
+
? safeState.status === 'before'
|
|
4109
|
+
? t('petCountdownBefore')
|
|
4110
|
+
: safeState.status === 'done'
|
|
4111
|
+
? t('classicMiniFinished')
|
|
4112
|
+
: safeState.status === 'rest'
|
|
4113
|
+
? t('classicMiniRest')
|
|
4114
|
+
: t('petCountdownCard')
|
|
4115
|
+
: t(petPageTitle[screen] || 'waterTitle');
|
|
2876
4116
|
const petHeader = () =>
|
|
2877
4117
|
h(
|
|
2878
4118
|
'div',
|
|
@@ -2893,8 +4133,46 @@ window.__ModuleLoader__.load({
|
|
|
2893
4133
|
),
|
|
2894
4134
|
h(
|
|
2895
4135
|
'strong',
|
|
2896
|
-
{ 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1
|
|
2897
|
-
|
|
4136
|
+
{ 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1 } },
|
|
4137
|
+
pageTitle(ui.screen),
|
|
4138
|
+
),
|
|
4139
|
+
ui.screen === 'income' &&
|
|
4140
|
+
data?.configured &&
|
|
4141
|
+
h(
|
|
4142
|
+
'button',
|
|
4143
|
+
{
|
|
4144
|
+
type: 'button',
|
|
4145
|
+
className: 'owc-icon',
|
|
4146
|
+
'aria-label': t('petWorkSettings'),
|
|
4147
|
+
title: t('petWorkSettings'),
|
|
4148
|
+
disabled: busy || !!ui.pending,
|
|
4149
|
+
onClick: openEditor,
|
|
4150
|
+
},
|
|
4151
|
+
iconSvg('sliders', 14),
|
|
4152
|
+
),
|
|
4153
|
+
['home', 'water-countdown'].includes(ui.screen) &&
|
|
4154
|
+
h(
|
|
4155
|
+
'button',
|
|
4156
|
+
{
|
|
4157
|
+
type: 'button',
|
|
4158
|
+
className: 'owc-icon',
|
|
4159
|
+
'aria-label': t('petWaterSettings'),
|
|
4160
|
+
title: t('petWaterSettings'),
|
|
4161
|
+
disabled: busy || waterBusy || !!ui.pending,
|
|
4162
|
+
onClick: openWaterEditor,
|
|
4163
|
+
},
|
|
4164
|
+
iconSvg('sliders', 14),
|
|
4165
|
+
),
|
|
4166
|
+
h(
|
|
4167
|
+
'button',
|
|
4168
|
+
{
|
|
4169
|
+
type: 'button',
|
|
4170
|
+
className: 'owc-icon',
|
|
4171
|
+
'data-panel-more': true,
|
|
4172
|
+
'aria-label': t('quickMore'),
|
|
4173
|
+
onClick: openQuickMenu,
|
|
4174
|
+
},
|
|
4175
|
+
'⋯',
|
|
2898
4176
|
),
|
|
2899
4177
|
h(
|
|
2900
4178
|
'button',
|
|
@@ -2911,102 +4189,115 @@ window.__ModuleLoader__.load({
|
|
|
2911
4189
|
const petHome = () =>
|
|
2912
4190
|
h(
|
|
2913
4191
|
'div',
|
|
2914
|
-
{
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
'
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
4192
|
+
{
|
|
4193
|
+
className: 'owc-pet-home' + (waterPrompt ? ' owc-water-due' : ''),
|
|
4194
|
+
'data-pet-home': true,
|
|
4195
|
+
'data-water-event': waterPrompt ? waterState.eventId : undefined,
|
|
4196
|
+
},
|
|
4197
|
+
waterHero(),
|
|
4198
|
+
waterState.phase === 'due' &&
|
|
4199
|
+
features.waterSnoozedUntil > now &&
|
|
2922
4200
|
h(
|
|
2923
4201
|
'p',
|
|
2924
|
-
{ className: 'owc-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
? t('petQuietActive')
|
|
2929
|
-
: waterSummary(),
|
|
4202
|
+
{ className: 'owc-hint' },
|
|
4203
|
+
t('waterSnooze') +
|
|
4204
|
+
' · ' +
|
|
4205
|
+
formatDuration(Math.ceil((features.waterSnoozedUntil - now) / 1000)),
|
|
2930
4206
|
),
|
|
4207
|
+
features.quiet &&
|
|
2931
4208
|
h(
|
|
2932
|
-
'
|
|
2933
|
-
{ className: 'owc-
|
|
4209
|
+
'p',
|
|
4210
|
+
{ className: 'owc-hint' },
|
|
4211
|
+
t('waterQuiet') +
|
|
4212
|
+
' · ' +
|
|
4213
|
+
formatDuration(Math.ceil((features.state.preferences.quietUntil - now) / 1000)),
|
|
4214
|
+
),
|
|
4215
|
+
h(
|
|
4216
|
+
'div',
|
|
4217
|
+
{ className: 'owc-detail-primary-actions' },
|
|
4218
|
+
h(
|
|
4219
|
+
'button',
|
|
4220
|
+
{
|
|
4221
|
+
type: 'button',
|
|
4222
|
+
className: 'owc-primary owc-water-record',
|
|
4223
|
+
disabled: !waterCanRecord || waterBusy,
|
|
4224
|
+
onClick: recordWater,
|
|
4225
|
+
},
|
|
4226
|
+
t('petRecord'),
|
|
4227
|
+
),
|
|
4228
|
+
waterState.phase === 'due' &&
|
|
4229
|
+
features.waterSnoozedUntil <= now &&
|
|
2934
4230
|
h(
|
|
2935
4231
|
'button',
|
|
2936
4232
|
{
|
|
2937
4233
|
type: 'button',
|
|
2938
|
-
className: 'owc-
|
|
2939
|
-
disabled:
|
|
2940
|
-
onClick:
|
|
4234
|
+
className: 'owc-text',
|
|
4235
|
+
disabled: waterBusy || features.busy || !features.waterCanSnooze,
|
|
4236
|
+
onClick: () => features.snoozeWater(),
|
|
2941
4237
|
},
|
|
2942
|
-
t('
|
|
2943
|
-
),
|
|
2944
|
-
h(
|
|
2945
|
-
'span',
|
|
2946
|
-
{ 'data-water-count': true },
|
|
2947
|
-
t('waterCount') + ': ' + waterState.count,
|
|
4238
|
+
t('waterSnooze'),
|
|
2948
4239
|
),
|
|
2949
|
-
),
|
|
2950
4240
|
),
|
|
2951
4241
|
h(
|
|
2952
4242
|
'div',
|
|
2953
|
-
{ className: 'owc-
|
|
2954
|
-
|
|
4243
|
+
{ className: 'owc-detail-secondary' },
|
|
4244
|
+
h(
|
|
4245
|
+
'button',
|
|
4246
|
+
{
|
|
4247
|
+
type: 'button',
|
|
4248
|
+
className: 'owc-text owc-secondary',
|
|
4249
|
+
disabled: busy || waterBusy || waterError || !waterReady,
|
|
4250
|
+
onClick: () =>
|
|
4251
|
+
waterAction(
|
|
4252
|
+
water.enabled
|
|
4253
|
+
? { type: water.paused ? 'resume' : 'pause' }
|
|
4254
|
+
: { type: 'preferences', enabled: true },
|
|
4255
|
+
),
|
|
4256
|
+
},
|
|
4257
|
+
t(!water.enabled ? 'waterEnable' : water.paused ? 'waterResume' : 'waterPause'),
|
|
4258
|
+
),
|
|
4259
|
+
waterState.phase === 'quiet' &&
|
|
2955
4260
|
h(
|
|
2956
4261
|
'button',
|
|
2957
4262
|
{
|
|
2958
4263
|
type: 'button',
|
|
2959
|
-
|
|
2960
|
-
disabled:
|
|
2961
|
-
onClick: () => waterAction({ type: 'quiet'
|
|
4264
|
+
className: 'owc-text',
|
|
4265
|
+
disabled: waterBusy,
|
|
4266
|
+
onClick: () => waterAction({ type: 'end-quiet' }),
|
|
2962
4267
|
},
|
|
2963
|
-
t(
|
|
4268
|
+
t('waterQuietEnd'),
|
|
2964
4269
|
),
|
|
2965
|
-
),
|
|
2966
|
-
),
|
|
2967
|
-
h(
|
|
2968
|
-
'div',
|
|
2969
|
-
{ className: 'owc-pet-quiet-status' },
|
|
2970
|
-
waterPrompt
|
|
2971
|
-
? h(
|
|
2972
|
-
'button',
|
|
2973
|
-
{
|
|
2974
|
-
type: 'button',
|
|
2975
|
-
disabled: waterBusy,
|
|
2976
|
-
onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
|
|
2977
|
-
},
|
|
2978
|
-
t('waterSnooze'),
|
|
2979
|
-
)
|
|
2980
|
-
: water.shift?.paused
|
|
2981
|
-
? t('petPausedHint')
|
|
2982
|
-
: waterState.phase === 'quiet'
|
|
2983
|
-
? h(
|
|
2984
|
-
React.Fragment,
|
|
2985
|
-
null,
|
|
2986
|
-
t('waterQuiet') +
|
|
2987
|
-
' · ' +
|
|
2988
|
-
formatDuration(waterState.quietRemaining / 1000),
|
|
2989
|
-
h(
|
|
2990
|
-
'button',
|
|
2991
|
-
{
|
|
2992
|
-
type: 'button',
|
|
2993
|
-
disabled: waterBusy,
|
|
2994
|
-
onClick: () => waterAction({ type: 'end-quiet' }),
|
|
2995
|
-
},
|
|
2996
|
-
t('waterQuietEnd'),
|
|
2997
|
-
),
|
|
2998
|
-
)
|
|
2999
|
-
: t('petQuietHint'),
|
|
3000
|
-
),
|
|
3001
|
-
h(
|
|
3002
|
-
'div',
|
|
3003
|
-
{ className: 'owc-pet-feedback' },
|
|
3004
|
-
waterNotice
|
|
3005
|
-
? t(waterNotice.key)
|
|
3006
|
-
: waterError
|
|
3007
|
-
? t('waterUnavailable')
|
|
3008
|
-
: t('petHomeHint'),
|
|
3009
4270
|
),
|
|
4271
|
+
water.enabled &&
|
|
4272
|
+
h(
|
|
4273
|
+
'details',
|
|
4274
|
+
{ className: 'owc-detail-more' },
|
|
4275
|
+
h('summary', null, t('waterQuiet')),
|
|
4276
|
+
h(
|
|
4277
|
+
'div',
|
|
4278
|
+
{ className: 'owc-detail-options' },
|
|
4279
|
+
[30, 60].map((minutes) =>
|
|
4280
|
+
h(
|
|
4281
|
+
'button',
|
|
4282
|
+
{
|
|
4283
|
+
type: 'button',
|
|
4284
|
+
key: minutes,
|
|
4285
|
+
disabled: !waterCanQuiet || waterBusy,
|
|
4286
|
+
onClick: (event) => {
|
|
4287
|
+
const menu = event.currentTarget.closest('details');
|
|
4288
|
+
menu.open = false;
|
|
4289
|
+
menu.querySelector('summary')?.focus();
|
|
4290
|
+
void waterAction({ type: 'quiet', minutes });
|
|
4291
|
+
},
|
|
4292
|
+
},
|
|
4293
|
+
t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
|
|
4294
|
+
),
|
|
4295
|
+
),
|
|
4296
|
+
),
|
|
4297
|
+
h('p', { className: 'owc-hint' }, t('petQuietHint')),
|
|
4298
|
+
),
|
|
4299
|
+
waterError &&
|
|
4300
|
+
h('p', { className: 'owc-error', role: 'alert' }, t('waterUnavailable')),
|
|
3010
4301
|
waterError &&
|
|
3011
4302
|
h(
|
|
3012
4303
|
'button',
|
|
@@ -3017,27 +4308,116 @@ window.__ModuleLoader__.load({
|
|
|
3017
4308
|
},
|
|
3018
4309
|
t('waterRetry'),
|
|
3019
4310
|
),
|
|
4311
|
+
);
|
|
4312
|
+
function openQuickMenu(event) {
|
|
4313
|
+
if (ui.pending || busy || waterBusy || features.busy) return;
|
|
4314
|
+
if (ui.menu) {
|
|
4315
|
+
closePetMenu();
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
menuTrigger.current = event.currentTarget;
|
|
4319
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
4320
|
+
dispatchUI({ type: 'menu', position: { x: rect.right, y: rect.top } });
|
|
4321
|
+
}
|
|
4322
|
+
const activeCustomTimer = () =>
|
|
4323
|
+
features.activeTimer &&
|
|
4324
|
+
h(
|
|
4325
|
+
'button',
|
|
4326
|
+
{
|
|
4327
|
+
type: 'button',
|
|
4328
|
+
className: 'owc-inline-timer',
|
|
4329
|
+
'data-active-custom-timer': true,
|
|
4330
|
+
onClick: (event) => navigateScreen('smoking', event),
|
|
4331
|
+
title: features.activeTimer.name,
|
|
4332
|
+
},
|
|
4333
|
+
iconSvg('alarm', 12),
|
|
4334
|
+
h('span', null, features.activeTimer.name),
|
|
3020
4335
|
h(
|
|
3021
|
-
'
|
|
3022
|
-
|
|
3023
|
-
|
|
4336
|
+
'strong',
|
|
4337
|
+
null,
|
|
4338
|
+
(features.activeTimer.phase === 'paused' ? 'Ⅱ ' : '') +
|
|
4339
|
+
formatDuration(Math.ceil(features.activeTimer.remaining / 1000)).replace(
|
|
4340
|
+
/^00:/,
|
|
4341
|
+
'',
|
|
4342
|
+
),
|
|
4343
|
+
),
|
|
4344
|
+
features.activeTimer.count > 1 ? ' +' + (features.activeTimer.count - 1) : null,
|
|
4345
|
+
);
|
|
4346
|
+
const quickActions = () =>
|
|
4347
|
+
h(
|
|
4348
|
+
'div',
|
|
4349
|
+
{ className: 'owc-quick-actions' },
|
|
4350
|
+
waterState.phase === 'due' &&
|
|
4351
|
+
h(
|
|
4352
|
+
'button',
|
|
4353
|
+
{
|
|
4354
|
+
type: 'button',
|
|
4355
|
+
'data-quick-drink': true,
|
|
4356
|
+
disabled: !waterCanRecord || waterBusy,
|
|
4357
|
+
onClick: recordWater,
|
|
4358
|
+
},
|
|
4359
|
+
iconSvg('water', 13),
|
|
4360
|
+
waterNotice?.kind === 'drink' ? t('waterRecorded') : t('petRecord'),
|
|
4361
|
+
),
|
|
4362
|
+
features.pendingCount > 0 &&
|
|
3024
4363
|
h(
|
|
3025
4364
|
'button',
|
|
3026
4365
|
{
|
|
3027
4366
|
type: 'button',
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
busy ||
|
|
3031
|
-
waterBusy ||
|
|
3032
|
-
waterError ||
|
|
3033
|
-
!waterContext ||
|
|
3034
|
-
water.shift?.id !== waterContext.id,
|
|
3035
|
-
onClick: () => waterAction({ type: water.shift?.paused ? 'resume' : 'pause' }),
|
|
4367
|
+
'aria-label': t('companionRemindersTitle'),
|
|
4368
|
+
onClick: (event) => navigateScreen('reminders', event),
|
|
3036
4369
|
},
|
|
3037
|
-
|
|
4370
|
+
iconSvg('alarm', 13),
|
|
4371
|
+
String(features.pendingCount),
|
|
3038
4372
|
),
|
|
4373
|
+
h(
|
|
4374
|
+
'button',
|
|
4375
|
+
{
|
|
4376
|
+
type: 'button',
|
|
4377
|
+
className: 'owc-quick-more',
|
|
4378
|
+
'data-quick-more': true,
|
|
4379
|
+
'aria-label': t('quickMore'),
|
|
4380
|
+
'aria-expanded': !!ui.menu,
|
|
4381
|
+
onClick: openQuickMenu,
|
|
4382
|
+
},
|
|
4383
|
+
'⋯',
|
|
3039
4384
|
),
|
|
3040
4385
|
);
|
|
4386
|
+
const contentForScreen = () => {
|
|
4387
|
+
if (ui.pending) return draftGuard();
|
|
4388
|
+
if (waterEditing) return waterSettings();
|
|
4389
|
+
if (editing) return editor();
|
|
4390
|
+
if (ui.screen === 'income')
|
|
4391
|
+
return h(
|
|
4392
|
+
React.Fragment,
|
|
4393
|
+
null,
|
|
4394
|
+
panelBody('clock'),
|
|
4395
|
+
features.render('wrap-entry'),
|
|
4396
|
+
h(
|
|
4397
|
+
'div',
|
|
4398
|
+
{ className: 'owc-extra' },
|
|
4399
|
+
h(
|
|
4400
|
+
'button',
|
|
4401
|
+
{
|
|
4402
|
+
type: 'button',
|
|
4403
|
+
className: 'owc-link-row',
|
|
4404
|
+
onClick: (event) => navigateScreen('earnings', event),
|
|
4405
|
+
disabled: !data?.configured,
|
|
4406
|
+
},
|
|
4407
|
+
t('petEarningsCard'),
|
|
4408
|
+
),
|
|
4409
|
+
),
|
|
4410
|
+
);
|
|
4411
|
+
if (ui.screen === 'smoking')
|
|
4412
|
+
return h(React.Fragment, null, smokingCountdownCard(), features.render('timers'));
|
|
4413
|
+
if (ui.screen === 'water-countdown' || ui.screen === 'home') return petHome();
|
|
4414
|
+
if (ui.screen === 'earnings') return panelBody('earnings');
|
|
4415
|
+
if (ui.screen === 'wrap') return features.render('wrap');
|
|
4416
|
+
if (ui.screen === 'preferences') return features.render('settings');
|
|
4417
|
+
if (ui.screen === 'data') return features.render('data');
|
|
4418
|
+
if (ui.screen === 'reminders') return features.render('reminders');
|
|
4419
|
+
return collapsed ? classicMini() : classicOverview();
|
|
4420
|
+
};
|
|
3041
4421
|
const contextMenu = () =>
|
|
3042
4422
|
ui.menu &&
|
|
3043
4423
|
h(
|
|
@@ -3091,9 +4471,9 @@ window.__ModuleLoader__.load({
|
|
|
3091
4471
|
[
|
|
3092
4472
|
['income', 'petCountdownCard'],
|
|
3093
4473
|
['home', 'petWaterCard'],
|
|
3094
|
-
['
|
|
4474
|
+
['smoking', 'smokingCountdown'],
|
|
3095
4475
|
['appearance', 'petAppearance'],
|
|
3096
|
-
['
|
|
4476
|
+
['preferences', 'companionPreferencesTitle'],
|
|
3097
4477
|
].map(([screen, label]) =>
|
|
3098
4478
|
h(
|
|
3099
4479
|
'button',
|
|
@@ -3101,14 +4481,34 @@ window.__ModuleLoader__.load({
|
|
|
3101
4481
|
key: screen,
|
|
3102
4482
|
type: 'button',
|
|
3103
4483
|
role: 'menuitem',
|
|
4484
|
+
'data-pet-menu-screen': screen,
|
|
3104
4485
|
tabIndex: -1,
|
|
3105
|
-
disabled:
|
|
4486
|
+
disabled:
|
|
4487
|
+
requestBusy.current ||
|
|
4488
|
+
waterBusy ||
|
|
4489
|
+
(!LOCAL_SCREENS.includes(screen) &&
|
|
4490
|
+
(hostBusy || !data || (!data.configured && screen !== 'schedule'))),
|
|
3106
4491
|
onClick: (event) => navigateScreen(screen, event),
|
|
3107
4492
|
},
|
|
3108
4493
|
t(label),
|
|
3109
4494
|
),
|
|
3110
4495
|
),
|
|
3111
4496
|
|
|
4497
|
+
(!data || error || state?.error) &&
|
|
4498
|
+
h(
|
|
4499
|
+
'button',
|
|
4500
|
+
{
|
|
4501
|
+
type: 'button',
|
|
4502
|
+
role: 'menuitem',
|
|
4503
|
+
tabIndex: -1,
|
|
4504
|
+
disabled: hostBusy,
|
|
4505
|
+
onClick: () => {
|
|
4506
|
+
closePetMenu();
|
|
4507
|
+
void reload(true);
|
|
4508
|
+
},
|
|
4509
|
+
},
|
|
4510
|
+
t('retry'),
|
|
4511
|
+
),
|
|
3112
4512
|
h('hr', null),
|
|
3113
4513
|
h(
|
|
3114
4514
|
'button',
|
|
@@ -3126,17 +4526,26 @@ window.__ModuleLoader__.load({
|
|
|
3126
4526
|
),
|
|
3127
4527
|
);
|
|
3128
4528
|
|
|
4529
|
+
if (!ready) return null;
|
|
3129
4530
|
if (petMode)
|
|
3130
4531
|
return h(
|
|
3131
4532
|
'section',
|
|
3132
4533
|
{
|
|
3133
4534
|
ref: box,
|
|
3134
4535
|
className: 'owc-v2 owc-pet-shell',
|
|
3135
|
-
'data-version': '1.
|
|
4536
|
+
'data-version': '1.2.2',
|
|
3136
4537
|
'data-pet-model': stageModel,
|
|
3137
|
-
'data-status':
|
|
4538
|
+
'data-status':
|
|
4539
|
+
error || state?.error
|
|
4540
|
+
? 'error'
|
|
4541
|
+
: data?.configured && safeState
|
|
4542
|
+
? safeState.status
|
|
4543
|
+
: data
|
|
4544
|
+
? 'setup'
|
|
4545
|
+
: 'loading',
|
|
3138
4546
|
'aria-label': t('companionLabel'),
|
|
3139
4547
|
style: {
|
|
4548
|
+
...featureStyle,
|
|
3140
4549
|
'--owc-alpha': String(view.opacity),
|
|
3141
4550
|
left: petGeometry.x,
|
|
3142
4551
|
top: petGeometry.y,
|
|
@@ -3160,11 +4569,11 @@ window.__ModuleLoader__.load({
|
|
|
3160
4569
|
else if (ui.pending) dispatchUI({ type: 'stay' });
|
|
3161
4570
|
else if (editing || waterEditing) navigateScreen(ui.back);
|
|
3162
4571
|
else if (petShowing) closePetPanel();
|
|
3163
|
-
else if (
|
|
3164
|
-
|
|
4572
|
+
else if (features.alert) features.dismissAlert();
|
|
4573
|
+
else if (autoWaterPrompt) dismissWaterPopup();
|
|
3165
4574
|
},
|
|
3166
4575
|
},
|
|
3167
|
-
h('style', null, STYLE),
|
|
4576
|
+
h('style', null, STYLE + COMPANION_APP_STYLE),
|
|
3168
4577
|
liveFeedback(),
|
|
3169
4578
|
h(
|
|
3170
4579
|
'div',
|
|
@@ -3189,33 +4598,84 @@ window.__ModuleLoader__.load({
|
|
|
3189
4598
|
petGeometry.artworkSize,
|
|
3190
4599
|
waterNotice?.token,
|
|
3191
4600
|
stageModel,
|
|
3192
|
-
PET_SPRITES,
|
|
4601
|
+
{ ...PET_SPRITES, 'custom-image': features.customImage },
|
|
3193
4602
|
true,
|
|
3194
4603
|
),
|
|
3195
4604
|
),
|
|
3196
4605
|
h(
|
|
3197
4606
|
'div',
|
|
3198
4607
|
{
|
|
3199
|
-
className: 'owc-pet-
|
|
3200
|
-
role: '
|
|
3201
|
-
'aria-label': t('
|
|
4608
|
+
className: 'owc-pet-timers',
|
|
4609
|
+
role: 'group',
|
|
4610
|
+
'aria-label': t('petCountdownGroup'),
|
|
3202
4611
|
},
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
4612
|
+
h(
|
|
4613
|
+
'button',
|
|
4614
|
+
{
|
|
4615
|
+
type: 'button',
|
|
4616
|
+
onClick: (event) =>
|
|
4617
|
+
navigateScreen(data?.configured ? 'income' : 'schedule', event),
|
|
4618
|
+
className: 'owc-pet-clock',
|
|
4619
|
+
'aria-label': t('petCountdownCard'),
|
|
4620
|
+
title: error || state?.error ? t('petRetryHint') : undefined,
|
|
4621
|
+
},
|
|
4622
|
+
iconSvg('alarm', 12),
|
|
4623
|
+
h(
|
|
4624
|
+
'span',
|
|
4625
|
+
{ className: 'owc-pet-clock-label', title: t(petClockStatus) },
|
|
4626
|
+
t(petClockStatus),
|
|
4627
|
+
),
|
|
4628
|
+
data?.configured &&
|
|
4629
|
+
safeState &&
|
|
4630
|
+
!error &&
|
|
4631
|
+
!['done', 'rest'].includes(safeState.status)
|
|
4632
|
+
? h('strong', null, duration)
|
|
4633
|
+
: null,
|
|
4634
|
+
),
|
|
4635
|
+
h(
|
|
4636
|
+
'div',
|
|
4637
|
+
{ className: 'owc-pet-timer-pair' },
|
|
4638
|
+
timerRow(
|
|
4639
|
+
'water',
|
|
4640
|
+
waterCountdownText(),
|
|
4641
|
+
waterState.phase === 'due',
|
|
4642
|
+
t('waterCountdown') + ' · ' + waterSummary(),
|
|
4643
|
+
),
|
|
4644
|
+
timerRow(
|
|
4645
|
+
'smoking',
|
|
4646
|
+
smokingCountdownText(),
|
|
4647
|
+
smokingState.phase === 'due',
|
|
4648
|
+
t('smokingCountdown') +
|
|
4649
|
+
' · ' +
|
|
4650
|
+
(smokingUnavailable
|
|
4651
|
+
? t('timerUnavailable')
|
|
4652
|
+
: t(
|
|
4653
|
+
{
|
|
4654
|
+
idle: 'timerIdle',
|
|
4655
|
+
running: 'timerRunning',
|
|
4656
|
+
paused: 'timerPaused',
|
|
4657
|
+
due: 'timerDue',
|
|
4658
|
+
}[smokingState.phase],
|
|
4659
|
+
)),
|
|
4660
|
+
),
|
|
4661
|
+
),
|
|
4662
|
+
activeCustomTimer(),
|
|
4663
|
+
quickActions(),
|
|
3206
4664
|
),
|
|
3207
4665
|
),
|
|
3208
4666
|
waterPrompt &&
|
|
3209
|
-
|
|
4667
|
+
!autoWaterPrompt &&
|
|
3210
4668
|
ui.screen !== 'home' &&
|
|
3211
4669
|
h(
|
|
3212
|
-
'
|
|
4670
|
+
'button',
|
|
3213
4671
|
{
|
|
4672
|
+
type: 'button',
|
|
3214
4673
|
className: 'owc-pet-water-alert owc-water-due',
|
|
3215
4674
|
'data-water-event': waterState.eventId,
|
|
3216
|
-
role: 'note',
|
|
3217
4675
|
'aria-label': t('petWaterAlert'),
|
|
3218
4676
|
title: t('petWaterAlert'),
|
|
4677
|
+
disabled: busy || waterBusy || !!ui.pending,
|
|
4678
|
+
onClick: openWaterPopup,
|
|
3219
4679
|
style: {
|
|
3220
4680
|
left: waterAlertLayout.left,
|
|
3221
4681
|
top: waterAlertLayout.top,
|
|
@@ -3224,7 +4684,8 @@ window.__ModuleLoader__.load({
|
|
|
3224
4684
|
},
|
|
3225
4685
|
},
|
|
3226
4686
|
iconSvg('water', 13),
|
|
3227
|
-
t('petWaterBadge'),
|
|
4687
|
+
h('span', { className: 'owc-pet-water-label' }, t('petWaterBadge')),
|
|
4688
|
+
iconSvg('chevronUp', 10),
|
|
3228
4689
|
),
|
|
3229
4690
|
contextMenu(),
|
|
3230
4691
|
!petShowing &&
|
|
@@ -3249,12 +4710,15 @@ window.__ModuleLoader__.load({
|
|
|
3249
4710
|
'div',
|
|
3250
4711
|
{
|
|
3251
4712
|
ref: petPanel,
|
|
3252
|
-
className: 'owc-pet-panel owc-pet-workspace',
|
|
3253
|
-
'data-pet-panel': petShowing ? 'open' : '
|
|
4713
|
+
className: 'owc-pet-panel owc-pet-workspace owc-detail-page',
|
|
4714
|
+
'data-pet-panel': petShowing ? 'open' : 'reminder',
|
|
3254
4715
|
'data-pet-screen': ui.screen || 'reminder',
|
|
3255
4716
|
'data-side': floatingPanel.side,
|
|
3256
4717
|
role: 'region',
|
|
3257
|
-
'aria-label':
|
|
4718
|
+
'aria-label':
|
|
4719
|
+
features.alert && !petShowing
|
|
4720
|
+
? features.alertTitle
|
|
4721
|
+
: t(petPageTitle[ui.screen] || 'waterTitle'),
|
|
3258
4722
|
style: {
|
|
3259
4723
|
left: floatingPanel.left,
|
|
3260
4724
|
top: floatingPanel.top,
|
|
@@ -3269,16 +4733,21 @@ window.__ModuleLoader__.load({
|
|
|
3269
4733
|
: h(
|
|
3270
4734
|
'div',
|
|
3271
4735
|
{ className: 'owc-head' },
|
|
3272
|
-
h(
|
|
4736
|
+
h(
|
|
4737
|
+
'strong',
|
|
4738
|
+
{ style: { flex: 1, fontSize: 12 } },
|
|
4739
|
+
features.alert ? features.alertTitle : t('waterTitle'),
|
|
4740
|
+
),
|
|
3273
4741
|
h(
|
|
3274
4742
|
'button',
|
|
3275
4743
|
{
|
|
3276
4744
|
type: 'button',
|
|
3277
4745
|
className: 'owc-icon',
|
|
3278
|
-
'aria-label': t('
|
|
4746
|
+
'aria-label': t('petDismissReminder'),
|
|
4747
|
+
title: t('petDismissReminder'),
|
|
3279
4748
|
disabled: waterBusy,
|
|
3280
4749
|
onClick: () =>
|
|
3281
|
-
|
|
4750
|
+
features.alert ? features.dismissAlert() : dismissWaterPopup(),
|
|
3282
4751
|
},
|
|
3283
4752
|
iconSvg('close', 14),
|
|
3284
4753
|
),
|
|
@@ -3293,20 +4762,16 @@ window.__ModuleLoader__.load({
|
|
|
3293
4762
|
? h(
|
|
3294
4763
|
React.Fragment,
|
|
3295
4764
|
null,
|
|
3296
|
-
|
|
3297
|
-
? draftGuard()
|
|
3298
|
-
: waterEditing
|
|
3299
|
-
? waterSettings()
|
|
3300
|
-
: editing
|
|
3301
|
-
? editor()
|
|
3302
|
-
: petDetails
|
|
3303
|
-
? panelBody('clock')
|
|
3304
|
-
: ui.screen === 'earnings'
|
|
3305
|
-
? panelBody('earnings')
|
|
3306
|
-
: petHome(),
|
|
4765
|
+
contentForScreen(),
|
|
3307
4766
|
notices(),
|
|
4767
|
+
features.feedback(),
|
|
3308
4768
|
)
|
|
3309
|
-
:
|
|
4769
|
+
: h(
|
|
4770
|
+
React.Fragment,
|
|
4771
|
+
null,
|
|
4772
|
+
features.alert ? features.render('alert') : waterReminder(),
|
|
4773
|
+
features.feedback(),
|
|
4774
|
+
),
|
|
3310
4775
|
),
|
|
3311
4776
|
),
|
|
3312
4777
|
),
|
|
@@ -3317,13 +4782,14 @@ window.__ModuleLoader__.load({
|
|
|
3317
4782
|
{
|
|
3318
4783
|
ref: box,
|
|
3319
4784
|
className:
|
|
3320
|
-
'owc-v2' +
|
|
4785
|
+
'owc-v2 owc-classic' +
|
|
4786
|
+
(!collapsed ? ' owc-detail-page' : '') +
|
|
3321
4787
|
(collapsed ? ' owc-compact' : '') +
|
|
3322
4788
|
(collapsed && waterQuickOpen ? ' owc-quick' : '') +
|
|
3323
4789
|
(editing || waterEditing ? ' owc-editing' : ''),
|
|
3324
4790
|
'aria-label': t('title'),
|
|
3325
|
-
'data-version': '1.
|
|
3326
|
-
'data-status':
|
|
4791
|
+
'data-version': '1.2.2',
|
|
4792
|
+
'data-status': classicClock.phase,
|
|
3327
4793
|
style: cardStyle,
|
|
3328
4794
|
onPointerDown: dragDown,
|
|
3329
4795
|
onPointerMove: dragMove,
|
|
@@ -3342,12 +4808,12 @@ window.__ModuleLoader__.load({
|
|
|
3342
4808
|
return;
|
|
3343
4809
|
event.preventDefault();
|
|
3344
4810
|
if (ui.pending) dispatchUI({ type: 'stay' });
|
|
3345
|
-
else if (
|
|
3346
|
-
|
|
4811
|
+
else if (ui.screen)
|
|
4812
|
+
navigateScreen(PET_EDITOR_SCREENS.includes(ui.screen) ? ui.back : null);
|
|
3347
4813
|
else setPreference({ mini: true });
|
|
3348
4814
|
},
|
|
3349
4815
|
},
|
|
3350
|
-
h('style', null, STYLE),
|
|
4816
|
+
h('style', null, STYLE + COMPANION_APP_STYLE),
|
|
3351
4817
|
h('div', { className: 'owc-glass', 'aria-hidden': 'true' }),
|
|
3352
4818
|
h(
|
|
3353
4819
|
'div',
|
|
@@ -3365,33 +4831,14 @@ window.__ModuleLoader__.load({
|
|
|
3365
4831
|
waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
|
|
3366
4832
|
),
|
|
3367
4833
|
),
|
|
3368
|
-
collapsed
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
(!view.companion || companion.announce) &&
|
|
3377
|
-
h('p', { className: 'owc-mini-feedback' }, t(waterNotice.key)),
|
|
3378
|
-
)
|
|
3379
|
-
: h(
|
|
3380
|
-
React.Fragment,
|
|
3381
|
-
null,
|
|
3382
|
-
panelHead(),
|
|
3383
|
-
!editing && !waterEditing && companionCard(),
|
|
3384
|
-
ui.pending
|
|
3385
|
-
? draftGuard()
|
|
3386
|
-
: waterEditing
|
|
3387
|
-
? waterSettings()
|
|
3388
|
-
: editing
|
|
3389
|
-
? editor()
|
|
3390
|
-
: panelBody(),
|
|
3391
|
-
notices(),
|
|
3392
|
-
!editing && !waterEditing && waterOverview(),
|
|
3393
|
-
),
|
|
3394
|
-
!collapsed && waterReminder(),
|
|
4834
|
+
!collapsed && classicHeader(),
|
|
4835
|
+
contentForScreen(),
|
|
4836
|
+
notices(ui.screen !== null),
|
|
4837
|
+
features.feedback(),
|
|
4838
|
+
ui.screen === null && (features.alert ? features.render('alert') : waterReminder()),
|
|
4839
|
+
ui.screen === null && activeCustomTimer(),
|
|
4840
|
+
ui.screen === null && quickActions(),
|
|
4841
|
+
contextMenu(),
|
|
3395
4842
|
),
|
|
3396
4843
|
);
|
|
3397
4844
|
}
|