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/pet-video.js
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
|
-
// Independent
|
|
1
|
+
// Independent media presentation; never writes settings or hydration records.
|
|
2
2
|
const PET_VIDEO_SUPPORT = new WeakMap();
|
|
3
3
|
|
|
4
|
+
export function petClipUrls(models, baseURI) {
|
|
5
|
+
try {
|
|
6
|
+
const base = new URL(baseURI);
|
|
7
|
+
if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password) return {};
|
|
8
|
+
const prefix = new URL('offwork-clock/media/', base);
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(models).map(([model, actions]) => [
|
|
11
|
+
model,
|
|
12
|
+
Object.fromEntries(
|
|
13
|
+
Object.entries(actions).flatMap(([action, path]) => {
|
|
14
|
+
if (typeof path !== 'string' || !path.startsWith('offwork-clock/media/')) return [];
|
|
15
|
+
const url = new URL(path, base);
|
|
16
|
+
return url.origin === base.origin &&
|
|
17
|
+
url.pathname.startsWith(prefix.pathname) &&
|
|
18
|
+
!url.search &&
|
|
19
|
+
!url.hash
|
|
20
|
+
? [[action, url.href]]
|
|
21
|
+
: [];
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
]),
|
|
25
|
+
);
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
4
31
|
export function petVideoAlphaSupported(host, source) {
|
|
5
32
|
if (PET_VIDEO_SUPPORT.has(host)) return PET_VIDEO_SUPPORT.get(host);
|
|
6
33
|
const result = new Promise((resolve) => {
|
|
7
34
|
const video = host.document.createElement('video');
|
|
8
|
-
let timer
|
|
35
|
+
let timer,
|
|
36
|
+
frame,
|
|
37
|
+
delayed,
|
|
38
|
+
done = false;
|
|
9
39
|
const finish = (supported) => {
|
|
40
|
+
if (done) return;
|
|
41
|
+
done = true;
|
|
10
42
|
host.clearTimeout(timer);
|
|
43
|
+
host.clearTimeout(delayed);
|
|
44
|
+
if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);
|
|
11
45
|
video.onloadeddata = video.onerror = null;
|
|
12
46
|
video.pause();
|
|
13
47
|
video.removeAttribute('src');
|
|
@@ -18,145 +52,241 @@ export function petVideoAlphaSupported(host, source) {
|
|
|
18
52
|
video.muted = true;
|
|
19
53
|
video.playsInline = true;
|
|
20
54
|
video.preload = 'auto';
|
|
21
|
-
|
|
55
|
+
const inspect = (presented = false) => {
|
|
56
|
+
if (done) return true;
|
|
22
57
|
try {
|
|
23
58
|
const canvas = host.document.createElement('canvas');
|
|
24
59
|
canvas.width = canvas.height = 16;
|
|
25
60
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
26
61
|
context.drawImage(video, 0, 0, 16, 16);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
62
|
+
const solid = context.getImageData(1, 1, 1, 1).data[3];
|
|
63
|
+
const clear = context.getImageData(14, 14, 1, 1).data[3];
|
|
64
|
+
if (solid > 240 && clear < 8) {
|
|
65
|
+
finish(true);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (presented && solid > 240 && clear > 240) {
|
|
69
|
+
finish(false);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
31
72
|
} catch {
|
|
32
|
-
|
|
73
|
+
/* A frame can be undecoded when loadeddata fires. */
|
|
33
74
|
}
|
|
75
|
+
return false;
|
|
34
76
|
};
|
|
35
|
-
video.
|
|
36
|
-
|
|
77
|
+
video.onloadeddata = () => {
|
|
78
|
+
if (inspect()) return;
|
|
79
|
+
if (video.requestVideoFrameCallback)
|
|
80
|
+
frame = video.requestVideoFrameCallback(() => inspect(true));
|
|
81
|
+
const poll = () => {
|
|
82
|
+
if (!inspect(true)) delayed = host.setTimeout(poll, 80);
|
|
83
|
+
};
|
|
84
|
+
delayed = host.setTimeout(poll, 80);
|
|
85
|
+
try {
|
|
86
|
+
Promise.resolve(video.play()).catch(() => {
|
|
87
|
+
if (!inspect()) finish(null);
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
if (!inspect()) finish(null);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
video.onerror = () => finish(null);
|
|
94
|
+
timer = host.setTimeout(() => {
|
|
95
|
+
if (!inspect()) finish(null);
|
|
96
|
+
}, 5000);
|
|
37
97
|
video.src = source;
|
|
38
98
|
video.load();
|
|
39
99
|
});
|
|
40
100
|
PET_VIDEO_SUPPORT.set(host, result);
|
|
101
|
+
result.then((supported) => {
|
|
102
|
+
if (supported === null && PET_VIDEO_SUPPORT.get(host) === result)
|
|
103
|
+
PET_VIDEO_SUPPORT.delete(host);
|
|
104
|
+
});
|
|
41
105
|
return result;
|
|
42
106
|
}
|
|
43
107
|
|
|
44
|
-
export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
108
|
+
export function createPetVideoMotion(element, host, base, clips, probeSource, options = {}) {
|
|
45
109
|
const layer = element.querySelector('.owc-pet-video-layer');
|
|
110
|
+
const poster = element.querySelector('.owc-pet-video-poster');
|
|
46
111
|
if (!layer || !clips) return base;
|
|
47
112
|
const document = host.document;
|
|
48
113
|
const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
114
|
+
const autoIdle = options.autoIdle !== false;
|
|
115
|
+
const useFallback = options.fallback !== false;
|
|
116
|
+
const failed = new Set(),
|
|
117
|
+
retired = new Map();
|
|
52
118
|
let enabled = false,
|
|
53
119
|
resting = false,
|
|
54
120
|
dragging = false,
|
|
55
|
-
unsupported = false
|
|
121
|
+
unsupported = false;
|
|
122
|
+
let destroyed = false,
|
|
56
123
|
idleTimer = null,
|
|
57
|
-
|
|
58
|
-
|
|
124
|
+
current = null,
|
|
125
|
+
displayed = null,
|
|
126
|
+
lastState = '';
|
|
59
127
|
const allowed = () => enabled && !destroyed && !document.hidden && !reduced?.matches;
|
|
128
|
+
const canIdle = () =>
|
|
129
|
+
autoIdle &&
|
|
130
|
+
allowed() &&
|
|
131
|
+
!resting &&
|
|
132
|
+
!dragging &&
|
|
133
|
+
!unsupported &&
|
|
134
|
+
Object.hasOwn(clips, 'idle') &&
|
|
135
|
+
!failed.has('idle');
|
|
136
|
+
function notify(phase, action = null) {
|
|
137
|
+
if (destroyed || lastState === phase + ':' + action) return;
|
|
138
|
+
lastState = phase + ':' + action;
|
|
139
|
+
options.onStateChange?.({ phase, action });
|
|
140
|
+
}
|
|
60
141
|
function clearIdleTimer() {
|
|
61
142
|
host.clearTimeout(idleTimer);
|
|
62
143
|
idleTimer = null;
|
|
63
144
|
}
|
|
64
145
|
function startIdle() {
|
|
65
|
-
if (
|
|
66
|
-
allowed() &&
|
|
67
|
-
!resting &&
|
|
68
|
-
!dragging &&
|
|
69
|
-
!current &&
|
|
70
|
-
!unsupported &&
|
|
71
|
-
Object.hasOwn(clips, 'idle') &&
|
|
72
|
-
!failed.has('idle')
|
|
73
|
-
)
|
|
74
|
-
play('idle');
|
|
146
|
+
if (canIdle() && !current) play('idle');
|
|
75
147
|
}
|
|
76
148
|
function scheduleIdle(delay = 0) {
|
|
77
149
|
clearIdleTimer();
|
|
78
|
-
if (!
|
|
150
|
+
if (!canIdle()) return;
|
|
79
151
|
idleTimer = host.setTimeout(() => {
|
|
80
152
|
idleTimer = null;
|
|
81
153
|
startIdle();
|
|
82
154
|
}, delay);
|
|
83
155
|
}
|
|
84
|
-
function dispose(
|
|
156
|
+
function dispose(run) {
|
|
157
|
+
if (!run || run.disposed) return;
|
|
158
|
+
run.disposed = true;
|
|
159
|
+
host.clearTimeout(run.timer);
|
|
160
|
+
const video = run.video;
|
|
161
|
+
if (!video) return;
|
|
85
162
|
video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
|
|
86
163
|
video.pause();
|
|
87
164
|
video.removeAttribute('src');
|
|
88
165
|
video.load();
|
|
89
166
|
video.remove();
|
|
90
167
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
168
|
+
function flushRetired() {
|
|
169
|
+
for (const [run, timer] of retired) {
|
|
170
|
+
host.clearTimeout(timer);
|
|
171
|
+
dispose(run);
|
|
172
|
+
}
|
|
173
|
+
retired.clear();
|
|
174
|
+
}
|
|
175
|
+
function retire(run, fade) {
|
|
176
|
+
if (!run?.video || !fade || !allowed()) return dispose(run);
|
|
96
177
|
host.clearTimeout(run.timer);
|
|
178
|
+
run.video.onplaying =
|
|
179
|
+
run.video.onended =
|
|
180
|
+
run.video.onerror =
|
|
181
|
+
run.video.onwaiting =
|
|
182
|
+
run.video.onstalled =
|
|
183
|
+
null;
|
|
184
|
+
run.video.pause();
|
|
185
|
+
run.video.style.opacity = '0';
|
|
186
|
+
const timer = host.setTimeout(() => {
|
|
187
|
+
retired.delete(run);
|
|
188
|
+
dispose(run);
|
|
189
|
+
}, 150);
|
|
190
|
+
retired.set(run, timer);
|
|
191
|
+
}
|
|
192
|
+
function settleVisibleFrame() {
|
|
193
|
+
if (!displayed?.video) {
|
|
194
|
+
if (poster?.style) {
|
|
195
|
+
poster.style.transition = 'none';
|
|
196
|
+
poster.style.opacity = '1';
|
|
197
|
+
poster.getBoundingClientRect?.();
|
|
198
|
+
poster.style.opacity = '';
|
|
199
|
+
poster.style.transition = '';
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
displayed.video.style.transition = 'none';
|
|
204
|
+
displayed.video.style.opacity = '1';
|
|
205
|
+
if (poster?.style) {
|
|
206
|
+
poster.style.transition = 'none';
|
|
207
|
+
poster.style.opacity = '0';
|
|
208
|
+
}
|
|
209
|
+
displayed.video.getBoundingClientRect?.();
|
|
210
|
+
displayed.video.style.transition = '';
|
|
211
|
+
if (poster?.style) {
|
|
212
|
+
poster.style.opacity = '';
|
|
213
|
+
poster.style.transition = '';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function stopMedia(fade = false) {
|
|
217
|
+
if (!fade && poster?.style) poster.style.transition = 'none';
|
|
218
|
+
clearIdleTimer();
|
|
219
|
+
flushRetired();
|
|
220
|
+
const pending = current,
|
|
221
|
+
visible = displayed;
|
|
222
|
+
current = displayed = null;
|
|
97
223
|
delete element.dataset.videoPlaying;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
retired.delete(video);
|
|
107
|
-
dispose(video);
|
|
108
|
-
}, 150);
|
|
109
|
-
retired.set(video, timer);
|
|
110
|
-
} else dispose(video);
|
|
224
|
+
delete element.dataset.videoLoading;
|
|
225
|
+
delete element.dataset.action;
|
|
226
|
+
if (pending && pending !== visible) dispose(pending);
|
|
227
|
+
retire(visible, fade);
|
|
228
|
+
if (!fade && poster?.style) {
|
|
229
|
+
poster.getBoundingClientRect?.();
|
|
230
|
+
poster.style.transition = '';
|
|
231
|
+
}
|
|
111
232
|
}
|
|
112
233
|
function fallback(run, broken = false) {
|
|
113
234
|
if (current !== run) return;
|
|
114
235
|
if (broken) failed.add(run.action);
|
|
115
|
-
|
|
116
|
-
|
|
236
|
+
stopMedia();
|
|
237
|
+
notify(unsupported ? 'unsupported' : 'unavailable', run.action);
|
|
238
|
+
if (allowed() && run.action !== 'idle' && useFallback) {
|
|
117
239
|
base.play(run.action);
|
|
118
240
|
scheduleIdle(1400);
|
|
119
241
|
}
|
|
120
242
|
}
|
|
121
243
|
function play(action) {
|
|
122
244
|
if (!allowed() || dragging || element.dataset.action === 'drag') return false;
|
|
123
|
-
if (action === 'idle' && (resting || current || unsupported))
|
|
245
|
+
if (action === 'idle' && (resting || (current && !options.interruptible) || unsupported))
|
|
246
|
+
return false;
|
|
124
247
|
if (
|
|
125
248
|
current &&
|
|
126
249
|
(action === 'look' ||
|
|
127
250
|
current.action === action ||
|
|
128
|
-
(current.action === 'celebrate' && action === 'greet'))
|
|
251
|
+
(!options.interruptible && current.action === 'celebrate' && action === 'greet'))
|
|
129
252
|
)
|
|
130
253
|
return false;
|
|
131
254
|
if (!Object.hasOwn(clips, action) || failed.has(action) || unsupported) {
|
|
132
255
|
if (action === 'idle') return false;
|
|
133
|
-
if (current?.action === 'idle')
|
|
134
|
-
const played = base.play(action);
|
|
256
|
+
if (current?.action === 'idle') stopMedia();
|
|
257
|
+
const played = useFallback && base.play(action);
|
|
135
258
|
if (played) scheduleIdle(1400);
|
|
136
|
-
|
|
259
|
+
if (!played) notify(unsupported ? 'unsupported' : 'unavailable', action);
|
|
260
|
+
return !!played;
|
|
137
261
|
}
|
|
138
|
-
|
|
262
|
+
clearIdleTimer();
|
|
263
|
+
settleVisibleFrame();
|
|
264
|
+
flushRetired();
|
|
265
|
+
if (current && current !== displayed) dispose(current);
|
|
139
266
|
base.release(true);
|
|
140
|
-
const run = { action, video: null, timer: null };
|
|
267
|
+
const run = { action, video: null, timer: null, disposed: false };
|
|
141
268
|
current = run;
|
|
142
|
-
element.dataset.action = action;
|
|
269
|
+
element.dataset.action = displayed?.action || action;
|
|
270
|
+
element.dataset.videoLoading = action;
|
|
271
|
+
notify('loading', action);
|
|
143
272
|
run.timer = host.setTimeout(() => fallback(run, true), 9000);
|
|
144
|
-
|
|
273
|
+
petVideoAlphaSupported(host, probeSource).then((supported) => {
|
|
145
274
|
if (current !== run || !allowed()) return;
|
|
146
|
-
if (
|
|
147
|
-
unsupported =
|
|
275
|
+
if (supported !== true) {
|
|
276
|
+
unsupported = supported === false;
|
|
148
277
|
return fallback(run);
|
|
149
278
|
}
|
|
150
279
|
const video = document.createElement('video');
|
|
151
280
|
run.video = video;
|
|
152
|
-
video.muted = true;
|
|
153
|
-
video.defaultMuted = true;
|
|
281
|
+
video.muted = video.defaultMuted = true;
|
|
154
282
|
video.playsInline = true;
|
|
155
283
|
video.loop = action === 'idle';
|
|
156
284
|
video.disablePictureInPicture = true;
|
|
157
285
|
video.preload = 'auto';
|
|
286
|
+
video.style.opacity = '0';
|
|
158
287
|
video.setAttribute('aria-hidden', 'true');
|
|
159
288
|
video.setAttribute('playsinline', '');
|
|
289
|
+
video.setAttribute('tabindex', '-1');
|
|
160
290
|
video.onplaying = () => {
|
|
161
291
|
if (current !== run || !allowed()) return;
|
|
162
292
|
if (!Number.isFinite(video.duration) || video.duration <= 0 || video.duration > 15)
|
|
@@ -166,7 +296,14 @@ export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
|
166
296
|
action === 'idle'
|
|
167
297
|
? null
|
|
168
298
|
: host.setTimeout(() => fallback(run, true), video.duration * 1000 + 2000);
|
|
299
|
+
const previous = displayed;
|
|
300
|
+
displayed = run;
|
|
301
|
+
video.style.opacity = '1';
|
|
302
|
+
element.dataset.action = action;
|
|
169
303
|
element.dataset.videoPlaying = 'true';
|
|
304
|
+
delete element.dataset.videoLoading;
|
|
305
|
+
if (previous !== run) retire(previous, true);
|
|
306
|
+
notify('playing', action);
|
|
170
307
|
};
|
|
171
308
|
video.onwaiting = video.onstalled = () => {
|
|
172
309
|
if (current !== run) return;
|
|
@@ -174,9 +311,15 @@ export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
|
174
311
|
run.timer = host.setTimeout(() => fallback(run, true), 4000);
|
|
175
312
|
};
|
|
176
313
|
video.onended = () => {
|
|
177
|
-
if (current
|
|
178
|
-
|
|
314
|
+
if (current !== run) return;
|
|
315
|
+
host.clearTimeout(run.timer);
|
|
316
|
+
if (canIdle()) {
|
|
317
|
+
current = null;
|
|
318
|
+
if (displayed !== run) dispose(run);
|
|
179
319
|
scheduleIdle();
|
|
320
|
+
} else {
|
|
321
|
+
stopMedia(true);
|
|
322
|
+
notify('idle');
|
|
180
323
|
}
|
|
181
324
|
};
|
|
182
325
|
video.onerror = () => fallback(run, true);
|
|
@@ -190,6 +333,11 @@ export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
|
190
333
|
});
|
|
191
334
|
return true;
|
|
192
335
|
}
|
|
336
|
+
function stop() {
|
|
337
|
+
stopMedia();
|
|
338
|
+
base.release(true);
|
|
339
|
+
notify('idle');
|
|
340
|
+
}
|
|
193
341
|
const visibility = () => {
|
|
194
342
|
if (!allowed()) stop();
|
|
195
343
|
else scheduleIdle();
|
|
@@ -198,16 +346,22 @@ export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
|
198
346
|
reduced?.addEventListener?.('change', visibility);
|
|
199
347
|
return {
|
|
200
348
|
play,
|
|
349
|
+
stop,
|
|
350
|
+
retry(action) {
|
|
351
|
+
failed.delete(action);
|
|
352
|
+
return play(action);
|
|
353
|
+
},
|
|
201
354
|
configure(next) {
|
|
202
|
-
if (!next.enabled || (next.resting && !resting))
|
|
355
|
+
if (!next.enabled || (next.resting && !resting)) stopMedia();
|
|
203
356
|
enabled = next.enabled;
|
|
204
357
|
resting = next.resting;
|
|
205
358
|
base.configure(next);
|
|
359
|
+
if (!enabled) notify('idle');
|
|
206
360
|
startIdle();
|
|
207
361
|
},
|
|
208
362
|
drag(direction) {
|
|
209
363
|
dragging = true;
|
|
210
|
-
|
|
364
|
+
stopMedia();
|
|
211
365
|
base.drag(direction);
|
|
212
366
|
},
|
|
213
367
|
release(cancelled) {
|
|
@@ -217,12 +371,7 @@ export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
|
217
371
|
},
|
|
218
372
|
destroy() {
|
|
219
373
|
destroyed = true;
|
|
220
|
-
|
|
221
|
-
for (const [video, timer] of retired) {
|
|
222
|
-
host.clearTimeout(timer);
|
|
223
|
-
dispose(video);
|
|
224
|
-
}
|
|
225
|
-
retired.clear();
|
|
374
|
+
stopMedia();
|
|
226
375
|
document.removeEventListener('visibilitychange', visibility);
|
|
227
376
|
reduced?.removeEventListener?.('change', visibility);
|
|
228
377
|
base.destroy();
|
package/pet-view.js
CHANGED
|
@@ -33,6 +33,8 @@ export const PET_MODELS = Object.freeze({
|
|
|
33
33
|
'xiaoxie-original': { ...PET_MODEL, widthScale: 1, heightScale: 1.1, label: 'petDroplet' },
|
|
34
34
|
});
|
|
35
35
|
export function petModel(id) {
|
|
36
|
+
if (id === 'custom-image')
|
|
37
|
+
return { id, renderer: 'image', widthScale: 1.3125, heightScale: 1.75, label: 'petCharacter' };
|
|
36
38
|
return Object.hasOwn(PET_MODELS, id) ? PET_MODELS[id] : PET_MODELS['whale-dress'];
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -42,6 +44,8 @@ export function petLayout(
|
|
|
42
44
|
size = 132,
|
|
43
45
|
contentHeight = 420,
|
|
44
46
|
modelId = 'xiaoxie-original',
|
|
47
|
+
footerHeight = 30,
|
|
48
|
+
footerWidth = 148,
|
|
45
49
|
) {
|
|
46
50
|
const model = petModel(modelId);
|
|
47
51
|
const originX = viewport.left ?? 0,
|
|
@@ -55,14 +59,14 @@ export function petLayout(
|
|
|
55
59
|
Math.min(
|
|
56
60
|
size,
|
|
57
61
|
Math.max(1, (width - edge * 2) / model.widthScale),
|
|
58
|
-
Math.max(1, Math.floor((height - edge * 2 -
|
|
62
|
+
Math.max(1, Math.floor((height - edge * 2 - footerHeight) / model.heightScale)),
|
|
59
63
|
),
|
|
60
64
|
);
|
|
61
65
|
const petWidth = Math.min(
|
|
62
66
|
Math.max(1, width - edge * 2),
|
|
63
|
-
Math.max(
|
|
67
|
+
Math.max(footerWidth, artworkSize * model.widthScale),
|
|
64
68
|
);
|
|
65
|
-
const petHeight = Math.ceil(artworkSize * model.heightScale) +
|
|
69
|
+
const petHeight = Math.ceil(artworkSize * model.heightScale) + footerHeight;
|
|
66
70
|
const bound = (n, max) => Math.max(Math.min(edge, max), Math.min(n, max));
|
|
67
71
|
const x = bound(
|
|
68
72
|
Number.isFinite(position?.x) ? position.x - originX : width - petWidth - 32,
|
|
@@ -253,6 +257,10 @@ export function petArtwork(
|
|
|
253
257
|
export const PET_MESSAGES = {
|
|
254
258
|
zh: {
|
|
255
259
|
petGreet: '与角色互动',
|
|
260
|
+
petNeedsSession: '等待会话',
|
|
261
|
+
petNeedsSetup: '请配置作息',
|
|
262
|
+
petLoadFailed: '读取失败',
|
|
263
|
+
petRetryHint: '右键菜单可重新读取设置',
|
|
256
264
|
petCharacter: '角色形象',
|
|
257
265
|
petWhaleActions: '鲸蓝少女 · 长裙动态',
|
|
258
266
|
petWhaleOrnateActions: '鲸蓝少女 · 短裙动态',
|
|
@@ -266,9 +274,29 @@ export const PET_MESSAGES = {
|
|
|
266
274
|
petHint: '左键互动 · 右键菜单 · 拖动移动',
|
|
267
275
|
petMode: '宠物模式',
|
|
268
276
|
petClock: '下班倒计时',
|
|
277
|
+
petPreviewActions: '动作试播',
|
|
278
|
+
petPreviewIdle: '待机',
|
|
279
|
+
petPreviewGreet: '挥手',
|
|
280
|
+
petPreviewDrink: '喝茶',
|
|
281
|
+
petPreviewStop: '停止',
|
|
282
|
+
petTryIdle: '试播待机',
|
|
283
|
+
petTryGreet: '试播挥手',
|
|
284
|
+
petTryDrink: '试播喝茶',
|
|
285
|
+
petStopPreview: '停止试播',
|
|
286
|
+
petPreviewHint: '仅预览,不记喝水次数,也不更换页面角色。',
|
|
287
|
+
petPreviewLoading: '正在准备动作…',
|
|
288
|
+
petPreviewPlaying: '正在试播',
|
|
289
|
+
petPreviewUnsupported: '当前浏览器不支持透明动作视频,已显示静态预览。',
|
|
290
|
+
petPreviewUnavailable: '动作暂时无法播放,已恢复静态预览。可以重试。',
|
|
291
|
+
petPreviewEnable: '勾选“角色动作反馈”后可试播;应用后才保存。',
|
|
292
|
+
petPreviewReduced: '系统已开启减少动态效果,当前显示静态预览。',
|
|
269
293
|
},
|
|
270
294
|
en: {
|
|
271
295
|
petGreet: 'Interact with character',
|
|
296
|
+
petNeedsSession: 'Open a session',
|
|
297
|
+
petNeedsSetup: 'Set your schedule',
|
|
298
|
+
petLoadFailed: 'Settings unavailable',
|
|
299
|
+
petRetryHint: 'Retry loading settings from the context menu',
|
|
272
300
|
petCharacter: 'Character',
|
|
273
301
|
petWhaleActions: 'Blue whale · Animated long dress',
|
|
274
302
|
petWhaleOrnateActions: 'Blue whale · Animated short dress',
|
|
@@ -282,6 +310,23 @@ export const PET_MESSAGES = {
|
|
|
282
310
|
petHint: 'Click to interact · right-click for menu · drag to move',
|
|
283
311
|
petMode: 'Pet mode',
|
|
284
312
|
petClock: 'Shift countdown',
|
|
313
|
+
petPreviewActions: 'Preview actions',
|
|
314
|
+
petPreviewIdle: 'Idle',
|
|
315
|
+
petPreviewGreet: 'Wave',
|
|
316
|
+
petPreviewDrink: 'Drink',
|
|
317
|
+
petPreviewStop: 'Stop',
|
|
318
|
+
petTryIdle: 'Preview idle',
|
|
319
|
+
petTryGreet: 'Preview wave',
|
|
320
|
+
petTryDrink: 'Preview drinking',
|
|
321
|
+
petStopPreview: 'Stop preview',
|
|
322
|
+
petPreviewHint: 'Preview only: no drinking record or change to the page character.',
|
|
323
|
+
petPreviewLoading: 'Preparing action…',
|
|
324
|
+
petPreviewPlaying: 'Previewing',
|
|
325
|
+
petPreviewUnsupported:
|
|
326
|
+
'This browser cannot show transparent action videos. Showing a static preview.',
|
|
327
|
+
petPreviewUnavailable: 'Action unavailable. Static preview restored; you can retry.',
|
|
328
|
+
petPreviewEnable: 'Enable character reactions to preview. Apply to save the choice.',
|
|
329
|
+
petPreviewReduced: 'Reduced motion is enabled. Showing a static preview.',
|
|
285
330
|
},
|
|
286
331
|
};
|
|
287
332
|
|
|
@@ -291,8 +336,9 @@ export const PET_STYLE = `
|
|
|
291
336
|
.owc-v2 button.owc-pet-handle{padding:0;background:none!important;border-radius:50%;min-height:0;display:block;cursor:grab;touch-action:none;line-height:0}
|
|
292
337
|
.owc-v2 button.owc-pet-handle:active{cursor:grabbing}
|
|
293
338
|
.owc-pet-art{overflow:visible;pointer-events:none;filter:drop-shadow(0 3px 5px #32557418)}
|
|
294
|
-
.owc-pet-video-art{display:block;position:relative}
|
|
339
|
+
.owc-pet-video-art{display:block;position:relative;isolation:isolate}
|
|
295
340
|
.owc-pet-video-poster,.owc-pet-video-layer,.owc-pet-video-layer video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none}
|
|
341
|
+
.owc-pet-video-poster,.owc-pet-video-layer video{mix-blend-mode:plus-lighter}
|
|
296
342
|
.owc-pet-video-poster{opacity:1;transition:opacity .12s linear}
|
|
297
343
|
.owc-pet-video-layer video{opacity:0;transition:opacity .12s linear}
|
|
298
344
|
.owc-pet-video-art[data-video-playing=true] .owc-pet-video-poster{opacity:0}
|
|
@@ -309,6 +355,27 @@ export const PET_STYLE = `
|
|
|
309
355
|
.owc-v2 .owc-pet-clock{display:flex;align-items:center;color:var(--owc-text);line-height:1.5;cursor:default;padding:4px 10px;min-height:26px;gap:5px;white-space:nowrap;border-radius:999px;font-size:11px;background:var(--dsw-alias-bg-overlay);border:1px solid color-mix(in srgb,var(--owc-accent) 25%,transparent);box-shadow:0 3px 12px #142e4812;font-variant-numeric:tabular-nums;max-width:100%;box-sizing:border-box;overflow:hidden}
|
|
310
356
|
.owc-pet-clock-label{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
311
357
|
.owc-pet-clock strong{flex:none}
|
|
358
|
+
.owc-v2 .owc-pet-timers{width:196px;max-width:100%;margin-top:4px;box-sizing:border-box;border:1px solid var(--owc-border);border-radius:13px;background:var(--owc-surface);box-shadow:var(--owc-shadow);overflow:hidden;font-variant-numeric:tabular-nums}
|
|
359
|
+
.owc-v2 .owc-pet-timers .owc-pet-clock{width:100%;height:34px;min-height:0;padding:5px 10px;gap:6px;border:0;border-radius:0;background:transparent;box-shadow:none;font-size:10px;color:var(--owc-muted)}
|
|
360
|
+
.owc-pet-timers .owc-pet-clock svg{flex-shrink:0}
|
|
361
|
+
.owc-pet-timers .owc-pet-clock strong{margin-left:auto;font-size:16px;line-height:1;color:var(--owc-text);letter-spacing:.2px}
|
|
362
|
+
.owc-pet-timer-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);border-top:1px solid var(--owc-border)}
|
|
363
|
+
.owc-v2 .owc-pet-timer-row{display:flex;align-items:center;justify-content:center;gap:5px;box-sizing:border-box;min-width:0;height:28px;padding:3px 6px;font-size:11px;white-space:nowrap;color:var(--owc-muted)}
|
|
364
|
+
.owc-pet-timer-row+.owc-pet-timer-row{border-left:1px solid var(--owc-border)}
|
|
365
|
+
.owc-pet-timer-label{display:flex;align-items:center;flex-shrink:0}
|
|
366
|
+
.owc-pet-timer-label svg{flex-shrink:0}
|
|
367
|
+
.owc-pet-timer-row strong{min-width:0;overflow:hidden;text-overflow:ellipsis;font-weight:550;color:var(--owc-text)}
|
|
368
|
+
.owc-timer-pause{font-size:9px;margin-right:3px;color:var(--owc-muted)}
|
|
369
|
+
.owc-v2 .owc-pet-timer-row[data-countdown-due=true]{background:color-mix(in srgb,var(--owc-accent) 8%,transparent);color:color-mix(in srgb,var(--owc-accent) 55%,var(--owc-text))}
|
|
370
|
+
.owc-pet-timer-row[data-countdown-due=true] strong{color:inherit}
|
|
371
|
+
.owc-v2 [data-countdown-due=true]{color:var(--owc-accent);border-color:var(--owc-accent)}
|
|
372
|
+
.owc-countdown-card{padding:16px;display:flex;flex-direction:column;gap:14px}
|
|
373
|
+
.owc-countdown-value{font-size:32px;line-height:1.3;font-weight:650;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
|
374
|
+
.owc-countdown-actions{display:flex;flex-wrap:wrap;gap:8px}
|
|
375
|
+
.owc-countdown-actions button{flex:1;min-width:110px;min-height:40px}
|
|
376
|
+
.owc-countdown-card label{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px}
|
|
377
|
+
.owc-v2 .owc-countdown-card select{flex:0 0 120px;width:120px;max-width:120px}
|
|
378
|
+
.owc-countdown-card p{margin:0;font-size:12px;line-height:1.6;overflow-wrap:anywhere}
|
|
312
379
|
.owc-pet-panel{position:fixed;border-radius:20px;cursor:default;touch-action:manipulation;max-width:calc(100vw - 24px)}
|
|
313
380
|
.owc-pet-panel>.owc-content{max-height:inherit}
|
|
314
381
|
.owc-pet-panel .owc-glass{opacity:max(var(--owc-alpha),.94)}
|