dsh-capyreporter 0.1.0
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/LICENSE +21 -0
- package/README.md +60 -0
- package/README.zh.md +60 -0
- package/assets/capybara.png +0 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +567 -0
- package/lib/index.js +494 -0
- package/package.json +69 -0
- package/runtime/electron-helper/index.html +160 -0
- package/runtime/electron-helper/main.js +140 -0
- package/runtime/electron-helper/package.json +5 -0
- package/runtime/electron-helper/preload.js +17 -0
- package/runtime/electron-helper/renderer.js +249 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
// dsh-capyreporter — client half (browser). Registered with the DSH module loader.
|
|
2
|
+
// Talks to the host half over webServer routes using plain fetch.
|
|
3
|
+
|
|
4
|
+
function makeFactory() {
|
|
5
|
+
return (require) => {
|
|
6
|
+
const React = require('react')
|
|
7
|
+
const { useState, useEffect } = React
|
|
8
|
+
|
|
9
|
+
const name = 'capyreporter'
|
|
10
|
+
const inject = ['slots']
|
|
11
|
+
const ROUTE = '/dsh-capyreporter'
|
|
12
|
+
|
|
13
|
+
const SETTINGS_CSS = [
|
|
14
|
+
'.dsh-pet-section { display:flex; flex-direction:column; gap:16px; font-size:13px; }',
|
|
15
|
+
'.dsh-pet-row { display:flex; align-items:center; justify-content:space-between; gap:14px; }',
|
|
16
|
+
'.dsh-pet-row-main { display:flex; flex-direction:column; gap:3px; min-width:0; }',
|
|
17
|
+
'.dsh-pet-title { font-weight:600; }',
|
|
18
|
+
'.dsh-pet-hint { opacity:.65; font-size:12px; line-height:1.5; }',
|
|
19
|
+
'.dsh-pet-toggle { position:relative; flex:0 0 auto; width:42px; height:23px; border-radius:12px; background:#88888855; border:0; cursor:pointer; padding:0; transition:background .15s; }',
|
|
20
|
+
'.dsh-pet-toggle[data-on="1"] { background:#4f8cff; }',
|
|
21
|
+
'.dsh-pet-toggle::after { content:""; position:absolute; top:3px; left:3px; width:17px; height:17px; border-radius:50%; background:#fff; transition:left .15s; box-shadow:0 1px 2px #0004; }',
|
|
22
|
+
'.dsh-pet-toggle[data-on="1"]::after { left:22px; }',
|
|
23
|
+
'.dsh-pet-preview { width:110px; height:110px; object-fit:contain; border-radius:10px; background:#88888822; border:1px solid #88888844; flex:0 0 auto; }',
|
|
24
|
+
'.dsh-pet-btn { border:1px solid #88888866; background:transparent; color:inherit; border-radius:8px; padding:5px 12px; cursor:pointer; font-size:12.5px; }',
|
|
25
|
+
'.dsh-pet-btn:hover { background:#88888833; }',
|
|
26
|
+
'.dsh-pet-btn-danger { border-color:#e5484d88; color:#e5484d; }',
|
|
27
|
+
'.dsh-pet-btn-danger:hover { background:#e5484d22; }',
|
|
28
|
+
'.dsh-pet-msg { font-size:12px; opacity:.8; line-height:1.5; }',
|
|
29
|
+
].join('\n')
|
|
30
|
+
|
|
31
|
+
const PET_CSS = [
|
|
32
|
+
'.dsh-pet-float { position:fixed; z-index:2147483000; pointer-events:auto; user-select:none; -webkit-user-select:none; }',
|
|
33
|
+
'.dsh-pet-img { display:block; width:132px; height:auto; pointer-events:auto; cursor:grab; filter:drop-shadow(0 6px 16px #0006); transform-origin:center bottom; transition:transform .12s ease-out; }',
|
|
34
|
+
'.dsh-pet-img.dragging { cursor:grabbing; }',
|
|
35
|
+
'.dsh-pet-img.bounce { transform:scale(1.12); }',
|
|
36
|
+
'.dsh-pet-bubble { position:absolute; bottom:100%; left:50%; transform:translateX(-50%); margin-bottom:8px; background:#fdfdf7; color:#22242c; border-radius:14px; padding:8px 12px; font-size:12.5px; line-height:1.45; box-shadow:0 4px 14px #0007; max-width:900px; min-width:90px; cursor:pointer; z-index:5; flex-direction:column; gap:3px; display:flex; box-sizing:border-box; }',
|
|
37
|
+
'.dsh-pet-bubble::after { content:""; position:absolute; bottom:-8px; left:50%; transform:translateX(-50%); border:8px solid transparent; border-top-color:#fdfdf7; border-bottom:0; }',
|
|
38
|
+
'.dsh-pet-bubble-head { font-weight:600; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; overflow-wrap:break-word; }',
|
|
39
|
+
'.dsh-pet-bubble-detail { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; overflow-wrap:break-word; opacity:.85; }',
|
|
40
|
+
'.dsh-pet-bubble .dsh-pet-hint { white-space:nowrap; }',
|
|
41
|
+
'.dsh-pet-bubble-log { max-height:150px; overflow-y:auto; display:flex; flex-direction:column; gap:2px; font-size:12px; line-height:1.4; scrollbar-width:thin; }',
|
|
42
|
+
'.dsh-pet-bubble-log::-webkit-scrollbar { width:6px; }',
|
|
43
|
+
'.dsh-pet-bubble-log::-webkit-scrollbar-thumb { background:#00000044; border-radius:3px; }',
|
|
44
|
+
'.dsh-pet-bubble-line { overflow-wrap:break-word; white-space:normal; }',
|
|
45
|
+
'.dsh-pet-hint { font-size:10.5px; opacity:.55; margin-top:3px; }',
|
|
46
|
+
'.dsh-pet-menu { position:absolute; bottom:0; left:100%; margin-left:8px; flex-direction:column; background:#23262f; color:#e8eaf0; border:1px solid #ffffff22; border-radius:10px; overflow:hidden; box-shadow:0 8px 24px #0009; z-index:9; }',
|
|
47
|
+
'.dsh-pet-menu-item { appearance:none; border:0; background:transparent; color:#e8eaf0; padding:8px 14px; font-size:12px; text-align:left; cursor:pointer; font-family:inherit; white-space:nowrap; }',
|
|
48
|
+
'.dsh-pet-menu-item:hover { background:#ffffff14; }',
|
|
49
|
+
].join('\n')
|
|
50
|
+
|
|
51
|
+
function ensureCss() {
|
|
52
|
+
try {
|
|
53
|
+
if (typeof document === 'undefined') return
|
|
54
|
+
if (document.querySelector('style[data-plugin-css="dsh-capyreporter"]')) return
|
|
55
|
+
const tag = document.createElement('style')
|
|
56
|
+
tag.dataset.pluginCss = 'dsh-capyreporter'
|
|
57
|
+
tag.textContent = SETTINGS_CSS + '\n' + PET_CSS
|
|
58
|
+
document.head.appendChild(tag)
|
|
59
|
+
} catch (e) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getJSON(path, opts) {
|
|
63
|
+
const r = await fetch(ROUTE + path, opts)
|
|
64
|
+
return r.json()
|
|
65
|
+
}
|
|
66
|
+
async function postJSON(path, body) {
|
|
67
|
+
return getJSON(path, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(body || {}),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bubbleModel(act) {
|
|
75
|
+
if (!act) return null
|
|
76
|
+
const done = act.completed
|
|
77
|
+
if (done) return { mode: 'done', at: done.at, head: '\u2705 Task complete \u2014 ' + (done.title || 'Task'), lines: [], detail: null, hint: 'click to expand \u00B7 double-click to dismiss' }
|
|
78
|
+
const n = act.runningCount | 0
|
|
79
|
+
if (n > 0) {
|
|
80
|
+
const first = (act.running && act.running[0]) || {}
|
|
81
|
+
const head = n > 1 ? '\u23F3 ' + n + ' tasks running \u2014 ' + (first.title || 'Session') : '\u23F3 Working: ' + (first.title || 'Session')
|
|
82
|
+
return { mode: 'working', at: null, head: head, lines: first.lines || [], detail: (first.preview || '').slice(0, 260) || null, hint: 'click to expand \u00B7 double-click to hide' }
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Content signature of the activity feed: the bubble auto-(re)appears whenever
|
|
88
|
+
// the signature changes (new preview, task count change, completion) and stays
|
|
89
|
+
// hidden after a click until then.
|
|
90
|
+
function activitySig(act) {
|
|
91
|
+
if (!act) return 'none'
|
|
92
|
+
if (act.completed) return 'done:' + act.completed.at
|
|
93
|
+
const n = act.runningCount | 0
|
|
94
|
+
if (n > 0) {
|
|
95
|
+
const first = (act.running && act.running[0]) || {}
|
|
96
|
+
return 'work:' + n + ':' + (first.title || '') + ':' + (first.preview || '')
|
|
97
|
+
}
|
|
98
|
+
return 'none'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function notifyDone(comp, lastAtRef) {
|
|
102
|
+
try {
|
|
103
|
+
if (lastAtRef.at === comp.at) return
|
|
104
|
+
if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return
|
|
105
|
+
lastAtRef.at = comp.at
|
|
106
|
+
new Notification('CapyReporter', { body: '\u2705 Task complete \u2014 ' + (comp.title || 'Task'), tag: 'dsh-capyreporter-complete' })
|
|
107
|
+
} catch (e) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function PetOverlay() {
|
|
111
|
+
const activity = React.useState(null)
|
|
112
|
+
const enabled = React.useState(true)
|
|
113
|
+
const hasCustom = React.useState(false)
|
|
114
|
+
const desktop = React.useState(false)
|
|
115
|
+
const menu = React.useState(false)
|
|
116
|
+
const bounce = React.useState(false)
|
|
117
|
+
const pos = React.useState({
|
|
118
|
+
left: 24,
|
|
119
|
+
top: typeof window !== 'undefined' ? Math.max(120, window.innerHeight - 420) : 220,
|
|
120
|
+
})
|
|
121
|
+
const drag = React.useState({ s: null })
|
|
122
|
+
const imgToken = React.useState(0)
|
|
123
|
+
const setActivity = activity[1]
|
|
124
|
+
const setEnabled = enabled[1]
|
|
125
|
+
const setHasCustom = hasCustom[1]
|
|
126
|
+
const setDesktop = desktop[1]
|
|
127
|
+
const setMenu = menu[1]
|
|
128
|
+
const setBounce = bounce[1]
|
|
129
|
+
const setPos = pos[1]
|
|
130
|
+
const dragState = drag[0]
|
|
131
|
+
const setImgToken = imgToken[1]
|
|
132
|
+
const lastNotified = React.useState({ at: null }) // plain object ref; stable across renders
|
|
133
|
+
const customRef = React.useState({ v: null }) // stable object ref for hasCustom change detection
|
|
134
|
+
const suppressed = React.useState({ sig: null }) // stable ref: bubble signature hidden by a double-click
|
|
135
|
+
const expanded = React.useState(false) // single-click expands the step log
|
|
136
|
+
const setExpanded = expanded[1]
|
|
137
|
+
const scale = React.useState(1) // pet size multiplier, owned by the host config
|
|
138
|
+
const setScale = scale[1]
|
|
139
|
+
const scaleRef = React.useRef(1)
|
|
140
|
+
scaleRef.current = scale[0]
|
|
141
|
+
const wheelBound = React.useRef(false)
|
|
142
|
+
const bindImg = React.useCallback((node) => {
|
|
143
|
+
if (!node || wheelBound.current) return
|
|
144
|
+
wheelBound.current = true
|
|
145
|
+
// Ctrl + wheel over the pet resizes her. Non-passive listener so the
|
|
146
|
+
// browser zoom gesture can be suppressed; the size is stored host-side
|
|
147
|
+
// and shared with the desktop window and the Settings slider.
|
|
148
|
+
node.addEventListener('wheel', (e) => {
|
|
149
|
+
if (!e.ctrlKey && !e.metaKey) return
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
const next = Math.min(2.5, Math.max(0.5, Math.round((scaleRef.current + (e.deltaY < 0 ? 0.1 : -0.1)) * 10) / 10))
|
|
152
|
+
if (next === scaleRef.current) return
|
|
153
|
+
scaleRef.current = next
|
|
154
|
+
setScale(next)
|
|
155
|
+
postJSON('/scale', { scale: next }).catch(() => {})
|
|
156
|
+
}, { passive: false })
|
|
157
|
+
}, [])
|
|
158
|
+
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
let alive = true
|
|
161
|
+
const poll = async () => {
|
|
162
|
+
try {
|
|
163
|
+
const a = await getJSON('/activity')
|
|
164
|
+
if (!alive) return
|
|
165
|
+
setActivity(a)
|
|
166
|
+
if (a && a.completed) notifyDone(a.completed, lastNotified[0])
|
|
167
|
+
} catch (e) {}
|
|
168
|
+
try {
|
|
169
|
+
const s = await getJSON('/state')
|
|
170
|
+
if (!alive) return
|
|
171
|
+
setEnabled(s.enabled)
|
|
172
|
+
setHasCustom(s.hasCustom)
|
|
173
|
+
setDesktop(!!s.desktop)
|
|
174
|
+
if (typeof s.scale === 'number') setScale(s.scale)
|
|
175
|
+
if (customRef[0].v !== s.hasCustom) {
|
|
176
|
+
customRef[0].v = s.hasCustom
|
|
177
|
+
setImgToken((x) => x + 1)
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
}
|
|
181
|
+
poll()
|
|
182
|
+
const iv = setInterval(poll, 1200)
|
|
183
|
+
return () => {
|
|
184
|
+
alive = false
|
|
185
|
+
clearInterval(iv)
|
|
186
|
+
}
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
// Hide the in-page overlay while the always-on-top desktop window is running,
|
|
190
|
+
// so there's only one pet. Falls back to the in-page pet if the desktop window isn't up.
|
|
191
|
+
if (!enabled[0] || desktop[0]) return null
|
|
192
|
+
const sig = activitySig(activity[0])
|
|
193
|
+
const raw = bubbleModel(activity[0])
|
|
194
|
+
const bubble = raw && sig !== suppressed[0].sig ? raw : null
|
|
195
|
+
|
|
196
|
+
const dismissDone = () => {
|
|
197
|
+
const at = activity[0] && activity[0].completed ? activity[0].completed.at : null
|
|
198
|
+
if (at) {
|
|
199
|
+
try {
|
|
200
|
+
postJSON('/dismiss', { at })
|
|
201
|
+
} catch (e) {}
|
|
202
|
+
setActivity(Object.assign({}, activity[0], { completed: null }))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const onLeftClick = () => {
|
|
206
|
+
if (activity[0] && activity[0].completed) {
|
|
207
|
+
dismissDone()
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
setBounce(true)
|
|
211
|
+
setTimeout(() => setBounce(false), 220)
|
|
212
|
+
}
|
|
213
|
+
const turnOff = async () => {
|
|
214
|
+
try {
|
|
215
|
+
await postJSON('/enabled', { enabled: false })
|
|
216
|
+
} catch (e) {}
|
|
217
|
+
setEnabled(false)
|
|
218
|
+
}
|
|
219
|
+
const onPointerDown = (e) => {
|
|
220
|
+
if (e.button !== 0) return
|
|
221
|
+
dragState.s = { sx: e.clientX, sy: e.clientY, bx: pos[0].left, by: pos[0].top }
|
|
222
|
+
}
|
|
223
|
+
const onPointerMove = (e) => {
|
|
224
|
+
const d = dragState.s
|
|
225
|
+
if (!d) return
|
|
226
|
+
const w = typeof window !== 'undefined' ? window.innerWidth : 800
|
|
227
|
+
const h = typeof window !== 'undefined' ? window.innerHeight : 600
|
|
228
|
+
const clamp = (v, max) => Math.max(0, Math.min(v, max))
|
|
229
|
+
const m = scaleRef.current
|
|
230
|
+
setPos({ left: clamp(d.bx + (e.clientX - d.sx), w - Math.round(140 * m)), top: clamp(d.by + (e.clientY - d.sy), h - Math.round(160 * m)) })
|
|
231
|
+
}
|
|
232
|
+
const onPointerUp = () => {
|
|
233
|
+
dragState.s = null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const menuItems = []
|
|
237
|
+
if (suppressed[0].sig !== null) menuItems.push(['Restore bubble', () => { suppressed[0].sig = null; setExpanded(false) }])
|
|
238
|
+
if (bubble && bubble.mode === 'done') menuItems.push(['Dismiss notification', dismissDone])
|
|
239
|
+
menuItems.push(['Hide pet', turnOff])
|
|
240
|
+
|
|
241
|
+
return React.createElement(
|
|
242
|
+
'div',
|
|
243
|
+
{ className: 'dsh-pet-float', style: { left: pos[0].left, top: pos[0].top } },
|
|
244
|
+
bubble
|
|
245
|
+
? React.createElement(
|
|
246
|
+
'div',
|
|
247
|
+
{ className: 'dsh-pet-bubble', onClick: () => setExpanded(!expanded[0]), onDoubleClick: () => {
|
|
248
|
+
suppressed[0].sig = sig
|
|
249
|
+
if (raw.mode === 'done') dismissDone()
|
|
250
|
+
setExpanded(false)
|
|
251
|
+
} },
|
|
252
|
+
React.createElement('div', { className: 'dsh-pet-bubble-head' }, bubble.head),
|
|
253
|
+
expanded[0] && bubble.lines.length
|
|
254
|
+
? React.createElement('div', { className: 'dsh-pet-bubble-log' },
|
|
255
|
+
bubble.lines.map((l, i) => React.createElement('div', { className: 'dsh-pet-bubble-line', key: i }, l)),
|
|
256
|
+
)
|
|
257
|
+
: bubble.detail
|
|
258
|
+
? React.createElement('div', { className: 'dsh-pet-bubble-detail' }, bubble.detail)
|
|
259
|
+
: null,
|
|
260
|
+
bubble.hint ? React.createElement('div', { className: 'dsh-pet-hint' }, bubble.hint) : null,
|
|
261
|
+
)
|
|
262
|
+
: null,
|
|
263
|
+
menu[0]
|
|
264
|
+
? React.createElement(
|
|
265
|
+
'div',
|
|
266
|
+
{ className: 'dsh-pet-menu' },
|
|
267
|
+
menuItems.map((it, i) =>
|
|
268
|
+
React.createElement(
|
|
269
|
+
'button',
|
|
270
|
+
{
|
|
271
|
+
className: 'dsh-pet-menu-item',
|
|
272
|
+
key: i,
|
|
273
|
+
onClick: () => {
|
|
274
|
+
setMenu(false)
|
|
275
|
+
it[1]()
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
it[0],
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
: null,
|
|
283
|
+
React.createElement('img', {
|
|
284
|
+
className: 'dsh-pet-img' + (bounce[0] ? ' bounce' : ''),
|
|
285
|
+
style: { width: Math.round(132 * scale[0]) + 'px' },
|
|
286
|
+
ref: bindImg,
|
|
287
|
+
src: ROUTE + '/image?v=' + imgToken[0],
|
|
288
|
+
draggable: false,
|
|
289
|
+
onPointerDown: onPointerDown,
|
|
290
|
+
onPointerMove: onPointerMove,
|
|
291
|
+
onPointerUp: onPointerUp,
|
|
292
|
+
onClick: onLeftClick,
|
|
293
|
+
onContextMenu: (e) => {
|
|
294
|
+
e.preventDefault()
|
|
295
|
+
setMenu(!menu[0])
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function PetSettings() {
|
|
302
|
+
const tick = React.useState(0)
|
|
303
|
+
const confirming = React.useState(false)
|
|
304
|
+
const msg = React.useState(null)
|
|
305
|
+
const enabled = React.useState(true)
|
|
306
|
+
const hasCustom = React.useState(false)
|
|
307
|
+
const desktopOn = React.useState(false)
|
|
308
|
+
const petScale = React.useState(1)
|
|
309
|
+
const setTick = tick[1]
|
|
310
|
+
const setConfirming = confirming[1]
|
|
311
|
+
const setMsg = msg[1]
|
|
312
|
+
const setEnabled = enabled[1]
|
|
313
|
+
const setHasCustom = hasCustom[1]
|
|
314
|
+
const setDesktopOn = desktopOn[1]
|
|
315
|
+
const setPetScale = petScale[1]
|
|
316
|
+
|
|
317
|
+
React.useEffect(() => {
|
|
318
|
+
let alive = true
|
|
319
|
+
const refresh = async () => {
|
|
320
|
+
try {
|
|
321
|
+
const s = await getJSON('/state')
|
|
322
|
+
if (!alive) return
|
|
323
|
+
setEnabled(s.enabled)
|
|
324
|
+
setHasCustom(s.hasCustom)
|
|
325
|
+
setDesktopOn(!!s.desktopEnabled)
|
|
326
|
+
if (typeof s.scale === 'number') setPetScale(s.scale)
|
|
327
|
+
setTick((x) => x + 1)
|
|
328
|
+
} catch (e) {}
|
|
329
|
+
}
|
|
330
|
+
refresh()
|
|
331
|
+
const iv = setInterval(refresh, 2500)
|
|
332
|
+
return () => {
|
|
333
|
+
alive = false
|
|
334
|
+
clearInterval(iv)
|
|
335
|
+
}
|
|
336
|
+
}, [])
|
|
337
|
+
|
|
338
|
+
const onToggle = async () => {
|
|
339
|
+
const next = !enabled[0]
|
|
340
|
+
setEnabled(next)
|
|
341
|
+
setTick((x) => x + 1)
|
|
342
|
+
try {
|
|
343
|
+
await postJSON('/enabled', { enabled: next })
|
|
344
|
+
} catch (e) {}
|
|
345
|
+
if (next) {
|
|
346
|
+
setMsg(null)
|
|
347
|
+
try {
|
|
348
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'default') Notification.requestPermission()
|
|
349
|
+
} catch (e) {}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const onDesktopToggle = async () => {
|
|
354
|
+
const next = !desktopOn[0]
|
|
355
|
+
setDesktopOn(next)
|
|
356
|
+
try {
|
|
357
|
+
await postJSON(next ? '/desktop/show' : '/desktop/hide', {})
|
|
358
|
+
} catch (e) {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const onSizeSlider = (ev) => {
|
|
362
|
+
const next = Math.min(2.5, Math.max(0.5, Number(ev.target.value) / 100 || 1))
|
|
363
|
+
setPetScale(next)
|
|
364
|
+
postJSON('/scale', { scale: next }).catch(() => {})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const onUpload = (ev) => {
|
|
368
|
+
const f = ev.target.files && ev.target.files[0]
|
|
369
|
+
ev.target.value = ''
|
|
370
|
+
if (!f) return
|
|
371
|
+
const reader = new FileReader()
|
|
372
|
+
reader.onload = async () => {
|
|
373
|
+
try {
|
|
374
|
+
const r = await postJSON('/image', { dataUrl: String(reader.result) })
|
|
375
|
+
if (r && r.ok) {
|
|
376
|
+
setHasCustom(true)
|
|
377
|
+
setMsg(null)
|
|
378
|
+
} else {
|
|
379
|
+
setMsg((r && r.error) || 'Upload failed.')
|
|
380
|
+
}
|
|
381
|
+
setTick((x) => x + 1)
|
|
382
|
+
} catch (e) {
|
|
383
|
+
setMsg('Upload failed: ' + ((e && e.message) || e))
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
reader.readAsDataURL(f)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const onReset = async () => {
|
|
390
|
+
try {
|
|
391
|
+
await postJSON('/reset', {})
|
|
392
|
+
} catch (e) {}
|
|
393
|
+
setHasCustom(false)
|
|
394
|
+
setMsg(null)
|
|
395
|
+
setTick((x) => x + 1)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const onUninstall = async () => {
|
|
399
|
+
if (!confirming[0]) {
|
|
400
|
+
setConfirming(true)
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
setConfirming(false)
|
|
404
|
+
try {
|
|
405
|
+
await postJSON('/enabled', { enabled: false })
|
|
406
|
+
await postJSON('/reset', {})
|
|
407
|
+
} catch (e) {}
|
|
408
|
+
setEnabled(false)
|
|
409
|
+
setHasCustom(false)
|
|
410
|
+
setMsg('Pet hidden and its data cleared. To remove the plugin itself, ask the assistant to remove the \u201Cdesktop pet\u201D plugin.')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const children = []
|
|
414
|
+
children.push(
|
|
415
|
+
React.createElement(
|
|
416
|
+
'div',
|
|
417
|
+
{ className: 'dsh-pet-row', key: 'toggle' },
|
|
418
|
+
React.createElement(
|
|
419
|
+
'div',
|
|
420
|
+
{ className: 'dsh-pet-row-main' },
|
|
421
|
+
React.createElement('div', { className: 'dsh-pet-title' }, 'Floating desktop pet'),
|
|
422
|
+
React.createElement(
|
|
423
|
+
'div',
|
|
424
|
+
{ className: 'dsh-pet-hint' },
|
|
425
|
+
'CapyReporter floats on this page over the interface. Drag her where you like; a speech bubble reports running work \u2014 click it to hide it until the next update or completion. \u201CTask complete\u201D stays until you click it. When you are away she sends a desktop notification when a task finishes.',
|
|
426
|
+
),
|
|
427
|
+
),
|
|
428
|
+
React.createElement('button', {
|
|
429
|
+
'data-on': enabled[0] ? '1' : '0',
|
|
430
|
+
className: 'dsh-pet-toggle',
|
|
431
|
+
'aria-label': enabled[0] ? 'Hide floating pet' : 'Show floating pet',
|
|
432
|
+
onClick: onToggle,
|
|
433
|
+
}),
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
children.push(
|
|
438
|
+
React.createElement(
|
|
439
|
+
'div',
|
|
440
|
+
{ className: 'dsh-pet-row', key: 'desktop' },
|
|
441
|
+
React.createElement(
|
|
442
|
+
'div',
|
|
443
|
+
{ className: 'dsh-pet-row-main' },
|
|
444
|
+
React.createElement('div', { className: 'dsh-pet-title' }, 'Always-on-top desktop window'),
|
|
445
|
+
React.createElement(
|
|
446
|
+
'div',
|
|
447
|
+
{ className: 'dsh-pet-hint' },
|
|
448
|
+
'Shows CapyReporter in a small transparent window that stays above other apps \u2014 even when this page or another app has focus. Turn off to keep her only on this page.',
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
React.createElement('button', {
|
|
452
|
+
'data-on': desktopOn[0] ? '1' : '0',
|
|
453
|
+
className: 'dsh-pet-toggle',
|
|
454
|
+
'aria-label': desktopOn[0] ? 'Hide desktop window' : 'Show desktop window',
|
|
455
|
+
onClick: onDesktopToggle,
|
|
456
|
+
}),
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
children.push(
|
|
461
|
+
React.createElement(
|
|
462
|
+
'div',
|
|
463
|
+
{ className: 'dsh-pet-row', key: 'size' },
|
|
464
|
+
React.createElement(
|
|
465
|
+
'div',
|
|
466
|
+
{ className: 'dsh-pet-row-main' },
|
|
467
|
+
React.createElement('div', { className: 'dsh-pet-title' }, 'Pet size'),
|
|
468
|
+
React.createElement(
|
|
469
|
+
'div',
|
|
470
|
+
{ className: 'dsh-pet-hint' },
|
|
471
|
+
'Drag the slider, or hold Ctrl and scroll over the pet (plain scroll works over the desktop window). The size applies to the on-page pet and the desktop window and survives restarts.',
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
React.createElement(
|
|
475
|
+
'div',
|
|
476
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
|
|
477
|
+
React.createElement('input', {
|
|
478
|
+
type: 'range',
|
|
479
|
+
min: 50,
|
|
480
|
+
max: 250,
|
|
481
|
+
step: 5,
|
|
482
|
+
value: Math.round(petScale[0] * 100),
|
|
483
|
+
onChange: onSizeSlider,
|
|
484
|
+
style: { width: '150px' },
|
|
485
|
+
'aria-label': 'Pet size percent',
|
|
486
|
+
}),
|
|
487
|
+
React.createElement('span', { style: { fontSize: '12px', opacity: 0.75, minWidth: '38px', textAlign: 'right' } }, Math.round(petScale[0] * 100) + '%'),
|
|
488
|
+
),
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
children.push(
|
|
493
|
+
React.createElement(
|
|
494
|
+
'div',
|
|
495
|
+
{ className: 'dsh-pet-row', key: 'image' },
|
|
496
|
+
React.createElement('img', { className: 'dsh-pet-preview', src: ROUTE + '/image?v=' + tick[0], alt: 'Current pet appearance' }),
|
|
497
|
+
React.createElement(
|
|
498
|
+
'div',
|
|
499
|
+
{ className: 'dsh-pet-row-main' },
|
|
500
|
+
React.createElement('div', { className: 'dsh-pet-title' }, 'Pet appearance'),
|
|
501
|
+
React.createElement('div', { className: 'dsh-pet-hint' }, 'Use a transparent PNG so she floats cleanly (under ~4 MB).'),
|
|
502
|
+
React.createElement(
|
|
503
|
+
'div',
|
|
504
|
+
{ style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
|
|
505
|
+
React.createElement(
|
|
506
|
+
'label',
|
|
507
|
+
{ className: 'dsh-pet-btn', style: { display: 'inline-flex', alignItems: 'center' } },
|
|
508
|
+
'Replace image\u2026',
|
|
509
|
+
React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: onUpload }),
|
|
510
|
+
),
|
|
511
|
+
hasCustom[0] ? React.createElement('button', { className: 'dsh-pet-btn', onClick: onReset }, 'Reset to CapyReporter') : null,
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
children.push(
|
|
518
|
+
React.createElement(
|
|
519
|
+
'div',
|
|
520
|
+
{ className: 'dsh-pet-row', key: 'uninstall', style: { alignItems: 'flex-start' } },
|
|
521
|
+
React.createElement(
|
|
522
|
+
'div',
|
|
523
|
+
{ className: 'dsh-pet-row-main' },
|
|
524
|
+
React.createElement('div', { className: 'dsh-pet-title' }, 'Uninstall'),
|
|
525
|
+
React.createElement(
|
|
526
|
+
'div',
|
|
527
|
+
{ className: 'dsh-pet-hint' },
|
|
528
|
+
confirming[0] ? 'Hide the pet and clear its custom image? Click again to confirm.' : 'Hides the pet and clears its data.',
|
|
529
|
+
),
|
|
530
|
+
),
|
|
531
|
+
React.createElement(
|
|
532
|
+
'button',
|
|
533
|
+
{ className: 'dsh-pet-btn dsh-pet-btn-danger', onClick: onUninstall },
|
|
534
|
+
confirming[0] ? 'Confirm uninstall' : 'Uninstall pet',
|
|
535
|
+
),
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if (msg[0]) children.push(React.createElement('div', { className: 'dsh-pet-msg', key: 'msg' }, msg[0]))
|
|
540
|
+
|
|
541
|
+
return React.createElement('div', { className: 'dsh-pet-section' }, children)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function apply(ctx) {
|
|
545
|
+
ensureCss()
|
|
546
|
+
ctx.slots.inject('shell.overlay', function* () {
|
|
547
|
+
yield ctx.slots.register(
|
|
548
|
+
{ name: 'shell.overlay', id: 'dsh-pet-overlay', order: 500 },
|
|
549
|
+
() => React.createElement(PetOverlay),
|
|
550
|
+
)
|
|
551
|
+
})
|
|
552
|
+
ctx.slots.inject('settings.section', function* () {
|
|
553
|
+
yield ctx.slots.register(
|
|
554
|
+
{ name: 'settings.section', id: 'dsh-capyreporter', order: 90, label: '\uD83D\uDC3E CapyReporter' },
|
|
555
|
+
() => React.createElement(PetSettings),
|
|
556
|
+
)
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { name, inject, apply }
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
window.__ModuleLoader__.load({
|
|
565
|
+
id: 'dsh-capyreporter',
|
|
566
|
+
factory: makeFactory(),
|
|
567
|
+
})
|