dsh-offwork 1.0.1 → 1.1.1
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 +19 -0
- package/README.md +38 -20
- package/assets/animations/NOTICE.md +15 -0
- package/assets/animations/alpha-probe.webm +0 -0
- package/assets/animations/ornate-drink.webm +0 -0
- package/assets/animations/ornate-greet.webm +0 -0
- package/assets/animations/ornate-idle.webm +0 -0
- package/assets/animations/whale-drink.webm +0 -0
- package/assets/animations/whale-greet.webm +0 -0
- package/assets/characters/NOTICE.md +11 -0
- package/assets/characters/whale-actions.webp +0 -0
- package/assets/characters/whale-dress.webp +0 -0
- package/assets/characters/whale-ornate-actions.webp +0 -0
- package/assets/characters/whale-ornate.webp +0 -0
- package/client.js +4033 -188
- package/client.source.js +2449 -214
- package/companion.js +185 -0
- package/hydration-store.js +101 -0
- package/hydration.js +303 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -2
- package/pet-interactions.js +138 -0
- package/pet-motion.js +110 -0
- package/pet-surfaces.js +194 -0
- package/pet-video.js +231 -0
- package/pet-view.js +323 -0
- package/tests/companion.test.mjs +86 -0
- package/tests/hydration-extras.test.mjs +226 -0
- package/tests/hydration-store.test.mjs +287 -0
- package/tests/hydration.test.mjs +211 -0
- package/tests/pet-character.test.mjs +62 -0
- package/tests/pet-interactions.test.mjs +88 -0
- package/tests/pet-motion.test.mjs +139 -0
- package/tests/pet-surfaces.test.mjs +128 -0
- package/tests/pet-video.test.mjs +337 -0
- package/tests/pet-view.test.mjs +48 -0
- package/tests/ui.test.mjs +22 -2
- package/tools/build.mjs +62 -6
- package/tools/check-package.mjs +20 -0
- package/ui-utils.js +22 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Ephemeral UI navigation only. Business data stays in its existing stores.
|
|
2
|
+
export const PET_EDITOR_SCREENS = ['water', 'schedule', 'appearance'];
|
|
3
|
+
export const initialPetUI = { screen: null, back: null, menu: null, pending: null };
|
|
4
|
+
export function petUIReducer(state, action) {
|
|
5
|
+
switch (action.type) {
|
|
6
|
+
case 'menu':
|
|
7
|
+
return { ...state, menu: action.position };
|
|
8
|
+
case 'dismiss-menu':
|
|
9
|
+
return { ...state, menu: null };
|
|
10
|
+
case 'stay':
|
|
11
|
+
return { ...state, pending: null };
|
|
12
|
+
case 'navigate': {
|
|
13
|
+
if (![null, 'home', 'income', 'earnings', ...PET_EDITOR_SCREENS].includes(action.screen))
|
|
14
|
+
return state;
|
|
15
|
+
if (state.screen === action.screen)
|
|
16
|
+
return { ...state, back: action.root ? null : state.back, menu: null, pending: null };
|
|
17
|
+
if (action.dirty && !action.discard)
|
|
18
|
+
return {
|
|
19
|
+
...state,
|
|
20
|
+
menu: null,
|
|
21
|
+
pending: { screen: action.screen, ...(action.root ? { root: true } : {}) },
|
|
22
|
+
};
|
|
23
|
+
const back = action.root
|
|
24
|
+
? null
|
|
25
|
+
: PET_EDITOR_SCREENS.includes(action.screen)
|
|
26
|
+
? PET_EDITOR_SCREENS.includes(state.screen)
|
|
27
|
+
? state.back
|
|
28
|
+
: state.screen
|
|
29
|
+
: state.back;
|
|
30
|
+
return { screen: action.screen, back, menu: null, pending: null };
|
|
31
|
+
}
|
|
32
|
+
default:
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function appearanceValues(view) {
|
|
37
|
+
return {
|
|
38
|
+
companion: view.companion,
|
|
39
|
+
companionMotion: view.companionMotion,
|
|
40
|
+
petSize: view.petSize,
|
|
41
|
+
petModel: view.petModel,
|
|
42
|
+
opacity: view.opacity,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export const PET_INTERACTION_MESSAGES = {
|
|
46
|
+
zh: {
|
|
47
|
+
petHome: '喝水',
|
|
48
|
+
petWaterCard: '喝水',
|
|
49
|
+
petCountdownCard: '下班倒计时',
|
|
50
|
+
petEarningsCard: '收入',
|
|
51
|
+
petWaterAlert: '喝水提醒已到,可从右键菜单打开喝水卡片',
|
|
52
|
+
petPausedHint: '本班已暂停,可手动恢复。',
|
|
53
|
+
petMenu: '宠物菜单',
|
|
54
|
+
petBack: '返回',
|
|
55
|
+
petCloseAll: '关闭面板',
|
|
56
|
+
petWorkSettings: '作息与薪资',
|
|
57
|
+
petWaterSettings: '提醒设置',
|
|
58
|
+
petAppearance: '外观',
|
|
59
|
+
petRecord: '喝了一杯',
|
|
60
|
+
petQuiet30: '免打扰 30 分钟',
|
|
61
|
+
petQuiet60: '免打扰 1 小时',
|
|
62
|
+
petUnsaved: '有未保存的修改',
|
|
63
|
+
petUnsavedHint: '保存后继续,或放弃这次修改。',
|
|
64
|
+
petSaveContinue: '保存并继续',
|
|
65
|
+
petDiscard: '放弃修改',
|
|
66
|
+
petKeepEditing: '继续编辑',
|
|
67
|
+
petApplyAppearance: '应用外观',
|
|
68
|
+
petAppearanceHint: '大小和透明度可实时预览。应用后保存;取消会还原。',
|
|
69
|
+
petQuietActive: '喝水提醒已静音',
|
|
70
|
+
petHomeHint: '喝水、歇一会儿,都在这里。',
|
|
71
|
+
petQuietHint: '按自然时间,到期自动恢复。',
|
|
72
|
+
petResume: '恢复本班提醒',
|
|
73
|
+
petDismissReminder: '稍后处理',
|
|
74
|
+
},
|
|
75
|
+
en: {
|
|
76
|
+
petHome: 'Water',
|
|
77
|
+
petWaterCard: 'Water',
|
|
78
|
+
petCountdownCard: 'Shift countdown',
|
|
79
|
+
petEarningsCard: 'Earnings',
|
|
80
|
+
petWaterAlert: 'Water is due; open Water from the context menu',
|
|
81
|
+
petPausedHint: 'Paused for this shift. Resume when ready.',
|
|
82
|
+
petMenu: 'Pet menu',
|
|
83
|
+
petBack: 'Back',
|
|
84
|
+
petCloseAll: 'Close panel',
|
|
85
|
+
petWorkSettings: 'Schedule and pay',
|
|
86
|
+
petWaterSettings: 'Reminders',
|
|
87
|
+
petAppearance: 'Appearance',
|
|
88
|
+
petRecord: 'Had a glass',
|
|
89
|
+
petQuiet30: 'Quiet for 30 minutes',
|
|
90
|
+
petQuiet60: 'Quiet for 1 hour',
|
|
91
|
+
petUnsaved: 'Unsaved changes',
|
|
92
|
+
petUnsavedHint: 'Save to continue, or discard these changes.',
|
|
93
|
+
petSaveContinue: 'Save and continue',
|
|
94
|
+
petDiscard: 'Discard changes',
|
|
95
|
+
petKeepEditing: 'Keep editing',
|
|
96
|
+
petApplyAppearance: 'Apply appearance',
|
|
97
|
+
petAppearanceHint:
|
|
98
|
+
'Preview size and opacity now. Apply to save; Cancel restores the previous appearance.',
|
|
99
|
+
petQuietActive: 'Water reminders are muted',
|
|
100
|
+
petHomeHint: 'A drink or a little break, right here.',
|
|
101
|
+
petQuietHint: 'Uses clock time and resumes automatically.',
|
|
102
|
+
petResume: 'Resume reminders',
|
|
103
|
+
petDismissReminder: 'Handle later',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
export const PET_INTERACTION_STYLE = `
|
|
107
|
+
.owc-pet-workspace .owc-glass{opacity:var(--owc-alpha)}
|
|
108
|
+
.owc-pet-water-card.owc-water-due{border-radius:14px}
|
|
109
|
+
.owc-pet-water-alert{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 12px;border-radius:12px;cursor:default;position:fixed!important;width:140px;box-sizing:border-box;min-height:30px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
|
|
110
|
+
.owc-pet-panel.owc-pet-workspace{display:flex;flex-direction:column;overflow:hidden}
|
|
111
|
+
.owc-pet-workspace>.owc-head{position:relative;flex:none;min-height:46px;padding:8px 12px;box-sizing:border-box}
|
|
112
|
+
.owc-pet-workspace>.owc-pet-scroll{position:relative;min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;scrollbar-width:none}
|
|
113
|
+
.owc-pet-scroll::-webkit-scrollbar{display:none}
|
|
114
|
+
.owc-pet-scroll .owc-save{position:sticky;bottom:0;background:var(--dsw-alias-bg-overlay);padding:10px 0;margin-bottom:0;z-index:2}
|
|
115
|
+
.owc-pet-scroll .owc-appearance legend{display:none}
|
|
116
|
+
.owc-pet-home{padding:14px}
|
|
117
|
+
.owc-pet-home-clock{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:12px}
|
|
118
|
+
.owc-pet-home-clock strong{font-size:24px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}
|
|
119
|
+
.owc-pet-water-card{padding:12px;border:1px solid color-mix(in srgb,var(--owc-accent) 16%,transparent);border-radius:14px;background:color-mix(in srgb,var(--owc-accent) 4%,transparent)}
|
|
120
|
+
.owc-pet-water-card .owc-water-status{font-size:12px;font-variant-numeric:tabular-nums;height:34px;display:flex;align-items:center;overflow:hidden;margin:0 0 9px}
|
|
121
|
+
.owc-pet-water-row{display:flex;align-items:center;justify-content:space-between;font-size:11px;gap:10px}
|
|
122
|
+
.owc-pet-water-row .owc-primary{flex:1;min-height:40px}
|
|
123
|
+
.owc-pet-quiet-row{display:flex;gap:6px;margin-top:12px}
|
|
124
|
+
.owc-pet-quiet-row button{flex:1;border:1px solid color-mix(in srgb,var(--owc-accent) 18%,transparent);font-size:11px;min-height:34px}
|
|
125
|
+
.owc-pet-quiet-status{min-height:32px;display:flex;align-items:center;justify-content:space-between;font-size:10.5px;gap:5px;color:var(--dsw-alias-label-secondary)}
|
|
126
|
+
.owc-pet-feedback{height:32px;overflow:hidden;display:flex;align-items:center;margin:6px 0 4px;font-size:11px;line-height:1.5;color:var(--owc-accent)}
|
|
127
|
+
.owc-pet-home-links{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px}
|
|
128
|
+
.owc-pet-home-links button{font-size:11px;min-height:32px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,transparent)}
|
|
129
|
+
.owc-pet-context{position:fixed;z-index:3;padding:6px;border:1px solid color-mix(in srgb,var(--owc-accent) 18%,transparent);background:var(--dsw-alias-bg-overlay);border-radius:12px;box-shadow:0 10px 28px #132c4626;box-sizing:border-box;overflow:auto;cursor:default;touch-action:manipulation}
|
|
130
|
+
.owc-pet-context button{display:flex;width:100%;justify-content:flex-start;min-height:34px;font-size:12px;padding:7px 10px}
|
|
131
|
+
.owc-pet-context button:focus-visible{outline-offset:-2px}
|
|
132
|
+
.owc-pet-context hr{border:0;border-top:1px solid color-mix(in srgb,var(--owc-accent) 12%,transparent);margin:5px 3px}
|
|
133
|
+
.owc-pet-unsaved{padding:20px 16px;line-height:1.6}
|
|
134
|
+
.owc-pet-unsaved h3{font-size:15px;margin:0 0 8px}
|
|
135
|
+
.owc-pet-unsaved p{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
136
|
+
.owc-pet-unsaved button{width:100%;margin-top:8px;min-height:36px}
|
|
137
|
+
.owc-pet-toast{position:fixed;width:208px;padding:9px 12px;box-sizing:border-box;border-radius:12px;background:var(--dsw-alias-bg-overlay);border:1px solid color-mix(in srgb,var(--owc-accent) 18%,transparent);font-size:11px;line-height:1.5;box-shadow:0 5px 16px #18354b18;pointer-events:none}
|
|
138
|
+
`;
|
package/pet-motion.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Independent, decorative motion only. Never writes preferences or drinking records.
|
|
2
|
+
const PET_ACTIONS = {
|
|
3
|
+
look: { duration: 650, transforms: ['none', 'rotate(-1deg) translateY(-1px)', 'none'] },
|
|
4
|
+
greet: {
|
|
5
|
+
duration: 900,
|
|
6
|
+
transforms: ['none', 'translateY(2px) rotate(-2deg)', 'translateY(-4px) rotate(1deg)', 'none'],
|
|
7
|
+
},
|
|
8
|
+
celebrate: {
|
|
9
|
+
duration: 1200,
|
|
10
|
+
transforms: [
|
|
11
|
+
'none',
|
|
12
|
+
'translateY(-7px) rotate(-1deg)',
|
|
13
|
+
'translateY(1px)',
|
|
14
|
+
'translateY(-3px) rotate(1deg)',
|
|
15
|
+
'none',
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
settle: {
|
|
19
|
+
duration: 550,
|
|
20
|
+
transforms: ['none', 'translateY(2px) scaleY(.985)', 'translateY(-2px)', 'none'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createPetMotion(element, host, options = {}) {
|
|
25
|
+
const document = host.document;
|
|
26
|
+
const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
27
|
+
let enabled = options.enabled ?? true;
|
|
28
|
+
let resting = options.resting ?? false;
|
|
29
|
+
let animation = null;
|
|
30
|
+
let destroyed = false;
|
|
31
|
+
const allowed = () => !destroyed && enabled && !document.hidden && !reduced?.matches;
|
|
32
|
+
function stop() {
|
|
33
|
+
const previous = animation;
|
|
34
|
+
animation = null;
|
|
35
|
+
previous?.cancel();
|
|
36
|
+
delete element.dataset.action;
|
|
37
|
+
element.style.transform = '';
|
|
38
|
+
}
|
|
39
|
+
function sync() {
|
|
40
|
+
if (!allowed()) stop();
|
|
41
|
+
element.dataset.motion = allowed() && !resting ? 'idle' : 'off';
|
|
42
|
+
}
|
|
43
|
+
function play(action) {
|
|
44
|
+
if (!allowed() || !Object.hasOwn(PET_ACTIONS, action) || !element.animate) return false;
|
|
45
|
+
if (element.dataset.action === 'drag') return false;
|
|
46
|
+
if (action === 'look' && element.dataset.action) return false;
|
|
47
|
+
if (action === 'greet' && element.dataset.action === 'celebrate') return false;
|
|
48
|
+
stop();
|
|
49
|
+
const spec = PET_ACTIONS[action];
|
|
50
|
+
element.dataset.action = action;
|
|
51
|
+
try {
|
|
52
|
+
const running = element.animate(
|
|
53
|
+
spec.transforms.map((transform) => ({ transform })),
|
|
54
|
+
{
|
|
55
|
+
duration: spec.duration,
|
|
56
|
+
easing: 'ease-in-out',
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
animation = running;
|
|
60
|
+
running.finished.then(
|
|
61
|
+
() => {
|
|
62
|
+
if (animation === running) {
|
|
63
|
+
animation = null;
|
|
64
|
+
delete element.dataset.action;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
() => {
|
|
68
|
+
if (animation === running) {
|
|
69
|
+
animation = null;
|
|
70
|
+
delete element.dataset.action;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
stop();
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
document.addEventListener('visibilitychange', sync);
|
|
81
|
+
reduced?.addEventListener?.('change', sync);
|
|
82
|
+
sync();
|
|
83
|
+
return {
|
|
84
|
+
play,
|
|
85
|
+
configure(next) {
|
|
86
|
+
if (next.resting && !resting) stop();
|
|
87
|
+
enabled = next.enabled;
|
|
88
|
+
resting = next.resting;
|
|
89
|
+
sync();
|
|
90
|
+
},
|
|
91
|
+
drag(direction) {
|
|
92
|
+
if (!allowed()) return;
|
|
93
|
+
stop();
|
|
94
|
+
element.dataset.action = 'drag';
|
|
95
|
+
element.style.transform = `translateY(-2px) rotate(${direction < 0 ? -2 : 2}deg)`;
|
|
96
|
+
},
|
|
97
|
+
release(cancelled = false) {
|
|
98
|
+
const dragged = element.dataset.action === 'drag';
|
|
99
|
+
stop();
|
|
100
|
+
if (dragged && !cancelled) play('settle');
|
|
101
|
+
},
|
|
102
|
+
destroy() {
|
|
103
|
+
destroyed = true;
|
|
104
|
+
stop();
|
|
105
|
+
element.dataset.motion = 'off';
|
|
106
|
+
document.removeEventListener('visibilitychange', sync);
|
|
107
|
+
reduced?.removeEventListener?.('change', sync);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/pet-surfaces.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Shared layout for our own pet cards and context menu.
|
|
2
|
+
export function petViewport(host) {
|
|
3
|
+
const visual = host.visualViewport;
|
|
4
|
+
return {
|
|
5
|
+
width: Math.max(1, Math.min(host.innerWidth, visual?.width ?? host.innerWidth)),
|
|
6
|
+
height: Math.max(1, Math.min(host.innerHeight, visual?.height ?? host.innerHeight)),
|
|
7
|
+
left: Math.max(0, visual?.offsetLeft ?? 0),
|
|
8
|
+
top: Math.max(0, visual?.offsetTop ?? 0),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function petSurfaceWidth(screen, viewport) {
|
|
12
|
+
const ideal =
|
|
13
|
+
{
|
|
14
|
+
income: 320,
|
|
15
|
+
earnings: 320,
|
|
16
|
+
home: 336,
|
|
17
|
+
water: 360,
|
|
18
|
+
appearance: 360,
|
|
19
|
+
schedule: 400,
|
|
20
|
+
menu: 224,
|
|
21
|
+
reminder: 304,
|
|
22
|
+
}[screen] ?? 336;
|
|
23
|
+
return Math.max(1, Math.min(ideal, viewport.width - 24));
|
|
24
|
+
}
|
|
25
|
+
export function placePetSurface(
|
|
26
|
+
anchor,
|
|
27
|
+
viewport,
|
|
28
|
+
{ width, height, kind = 'card', previous = null, point = null, obstacles = [] },
|
|
29
|
+
) {
|
|
30
|
+
const edge = Math.min(
|
|
31
|
+
12,
|
|
32
|
+
Math.max(0, (viewport.width - 1) / 2),
|
|
33
|
+
Math.max(0, (viewport.height - 1) / 2),
|
|
34
|
+
),
|
|
35
|
+
gap = 12;
|
|
36
|
+
const left = (viewport.left ?? 0) + edge,
|
|
37
|
+
top = (viewport.top ?? 0) + edge;
|
|
38
|
+
const right = (viewport.left ?? 0) + viewport.width - edge;
|
|
39
|
+
const bottom = (viewport.top ?? 0) + viewport.height - edge;
|
|
40
|
+
const w = Math.max(1, Math.min(width, right - left));
|
|
41
|
+
const natural = Math.max(1, height),
|
|
42
|
+
available = Math.max(1, bottom - top);
|
|
43
|
+
const full = Math.min(natural, available);
|
|
44
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
45
|
+
const candidates = [];
|
|
46
|
+
const add = (x, y, h, side) => {
|
|
47
|
+
h = Math.max(1, Math.min(h, available));
|
|
48
|
+
x = clamp(x, left, right - w);
|
|
49
|
+
y = clamp(y, top, bottom - h);
|
|
50
|
+
const intersection = (rect) =>
|
|
51
|
+
Math.max(0, Math.min(x + w, rect.x + rect.width) - Math.max(x, rect.x)) *
|
|
52
|
+
Math.max(0, Math.min(y + h, rect.y + rect.height) - Math.max(y, rect.y));
|
|
53
|
+
const overlap =
|
|
54
|
+
intersection(anchor) +
|
|
55
|
+
obstacles.reduce(
|
|
56
|
+
(sum, rect) =>
|
|
57
|
+
sum + intersection({ x: rect.left, y: rect.top, width: rect.width, height: rect.height }),
|
|
58
|
+
0,
|
|
59
|
+
);
|
|
60
|
+
const distance = previous ? Math.abs(previous.left - x) + Math.abs(previous.top - y) : 0;
|
|
61
|
+
candidates.push({
|
|
62
|
+
left: x,
|
|
63
|
+
top: y,
|
|
64
|
+
width: w,
|
|
65
|
+
height: h,
|
|
66
|
+
maxHeight: h,
|
|
67
|
+
side: overlap ? 'overlay' : side,
|
|
68
|
+
overlap,
|
|
69
|
+
clipped: natural - h,
|
|
70
|
+
distance,
|
|
71
|
+
order: candidates.length,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
if (previous && Math.abs(previous.width - w) < 1)
|
|
75
|
+
add(previous.left, previous.top, full, previous.side);
|
|
76
|
+
if (kind === 'menu' && point) {
|
|
77
|
+
add(point.x + 8, point.y + 8, full, 'context-right');
|
|
78
|
+
add(point.x - w - 8, point.y + 8, full, 'context-left');
|
|
79
|
+
add(point.x + 8, point.y - full - 8, full, 'context-above-right');
|
|
80
|
+
add(point.x - w - 8, point.y - full - 8, full, 'context-above-left');
|
|
81
|
+
}
|
|
82
|
+
const preferredY = kind === 'menu' && point ? point.y + 6 : anchor.y + anchor.height - full;
|
|
83
|
+
add(anchor.x - w - gap, preferredY, full, 'left');
|
|
84
|
+
add(anchor.x + anchor.width + gap, preferredY, full, 'right');
|
|
85
|
+
const centered = anchor.x + anchor.width / 2 - w / 2;
|
|
86
|
+
add(centered, anchor.y - gap - full, full, 'above');
|
|
87
|
+
add(centered, anchor.y + anchor.height + gap, full, 'below');
|
|
88
|
+
const above = anchor.y - gap - top,
|
|
89
|
+
below = bottom - anchor.y - anchor.height - gap;
|
|
90
|
+
const minimum = Math.min(full, kind === 'menu' ? 160 : 240);
|
|
91
|
+
if (above >= minimum && above < full) add(centered, top, above, 'above');
|
|
92
|
+
if (below >= minimum && below < full)
|
|
93
|
+
add(centered, anchor.y + anchor.height + gap, below, 'below');
|
|
94
|
+
candidates.sort(
|
|
95
|
+
(a, b) =>
|
|
96
|
+
Number(a.overlap > 0) - Number(b.overlap > 0) ||
|
|
97
|
+
a.overlap - b.overlap ||
|
|
98
|
+
a.clipped - b.clipped ||
|
|
99
|
+
a.distance - b.distance ||
|
|
100
|
+
a.order - b.order,
|
|
101
|
+
);
|
|
102
|
+
const { overlap, clipped, distance, order, ...result } = candidates[0];
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
export const PET_SURFACE_MESSAGES = {
|
|
106
|
+
zh: {
|
|
107
|
+
petAppearancePreview: '角色预览',
|
|
108
|
+
petWaterBadge: '该喝水啦',
|
|
109
|
+
petClassicPreview: '应用后使用计时卡片',
|
|
110
|
+
petSurfaceHint: '内容较长时可用滚轮或触控滚动',
|
|
111
|
+
},
|
|
112
|
+
en: {
|
|
113
|
+
petAppearancePreview: 'Character preview',
|
|
114
|
+
petWaterBadge: 'Time for water',
|
|
115
|
+
petClassicPreview: 'Apply to use the clock card',
|
|
116
|
+
petSurfaceHint: 'Scroll with the wheel or touch when content is longer',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
export const PET_SURFACE_STYLE = `
|
|
120
|
+
.owc-v2{--owc-accent:#2563eb;--owc-on-accent:#fff;--owc-surface:var(--dsw-alias-bg-overlay,#fff);--owc-text:var(--dsw-alias-label-primary,#172033);--owc-muted:var(--dsw-alias-label-secondary,#526074);--owc-border:color-mix(in srgb,var(--owc-text) 13%,transparent);--owc-subtle:color-mix(in srgb,var(--owc-text) 4%,var(--owc-surface));--owc-hover:color-mix(in srgb,var(--owc-accent) 8%,var(--owc-surface));--owc-radius:16px;--owc-control-radius:10px;--owc-shadow:0 10px 32px color-mix(in srgb,var(--owc-text) 13%,transparent);--owc-control-height:36px;color:var(--owc-text);line-height:1.5}
|
|
121
|
+
.owc-v2 .owc-content{max-height:var(--owc-viewport-height,calc(100dvh - 24px))}
|
|
122
|
+
.owc-v2:not(.owc-pet-shell) .owc-head{position:sticky;top:0;z-index:2;background:var(--owc-surface)}
|
|
123
|
+
.owc-v2:not(.owc-pet-shell) .owc-save{position:sticky;bottom:0;z-index:2;background:var(--owc-surface);padding:12px 0}
|
|
124
|
+
.owc-v2 .owc-pet-toast{overflow-wrap:anywhere;overflow:auto;scrollbar-width:none}
|
|
125
|
+
.owc-v2 .owc-pet-water-alert{border-color:var(--owc-accent)!important;color:var(--owc-accent)}
|
|
126
|
+
.owc-v2 .owc-glass{background:var(--owc-surface);border:1px solid var(--owc-border);box-shadow:var(--owc-shadow);opacity:var(--owc-alpha,.94)}
|
|
127
|
+
.owc-v2 .owc-pet-workspace{border-radius:var(--owc-radius);container-type:inline-size}
|
|
128
|
+
.owc-v2 .owc-head{margin:0;padding:10px 16px;gap:8px;min-height:52px;border-bottom:1px solid var(--owc-border);box-sizing:border-box}
|
|
129
|
+
.owc-v2 [data-pet-page-title]{font-size:14px!important;line-height:1.4;min-width:0;overflow-wrap:anywhere;font-weight:600}
|
|
130
|
+
.owc-v2 [data-pet-page-title]:focus{outline:none}
|
|
131
|
+
.owc-v2 :is(.owc-pet-workspace,.owc-pet-context) button{min-height:var(--owc-control-height);padding:7px 10px;font-size:13px;line-height:1.4;border-radius:var(--owc-control-radius);box-sizing:border-box}
|
|
132
|
+
.owc-v2 :is(.owc-pet-workspace,.owc-pet-context) button:hover:not(:disabled){background:var(--owc-hover)}
|
|
133
|
+
.owc-v2 button.owc-primary{min-height:40px;background:var(--owc-accent);color:var(--owc-on-accent);border-radius:var(--owc-control-radius);font-size:13px}
|
|
134
|
+
.owc-v2 button.owc-primary:hover:not(:disabled){background:color-mix(in srgb,var(--owc-accent) 88%,#000)}
|
|
135
|
+
.owc-v2 :is(button,input,select,summary):focus-visible{outline:2px solid var(--owc-accent);outline-offset:2px}
|
|
136
|
+
.owc-v2 .owc-icon{flex:none;min-width:36px;padding:7px}
|
|
137
|
+
.owc-v2 .owc-main,.owc-v2 .owc-form,.owc-v2 .owc-water-form,.owc-v2 .owc-pet-home{padding:16px;min-width:0;box-sizing:border-box}
|
|
138
|
+
.owc-v2 .owc-main .owc-time{font-size:clamp(28px,10cqw,40px);font-weight:500;letter-spacing:-.5px;overflow-wrap:anywhere}
|
|
139
|
+
.owc-v2 .owc-money{font-size:clamp(24px,9cqw,34px);overflow-wrap:anywhere;line-height:1.35}
|
|
140
|
+
.owc-v2 :is(.owc-hint,.owc-muted,.owc-meta span,.owc-field>span,.owc-stats span,.owc-status,.owc-error,.owc-note){font-size:12px;line-height:1.55;color:var(--owc-muted)}
|
|
141
|
+
.owc-v2 .owc-error{color:var(--dsw-alias-state-error-primary,#dc2626)}
|
|
142
|
+
.owc-v2 .owc-meta span{white-space:normal;overflow:visible;text-overflow:clip;overflow-wrap:anywhere}
|
|
143
|
+
.owc-v2 .owc-row-actions{flex-wrap:wrap;gap:4px;margin:12px 0 0}
|
|
144
|
+
.owc-v2 .owc-stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;border-color:var(--owc-border)}
|
|
145
|
+
.owc-v2 .owc-stats>div{min-width:0;background:var(--owc-subtle);border:1px solid var(--owc-border);border-radius:12px;padding:12px}
|
|
146
|
+
.owc-v2 .owc-stats strong{font-size:14px;overflow-wrap:anywhere}
|
|
147
|
+
.owc-v2 .owc-setting-group{padding:14px 12px 4px;background:var(--owc-subtle);border-color:var(--owc-border);border-radius:12px}
|
|
148
|
+
.owc-v2 .owc-setting-group legend{font-size:13px}
|
|
149
|
+
.owc-v2 .owc-field{margin-bottom:12px;min-width:0}
|
|
150
|
+
.owc-v2 .owc-row2{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
|
151
|
+
.owc-v2 :is(input[type=text],input[type=time],input[type=number],select){max-width:100%;min-width:0;min-height:var(--owc-control-height);font-size:13px;border-color:var(--owc-border);border-radius:var(--owc-control-radius);background:var(--owc-surface);color:var(--owc-text)}
|
|
152
|
+
.owc-v2 :is(input[type=checkbox],input[type=radio],input[type=range]){accent-color:var(--owc-accent)}
|
|
153
|
+
.owc-v2 .owc-check{min-height:30px;font-size:13px;gap:9px}
|
|
154
|
+
.owc-v2 .owc-private{flex-wrap:wrap;border-color:var(--owc-border);background:var(--owc-surface);border-radius:var(--owc-control-radius)}
|
|
155
|
+
.owc-v2 .owc-days{gap:4px}
|
|
156
|
+
.owc-v2 .owc-days button{min-width:0;padding:5px 2px;font-size:12px}
|
|
157
|
+
.owc-v2 .owc-seg button{min-width:0}
|
|
158
|
+
.owc-v2 .owc-water-presets{gap:8px}
|
|
159
|
+
.owc-v2 .owc-water-presets button{font-size:12px;padding:7px 4px;border-color:var(--owc-border);white-space:normal}
|
|
160
|
+
.owc-v2 .owc-water-form p{font-size:12px;line-height:1.55;overflow-wrap:anywhere}
|
|
161
|
+
.owc-v2 .owc-water-card,.owc-v2 .owc-pet-water-card{padding:14px;border-color:var(--owc-border);background:var(--owc-subtle);border-radius:12px}
|
|
162
|
+
.owc-v2 .owc-pet-water-card .owc-water-status{height:auto;min-height:40px;overflow:visible;font-size:13px;line-height:1.5;overflow-wrap:anywhere;margin-bottom:10px}
|
|
163
|
+
.owc-v2 .owc-pet-water-row{font-size:12px;flex-wrap:wrap}
|
|
164
|
+
.owc-v2 .owc-pet-water-row .owc-primary{min-width:120px}
|
|
165
|
+
.owc-v2 .owc-pet-quiet-row{gap:8px;margin-top:14px}
|
|
166
|
+
.owc-v2 .owc-pet-quiet-row button{min-width:0;font-size:12px;white-space:normal;border-color:var(--owc-border)}
|
|
167
|
+
.owc-v2 .owc-pet-quiet-status{min-height:38px;height:auto;flex-wrap:wrap;font-size:12px;line-height:1.5;color:var(--owc-muted)}
|
|
168
|
+
.owc-v2 .owc-pet-feedback{height:auto;min-height:36px;overflow:visible;font-size:12px;line-height:1.5;margin:8px 0;overflow-wrap:anywhere}
|
|
169
|
+
.owc-v2 .owc-pet-home-links{gap:8px}
|
|
170
|
+
.owc-v2 .owc-pet-home-links button{font-size:12px;background:var(--owc-subtle);border:1px solid var(--owc-border)}
|
|
171
|
+
.owc-v2 .owc-pet-scroll{scrollbar-width:none;scroll-padding-block:12px 64px}
|
|
172
|
+
.owc-v2 .owc-pet-natural{display:flow-root;min-width:0;width:100%}
|
|
173
|
+
.owc-v2 .owc-pet-natural>.owc-form:last-child,.owc-v2 .owc-pet-natural>.owc-water-form:last-child{padding-bottom:0}
|
|
174
|
+
.owc-v2 .owc-pet-scroll .owc-save{background:var(--owc-surface);padding:12px 0;border-top:1px solid var(--owc-border);gap:8px;flex-wrap:wrap}
|
|
175
|
+
.owc-v2 .owc-save button{min-width:72px}
|
|
176
|
+
.owc-v2 .owc-appearance-preview{height:260px;display:flex;align-items:center;justify-content:center;border-radius:12px;background:var(--owc-subtle);margin-bottom:16px;overflow:hidden}
|
|
177
|
+
.owc-v2 .owc-appearance-preview .owc-pet-art{flex:none;animation:none}
|
|
178
|
+
.owc-v2 .owc-appearance-preview .owc-pet-art *{animation:none!important}
|
|
179
|
+
.owc-v2 .owc-appearance-preview p{font-size:13px;color:var(--owc-muted);padding:16px;text-align:center}
|
|
180
|
+
.owc-v2 .owc-pet-context{padding:0;background:var(--owc-surface);border:1px solid var(--owc-border);border-radius:var(--owc-radius);box-shadow:var(--owc-shadow);scrollbar-width:none}
|
|
181
|
+
.owc-v2 .owc-pet-context::-webkit-scrollbar{display:none}
|
|
182
|
+
.owc-v2 .owc-menu-natural{padding:6px;display:flow-root;box-sizing:border-box;min-width:0}
|
|
183
|
+
.owc-v2 .owc-pet-context button{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start;font-size:13px}
|
|
184
|
+
.owc-v2 .owc-pet-context hr{border-color:var(--owc-border);margin:6px 4px}
|
|
185
|
+
.owc-v2 .owc-pet-toast,.owc-v2 .owc-pet-clock,.owc-v2 .owc-pet-water-alert{background:var(--owc-surface)!important;color:var(--owc-text);border:1px solid var(--owc-border)!important;box-shadow:var(--owc-shadow)}
|
|
186
|
+
.owc-v2 .owc-pet-toast{border-radius:12px;font-size:12px}
|
|
187
|
+
.owc-v2 .owc-pet-unsaved{padding:20px 16px}
|
|
188
|
+
.owc-v2 .owc-pet-unsaved p{font-size:13px;overflow-wrap:anywhere}
|
|
189
|
+
@container (max-width:310px){.owc-v2 .owc-row2{grid-template-columns:1fr}.owc-v2 .owc-stats{gap:8px}.owc-v2 .owc-days{gap:2px}.owc-v2 .owc-days button{font-size:11px}}
|
|
190
|
+
@media (pointer:coarse){.owc-v2{--owc-control-height:44px}.owc-v2 .owc-icon{min-width:44px}.owc-v2 button.owc-primary{min-height:44px}.owc-v2 :is(input[type=text],input[type=time],input[type=number],select){font-size:16px}.owc-v2 input[type=range]{min-height:36px}.owc-v2 .owc-check{min-height:44px}}
|
|
191
|
+
@media (max-height:600px){.owc-v2 .owc-appearance-preview{height:120px}.owc-v2 .owc-appearance-preview svg{transform:scale(.55)}.owc-v2 .owc-appearance-preview img{transform:scale(.45)}}
|
|
192
|
+
@media (prefers-contrast:more){.owc-v2{--owc-border:var(--owc-text)}.owc-v2 .owc-glass{opacity:1!important}}
|
|
193
|
+
@media (prefers-reduced-transparency:reduce){.owc-v2 .owc-glass{opacity:1!important;backdrop-filter:none;-webkit-backdrop-filter:none}}
|
|
194
|
+
`;
|
package/pet-video.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Independent transparent-video playback. Presentation only; never records an action.
|
|
2
|
+
const PET_VIDEO_SUPPORT = new WeakMap();
|
|
3
|
+
|
|
4
|
+
export function petVideoAlphaSupported(host, source) {
|
|
5
|
+
if (PET_VIDEO_SUPPORT.has(host)) return PET_VIDEO_SUPPORT.get(host);
|
|
6
|
+
const result = new Promise((resolve) => {
|
|
7
|
+
const video = host.document.createElement('video');
|
|
8
|
+
let timer;
|
|
9
|
+
const finish = (supported) => {
|
|
10
|
+
host.clearTimeout(timer);
|
|
11
|
+
video.onloadeddata = video.onerror = null;
|
|
12
|
+
video.pause();
|
|
13
|
+
video.removeAttribute('src');
|
|
14
|
+
video.load();
|
|
15
|
+
resolve(supported);
|
|
16
|
+
};
|
|
17
|
+
if (!source || !video.canPlayType('video/webm; codecs="vp9"')) return finish(false);
|
|
18
|
+
video.muted = true;
|
|
19
|
+
video.playsInline = true;
|
|
20
|
+
video.preload = 'auto';
|
|
21
|
+
video.onloadeddata = () => {
|
|
22
|
+
try {
|
|
23
|
+
const canvas = host.document.createElement('canvas');
|
|
24
|
+
canvas.width = canvas.height = 16;
|
|
25
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
26
|
+
context.drawImage(video, 0, 0, 16, 16);
|
|
27
|
+
finish(
|
|
28
|
+
context.getImageData(1, 1, 1, 1).data[3] > 240 &&
|
|
29
|
+
context.getImageData(14, 14, 1, 1).data[3] < 8,
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
finish(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
video.onerror = () => finish(false);
|
|
36
|
+
timer = host.setTimeout(() => finish(false), 3000);
|
|
37
|
+
video.src = source;
|
|
38
|
+
video.load();
|
|
39
|
+
});
|
|
40
|
+
PET_VIDEO_SUPPORT.set(host, result);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
45
|
+
const layer = element.querySelector('.owc-pet-video-layer');
|
|
46
|
+
if (!layer || !clips) return base;
|
|
47
|
+
const document = host.document;
|
|
48
|
+
const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
49
|
+
const capability = petVideoAlphaSupported(host, probeSource);
|
|
50
|
+
const failed = new Set();
|
|
51
|
+
const retired = new Map();
|
|
52
|
+
let enabled = false,
|
|
53
|
+
resting = false,
|
|
54
|
+
dragging = false,
|
|
55
|
+
unsupported = false,
|
|
56
|
+
idleTimer = null,
|
|
57
|
+
destroyed = false,
|
|
58
|
+
current = null;
|
|
59
|
+
const allowed = () => enabled && !destroyed && !document.hidden && !reduced?.matches;
|
|
60
|
+
function clearIdleTimer() {
|
|
61
|
+
host.clearTimeout(idleTimer);
|
|
62
|
+
idleTimer = null;
|
|
63
|
+
}
|
|
64
|
+
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');
|
|
75
|
+
}
|
|
76
|
+
function scheduleIdle(delay = 0) {
|
|
77
|
+
clearIdleTimer();
|
|
78
|
+
if (!Object.hasOwn(clips, 'idle') || failed.has('idle') || unsupported || destroyed) return;
|
|
79
|
+
idleTimer = host.setTimeout(() => {
|
|
80
|
+
idleTimer = null;
|
|
81
|
+
startIdle();
|
|
82
|
+
}, delay);
|
|
83
|
+
}
|
|
84
|
+
function dispose(video) {
|
|
85
|
+
video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
|
|
86
|
+
video.pause();
|
|
87
|
+
video.removeAttribute('src');
|
|
88
|
+
video.load();
|
|
89
|
+
video.remove();
|
|
90
|
+
}
|
|
91
|
+
function stop(fade = false) {
|
|
92
|
+
clearIdleTimer();
|
|
93
|
+
const run = current;
|
|
94
|
+
if (!run) return;
|
|
95
|
+
current = null;
|
|
96
|
+
host.clearTimeout(run.timer);
|
|
97
|
+
delete element.dataset.videoPlaying;
|
|
98
|
+
if (element.dataset.action === run.action) delete element.dataset.action;
|
|
99
|
+
if (!run.video) return;
|
|
100
|
+
const video = run.video;
|
|
101
|
+
video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
|
|
102
|
+
video.pause();
|
|
103
|
+
video.style.opacity = '0';
|
|
104
|
+
if (fade && allowed()) {
|
|
105
|
+
const timer = host.setTimeout(() => {
|
|
106
|
+
retired.delete(video);
|
|
107
|
+
dispose(video);
|
|
108
|
+
}, 150);
|
|
109
|
+
retired.set(video, timer);
|
|
110
|
+
} else dispose(video);
|
|
111
|
+
}
|
|
112
|
+
function fallback(run, broken = false) {
|
|
113
|
+
if (current !== run) return;
|
|
114
|
+
if (broken) failed.add(run.action);
|
|
115
|
+
stop();
|
|
116
|
+
if (allowed() && run.action !== 'idle') {
|
|
117
|
+
base.play(run.action);
|
|
118
|
+
scheduleIdle(1400);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function play(action) {
|
|
122
|
+
if (!allowed() || dragging || element.dataset.action === 'drag') return false;
|
|
123
|
+
if (action === 'idle' && (resting || current || unsupported)) return false;
|
|
124
|
+
if (
|
|
125
|
+
current &&
|
|
126
|
+
(action === 'look' ||
|
|
127
|
+
current.action === action ||
|
|
128
|
+
(current.action === 'celebrate' && action === 'greet'))
|
|
129
|
+
)
|
|
130
|
+
return false;
|
|
131
|
+
if (!Object.hasOwn(clips, action) || failed.has(action) || unsupported) {
|
|
132
|
+
if (action === 'idle') return false;
|
|
133
|
+
if (current?.action === 'idle') stop();
|
|
134
|
+
const played = base.play(action);
|
|
135
|
+
if (played) scheduleIdle(1400);
|
|
136
|
+
return played;
|
|
137
|
+
}
|
|
138
|
+
stop();
|
|
139
|
+
base.release(true);
|
|
140
|
+
const run = { action, video: null, timer: null };
|
|
141
|
+
current = run;
|
|
142
|
+
element.dataset.action = action;
|
|
143
|
+
run.timer = host.setTimeout(() => fallback(run, true), 9000);
|
|
144
|
+
capability.then((supported) => {
|
|
145
|
+
if (current !== run || !allowed()) return;
|
|
146
|
+
if (!supported) {
|
|
147
|
+
unsupported = true;
|
|
148
|
+
return fallback(run);
|
|
149
|
+
}
|
|
150
|
+
const video = document.createElement('video');
|
|
151
|
+
run.video = video;
|
|
152
|
+
video.muted = true;
|
|
153
|
+
video.defaultMuted = true;
|
|
154
|
+
video.playsInline = true;
|
|
155
|
+
video.loop = action === 'idle';
|
|
156
|
+
video.disablePictureInPicture = true;
|
|
157
|
+
video.preload = 'auto';
|
|
158
|
+
video.setAttribute('aria-hidden', 'true');
|
|
159
|
+
video.setAttribute('playsinline', '');
|
|
160
|
+
video.onplaying = () => {
|
|
161
|
+
if (current !== run || !allowed()) return;
|
|
162
|
+
if (!Number.isFinite(video.duration) || video.duration <= 0 || video.duration > 15)
|
|
163
|
+
return fallback(run, true);
|
|
164
|
+
host.clearTimeout(run.timer);
|
|
165
|
+
run.timer =
|
|
166
|
+
action === 'idle'
|
|
167
|
+
? null
|
|
168
|
+
: host.setTimeout(() => fallback(run, true), video.duration * 1000 + 2000);
|
|
169
|
+
element.dataset.videoPlaying = 'true';
|
|
170
|
+
};
|
|
171
|
+
video.onwaiting = video.onstalled = () => {
|
|
172
|
+
if (current !== run) return;
|
|
173
|
+
host.clearTimeout(run.timer);
|
|
174
|
+
run.timer = host.setTimeout(() => fallback(run, true), 4000);
|
|
175
|
+
};
|
|
176
|
+
video.onended = () => {
|
|
177
|
+
if (current === run) {
|
|
178
|
+
stop(true);
|
|
179
|
+
scheduleIdle();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
video.onerror = () => fallback(run, true);
|
|
183
|
+
video.src = clips[action];
|
|
184
|
+
layer.appendChild(video);
|
|
185
|
+
try {
|
|
186
|
+
Promise.resolve(video.play()).catch(() => fallback(run, true));
|
|
187
|
+
} catch {
|
|
188
|
+
fallback(run, true);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const visibility = () => {
|
|
194
|
+
if (!allowed()) stop();
|
|
195
|
+
else scheduleIdle();
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener('visibilitychange', visibility);
|
|
198
|
+
reduced?.addEventListener?.('change', visibility);
|
|
199
|
+
return {
|
|
200
|
+
play,
|
|
201
|
+
configure(next) {
|
|
202
|
+
if (!next.enabled || (next.resting && !resting)) stop();
|
|
203
|
+
enabled = next.enabled;
|
|
204
|
+
resting = next.resting;
|
|
205
|
+
base.configure(next);
|
|
206
|
+
startIdle();
|
|
207
|
+
},
|
|
208
|
+
drag(direction) {
|
|
209
|
+
dragging = true;
|
|
210
|
+
stop();
|
|
211
|
+
base.drag(direction);
|
|
212
|
+
},
|
|
213
|
+
release(cancelled) {
|
|
214
|
+
dragging = false;
|
|
215
|
+
base.release(cancelled);
|
|
216
|
+
scheduleIdle(cancelled ? 0 : 650);
|
|
217
|
+
},
|
|
218
|
+
destroy() {
|
|
219
|
+
destroyed = true;
|
|
220
|
+
stop();
|
|
221
|
+
for (const [video, timer] of retired) {
|
|
222
|
+
host.clearTimeout(timer);
|
|
223
|
+
dispose(video);
|
|
224
|
+
}
|
|
225
|
+
retired.clear();
|
|
226
|
+
document.removeEventListener('visibilitychange', visibility);
|
|
227
|
+
reduced?.removeEventListener?.('change', visibility);
|
|
228
|
+
base.destroy();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|