dsh-todo-float-ball 0.8.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/CHANGELOG.md +431 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/README.zh.md +92 -0
- package/cordis.patch.yml +8 -0
- package/docs/DATA-CHANNELS.md +59 -0
- package/lib/client.js +1115 -0
- package/lib/index.js +36 -0
- package/package.json +55 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-todo-float-ball",
|
|
3
|
+
factory: function (require) {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// dsh-todo-float-ball — client half (browser). v0.3.0
|
|
9
|
+
//
|
|
10
|
+
// A persistent, draggable floating ball that mirrors todo_write lists.
|
|
11
|
+
//
|
|
12
|
+
// v0.3.0 — data layer rebuilt on the official `sessions` service:
|
|
13
|
+
// * inject: ["sessions"] — the runtime's global session registry.
|
|
14
|
+
// Its list snapshot carries, for EVERY session, the projection
|
|
15
|
+
// values computed by the host (todos, title/plan/...), plus the
|
|
16
|
+
// current session id and a subscribe() for changes.
|
|
17
|
+
// * This gives us multi-session todo data and instant session-switch
|
|
18
|
+
// detection with ZERO interception — no fetch/WS wrapping (desktop
|
|
19
|
+
// renders go through the shell's __DSH_TRANSPORT__ Electron bridge,
|
|
20
|
+
// which page-level taps can never see).
|
|
21
|
+
// * Fallback: if ctx.sessions is unavailable, fall back to observing
|
|
22
|
+
// the official todo panel DOM ([data-testid="todo-panel"]).
|
|
23
|
+
//
|
|
24
|
+
// Features: auto-follow active conversation; pin any conversation (📌)
|
|
25
|
+
// for cross-session monitoring; pinned list persists in localStorage;
|
|
26
|
+
// per-status colors; Shadow DOM isolation; draggable ball.
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
var BALL_ID = "dsh-tfb-root";
|
|
30
|
+
var POS_KEY = "dsh-todo-float-ball-pos";
|
|
31
|
+
var PIN_KEY = "dsh-todo-float-ball-pinned";
|
|
32
|
+
var CAPSULE_KEY = "dsh-todo-float-ball-capsule";
|
|
33
|
+
var MAX_CONTENT = 80;
|
|
34
|
+
|
|
35
|
+
// ---------- multi-session state ----------
|
|
36
|
+
var bySession = {}; // sessionId -> { todos:[{content,status}], sig }
|
|
37
|
+
var titles = {}; // sessionId -> display title
|
|
38
|
+
var pinned = []; // [sessionId,...] (persisted)
|
|
39
|
+
var currentSid = null; // active session (from sessions.list.current)
|
|
40
|
+
var hasSessionsSvc = false;
|
|
41
|
+
var currentSessions = null;
|
|
42
|
+
var expanded = false;
|
|
43
|
+
var capsule = false; // right-click: pill mode showing active task
|
|
44
|
+
var openPin = {}; // pinned sessionId -> expanded? (UI state)
|
|
45
|
+
var editing = { sid: null }; // inline rename editor state (one at a time)
|
|
46
|
+
var ui = { root: null, ball: null, panel: null, list: null, summary: null, ctitle: null, pinBtn: null, pinSec: null };
|
|
47
|
+
|
|
48
|
+
function normSid(sid) {
|
|
49
|
+
return (typeof sid === "string" && sid) ? sid : "current";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------- pinned persistence ----------
|
|
53
|
+
function loadPinned() {
|
|
54
|
+
try {
|
|
55
|
+
var raw = localStorage.getItem(PIN_KEY);
|
|
56
|
+
if (raw) {
|
|
57
|
+
var arr = JSON.parse(raw);
|
|
58
|
+
if (Array.isArray(arr)) pinned = arr.filter(function (x) { return typeof x === "string" && x; });
|
|
59
|
+
}
|
|
60
|
+
} catch (e) { pinned = []; }
|
|
61
|
+
}
|
|
62
|
+
function savePinned() {
|
|
63
|
+
try { localStorage.setItem(PIN_KEY, JSON.stringify(pinned)); } catch (e) {}
|
|
64
|
+
}
|
|
65
|
+
function isPinned(sid) { return pinned.indexOf(sid) >= 0; }
|
|
66
|
+
function pin(sid) {
|
|
67
|
+
sid = normSid(sid);
|
|
68
|
+
if (isPinned(sid)) return;
|
|
69
|
+
pinned.push(sid);
|
|
70
|
+
savePinned();
|
|
71
|
+
render();
|
|
72
|
+
}
|
|
73
|
+
function unpin(sid) {
|
|
74
|
+
var i = pinned.indexOf(sid);
|
|
75
|
+
if (i >= 0) { pinned.splice(i, 1); savePinned(); render(); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------- helpers ----------
|
|
79
|
+
function esc(s) {
|
|
80
|
+
return String(s)
|
|
81
|
+
.replace(/&/g, "&")
|
|
82
|
+
.replace(/</g, "<")
|
|
83
|
+
.replace(/>/g, ">")
|
|
84
|
+
.replace(/"/g, """);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function countsOf(list) {
|
|
88
|
+
var done = 0, active = 0;
|
|
89
|
+
for (var i = 0; i < list.length; i++) {
|
|
90
|
+
if (list[i].status === "completed") done++;
|
|
91
|
+
else if (list[i].status === "in_progress") active++;
|
|
92
|
+
}
|
|
93
|
+
return { done: done, active: active, total: list.length };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sessionTodos(sid) {
|
|
97
|
+
var b = bySession[normSid(sid)];
|
|
98
|
+
return b ? b.todos : null;
|
|
99
|
+
}
|
|
100
|
+
function sessionTitle(sid) {
|
|
101
|
+
sid = normSid(sid);
|
|
102
|
+
if (titles[sid]) return titles[sid];
|
|
103
|
+
return sid === "current" ? "" : sid.slice(0, 10) + "…";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------- rename (official API) ----------
|
|
107
|
+
// Path (verified against dsh-client-runtime): the sessions facade exposes
|
|
108
|
+
// .manager; SessionManager lazily builds resident Session records
|
|
109
|
+
// (.sessions Map, .get(sessionId) is side-effect-safe); each Session has
|
|
110
|
+
// .rename(title) → api.sessions.rename → host normalizes → title
|
|
111
|
+
// projection updates → our sessions.list subscription re-renders.
|
|
112
|
+
function renameSession(sid, title) {
|
|
113
|
+
sid = normSid(sid);
|
|
114
|
+
var clean = (typeof title === "string" ? title.trim() : "");
|
|
115
|
+
if (!clean) return Promise.resolve(false);
|
|
116
|
+
try {
|
|
117
|
+
var mgr = currentSessions && currentSessions.manager;
|
|
118
|
+
var session = mgr && mgr.sessions && typeof mgr.sessions.get === "function" ? mgr.sessions.get(sid) : null;
|
|
119
|
+
if (session && typeof session.rename === "function") {
|
|
120
|
+
return Promise.resolve(session.rename(clean)).then(function (result) {
|
|
121
|
+
if (result && result.ok) {
|
|
122
|
+
titles[sid] = (result.value && result.value.title) || clean;
|
|
123
|
+
render();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
render();
|
|
127
|
+
return false;
|
|
128
|
+
}).catch(function () { render(); return false; });
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {}
|
|
131
|
+
return Promise.resolve(false);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function startRename(sid) {
|
|
135
|
+
if (!hasSessionsSvc) return;
|
|
136
|
+
sid = normSid(sid);
|
|
137
|
+
if (editing.sid) return; // one editor at a time
|
|
138
|
+
var isCurrent = sid === normSid(currentSid);
|
|
139
|
+
var host = isCurrent ? ui.ctitle
|
|
140
|
+
: (ui.pinSec ? ui.pinSec.querySelector('.tfb-pinrow[data-sid="' + sid + '"] .tfb-pinname') : null);
|
|
141
|
+
if (!host) return;
|
|
142
|
+
editing.sid = sid;
|
|
143
|
+
var old = sessionTitle(sid);
|
|
144
|
+
host.innerHTML = '<input class="tfb-rename" data-rename="1" value="' + esc(old) + '" placeholder="会话名称">';
|
|
145
|
+
var input = host.querySelector("input");
|
|
146
|
+
if (!input) { editing.sid = null; return; }
|
|
147
|
+
input.focus();
|
|
148
|
+
try { input.select(); } catch (e) {}
|
|
149
|
+
var done = false;
|
|
150
|
+
var commit = function () {
|
|
151
|
+
if (done) return; done = true;
|
|
152
|
+
var v = input.value;
|
|
153
|
+
editing.sid = null;
|
|
154
|
+
if (v && v.trim() && v.trim() !== old) renameSession(sid, v);
|
|
155
|
+
else render();
|
|
156
|
+
};
|
|
157
|
+
var cancel = function () {
|
|
158
|
+
if (done) return; done = true;
|
|
159
|
+
editing.sid = null;
|
|
160
|
+
render();
|
|
161
|
+
};
|
|
162
|
+
input.addEventListener("keydown", function (e) {
|
|
163
|
+
if (e.key === "Enter") { e.preventDefault(); commit(); }
|
|
164
|
+
else if (e.key === "Escape") { e.preventDefault(); cancel(); }
|
|
165
|
+
});
|
|
166
|
+
input.addEventListener("blur", commit);
|
|
167
|
+
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
168
|
+
input.addEventListener("pointerdown", function (e) { e.stopPropagation(); });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function currentList() { return sessionTodos(currentSid) || []; }
|
|
172
|
+
|
|
173
|
+
function summarize(list) {
|
|
174
|
+
var c = countsOf(list);
|
|
175
|
+
var head = c.done + "/" + c.total;
|
|
176
|
+
var detail = "";
|
|
177
|
+
if (c.active > 0) {
|
|
178
|
+
var first = null;
|
|
179
|
+
for (var i = 0; i < list.length; i++) {
|
|
180
|
+
if (list[i].status === "in_progress") { first = list[i].content; break; }
|
|
181
|
+
}
|
|
182
|
+
if (first) detail = first.length > 30 ? first.slice(0, 30) + "…" : first;
|
|
183
|
+
if (c.active > 1) detail += " (+" + (c.active - 1) + ")";
|
|
184
|
+
}
|
|
185
|
+
return { head: head, detail: detail, c: c };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function ballState() {
|
|
189
|
+
var list = currentList();
|
|
190
|
+
if (!list.length) return { cls: "tfb-idle", label: "✓" };
|
|
191
|
+
var c = countsOf(list);
|
|
192
|
+
if (c.done === c.total) return { cls: "tfb-done", label: "✓" };
|
|
193
|
+
return { cls: c.active > 0 ? "tfb-active" : "tfb-pending", label: String(c.done) + "/" + String(c.total) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------- durable persistence (user requirement: cross-turn /
|
|
197
|
+
// cross-refresh record survival) ----------
|
|
198
|
+
// Every accepted snapshot is written to localStorage keyed by session.
|
|
199
|
+
// On restore we take whichever side is "further along" (more completed
|
|
200
|
+
// items, tie-break by total, then by recency) so nothing — a stale
|
|
201
|
+
// snapshot poll, a page reload, a projection rebuild — can drag the
|
|
202
|
+
// record BACKWARD. Completed items are history, not transients.
|
|
203
|
+
var PERSIST_KEY = "dsh-todo-float-ball-persist-v1";
|
|
204
|
+
var persistTimer = null;
|
|
205
|
+
function loadPersisted() {
|
|
206
|
+
var out = {};
|
|
207
|
+
try {
|
|
208
|
+
var raw = localStorage.getItem(PERSIST_KEY);
|
|
209
|
+
if (raw) out = JSON.parse(raw) || {};
|
|
210
|
+
} catch (e) { out = {}; }
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
function persistSoon() {
|
|
214
|
+
if (persistTimer) return;
|
|
215
|
+
persistTimer = setTimeout(function () {
|
|
216
|
+
persistTimer = null;
|
|
217
|
+
try {
|
|
218
|
+
var out = {};
|
|
219
|
+
for (var sid in bySession) {
|
|
220
|
+
var b = bySession[sid];
|
|
221
|
+
if (b && b.todos && b.todos.length) {
|
|
222
|
+
out[sid] = { todos: b.todos, sig: b.sig, at: b.at || 0 };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
localStorage.setItem(PERSIST_KEY, JSON.stringify(out));
|
|
226
|
+
} catch (e) {}
|
|
227
|
+
}, 400);
|
|
228
|
+
}
|
|
229
|
+
// progress score: completed count dominates, then total, then recency.
|
|
230
|
+
// planKey: the sorted content set — identical set = same plan (progress
|
|
231
|
+
// within one plan must never regress); different set = a new plan.
|
|
232
|
+
function planKey(todos) {
|
|
233
|
+
return todos.map(function (t) { return t.content; }).sort().join("\n");
|
|
234
|
+
}
|
|
235
|
+
function progressScore(todos, at) {
|
|
236
|
+
var c = countsOf(todos);
|
|
237
|
+
return c.done * 10000 + c.total * 100 + Math.min(99, Math.floor(((at || 0) / 60000) % 100));
|
|
238
|
+
}
|
|
239
|
+
function restorePersisted() {
|
|
240
|
+
try {
|
|
241
|
+
var saved = loadPersisted();
|
|
242
|
+
for (var sid in saved) {
|
|
243
|
+
var rec = saved[sid];
|
|
244
|
+
if (!rec || !Array.isArray(rec.todos) || rec.todos.length === 0) continue;
|
|
245
|
+
var norm = [];
|
|
246
|
+
for (var i = 0; i < rec.todos.length; i++) {
|
|
247
|
+
var it = rec.todos[i];
|
|
248
|
+
if (it && typeof it.content === "string" && it.content.trim()) {
|
|
249
|
+
norm.push({ content: it.content, status: it.status });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!norm.length) continue;
|
|
253
|
+
var mem = bySession[sid];
|
|
254
|
+
if (mem && mem.todos && mem.todos.length) {
|
|
255
|
+
// different plan: the live snapshot wins (it is what the host
|
|
256
|
+
// currently holds); same plan: keep the further-along record
|
|
257
|
+
if (planKey(mem.todos) !== planKey(norm)) continue;
|
|
258
|
+
if (progressScore(norm, rec.at) <= progressScore(mem.todos, mem.at)) continue;
|
|
259
|
+
}
|
|
260
|
+
bySession[sid] = { todos: norm, sig: rec.sig || norm.map(function (t) { return t.status + "|" + t.content; }).join("\n"), stale: false, at: rec.at || 0 };
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------- data ingestion ----------
|
|
266
|
+
function setSessionTodos(sid, list) {
|
|
267
|
+
sid = normSid(sid);
|
|
268
|
+
if (!Array.isArray(list)) return;
|
|
269
|
+
var norm = [];
|
|
270
|
+
for (var i = 0; i < list.length; i++) {
|
|
271
|
+
var it = list[i];
|
|
272
|
+
if (!it || typeof it !== "object") continue;
|
|
273
|
+
var st = it.status;
|
|
274
|
+
if (st !== "pending" && st !== "in_progress" && st !== "completed") continue;
|
|
275
|
+
var content = typeof it.content === "string" ? it.content : "";
|
|
276
|
+
if (!content.trim()) continue;
|
|
277
|
+
norm.push({ content: content, status: st });
|
|
278
|
+
}
|
|
279
|
+
if (norm.length === 0) {
|
|
280
|
+
// STICKY: the host resets the todos projection to null at every
|
|
281
|
+
// turn/start. Never wipe the last known list — only a real
|
|
282
|
+
// todo_write snapshot (or a strictly-more-complete persisted one)
|
|
283
|
+
// changes it. Per-session bucketing keeps this from bleeding
|
|
284
|
+
// across conversations.
|
|
285
|
+
var b0 = bySession[sid];
|
|
286
|
+
if (b0) b0.stale = true;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
var sig = norm.map(function (t) { return t.status + "|" + t.content; }).join("\n");
|
|
290
|
+
var b = bySession[sid];
|
|
291
|
+
if (b && b.sig === sig) { b.stale = false; return; }
|
|
292
|
+
// MONOTONIC GUARD (plan-aware, v0.6.4): a todo_write is a whole-list
|
|
293
|
+
// replacement — a NEW plan legitimately starts at 0 done, so the old
|
|
294
|
+
// "fewer done = stale snapshot" check was rejecting freshly assigned
|
|
295
|
+
// tasks (user bug: new plan never showed, ball stuck at "all done").
|
|
296
|
+
// The guard now only applies WITHIN the same plan (identical content
|
|
297
|
+
// set): same tasks but fewer completed = an older snapshot → reject.
|
|
298
|
+
// Different content set = a new/edited plan → always accept.
|
|
299
|
+
if (b && b.todos && b.todos.length && planKey(b.todos) === planKey(norm)) {
|
|
300
|
+
var cMem = countsOf(b.todos);
|
|
301
|
+
var cNew = countsOf(norm);
|
|
302
|
+
if (cNew.done < cMem.done) {
|
|
303
|
+
return; // same plan, progress regressed — stale snapshot, keep ours
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
bySession[sid] = { todos: norm, sig: sig, stale: false, at: Date.now() };
|
|
307
|
+
render();
|
|
308
|
+
persistSoon();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Primary channel: the official sessions service.
|
|
312
|
+
// list snapshot shape (from dsh-client-runtime):
|
|
313
|
+
// { ids:[...], byId:{ sid:{ id, displayTitle, running, projectionValues:{todos,title,...}, ... } },
|
|
314
|
+
// current: sid|undefined, phase }
|
|
315
|
+
function syncFromSessions() {
|
|
316
|
+
try {
|
|
317
|
+
var snap = currentSessions.list.getSnapshot();
|
|
318
|
+
if (!snap || !snap.byId) return;
|
|
319
|
+
for (var id in snap.byId) {
|
|
320
|
+
var rec = snap.byId[id];
|
|
321
|
+
if (!rec) continue;
|
|
322
|
+
if (typeof rec.displayTitle === "string" && rec.displayTitle) titles[id] = rec.displayTitle;
|
|
323
|
+
var pv = rec.projectionValues;
|
|
324
|
+
if (pv && typeof pv === "object" && "todos" in pv) {
|
|
325
|
+
setSessionTodos(id, pv.todos === null ? [] : pv.todos);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (snap.current) currentSid = snap.current;
|
|
329
|
+
render();
|
|
330
|
+
} catch (e) {}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fallback channel: observe the official todo panel DOM. Expanded panel
|
|
334
|
+
// renders ul > li[data-status]; collapsed renders only header counts
|
|
335
|
+
// (parsed into a skeleton — better than nothing when sessions service
|
|
336
|
+
// is unavailable).
|
|
337
|
+
function installPanelObserver() {
|
|
338
|
+
var mo = new MutationObserver(function () {
|
|
339
|
+
if (installPanelObserver._t) return;
|
|
340
|
+
installPanelObserver._t = setTimeout(function () {
|
|
341
|
+
installPanelObserver._t = 0;
|
|
342
|
+
try { readOfficialPanel(); } catch (e) {}
|
|
343
|
+
}, 200);
|
|
344
|
+
});
|
|
345
|
+
function start() {
|
|
346
|
+
try {
|
|
347
|
+
mo.observe(document.documentElement || document.body, { childList: true, subtree: true });
|
|
348
|
+
readOfficialPanel();
|
|
349
|
+
} catch (e) {}
|
|
350
|
+
}
|
|
351
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
|
|
352
|
+
else start();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
var ZH_NUM = { "零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, "十": 10 };
|
|
356
|
+
|
|
357
|
+
function parseZhNum(s) {
|
|
358
|
+
s = String(s).trim();
|
|
359
|
+
if (/^\d+$/.test(s)) return parseInt(s, 10);
|
|
360
|
+
if (ZH_NUM[s] !== undefined) return ZH_NUM[s];
|
|
361
|
+
if (/^十$/.test(s)) return 10;
|
|
362
|
+
var m = /^([一二两三四五六七八九])?十([一二三四五六七八九])?$/.exec(s);
|
|
363
|
+
if (m) return (m[1] ? ZH_NUM[m[1]] : 1) * 10 + (m[2] ? ZH_NUM[m[2]] : 0);
|
|
364
|
+
return NaN;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function readOfficialPanel() {
|
|
368
|
+
var panel = document.querySelector('[data-testid="todo-panel"]');
|
|
369
|
+
if (!panel) return;
|
|
370
|
+
var items = panel.querySelectorAll("ul li[data-status]");
|
|
371
|
+
if (items.length > 0) {
|
|
372
|
+
var list = [];
|
|
373
|
+
for (var i = 0; i < items.length; i++) {
|
|
374
|
+
var li = items[i];
|
|
375
|
+
var st = li.getAttribute("data-status");
|
|
376
|
+
var contentEl = li.querySelector("span:last-child") || li.lastElementChild;
|
|
377
|
+
var content = contentEl ? (contentEl.textContent || "").trim() : "";
|
|
378
|
+
if (content) list.push({ content: content, status: st || "pending" });
|
|
379
|
+
}
|
|
380
|
+
if (list.length > 0) { setSessionTodos(currentSid, list); return; }
|
|
381
|
+
}
|
|
382
|
+
// collapsed: skeleton only if the current session has no real data
|
|
383
|
+
var cur = bySession[normSid(currentSid)];
|
|
384
|
+
var hasReal = cur && cur.todos && cur.todos.length > 0 && !cur.__skeleton;
|
|
385
|
+
if (hasReal) return;
|
|
386
|
+
var label = panel.querySelector('[class*="progress"]');
|
|
387
|
+
if (!label) return;
|
|
388
|
+
var text = (label.textContent || "").trim();
|
|
389
|
+
if (!text) return;
|
|
390
|
+
var segs = text.split("·");
|
|
391
|
+
var done = NaN, active = NaN, pending = NaN;
|
|
392
|
+
for (var k = 0; k < segs.length; k++) {
|
|
393
|
+
var s = segs[k].trim();
|
|
394
|
+
var n = parseZhNum(s.split(" ")[0]);
|
|
395
|
+
if (isNaN(n)) continue;
|
|
396
|
+
if (s.indexOf("完成") >= 0) done = n;
|
|
397
|
+
else if (s.indexOf("进行") >= 0) active = n;
|
|
398
|
+
else if (s.indexOf("待办") >= 0) pending = n;
|
|
399
|
+
}
|
|
400
|
+
if (isNaN(done) && isNaN(active) && isNaN(pending)) return;
|
|
401
|
+
// only show the skeleton when we have nothing real, and mark it
|
|
402
|
+
if (bySession[normSid(currentSid)] && !bySession[normSid(currentSid)].__skeleton) return;
|
|
403
|
+
var skel = [];
|
|
404
|
+
for (var d = 0; d < (isNaN(done) ? 0 : done); d++) skel.push({ content: "已完成任务", status: "completed" });
|
|
405
|
+
for (var a = 0; a < (isNaN(active) ? 0 : active); a++) skel.push({ content: "进行中任务", status: "in_progress" });
|
|
406
|
+
for (var p = 0; p < (isNaN(pending) ? 0 : pending); p++) skel.push({ content: "待办任务", status: "pending" });
|
|
407
|
+
if (skel.length > 0) {
|
|
408
|
+
setSessionTodos(currentSid, skel);
|
|
409
|
+
var bb = bySession[normSid(currentSid)];
|
|
410
|
+
if (bb) bb.__skeleton = true;
|
|
411
|
+
render();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------- UI ----------
|
|
416
|
+
// ---------- theme (dual skin, user requirement) ----------
|
|
417
|
+
// "nebula" — skill-orb style clone (v0.6.2): nebula body + flowing
|
|
418
|
+
// bands + rotating conic rim + breathing + glow text.
|
|
419
|
+
// "graphite" — v0.5.1 dark-gem style: same glass family as
|
|
420
|
+
// font-enhancer, teal accent + thin progress ring.
|
|
421
|
+
// Persisted in localStorage; switched from the shared "悬浮球导航"
|
|
422
|
+
// settings section (dsh-skill-browser provides the button).
|
|
423
|
+
var THEME_KEY = "dsh-tfb-theme";
|
|
424
|
+
var currentTheme = "nebula";
|
|
425
|
+
function loadTheme() {
|
|
426
|
+
try {
|
|
427
|
+
var v = localStorage.getItem(THEME_KEY);
|
|
428
|
+
if (v === "blue") currentTheme = "blue"; else if (v === "nebula") currentTheme = "nebula"; else if (v === "nebB") currentTheme = "nebB"; else if (v === "nebC") currentTheme = "nebC"; else if (v === "glass") currentTheme = "glass"; else currentTheme = "graphite";
|
|
429
|
+
} catch (e) {}
|
|
430
|
+
}
|
|
431
|
+
function setTheme(t) {
|
|
432
|
+
if (t !== "nebula" && t !== "graphite" && t !== "blue" && t !== "glass" && t !== "nebB" && t !== "nebC") return;
|
|
433
|
+
currentTheme = t;
|
|
434
|
+
try { localStorage.setItem(THEME_KEY, t); } catch (e) {}
|
|
435
|
+
render();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function cssText() {
|
|
439
|
+
var th = currentTheme;
|
|
440
|
+
// Skeleton fields that all four skins share; shape differs per skin.
|
|
441
|
+
var SIZE = "width:46px;height:46px;";
|
|
442
|
+
// Per-skin ball rules (status variants + ring + band/rim).
|
|
443
|
+
var ball = [];
|
|
444
|
+
if (th === "nebula") {
|
|
445
|
+
ball = [
|
|
446
|
+
".tfb-ball{position:fixed;right:18px;bottom:18px;z-index:2147483600;border-radius:50%;border:1px solid rgba(255,255,255,.28);cursor:grab;user-select:none;touch-action:none;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;color:#eafff7;font-size:13px;font-weight:800;line-height:1.1;background:radial-gradient(circle at 50% 60%, #2dd4a8 0%, #17a284 20%, #12876f 45%, #0a3a32 68%, #041715 100%);box-shadow:0 0 22px rgba(45,212,168,.5),0 0 4px rgba(120,255,214,.35),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.4),inset 0 -8px 16px rgba(45,212,168,.16),inset 0 0 14px rgba(20,180,150,.25);text-shadow:0 0 6px rgba(120,255,214,.75),0 2px 4px rgba(0,40,30,.8);filter:drop-shadow(0 0 6px rgba(160,255,225,.85));animation:tfbBreathe 3.2s ease-in-out infinite;transition:transform .15s ease}" + SIZE.replace(";", ";"),
|
|
447
|
+
".tfb-orb-band{display:none}",
|
|
448
|
+
".tfb-orb-rim{position:absolute;inset:-1px;border-radius:50%;z-index:1;pointer-events:none;background:conic-gradient(from 200deg, rgba(255,255,255,.55), rgba(120,240,205,.35), rgba(10,200,170,.4), rgba(255,255,255,.15), rgba(45,212,168,.4), rgba(255,255,255,.55));-webkit-mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);animation:tfbRimSpin 7s linear infinite;filter:blur(.5px)}",
|
|
449
|
+
".tfb-ball .tfb-ring{display:none}",
|
|
450
|
+
".tfb-ball .tfb-label{animation:tfbGlow 2.4s ease-in-out infinite}",
|
|
451
|
+
".tfb-ball.tfb-active{color:#fff3dc;background:radial-gradient(circle at 50% 60%, #ffaa33 0%, #e08410 20%, #b56708 45%, #4a2a06 70%, #170c02 100%);box-shadow:0 0 22px rgba(255,170,51,.55),0 0 4px rgba(255,210,120,.4),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.4),inset 0 -8px 16px rgba(255,170,51,.16),inset 0 0 14px rgba(230,140,20,.25);text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(255,215,140,.95),0 0 20px rgba(255,160,40,.8),0 2px 4px rgba(60,30,0,.8);filter:drop-shadow(0 0 6px rgba(255,200,110,.9))}",
|
|
452
|
+
".tfb-ball.tfb-active .tfb-orb-rim{background:conic-gradient(from 200deg, rgba(255,255,255,.6), rgba(255,200,110,.4), rgba(255,150,30,.45), rgba(255,255,255,.15), rgba(255,190,80,.4), rgba(255,255,255,.6));animation-duration:2.6s}",
|
|
453
|
+
".tfb-ball.tfb-done{color:#eafff4;background:radial-gradient(circle at 50% 60%, #5dffa8 0%, #22c070 20%, #12875c 45%, #073c26 70%, #02150c 100%);box-shadow:0 0 26px rgba(93,255,168,.6),0 0 5px rgba(180,255,215,.45),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.45),inset 0 -8px 16px rgba(93,255,168,.18),inset 0 0 14px rgba(40,200,120,.3);text-shadow:0 0 4px rgba(255,255,255,1),0 0 12px rgba(190,255,220,1),0 0 24px rgba(60,255,150,.85),0 2px 4px rgba(0,40,20,.8);filter:drop-shadow(0 0 8px rgba(140,255,195,.95))}",
|
|
454
|
+
".tfb-ball.tfb-pending{color:#e8f3ff;background:radial-gradient(circle at 50% 60%, #7fc4ff 0%, #4f92e0 20%, #2f6cb0 45%, #12305c 70%, #04101e 100%);box-shadow:0 0 22px rgba(127,196,255,.5),0 0 4px rgba(180,220,255,.35),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.4),inset 0 -8px 16px rgba(127,196,255,.15),inset 0 0 14px rgba(70,140,220,.25);text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(190,225,255,.9),0 0 20px rgba(90,170,255,.75),0 2px 4px rgba(0,20,45,.8);filter:drop-shadow(0 0 6px rgba(170,215,255,.85))}"
|
|
455
|
+
];
|
|
456
|
+
} else if (th === "blue") {
|
|
457
|
+
ball = [
|
|
458
|
+
".tfb-ball{position:fixed;right:18px;bottom:18px;z-index:2147483600;border-radius:50%;border:1px solid rgba(255,255,255,.28);cursor:grab;user-select:none;touch-action:none;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;color:#a8c8ff;font-size:13px;font-weight:800;line-height:1.1;background:radial-gradient(circle at 30% 25%,rgba(255,255,255,.18),rgba(26,40,66,.96) 70%);box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.18),0 0 18px rgba(100,160,255,.5);transition:transform .15s ease}" + SIZE.replace(";", ";"),
|
|
459
|
+
".tfb-orb-band{display:none}",
|
|
460
|
+
".tfb-orb-rim{display:none}",
|
|
461
|
+
".tfb-ball .tfb-ring{position:absolute;inset:-1px;border-radius:50%;pointer-events:none;background:conic-gradient(var(--rc,#6aa8ff) calc(var(--pct,0)*1%),rgba(255,255,255,.08) 0);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 5px),#000 calc(100% - 4px));mask:radial-gradient(farthest-side,transparent calc(100% - 5px),#000 calc(100% - 4px));transition:background .4s ease;filter:drop-shadow(0 0 6px rgba(100,160,255,.5))}",
|
|
462
|
+
".tfb-ball .tfb-label{text-shadow:0 0 10px currentColor,0 0 3px currentColor}",
|
|
463
|
+
".tfb-ball.tfb-active{color:#ffcf8a;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.18),0 0 16px rgba(255,170,51,.5);animation:tfb-pulse 1.6s ease-in-out infinite}",
|
|
464
|
+
".tfb-ball.tfb-active .tfb-ring{--rc:#ffaa33;filter:drop-shadow(0 0 8px rgba(255,170,51,.6))}",
|
|
465
|
+
".tfb-ball.tfb-done{color:#a5ffd4;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.18),0 0 18px rgba(93,255,168,.5)}",
|
|
466
|
+
".tfb-ball.tfb-done .tfb-ring{--rc:#5dffa8;filter:drop-shadow(0 0 10px rgba(93,255,168,.7))}",
|
|
467
|
+
".tfb-ball.tfb-pending{color:#bcd9ff;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.18),0 0 14px rgba(127,196,255,.5)}",
|
|
468
|
+
".tfb-ball.tfb-pending .tfb-ring{--rc:#7fc4ff;filter:drop-shadow(0 0 8px rgba(127,196,255,.6))}"
|
|
469
|
+
];
|
|
470
|
+
} else if (th === "nebB") {
|
|
471
|
+
ball = [
|
|
472
|
+
".tfb-ball{position:fixed;right:18px;bottom:18px;z-index:2147483600;border-radius:50%;border:1px solid rgba(255,255,255,.28);cursor:grab;user-select:none;touch-action:none;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;color:#d9fff0;font-size:13px;font-weight:800;line-height:1.1;background:radial-gradient(circle at 50% 62%, rgba(210,255,244,.5) 0%, rgba(45,212,168,.75) 14%, rgba(18,160,130,.85) 30%, rgba(12,110,92,.95) 50%, rgba(6,40,34,1) 72%, rgba(2,16,14,1) 100%);box-shadow:0 0 20px rgba(45,212,168,.45),inset 0 2px 5px rgba(255,255,255,.45),inset 0 -10px 18px rgba(45,212,168,.25),inset 0 0 18px rgba(10,60,50,.5);text-shadow:0 0 6px rgba(120,255,214,.75),0 2px 4px rgba(0,40,30,.8);animation:tfbBreathe 3.2s ease-in-out infinite;transition:transform .15s ease}" + SIZE,
|
|
473
|
+
".tfb-orb-band{display:none}",
|
|
474
|
+
".tfb-orb-rim{position:absolute;inset:-1px;border-radius:50%;z-index:1;pointer-events:none;background:conic-gradient(from 200deg, rgba(255,255,255,.45), rgba(120,240,205,.3), rgba(10,200,170,.35), rgba(255,255,255,.12), rgba(45,212,168,.35), rgba(255,255,255,.45));-webkit-mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);animation:tfbRimSpin 7s linear infinite;filter:blur(.5px)}",
|
|
475
|
+
".tfb-ball .tfb-ring{display:none}",
|
|
476
|
+
".tfb-ball .tfb-label{animation:tfbGlow 2.4s ease-in-out infinite}",
|
|
477
|
+
".tfb-ball.tfb-active{color:#ffd9a8;background:radial-gradient(circle at 50% 62%, rgba(255,240,210,.55) 0%, rgba(255,180,70,.8) 14%, rgba(220,130,25,.9) 30%, rgba(150,85,15,.95) 50%, rgba(60,32,6,1) 72%, rgba(24,12,2,1) 100%);box-shadow:0 0 20px rgba(255,170,51,.5),inset 0 2px 5px rgba(255,255,255,.5),inset 0 -10px 18px rgba(255,170,51,.28),inset 0 0 18px rgba(90,50,10,.5)}",
|
|
478
|
+
".tfb-ball.tfb-active .tfb-orb-rim{background:conic-gradient(from 200deg, rgba(255,255,255,.5), rgba(255,200,110,.35), rgba(255,150,30,.4), rgba(255,255,255,.12), rgba(255,190,80,.35), rgba(255,255,255,.5));animation-duration:2.6s}",
|
|
479
|
+
".tfb-ball.tfb-done{color:#d9fff0;background:radial-gradient(circle at 50% 62%, rgba(220,255,236,.5) 0%, rgba(93,255,168,.75) 14%, rgba(30,190,105,.85) 30%, rgba(15,130,75,.95) 50%, rgba(5,45,28,1) 72%, rgba(2,16,10,1) 100%);box-shadow:0 0 20px rgba(93,255,168,.5),inset 0 2px 5px rgba(255,255,255,.5),inset 0 -10px 18px rgba(93,255,168,.25),inset 0 0 18px rgba(8,60,38,.5)}",
|
|
480
|
+
".tfb-ball.tfb-pending{color:#dbeafe;background:radial-gradient(circle at 50% 62%, rgba(220,240,255,.5) 0%, rgba(127,196,255,.7) 14%, rgba(59,130,196,.85) 30%, rgba(35,90,150,.95) 50%, rgba(10,35,65,1) 72%, rgba(3,12,24,1) 100%);box-shadow:0 0 20px rgba(127,196,255,.45),inset 0 2px 5px rgba(255,255,255,.45),inset 0 -10px 18px rgba(127,196,255,.22),inset 0 0 18px rgba(15,45,85,.5)}"
|
|
481
|
+
];
|
|
482
|
+
} else if (th === "nebC") {
|
|
483
|
+
ball = [
|
|
484
|
+
".tfb-ball{position:fixed;right:18px;bottom:18px;z-index:2147483600;border-radius:50%;border:1px solid rgba(255,255,255,.28);cursor:grab;user-select:none;touch-action:none;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;color:#d9fff0;font-size:13px;font-weight:800;line-height:1.1;background:radial-gradient(circle at 50% 55%, #1a8a70 0%, #0f6b58 30%, #083f35 60%, #031511 100%);box-shadow:0 0 18px rgba(45,212,168,.35),inset 0 2px 6px rgba(255,255,255,.28),inset 0 -8px 14px rgba(0,0,0,.5);text-shadow:0 0 6px rgba(120,255,214,.75),0 2px 4px rgba(0,40,30,.8);animation:tfbBreathe 3.2s ease-in-out infinite;transition:transform .15s ease}" + SIZE,
|
|
485
|
+
".tfb-ball::before{content:\"\";position:absolute;inset:4px 6px 58% 6px;border-radius:50%;background:radial-gradient(ellipse at 50% 30%, rgba(140,255,225,.55) 0%, rgba(140,255,225,.12) 60%, transparent 80%);filter:blur(1px);z-index:1;pointer-events:none}",
|
|
486
|
+
".tfb-orb-band{display:none}",
|
|
487
|
+
".tfb-orb-rim{position:absolute;inset:-1px;border-radius:50%;z-index:1;pointer-events:none;background:conic-gradient(from 200deg, rgba(255,255,255,.45), rgba(120,240,205,.3), rgba(10,200,170,.35), rgba(255,255,255,.12), rgba(45,212,168,.35), rgba(255,255,255,.45));-webkit-mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);animation:tfbRimSpin 7s linear infinite;filter:blur(.5px)}",
|
|
488
|
+
".tfb-ball .tfb-ring{display:none}",
|
|
489
|
+
".tfb-ball .tfb-label{animation:tfbGlow 2.4s ease-in-out infinite}",
|
|
490
|
+
".tfb-ball.tfb-active{color:#ffd9a8;background:radial-gradient(circle at 50% 55%, #d98a1a 0%, #a86410 30%, #5c3608 60%, #201003 100%);box-shadow:0 0 18px rgba(255,170,51,.4),inset 0 2px 6px rgba(255,255,255,.3),inset 0 -8px 14px rgba(0,0,0,.5)}",
|
|
491
|
+
".tfb-ball.tfb-active::before{background:radial-gradient(ellipse at 50% 30%, rgba(255,220,160,.55) 0%, rgba(255,220,160,.12) 60%, transparent 80%)}",
|
|
492
|
+
".tfb-ball.tfb-active .tfb-orb-rim{background:conic-gradient(from 200deg, rgba(255,255,255,.5), rgba(255,200,110,.35), rgba(255,150,30,.4), rgba(255,255,255,.12), rgba(255,190,80,.35), rgba(255,255,255,.5));animation-duration:2.6s}",
|
|
493
|
+
".tfb-ball.tfb-done{color:#d9fff0;background:radial-gradient(circle at 50% 55%, #17a06a 0%, #0f7a4e 30%, #084a30 60%, #03150c 100%);box-shadow:0 0 18px rgba(93,255,168,.4),inset 0 2px 6px rgba(255,255,255,.28),inset 0 -8px 14px rgba(0,0,0,.5)}",
|
|
494
|
+
".tfb-ball.tfb-done::before{background:radial-gradient(ellipse at 50% 30%, rgba(180,255,220,.55) 0%, rgba(180,255,220,.12) 60%, transparent 80%)}",
|
|
495
|
+
".tfb-ball.tfb-pending{color:#dbeafe;background:radial-gradient(circle at 50% 55%, #2a6cb0 0%, #1c5288 30%, #10315c 60%, #04101e 100%);box-shadow:0 0 18px rgba(127,196,255,.35),inset 0 2px 6px rgba(255,255,255,.28),inset 0 -8px 14px rgba(0,0,0,.5)}",
|
|
496
|
+
".tfb-ball.tfb-pending::before{background:radial-gradient(ellipse at 50% 30%, rgba(180,215,255,.55) 0%, rgba(180,215,255,.12) 60%, transparent 80%)}"
|
|
497
|
+
];
|
|
498
|
+
} else if (th === "glass") {
|
|
499
|
+
ball = [
|
|
500
|
+
".tfb-ball{position:fixed;right:18px;bottom:18px;z-index:2147483600;border-radius:50%;border:1px solid rgba(255,255,255,.4);cursor:grab;user-select:none;touch-action:none;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;color:#16324a;font-size:13px;font-weight:800;line-height:1.1;background:linear-gradient(145deg,rgba(190,225,245,.4) 0%,rgba(130,170,205,.28) 50%,rgba(85,115,150,.42) 100%);backdrop-filter:blur(4px);box-shadow:inset 0 1px 5px rgba(255,255,255,.55),inset 0 -8px 14px rgba(255,255,255,.1),0 4px 16px rgba(0,0,0,.35);transition:transform .15s ease}" + SIZE,
|
|
501
|
+
".tfb-orb-band{display:none}",
|
|
502
|
+
".tfb-orb-rim{display:none}",
|
|
503
|
+
".tfb-ball::before{content:\"\";position:absolute;inset:3px 3px 55% 3px;border-radius:50% 50% 40% 40%;background:linear-gradient(rgba(255,255,255,.55),rgba(255,255,255,.06));filter:blur(.5px);pointer-events:none}",
|
|
504
|
+
".tfb-ball .tfb-ring{position:absolute;inset:-1px;border-radius:50%;pointer-events:none;background:conic-gradient(var(--rc,#3b82c4) calc(var(--pct,0)*1%),rgba(255,255,255,.25) 0);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 4px),#000 calc(100% - 3px));mask:radial-gradient(farthest-side,transparent calc(100% - 4px),#000 calc(100% - 3px));transition:background .4s ease;filter:drop-shadow(0 0 5px rgba(80,140,200,.5))}",
|
|
505
|
+
".tfb-ball .tfb-label,.tfb-ball .tfb-sub{text-shadow:0 1px 2px rgba(255,255,255,.6)}",
|
|
506
|
+
".tfb-ball.tfb-active{color:#7a4a08;box-shadow:inset 0 1px 5px rgba(255,255,255,.55),inset 0 -8px 14px rgba(255,190,120,.15),0 4px 16px rgba(0,0,0,.35),0 0 14px rgba(255,170,51,.35)}",
|
|
507
|
+
".tfb-ball.tfb-active .tfb-ring{--rc:#ffaa33;filter:drop-shadow(0 0 7px rgba(255,170,51,.6))}",
|
|
508
|
+
".tfb-ball.tfb-done{color:#14532d;box-shadow:inset 0 1px 5px rgba(255,255,255,.55),inset 0 -8px 14px rgba(160,255,200,.15),0 4px 16px rgba(0,0,0,.35),0 0 14px rgba(93,255,168,.35)}",
|
|
509
|
+
".tfb-ball.tfb-done .tfb-ring{--rc:#16a34a;filter:drop-shadow(0 0 7px rgba(22,163,74,.6))}",
|
|
510
|
+
".tfb-ball.tfb-pending{color:#1e3a5f;box-shadow:inset 0 1px 5px rgba(255,255,255,.55),inset 0 -8px 14px rgba(170,205,255,.15),0 4px 16px rgba(0,0,0,.35),0 0 12px rgba(100,160,255,.3)}",
|
|
511
|
+
".tfb-ball.tfb-pending .tfb-ring{--rc:#3b82c4;filter:drop-shadow(0 0 6px rgba(59,130,196,.55))}"
|
|
512
|
+
];
|
|
513
|
+
} else { // graphite
|
|
514
|
+
ball = [
|
|
515
|
+
".tfb-ball{position:fixed;right:18px;bottom:18px;z-index:2147483600;border-radius:50%;border:1px solid rgba(255,255,255,.28);cursor:grab;user-select:none;touch-action:none;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;color:#7defc4;font-size:13px;font-weight:800;line-height:1.1;background:radial-gradient(circle at 30% 25%,rgba(255,255,255,.15),rgba(40,44,54,.95) 70%);box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15),0 0 14px rgba(45,212,168,.28);transition:transform .15s ease}" + SIZE.replace(";", ";"),
|
|
516
|
+
".tfb-orb-band{display:none}",
|
|
517
|
+
".tfb-orb-rim{display:none}",
|
|
518
|
+
".tfb-ball .tfb-ring{position:absolute;inset:-1px;border-radius:50%;pointer-events:none;background:conic-gradient(var(--rc,#2dd4a8) calc(var(--pct,0)*1%),rgba(255,255,255,.07) 0);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 5px),#000 calc(100% - 4px));mask:radial-gradient(farthest-side,transparent calc(100% - 5px),#000 calc(100% - 4px));transition:background .4s ease}",
|
|
519
|
+
".tfb-ball .tfb-label{text-shadow:0 0 8px currentColor,0 0 2px currentColor}",
|
|
520
|
+
".tfb-ball.tfb-active{color:#ffcf8a;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15),0 0 16px rgba(255,170,51,.45);animation:tfb-pulse 1.6s ease-in-out infinite}",
|
|
521
|
+
".tfb-ball.tfb-active .tfb-ring{--rc:#ffaa33}",
|
|
522
|
+
".tfb-ball.tfb-done{color:#8dffc8;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15),0 0 16px rgba(93,255,168,.4)}",
|
|
523
|
+
".tfb-ball.tfb-done .tfb-ring{--rc:#5dffa8}",
|
|
524
|
+
".tfb-ball.tfb-pending{color:#a8c8ec;box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15),0 0 12px rgba(127,196,255,.3)}",
|
|
525
|
+
".tfb-ball.tfb-pending .tfb-ring{--rc:#7fc4ff}"
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
return [
|
|
529
|
+
":host{all:initial}",
|
|
530
|
+
".tfb-root{position:static;pointer-events:none;font-family:'Segoe UI','Microsoft YaHei',system-ui,sans-serif}",
|
|
531
|
+
".tfb-ball:hover{transform:scale(1.08)}",
|
|
532
|
+
".tfb-ball:active{cursor:grabbing;transform:scale(.95)}"
|
|
533
|
+
].concat(ball, [
|
|
534
|
+
"@keyframes tfbBandFlow{0%,100%{transform:translateY(-3px) scaleY(.94);opacity:.8}50%{transform:translateY(3px) scaleY(1.12);opacity:1}}",
|
|
535
|
+
"@keyframes tfbRimSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}",
|
|
536
|
+
"@keyframes tfbBreathe{0%,100%{transform:scale(.96);filter:brightness(.92)}50%{transform:scale(1.05);filter:brightness(1.12)}}",
|
|
537
|
+
"@keyframes tfbGlow{0%,100%{text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(150,255,220,.9),0 0 20px rgba(30,220,180,.75)}50%{text-shadow:0 0 6px rgba(255,255,255,1),0 0 16px rgba(200,255,235,1),0 0 32px rgba(60,240,195,.95)}}",
|
|
538
|
+
"@keyframes tfb-pulse{0%,100%{box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15),0 0 16px rgba(255,170,51,.45),0 0 0 0 rgba(255,170,51,.5)}50%{box-shadow:0 4px 24px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.15),0 0 16px rgba(255,170,51,.55),0 0 0 10px rgba(255,170,51,0)}}",
|
|
539
|
+
".tfb-ball.tfb-active .tfb-label{animation:none;text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(255,215,140,.95),0 0 20px rgba(255,160,40,.8)}",
|
|
540
|
+
".tfb-ball.tfb-done .tfb-label{animation:none;text-shadow:0 0 6px rgba(255,255,255,1),0 0 14px rgba(190,255,220,1),0 0 28px rgba(60,255,150,.9)}",
|
|
541
|
+
// capsule mode (right-click toggle): long pill, wider, text-first
|
|
542
|
+
".tfb-ball.tfb-capsule{width:auto;min-width:220px;max-width:min(520px,60vw);height:44px;border-radius:22px;flex-direction:row;gap:8px;padding:0 16px;font-size:12px;justify-content:flex-start}",
|
|
543
|
+
".tfb-ball.tfb-capsule .tfb-orb-band{inset:1px}",
|
|
544
|
+
".tfb-ball.tfb-capsule .tfb-orb-rim{display:none}",
|
|
545
|
+
".tfb-ball.tfb-capsule .tfb-ring{display:none}",
|
|
546
|
+
".tfb-ball.tfb-capsule .tfb-label{flex-shrink:0}",
|
|
547
|
+
".tfb-ball.tfb-capsule .tfb-sub{flex:1;min-width:0;max-width:none;font-size:12px;text-align:left;overflow:hidden;white-space:nowrap}",
|
|
548
|
+
".tfb-ball.tfb-capsule .tfb-capsule-text{display:inline-block;white-space:nowrap}",
|
|
549
|
+
".tfb-ball.tfb-capsule .tfb-capsule-text.tfb-marquee{animation:tfbMarquee 9s ease-in-out infinite alternate}",
|
|
550
|
+
"@keyframes tfbMarquee{from{transform:translateX(0)}to{transform:translateX(var(--shift,-40px))}}",
|
|
551
|
+
".tfb-panel{position:fixed;z-index:2147483600;width:300px;max-width:calc(100vw - 24px);max-height:74vh;overflow-y:auto;overflow-x:hidden;pointer-events:auto;display:none;color:#e6e6e6;font-size:13px;line-height:1.5;border-radius:14px;padding:12px 14px 10px;background:rgba(16,18,22,.92);border:1px solid rgba(255,255,255,.08);box-shadow:0 20px 60px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.06);backdrop-filter:blur(24px)}",
|
|
552
|
+
".tfb-panel.tfb-open{display:block}",
|
|
553
|
+
".tfb-head{display:flex;align-items:center;gap:6px;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid rgba(45,212,168,.25)}",
|
|
554
|
+
".tfb-title{font-weight:700;font-size:13px;white-space:nowrap}",
|
|
555
|
+
".tfb-ctitle{flex:1;min-width:0;font-size:11px;color:#9aa3b2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:text}",
|
|
556
|
+
".tfb-summary{font-size:11px;color:#9aa3b2;white-space:nowrap}",
|
|
557
|
+
".tfb-btn{border:0;background:transparent;color:#9aa3b2;font-size:13px;cursor:pointer;padding:2px 5px;border-radius:6px;flex-shrink:0}",
|
|
558
|
+
".tfb-btn:hover{color:#e6e6e6;background:rgba(255,255,255,.06)}",
|
|
559
|
+
".tfb-btn.tfb-on{color:#ffd27a}",
|
|
560
|
+
".tfb-list{list-style:none;margin:0;padding:0}",
|
|
561
|
+
".tfb-item{display:flex;align-items:flex-start;gap:8px;padding:5px 4px;border-radius:8px;word-break:break-word}",
|
|
562
|
+
".tfb-item:nth-child(odd){background:rgba(255,255,255,.03)}",
|
|
563
|
+
".tfb-ico{flex-shrink:0;width:16px;height:16px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:10px;margin-top:1px}",
|
|
564
|
+
".tfb-item[data-status='completed'] .tfb-ico{background:rgba(74,222,128,.18);color:#5dffa8;border:1px solid rgba(93,255,168,.4)}",
|
|
565
|
+
".tfb-item[data-status='in_progress'] .tfb-ico{background:rgba(255,170,51,.2);color:#ffaa33;border:1px solid rgba(255,170,51,.45)}",
|
|
566
|
+
".tfb-item[data-status='pending'] .tfb-ico{background:rgba(127,196,255,.14);color:#7fc4ff;border:1px dashed rgba(127,196,255,.5)}",
|
|
567
|
+
".tfb-item[data-status='in_progress']{background:rgba(255,170,51,.06)}",
|
|
568
|
+
".tfb-item[data-status='in_progress'] .tfb-txt{color:#ffcf8a;font-weight:600}",
|
|
569
|
+
".tfb-item[data-status='completed'] .tfb-txt{color:#7fae94;text-decoration:line-through}",
|
|
570
|
+
".tfb-item[data-status='pending'] .tfb-txt{color:#aebfd4}",
|
|
571
|
+
".tfb-txt{flex:1}",
|
|
572
|
+
".tfb-empty{color:#8b93a1;font-size:12px;text-align:center;padding:12px 0}",
|
|
573
|
+
".tfb-sep{margin:10px 0 6px;padding-top:8px;border-top:1px solid rgba(255,255,255,.07);display:flex;align-items:center;justify-content:space-between}",
|
|
574
|
+
".tfb-sep-label{font-size:11px;color:#9aa3b2;font-weight:600}",
|
|
575
|
+
".tfb-pinrow{display:flex;align-items:center;gap:6px;padding:6px 4px;border-radius:8px;cursor:pointer}",
|
|
576
|
+
".tfb-pinrow:hover{background:rgba(255,255,255,.05)}",
|
|
577
|
+
".tfb-pinname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}",
|
|
578
|
+
".tfb-pincount{font-size:11px;color:#9aa3b2;white-space:nowrap}",
|
|
579
|
+
".tfb-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}",
|
|
580
|
+
".tfb-dot-active{background:#fbbf24;box-shadow:0 0 6px rgba(251,191,36,.8);animation:tfb-dotpulse 1.4s ease-in-out infinite}",
|
|
581
|
+
".tfb-dot-done{background:#4ade80}",
|
|
582
|
+
".tfb-dot-none{background:#6b7280}",
|
|
583
|
+
"@keyframes tfb-dotpulse{0%,100%{opacity:1}50%{opacity:.4}}",
|
|
584
|
+
".tfb-pinlist{list-style:none;margin:2px 0 4px;padding:0 0 0 10px;border-left:2px solid rgba(255,255,255,.08)}",
|
|
585
|
+
".tfb-hint{font-size:10px;color:#6b7280;padding:6px 0 0;display:flex;align-items:center;justify-content:center;gap:4px;flex-wrap:wrap}",
|
|
586
|
+
".tfb-theme-menu{display:none;margin:-2px 0 8px;padding:6px;border:1px solid rgba(45,212,168,.25);border-radius:10px;background:rgba(12,16,20,.96)}",
|
|
587
|
+
".tfb-theme-menu.tfb-open{display:block}",
|
|
588
|
+
".tfb-theme-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;cursor:pointer;font-size:12px;color:#cdd6e2}",
|
|
589
|
+
".tfb-theme-row:hover{background:rgba(255,255,255,.06)}",
|
|
590
|
+
".tfb-theme-row.tfb-cur{color:#ffd27a;font-weight:600}",
|
|
591
|
+
".tfb-theme-row .tfb-theme-check{flex-shrink:0;width:14px;text-align:center}",
|
|
592
|
+
".tfb-theme-row .tfb-theme-label{flex:1;min-width:0}",
|
|
593
|
+
".tfb-rename{flex:1;min-width:0;padding:3px 8px;border-radius:6px;border:1px solid rgba(255,210,122,.5);background:rgba(0,0,0,.4);color:#ffd27a;font:inherit;font-size:12px;outline:none}",
|
|
594
|
+
".tfb-rename:focus{border-color:#ffd27a}",
|
|
595
|
+
".tfb-ctitle[contenteditable='true']{outline:none;border-bottom:1px dashed #ffd27a;color:#ffd27a;cursor:text}"
|
|
596
|
+
]).join("\n");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function listHtml(list) {
|
|
600
|
+
var html = "";
|
|
601
|
+
for (var i = 0; i < list.length; i++) {
|
|
602
|
+
var t = list[i];
|
|
603
|
+
var ico = t.status === "completed" ? "✓" : t.status === "in_progress" ? "▶" : "○";
|
|
604
|
+
var txt = t.content.length > MAX_CONTENT ? t.content.slice(0, MAX_CONTENT) + "…" : t.content;
|
|
605
|
+
html += '<li class="tfb-item" data-status="' + t.status + '"><span class="tfb-ico">' + ico + '</span><span class="tfb-txt">' + esc(txt) + "</span></li>";
|
|
606
|
+
}
|
|
607
|
+
return html;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function pinDotClass(sid) {
|
|
611
|
+
var list = sessionTodos(sid);
|
|
612
|
+
if (!list || !list.length) return "tfb-dot-none";
|
|
613
|
+
var c = countsOf(list);
|
|
614
|
+
if (c.done === c.total) return "tfb-dot-done";
|
|
615
|
+
if (c.active > 0) return "tfb-dot-active";
|
|
616
|
+
return "tfb-dot-none";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderList() {
|
|
620
|
+
if (!ui.list) return;
|
|
621
|
+
var c = currentList().length ? countsOf(currentList()) : { done: 0, active: 0, total: 0 };
|
|
622
|
+
if (ui.ctitle) {
|
|
623
|
+
// while the rename editor is live, don't rebuild the title cell
|
|
624
|
+
if (editing.sid === normSid(currentSid)) { /* keep editor */ }
|
|
625
|
+
else {
|
|
626
|
+
ui.ctitle.textContent = currentSid ? sessionTitle(currentSid) : "";
|
|
627
|
+
ui.ctitle.title = currentSid ? "点击重命名此对话" : "";
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (ui.summary) {
|
|
631
|
+
var parts = [];
|
|
632
|
+
if (c.done > 0) parts.push(c.done + " 完成");
|
|
633
|
+
if (c.active > 0) parts.push(c.active + " 进行中");
|
|
634
|
+
var pend = c.total - c.done - c.active;
|
|
635
|
+
if (pend > 0) parts.push(pend + " 待办");
|
|
636
|
+
ui.summary.textContent = parts.join(" · ");
|
|
637
|
+
}
|
|
638
|
+
if (ui.pinBtn) {
|
|
639
|
+
var on = currentSid && isPinned(currentSid);
|
|
640
|
+
ui.pinBtn.classList.toggle("tfb-on", !!on);
|
|
641
|
+
ui.pinBtn.title = on ? "取消固定当前会话" : "固定当前会话(切换对话后仍监控)";
|
|
642
|
+
ui.pinBtn.textContent = on ? "📍" : "📌";
|
|
643
|
+
}
|
|
644
|
+
var list = currentList();
|
|
645
|
+
if (!list.length) {
|
|
646
|
+
ui.list.innerHTML = '<li class="tfb-empty">当前会话暂无任务清单<br><span style="font-size:10px">AI 使用 todo_write 后自动显示;📌 可固定会话</span></li>';
|
|
647
|
+
} else {
|
|
648
|
+
ui.list.innerHTML = listHtml(list);
|
|
649
|
+
// user requirement 4: when work remains, remind how to keep the AI
|
|
650
|
+
// going (this plugin is a read-only monitor — continuous thinking
|
|
651
|
+
// is driven by dsh-client-auto-continue, already installed here).
|
|
652
|
+
var cc = countsOf(list);
|
|
653
|
+
if (cc.done < cc.total) {
|
|
654
|
+
var left = cc.total - cc.done;
|
|
655
|
+
ui.list.innerHTML += '<li class="tfb-empty" style="padding:6px 0 2px;font-size:10px">⚠️ 还有 ' + left + ' 项未完成 — 装/启用 dsh-client-auto-continue 可让 AI 连续思考直到清单完成</li>';
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
renderPinned();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderPinned() {
|
|
662
|
+
if (!ui.pinSec) return;
|
|
663
|
+
if (pinned.length === 0) { ui.pinSec.innerHTML = ""; return; }
|
|
664
|
+
if (editing.sid) return; // a rename editor is live — don't rebuild under it
|
|
665
|
+
var html = '<div class="tfb-sep"><span class="tfb-sep-label">📌 已固定 (' + pinned.length + ')</span></div>';
|
|
666
|
+
for (var i = 0; i < pinned.length; i++) {
|
|
667
|
+
var sid = pinned[i];
|
|
668
|
+
var list = sessionTodos(sid);
|
|
669
|
+
var c = list ? countsOf(list) : null;
|
|
670
|
+
var tname = sessionTitle(sid);
|
|
671
|
+
var open = !!openPin[sid];
|
|
672
|
+
html += '<div class="tfb-pinrow" data-act="togglePin" data-sid="' + esc(sid) + '">';
|
|
673
|
+
html += '<span class="tfb-dot ' + pinDotClass(sid) + '"></span>';
|
|
674
|
+
html += '<span class="tfb-pinname" title="' + esc(tname) + '">' + esc(tname) + "</span>";
|
|
675
|
+
html += '<span class="tfb-pincount">' + (c ? c.done + "/" + c.total : "…") + "</span>";
|
|
676
|
+
html += '<button class="tfb-btn" data-act="renamePin" data-sid="' + esc(sid) + '" title="重命名">✏️</button>';
|
|
677
|
+
html += '<button class="tfb-btn" data-act="unpin" data-sid="' + esc(sid) + '" title="取消固定">✖</button>';
|
|
678
|
+
html += "</div>";
|
|
679
|
+
if (open) {
|
|
680
|
+
if (list && list.length) {
|
|
681
|
+
html += '<ul class="tfb-pinlist">' + listHtml(list) + "</ul>";
|
|
682
|
+
} else {
|
|
683
|
+
html += '<ul class="tfb-pinlist"><li class="tfb-empty" style="padding:6px 0">暂无数据(该会话产生 todo_write 后自动出现)</li></ul>';
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
ui.pinSec.innerHTML = html;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
var THEME_NAMES = { nebula: "🌌 星云·流光", nebB: "🌟 星云·宝石", nebC: "🌙 星云·顶弧", graphite: "💎 石墨版", blue: "🔵 蓝宝石版", glass: "🧊 玻璃版" };
|
|
691
|
+
|
|
692
|
+
function renderThemeMenu() {
|
|
693
|
+
if (!ui.themeMenu) return;
|
|
694
|
+
var html = "";
|
|
695
|
+
var order = ["nebula", "nebB", "nebC", "graphite", "blue", "glass"];
|
|
696
|
+
for (var i = 0; i < order.length; i++) {
|
|
697
|
+
var th = order[i];
|
|
698
|
+
var cur = th === currentTheme;
|
|
699
|
+
html += '<div class="tfb-theme-row' + (cur ? " tfb-cur" : "") + '" data-act="setTheme" data-sid="' + th + '">' +
|
|
700
|
+
'<span class="tfb-theme-check">' + (cur ? "✓" : " ") + "</span>" +
|
|
701
|
+
'<span class="tfb-theme-label">' + (THEME_NAMES[th] || th) + "</span>" +
|
|
702
|
+
"</div>";
|
|
703
|
+
}
|
|
704
|
+
ui.themeMenu.innerHTML = html;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function renderBall() {
|
|
708
|
+
if (!ui.ball) return;
|
|
709
|
+
var st = ballState();
|
|
710
|
+
ui.ball.className = "tfb-ball " + st.cls + (capsule ? " tfb-capsule" : "");
|
|
711
|
+
var labelEl = ui.ball.querySelector(".tfb-label");
|
|
712
|
+
if (labelEl) labelEl.textContent = st.label;
|
|
713
|
+
// progress ring: --pct = done/total*100 (0 when no list; 100 when all
|
|
714
|
+
// done). The conic-gradient ring visualizes completion at a glance.
|
|
715
|
+
var pct = 0;
|
|
716
|
+
var list = currentList();
|
|
717
|
+
if (list.length) {
|
|
718
|
+
var c0 = countsOf(list);
|
|
719
|
+
pct = c0.total > 0 ? Math.round(c0.done / c0.total * 100) : 0;
|
|
720
|
+
}
|
|
721
|
+
try { ui.ball.style.setProperty("--pct", String(pct)); } catch (e) {}
|
|
722
|
+
var sub = "";
|
|
723
|
+
if (capsule) {
|
|
724
|
+
// capsule mode: show the running task (or overall status) in full;
|
|
725
|
+
// overflowing text scrolls back and forth (marquee)
|
|
726
|
+
if (list.length) {
|
|
727
|
+
var c1 = countsOf(list);
|
|
728
|
+
if (c1.done === c1.total) sub = "✓ 全部完成 " + c1.done + "/" + c1.total;
|
|
729
|
+
else {
|
|
730
|
+
var running = "";
|
|
731
|
+
for (var r = 0; r < list.length; r++) {
|
|
732
|
+
if (list[r].status === "in_progress") { running = list[r].content; break; }
|
|
733
|
+
}
|
|
734
|
+
sub = running
|
|
735
|
+
? "▶ " + running
|
|
736
|
+
: "待办 " + c1.done + "/" + c1.total;
|
|
737
|
+
}
|
|
738
|
+
} else if (pinned.length) {
|
|
739
|
+
for (var i = 0; i < pinned.length; i++) {
|
|
740
|
+
var pl = sessionTodos(pinned[i]);
|
|
741
|
+
if (pl && countsOf(pl).active > 0) { sub = "📌 " + (sessionTitle(pinned[i]) || "固定会话") + " 进行中"; break; }
|
|
742
|
+
}
|
|
743
|
+
if (!sub) sub = "📋 暂无进行中任务";
|
|
744
|
+
} else {
|
|
745
|
+
sub = "📋 暂无任务";
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// ROUND-BALL MODE: show ONLY the progress (user requirement) — no
|
|
749
|
+
// task content on the ball face, since it never fits. The label is
|
|
750
|
+
// done/total (or ✓); the sub line stays empty.
|
|
751
|
+
var subEl = ui.ball.querySelector(".tfb-sub");
|
|
752
|
+
if (subEl) {
|
|
753
|
+
if (capsule) {
|
|
754
|
+
subEl.innerHTML = '<span class="tfb-capsule-text">' + esc(sub) + "</span>";
|
|
755
|
+
var textEl = subEl.querySelector(".tfb-capsule-text");
|
|
756
|
+
if (textEl) {
|
|
757
|
+
// measure AFTER layout: if the text overflows the pill, set the
|
|
758
|
+
// scroll distance so the marquee ping-pongs exactly the overflow
|
|
759
|
+
requestAnimationFrame(function () {
|
|
760
|
+
try {
|
|
761
|
+
var over = textEl.scrollWidth - subEl.clientWidth;
|
|
762
|
+
if (over > 8) {
|
|
763
|
+
textEl.style.setProperty("--shift", "-" + (over + 6) + "px");
|
|
764
|
+
textEl.classList.add("tfb-marquee");
|
|
765
|
+
} else {
|
|
766
|
+
textEl.classList.remove("tfb-marquee");
|
|
767
|
+
}
|
|
768
|
+
} catch (e2) {}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
subEl.textContent = "";
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
ui.ball.title = capsule
|
|
776
|
+
? "胶囊模式(右键切回圆球;左键仍可展开面板)"
|
|
777
|
+
: (list.length
|
|
778
|
+
? "Todo 悬浮球 — 当前 " + summarize(list).head + "(" + pct + "% 完成;左键展开/折叠 · 右键胶囊 · 可拖动)"
|
|
779
|
+
: "Todo 悬浮球 — 当前会话暂无任务(左键展开 · 右键胶囊 · 可拖动)");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function render() {
|
|
783
|
+
renderBall();
|
|
784
|
+
renderList();
|
|
785
|
+
if (expanded && ui.panel) positionPanel();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function positionPanel() {
|
|
789
|
+
if (!ui.panel || !ui.ball) return;
|
|
790
|
+
var r = ui.ball.getBoundingClientRect();
|
|
791
|
+
var pw = ui.panel.offsetWidth || 300;
|
|
792
|
+
var ph = ui.panel.offsetHeight || 320;
|
|
793
|
+
var left = r.left + r.width / 2 - pw / 2;
|
|
794
|
+
left = Math.max(8, Math.min(window.innerWidth - pw - 8, left));
|
|
795
|
+
var top = r.top - ph - 12;
|
|
796
|
+
if (top < 8) top = r.bottom + 12;
|
|
797
|
+
ui.panel.style.left = left + "px";
|
|
798
|
+
ui.panel.style.top = top + "px";
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function show() {
|
|
802
|
+
try { applyThemeNow(); } catch (e) {}
|
|
803
|
+
expanded = true;
|
|
804
|
+
if (ui.panel) { ui.panel.classList.add("tfb-open"); positionPanel(); }
|
|
805
|
+
}
|
|
806
|
+
function hide() {
|
|
807
|
+
try { applyThemeNow(); } catch (e) {}
|
|
808
|
+
expanded = false;
|
|
809
|
+
if (ui.panel) ui.panel.classList.remove("tfb-open");
|
|
810
|
+
// 字体插件同款:收起面板时刷新整个插件界面(重载 UI,主题/结构即时生效)
|
|
811
|
+
setTimeout(function () { try { refreshPlugin(); } catch (e) {} }, 120);
|
|
812
|
+
}
|
|
813
|
+
function refreshPlugin() {
|
|
814
|
+
try {
|
|
815
|
+
var h = document.getElementById(BALL_ID);
|
|
816
|
+
if (h) h.remove();
|
|
817
|
+
ui = { root: null, ball: null, panel: null, list: null, summary: null, ctitle: null, pinBtn: null, pinSec: null, themeMenu: null };
|
|
818
|
+
expanded = false;
|
|
819
|
+
buildUI();
|
|
820
|
+
applyBallVisibility();
|
|
821
|
+
} catch (e) {}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function buildUI() {
|
|
825
|
+
if (document.getElementById(BALL_ID)) return;
|
|
826
|
+
if (!document.body) return;
|
|
827
|
+
try { fetch("/dsh-todo-float-ball/client-alive").catch(function () {}); } catch (e) {}
|
|
828
|
+
|
|
829
|
+
var host = document.createElement("div");
|
|
830
|
+
host.id = BALL_ID;
|
|
831
|
+
|
|
832
|
+
var shadow = host.attachShadow({ mode: "open" });
|
|
833
|
+
var style = document.createElement("style");
|
|
834
|
+
style.textContent = cssText();
|
|
835
|
+
shadow.appendChild(style);
|
|
836
|
+
|
|
837
|
+
var root = document.createElement("div");
|
|
838
|
+
root.className = "tfb-root";
|
|
839
|
+
root.innerHTML =
|
|
840
|
+
'<button class="tfb-ball tfb-idle" type="button">' +
|
|
841
|
+
'<span class="tfb-orb-band"></span>' +
|
|
842
|
+
'<span class="tfb-orb-rim"></span>' +
|
|
843
|
+
'<span class="tfb-ring"></span>' +
|
|
844
|
+
'<span class="tfb-label">✓</span><span class="tfb-sub"></span>' +
|
|
845
|
+
"</button>" +
|
|
846
|
+
'<div class="tfb-panel">' +
|
|
847
|
+
'<div class="tfb-head">' +
|
|
848
|
+
'<span class="tfb-title">📋</span>' +
|
|
849
|
+
'<span class="tfb-ctitle"></span>' +
|
|
850
|
+
'<span class="tfb-summary"></span>' +
|
|
851
|
+
'<button class="tfb-btn" data-act="pin" type="button">📌</button>' +
|
|
852
|
+
'<button class="tfb-btn" data-act="themeMenu" type="button" title="切换皮肤">🎨</button>' +
|
|
853
|
+
'<button class="tfb-btn" data-act="close" type="button" title="折叠">✕</button>' +
|
|
854
|
+
"</div>" +
|
|
855
|
+
'<div class="tfb-theme-menu"></div>' +
|
|
856
|
+
'<ul class="tfb-list"></ul>' +
|
|
857
|
+
'<div class="tfb-pinsec"></div>' +
|
|
858
|
+
'<div class="tfb-hint"><button class="tfb-btn" data-act="resetPos" type="button" title="一键归位右下角">🎯 归位</button> · 切换对话自动跟随 · 📌 固定 · 🎨 皮肤 · Esc 折叠</div>' +
|
|
859
|
+
"</div>";
|
|
860
|
+
shadow.appendChild(root);
|
|
861
|
+
|
|
862
|
+
// Mount to <html>, not body — DSH gives body containers transform/filter
|
|
863
|
+
// which break position:fixed children (font-enhancer verified fix).
|
|
864
|
+
var mount = document.documentElement || document.body;
|
|
865
|
+
if (!host.parentNode) mount.appendChild(host);
|
|
866
|
+
|
|
867
|
+
ui.root = root;
|
|
868
|
+
ui.ball = root.querySelector(".tfb-ball");
|
|
869
|
+
ui.panel = root.querySelector(".tfb-panel");
|
|
870
|
+
ui.list = root.querySelector(".tfb-list");
|
|
871
|
+
ui.ctitle = root.querySelector(".tfb-ctitle");
|
|
872
|
+
ui.summary = root.querySelector(".tfb-summary");
|
|
873
|
+
ui.pinBtn = root.querySelector('[data-act="pin"]');
|
|
874
|
+
ui.pinSec = root.querySelector(".tfb-pinsec");
|
|
875
|
+
ui.themeMenu = root.querySelector(".tfb-theme-menu");
|
|
876
|
+
|
|
877
|
+
// restore position (clamped inside viewport)
|
|
878
|
+
var bw = ui.ball.offsetWidth || 56, bh = ui.ball.offsetHeight || 56;
|
|
879
|
+
try {
|
|
880
|
+
var raw = localStorage.getItem(POS_KEY);
|
|
881
|
+
if (raw) {
|
|
882
|
+
var pos = JSON.parse(raw);
|
|
883
|
+
if (pos && typeof pos.x === "number" && typeof pos.y === "number" &&
|
|
884
|
+
pos.x >= 0 && pos.y >= 0 &&
|
|
885
|
+
pos.x <= window.innerWidth - bw && pos.y <= window.innerHeight - bh) {
|
|
886
|
+
ui.ball.style.left = pos.x + "px";
|
|
887
|
+
ui.ball.style.top = pos.y + "px";
|
|
888
|
+
ui.ball.style.right = "auto";
|
|
889
|
+
ui.ball.style.bottom = "auto";
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
} catch (e) {}
|
|
893
|
+
|
|
894
|
+
// drag + click-to-toggle (5px threshold separates the two)
|
|
895
|
+
var drag = { active: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
|
|
896
|
+
ui.ball.addEventListener("pointerdown", function (e) {
|
|
897
|
+
drag.active = true; drag.moved = false;
|
|
898
|
+
drag.sx = e.clientX; drag.sy = e.clientY;
|
|
899
|
+
var r = ui.ball.getBoundingClientRect();
|
|
900
|
+
drag.ox = r.left; drag.oy = r.top;
|
|
901
|
+
try { ui.ball.setPointerCapture(e.pointerId); } catch (err) {}
|
|
902
|
+
e.preventDefault();
|
|
903
|
+
});
|
|
904
|
+
ui.ball.addEventListener("pointermove", function (e) {
|
|
905
|
+
if (!drag.active) return;
|
|
906
|
+
var dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
|
|
907
|
+
if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 5) return;
|
|
908
|
+
drag.moved = true;
|
|
909
|
+
var nx = Math.max(0, Math.min(window.innerWidth - (ui.ball.offsetWidth || bw), drag.ox + dx));
|
|
910
|
+
var ny = Math.max(0, Math.min(window.innerHeight - (ui.ball.offsetHeight || bh), drag.oy + dy));
|
|
911
|
+
ui.ball.style.left = nx + "px";
|
|
912
|
+
ui.ball.style.top = ny + "px";
|
|
913
|
+
ui.ball.style.right = "auto";
|
|
914
|
+
ui.ball.style.bottom = "auto";
|
|
915
|
+
try { localStorage.setItem(POS_KEY, JSON.stringify({ x: Math.round(nx), y: Math.round(ny) })); } catch (err) {}
|
|
916
|
+
if (expanded) positionPanel();
|
|
917
|
+
});
|
|
918
|
+
function endDrag(e) {
|
|
919
|
+
if (!drag.active) return;
|
|
920
|
+
drag.active = false;
|
|
921
|
+
try { ui.ball.releasePointerCapture(e.pointerId); } catch (err) {}
|
|
922
|
+
if (!drag.moved) { if (expanded) hide(); else show(); }
|
|
923
|
+
}
|
|
924
|
+
ui.ball.addEventListener("pointerup", endDrag);
|
|
925
|
+
ui.ball.addEventListener("pointercancel", endDrag);
|
|
926
|
+
|
|
927
|
+
// RIGHT-CLICK: toggle capsule mode (long pill showing the running
|
|
928
|
+
// task); persists in localStorage.
|
|
929
|
+
ui.ball.addEventListener("contextmenu", function (e) {
|
|
930
|
+
e.preventDefault();
|
|
931
|
+
capsule = !capsule;
|
|
932
|
+
try { localStorage.setItem(CAPSULE_KEY, capsule ? "1" : "0"); } catch (err) {}
|
|
933
|
+
render();
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// delegated actions inside the shadow root
|
|
937
|
+
root.addEventListener("click", function (e) {
|
|
938
|
+
var t = e.target && e.target.closest ? e.target.closest("[data-act]") : null;
|
|
939
|
+
if (!t) {
|
|
940
|
+
// clicking the title text (not while editing) starts a rename
|
|
941
|
+
if (e.target === ui.ctitle && currentSid && hasSessionsSvc) startRename(currentSid);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
var act = t.getAttribute("data-act");
|
|
945
|
+
var sid = t.getAttribute("data-sid");
|
|
946
|
+
if (act === "close") hide();
|
|
947
|
+
else if (act === "pin") {
|
|
948
|
+
if (!currentSid) return;
|
|
949
|
+
if (isPinned(currentSid)) unpin(currentSid); else pin(currentSid);
|
|
950
|
+
} else if (act === "resetPos") {
|
|
951
|
+
try { localStorage.removeItem(POS_KEY); } catch (err) {}
|
|
952
|
+
if (ui.ball) { ui.ball.style.left = "auto"; ui.ball.style.top = "auto"; ui.ball.style.right = "18px"; ui.ball.style.bottom = "18px"; }
|
|
953
|
+
} else if (act === "themeMenu") {
|
|
954
|
+
// toggle the skin picker inside the panel
|
|
955
|
+
if (ui.themeMenu) ui.themeMenu.classList.toggle("tfb-open");
|
|
956
|
+
renderThemeMenu();
|
|
957
|
+
e.stopPropagation();
|
|
958
|
+
} else if (act === "setTheme" && sid) {
|
|
959
|
+
// pick a skin from the menu
|
|
960
|
+
currentTheme = sid;
|
|
961
|
+
try { localStorage.setItem(THEME_KEY, sid); } catch (err) {}
|
|
962
|
+
try {
|
|
963
|
+
var stEl = ui.root.querySelector("style");
|
|
964
|
+
if (stEl) stEl.textContent = cssText();
|
|
965
|
+
} catch (err) {}
|
|
966
|
+
render();
|
|
967
|
+
renderThemeMenu();
|
|
968
|
+
e.stopPropagation();
|
|
969
|
+
} else if (act === "togglePin" && sid) {
|
|
970
|
+
openPin[sid] = !openPin[sid];
|
|
971
|
+
render();
|
|
972
|
+
} else if (act === "renamePin" && sid) {
|
|
973
|
+
e.stopPropagation();
|
|
974
|
+
startRename(sid);
|
|
975
|
+
} else if (act === "unpin" && sid) {
|
|
976
|
+
e.stopPropagation();
|
|
977
|
+
unpin(sid);
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
window.addEventListener("resize", function () { if (expanded) positionPanel(); });
|
|
982
|
+
document.addEventListener("keydown", function (e) { if (e.key === "Escape" && expanded) hide(); });
|
|
983
|
+
|
|
984
|
+
render();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ---------- entry ----------
|
|
988
|
+
var inject = ["sessions"];
|
|
989
|
+
var currentSessions = null;
|
|
990
|
+
|
|
991
|
+
// Skin buttons live in the sibling settings section (React-rendered);
|
|
992
|
+
// React synthetic events / refs proved unreliable there, so the buttons
|
|
993
|
+
// carry plain data attributes and we delegate at the DOCUMENT capture
|
|
994
|
+
// phase — immune to any re-render, and capture fires before anything
|
|
995
|
+
// can stopPropagation.
|
|
996
|
+
function installThemeDelegation() {
|
|
997
|
+
if (window.__dshtfbThemeDelegated) return;
|
|
998
|
+
window.__dshtfbThemeDelegated = true;
|
|
999
|
+
document.addEventListener("click", function (e) {
|
|
1000
|
+
var t = e.target && e.target.closest ? e.target.closest("[data-tfb-theme]") : null;
|
|
1001
|
+
if (!t) return;
|
|
1002
|
+
var theme = t.getAttribute("data-tfb-theme");
|
|
1003
|
+
if (theme !== "nebula" && theme !== "graphite" && theme !== "blue" && theme !== "glass" && theme !== "nebB" && theme !== "nebC") return;
|
|
1004
|
+
try { localStorage.setItem(THEME_KEY, theme); } catch (err) {}
|
|
1005
|
+
currentTheme = theme;
|
|
1006
|
+
applyThemeNow();
|
|
1007
|
+
}, true);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ---------- ball visibility (driven by the shared settings section) ----------
|
|
1011
|
+
// The show/hide toggle lives in the shared "悬浮球导航" settings section
|
|
1012
|
+
// provided by dsh-skill-browser (same pattern as the font ball). We only
|
|
1013
|
+
// read the shared localStorage key and apply it defensively.
|
|
1014
|
+
var BALL_VIS_KEY = "dsh-tfb-ball-visible";
|
|
1015
|
+
function ballVisible() {
|
|
1016
|
+
try { var v = localStorage.getItem(BALL_VIS_KEY); return v == null ? true : v === "1"; } catch (e) { return true; }
|
|
1017
|
+
}
|
|
1018
|
+
function applyBallVisibility() {
|
|
1019
|
+
var b = document.querySelector("#" + BALL_ID);
|
|
1020
|
+
if (!b) return;
|
|
1021
|
+
var show = ballVisible();
|
|
1022
|
+
// Uninstall-fallback (ecosystem safety): the show/hide toggle lives in
|
|
1023
|
+
// the shared "悬浮球导航" section provided by dsh-skill-browser. If
|
|
1024
|
+
// that plugin is GONE and the stored flag says hidden, the ball would
|
|
1025
|
+
// be stuck invisible with no way back — in that case ignore the flag
|
|
1026
|
+
// and show the ball again.
|
|
1027
|
+
if (!show) {
|
|
1028
|
+
var providerGone = !document.getElementById("dsh-skill-browser-ball") &&
|
|
1029
|
+
!document.querySelector('[data-dsb-slot]');
|
|
1030
|
+
if (providerGone) show = true;
|
|
1031
|
+
}
|
|
1032
|
+
b.style.setProperty("display", show ? "" : "none", "important");
|
|
1033
|
+
}
|
|
1034
|
+
function watchBallVisibility() {
|
|
1035
|
+
// Long-lived, low-frequency: the shared settings section writes the
|
|
1036
|
+
// theme/visibility keys and expects them picked up — so this watcher
|
|
1037
|
+
// must NOT expire (the old 20-min cap made skin switching die after
|
|
1038
|
+
// 20 minutes). The sibling plugin also calls window.__dshtfbApplyTheme
|
|
1039
|
+
// directly for instant effect; the poll is just a safety net.
|
|
1040
|
+
setInterval(function () {
|
|
1041
|
+
try { applyBallVisibility(); } catch (e) {}
|
|
1042
|
+
try {
|
|
1043
|
+
var v = null;
|
|
1044
|
+
try { v = localStorage.getItem(THEME_KEY); } catch (e2) {}
|
|
1045
|
+
if ((v === "graphite" || v === "nebula" || v === "blue" || v === "glass" || v === "nebB" || v === "nebC") && v !== currentTheme) applyThemeNow();
|
|
1046
|
+
} catch (e) {}
|
|
1047
|
+
}, 2000);
|
|
1048
|
+
}
|
|
1049
|
+
// Immediate theme application — exposed as window.__dshtfbApplyTheme so
|
|
1050
|
+
// the sibling settings section can invoke it synchronously on click.
|
|
1051
|
+
function applyThemeNow() {
|
|
1052
|
+
try {
|
|
1053
|
+
var v = localStorage.getItem(THEME_KEY);
|
|
1054
|
+
if (v === "blue") currentTheme = "blue"; else if (v === "nebula") currentTheme = "nebula"; else if (v === "nebB") currentTheme = "nebB"; else if (v === "nebC") currentTheme = "nebC"; else if (v === "glass") currentTheme = "glass"; else currentTheme = "graphite";
|
|
1055
|
+
} catch (e) {}
|
|
1056
|
+
try {
|
|
1057
|
+
if (ui.root) {
|
|
1058
|
+
var st = ui.root.querySelector("style");
|
|
1059
|
+
if (st) st.textContent = cssText();
|
|
1060
|
+
}
|
|
1061
|
+
} catch (e) {}
|
|
1062
|
+
render();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function apply(ctx) {
|
|
1066
|
+
loadPinned();
|
|
1067
|
+
loadTheme();
|
|
1068
|
+
try { capsule = localStorage.getItem(CAPSULE_KEY) === "1"; } catch (e) {}
|
|
1069
|
+
restorePersisted();
|
|
1070
|
+
// primary data channel: the official sessions service
|
|
1071
|
+
try {
|
|
1072
|
+
if (ctx && ctx.sessions && ctx.sessions.list && typeof ctx.sessions.list.getSnapshot === "function") {
|
|
1073
|
+
currentSessions = ctx.sessions;
|
|
1074
|
+
hasSessionsSvc = true;
|
|
1075
|
+
try { ctx.sessions.list.subscribe(syncFromSessions); } catch (e) {}
|
|
1076
|
+
syncFromSessions();
|
|
1077
|
+
}
|
|
1078
|
+
} catch (e) {}
|
|
1079
|
+
// global hook: lets the shared settings section switch the theme
|
|
1080
|
+
// synchronously on click (no polling delay).
|
|
1081
|
+
try {
|
|
1082
|
+
window.__dshtfbApplyTheme = applyThemeNow;
|
|
1083
|
+
} catch (e) {}
|
|
1084
|
+
// document-level delegation for the sibling skin buttons (React
|
|
1085
|
+
// synthetic events / refs unreliable in that slot — plain
|
|
1086
|
+
// data-attribute buttons + capture-phase listener always fire)
|
|
1087
|
+
installThemeDelegation();
|
|
1088
|
+
// UI
|
|
1089
|
+
if (document.readyState === "loading") {
|
|
1090
|
+
document.addEventListener("DOMContentLoaded", buildUI);
|
|
1091
|
+
} else {
|
|
1092
|
+
buildUI();
|
|
1093
|
+
}
|
|
1094
|
+
applyBallVisibility();
|
|
1095
|
+
watchBallVisibility();
|
|
1096
|
+
// fallback channel only when the sessions service is unavailable
|
|
1097
|
+
if (!hasSessionsSvc) installPanelObserver();
|
|
1098
|
+
// watchdog keeps the ball alive across shell rebuilds
|
|
1099
|
+
var tries = 0;
|
|
1100
|
+
var t = setInterval(function () {
|
|
1101
|
+
tries++;
|
|
1102
|
+
try {
|
|
1103
|
+
if (!document.getElementById(BALL_ID)) buildUI();
|
|
1104
|
+
else renderBall();
|
|
1105
|
+
if (hasSessionsSvc) syncFromSessions();
|
|
1106
|
+
} catch (e) {}
|
|
1107
|
+
if (tries > 300) clearInterval(t);
|
|
1108
|
+
}, 2000);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
exports.apply = apply;
|
|
1112
|
+
exports.inject = inject;
|
|
1113
|
+
return module.exports;
|
|
1114
|
+
}
|
|
1115
|
+
});
|