nio-cat 0.9.7 → 0.9.9
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/bin/nio.js +4 -2
- package/package.json +1 -1
- package/src/agents/claude.js +2 -0
- package/src/call.js +246 -0
- package/src/hub.js +21 -0
- package/src/server.js +8 -1
- package/src/terminal.js +147 -0
- package/web/app/app.js +149 -38
- package/web/app/call.css +80 -0
- package/web/app/call.js +435 -0
- package/web/app/demo.js +1 -0
- package/web/app/index.html +1 -0
- package/web/app/notes.css +96 -25
- package/web/app/notes.js +275 -32
- package/web/app/style.css +57 -0
- package/web/sw.js +2 -2
package/web/app/notes.js
CHANGED
|
@@ -15,6 +15,7 @@ const I = {
|
|
|
15
15
|
tag: '<path d="M4 4h7.500l8.500 8.500-7.500 7.500L4 11.500z"/><circle cx="8.500" cy="8.500" r="1.200"/>',
|
|
16
16
|
spark: '<path d="M12 3.500l1.800 5.200 5.200 1.800-5.200 1.800L12 17.500l-1.800-5.200L5 10.500l5.200-1.800zM18.500 16l.7 2 2 .7-2 .7-.7 2-.7-2-2-.7 2-.7z"/>',
|
|
17
17
|
close: '<path d="M6 6l12 12M18 6L6 18"/>',
|
|
18
|
+
back: '<path d="m10 5-7 7 7 7M3 12h18"/>',
|
|
18
19
|
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
19
20
|
search: '<circle cx="11" cy="11" r="6.500"/><path d="M16 16l4 4"/>',
|
|
20
21
|
grid: '<rect x="4" y="4" width="7" height="7" rx="1.500"/><rect x="13" y="4" width="7" height="7" rx="1.500"/><rect x="4" y="13" width="7" height="7" rx="1.500"/><rect x="13" y="13" width="7" height="7" rx="1.500"/>',
|
|
@@ -23,6 +24,7 @@ const I = {
|
|
|
23
24
|
pause: '<path d="M8 5.500v13M16 5.500v13"/>',
|
|
24
25
|
phone: '<rect x="7" y="3" width="10" height="18" rx="2.500"/><path d="M11 18h2"/>',
|
|
25
26
|
check: '<path d="M5 12.500l4.500 4.500L19 7.500"/>',
|
|
27
|
+
more: '<circle cx="6" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="18" cy="12" r="1.5"/>',
|
|
26
28
|
};
|
|
27
29
|
const COLORS = [['', 'Sense color'], ['lemon', 'Llimona'], ['peach', 'Préssec'], ['moss', 'Molsa'], ['sky', 'Cel'], ['lilac', 'Lila'], ['rose', 'Rosa'], ['sand', 'Sorra']];
|
|
28
30
|
const ACTIONS = [
|
|
@@ -46,6 +48,15 @@ export function createNotes(ctx) {
|
|
|
46
48
|
let root = null, notes = new Map(), loaded = false, loading = false, engines = null;
|
|
47
49
|
let view = 'notes', q = '', layout = LSget('nio.notes.layout', 'grid');
|
|
48
50
|
let ed = null, aborter = null;
|
|
51
|
+
const historyNotes = new Map();
|
|
52
|
+
const pendingSaves = new Set();
|
|
53
|
+
let loadError = '';
|
|
54
|
+
const machineKey = () => ctx.machine()?.id || ctx.machine()?.url;
|
|
55
|
+
let notesMachine = machineKey();
|
|
56
|
+
addEventListener('beforeunload', (event) => {
|
|
57
|
+
if (!pendingSaves.size && !ed?.rec) return;
|
|
58
|
+
event.preventDefault(); event.returnValue = '';
|
|
59
|
+
});
|
|
49
60
|
|
|
50
61
|
// ---------------------------------------------------------------- dades
|
|
51
62
|
const all = () => [...notes.values()];
|
|
@@ -65,8 +76,19 @@ export function createNotes(ctx) {
|
|
|
65
76
|
}
|
|
66
77
|
async function load() {
|
|
67
78
|
if (loading) return; loading = true;
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
const requestedMachine = machineKey();
|
|
80
|
+
loadError = '';
|
|
81
|
+
try {
|
|
82
|
+
const r = await api('/api/notes');
|
|
83
|
+
if (requestedMachine !== machineKey()) { loading = false; if (root) load(); return; }
|
|
84
|
+
notes = new Map(r.notes.map((n) => [n.id, n])); loaded = true;
|
|
85
|
+
for (const entry of pendingSaves) if (entry.id && entry.machineKey === machineKey()) notes.set(entry.id, entry.n);
|
|
86
|
+
if (ed?.id) notes.set(ed.id, ed.n);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (requestedMachine !== machineKey()) { loading = false; if (root) load(); return; }
|
|
90
|
+
loadError = e.message;
|
|
91
|
+
}
|
|
70
92
|
loading = false; drawAll();
|
|
71
93
|
}
|
|
72
94
|
const upsert = (n) => { const old = notes.get(n.id); notes.set(n.id, n); return old; };
|
|
@@ -74,35 +96,52 @@ export function createNotes(ctx) {
|
|
|
74
96
|
// ---------------------------------------------------------------- pàgina
|
|
75
97
|
function mount(el) {
|
|
76
98
|
root = el; aborter?.abort(); aborter = new AbortController();
|
|
99
|
+
if (notesMachine !== machineKey()) { notesMachine = machineKey(); notes.clear(); loaded = false; engines = null; q = ''; view = 'notes'; }
|
|
100
|
+
if (history.state?.nioNote) { const { nioNote, ...rest } = history.state; history.replaceState(rest, '', location.href); }
|
|
77
101
|
root.innerHTML = `<div class="nt">
|
|
78
102
|
<div class="nt-top">
|
|
79
103
|
<div class="nt-title"><h1>Notes</h1><p>Escriu, grava i deixa que un model ho ordeni. Es desa a l’ordinador.</p></div>
|
|
80
|
-
|
|
104
|
+
${ctx.isPhone() ? '' : `<div class="nt-tools"><button class="btn" type="button" data-nt="phone">${svg(I.phone)}<span>Mòbil com a micròfon</span></button></div>`}
|
|
81
105
|
</div>
|
|
82
106
|
<div class="nt-bar">
|
|
83
107
|
<label class="nt-search">${svg(I.search)}<input type="search" id="ntQ" placeholder="Busca a les notes" autocomplete="off" aria-label="Busca a les notes"></label>
|
|
84
108
|
<button class="icon-btn" type="button" data-nt="layout" aria-label="Canvia la vista" id="ntLayout"></button>
|
|
85
109
|
</div>
|
|
86
|
-
<div class="nt-new" id="ntNew"
|
|
110
|
+
<div class="nt-new" id="ntNew"><button class="nt-write" type="button" data-nt="new">${svg(I.plus)}<span>Escriu una nota…</span></button>
|
|
87
111
|
<span class="nt-new-b"><button type="button" data-nt="newlist" aria-label="Nova llista" title="Nova llista">${svg(I.list)}</button><button type="button" data-nt="newmic" aria-label="Grava una nota" title="Grava una nota">${svg(I.mic)}</button></span></div>
|
|
88
112
|
<nav class="nt-filters" id="ntFilters" aria-label="Filtres"></nav>
|
|
113
|
+
<div id="ntPending" aria-live="polite"></div>
|
|
114
|
+
<p class="nt-results" id="ntResults" aria-live="polite"></p>
|
|
89
115
|
<div id="ntGrid" class="nt-body"></div>
|
|
90
116
|
</div>`;
|
|
91
117
|
const sig = { signal: aborter.signal };
|
|
118
|
+
$('#ntQ', root).value = q;
|
|
92
119
|
$('#ntQ', root).addEventListener('input', (e) => { q = e.target.value; drawGrid(); }, sig);
|
|
93
120
|
root.addEventListener('click', onClick, sig);
|
|
94
|
-
|
|
121
|
+
bindSwipe($('#ntGrid', root), sig);
|
|
122
|
+
root.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target.matches?.('.nt-card')) { e.preventDefault(); e.target.click(); } }, sig);
|
|
123
|
+
addEventListener('popstate', () => {
|
|
124
|
+
if (ed && !history.state?.nioNote) closeEditor(false, true);
|
|
125
|
+
else if (!ed && history.state?.nioNote && location.hash.startsWith('#/notes')) {
|
|
126
|
+
const entry = history.state.nioNote;
|
|
127
|
+
openEditor(historyNotes.get(entry.key) || entry.id, null, { fromHistory: true, historyKey: entry.key });
|
|
128
|
+
}
|
|
129
|
+
}, sig);
|
|
95
130
|
drawAll();
|
|
96
|
-
load();
|
|
97
|
-
if (/[?&]mic=1/.test(location.hash))
|
|
131
|
+
load().then(schedule);
|
|
132
|
+
if (/[?&]mic=1/.test(location.hash)) {
|
|
133
|
+
history.replaceState(history.state, '', location.hash.replace(/([?&])mic=1(&?)/, (_, sep, next) => next ? sep : ''));
|
|
134
|
+
toast('Per gravar una nota, prem el botó del micròfon.');
|
|
135
|
+
}
|
|
98
136
|
}
|
|
99
|
-
function unmount() { aborter?.abort(); closeEditor(true); root = null; }
|
|
137
|
+
function unmount() { aborter?.abort(); clearTimeout(set.t); clearTimeout(pollT); closeEditor(true); root = null; }
|
|
100
138
|
const drawAll = () => { if (!root) return; drawLayout(); drawFilters(); drawGrid(); };
|
|
101
139
|
function drawLayout() { const b = $('#ntLayout', root); if (b) b.innerHTML = svg(layout === 'grid' ? I.rows : I.grid); root.querySelector('.nt').dataset.layout = layout; }
|
|
102
140
|
function drawFilters() {
|
|
141
|
+
if (!root) return;
|
|
103
142
|
const labs = labelsOf(), nTrash = all().filter((n) => n.trashed).length;
|
|
104
143
|
const chip = (v, label, extra = '') => `<button type="button" class="nt-chip" data-nt="view" data-v="${esc(v)}" ${view === v ? 'aria-current="true"' : ''}>${extra}${esc(label)}</button>`;
|
|
105
|
-
$('#ntFilters', root).innerHTML = chip('notes', 'Notes')
|
|
144
|
+
$('#ntFilters', root).innerHTML = `<div class="nt-filter-views">${chip('notes', 'Notes')}${chip('archive', 'Arxivades', svg(I.archive))}${chip('trash', nTrash ? `Paperera · ${nTrash}` : 'Paperera', svg(I.trash))}</div>${labs.length ? `<div class="nt-filter-labels"><span>Etiquetes</span>${labs.map((l) => chip(`l:${l}`, l, svg(I.tag))).join('')}</div>` : ''}`;
|
|
106
145
|
}
|
|
107
146
|
function cardHtml(n) {
|
|
108
147
|
const items = n.items || [];
|
|
@@ -123,9 +162,14 @@ export function createNotes(ctx) {
|
|
|
123
162
|
</article>`;
|
|
124
163
|
}
|
|
125
164
|
function drawGrid() {
|
|
165
|
+
if (!root) return;
|
|
166
|
+
if (swipe?.card) { swipe.redraw = true; return; } // no es toca la llista amb una targeta sota el dit
|
|
126
167
|
const g = $('#ntGrid', root); if (!g) return;
|
|
168
|
+
$('#ntPending', root).innerHTML = [...pendingSaves].some((entry) => entry.error && entry.machineKey === machineKey()) ? '<div class="nt-warning"><span>Hi ha canvis pendents de desar. Mantén aquesta pàgina oberta.</span><button class="btn sm" type="button" data-nt="retry-save">Torna a desar</button></div>' : '';
|
|
169
|
+
if (loadError) { g.innerHTML = `<div class="nt-empty" role="alert"><p>${esc(loadError)}</p><button class="btn" type="button" data-nt="reload">Torna-ho a provar</button></div>`; return; }
|
|
127
170
|
if (!loaded) { g.innerHTML = '<div class="nt-grid"><i class="nt-skel"></i><i class="nt-skel tall"></i><i class="nt-skel"></i></div>'; return; }
|
|
128
171
|
const list = visible();
|
|
172
|
+
$('#ntResults', root).textContent = `${list.length} ${list.length === 1 ? 'nota' : 'notes'}${q ? ` per a «${q}»` : ''}`;
|
|
129
173
|
if (!list.length) {
|
|
130
174
|
g.innerHTML = `<p class="nt-empty">${q ? 'Cap nota coincideix amb la cerca.' : view === 'trash' ? 'La paperera és buida. Les notes s’hi esborren soles als 30 dies.' : view === 'archive' ? 'No hi ha notes arxivades.' : view.startsWith('l:') ? 'Cap nota amb aquesta etiqueta.' : 'Encara no hi ha cap nota. Escriu-ne una o grava-la amb la veu.'}</p>`;
|
|
131
175
|
return;
|
|
@@ -138,8 +182,15 @@ export function createNotes(ctx) {
|
|
|
138
182
|
|
|
139
183
|
// ---------------------------------------------------------------- clics de la pàgina
|
|
140
184
|
async function onClick(e) {
|
|
185
|
+
if (Date.now() - swipedAt < 400) { e.preventDefault(); e.stopPropagation(); return; }
|
|
141
186
|
const t = e.target.closest('[data-nt]'), card = e.target.closest('.nt-card');
|
|
142
187
|
const act = t?.dataset.nt, n = card ? notes.get(card.dataset.id) : null;
|
|
188
|
+
if (act === 'reload') return load();
|
|
189
|
+
if (act === 'retry-save') {
|
|
190
|
+
t.disabled = true;
|
|
191
|
+
await Promise.all([...pendingSaves].filter((entry) => entry.machineKey === machineKey()).map((entry) => flush(entry)));
|
|
192
|
+
drawGrid(); return;
|
|
193
|
+
}
|
|
143
194
|
if (act === 'view') { view = t.dataset.v; drawFilters(); drawGrid(); return; }
|
|
144
195
|
if (act === 'layout') { layout = layout === 'grid' ? 'list' : 'grid'; LSset('nio.notes.layout', layout); drawLayout(); return; }
|
|
145
196
|
if (act === 'phone') return phoneSheet();
|
|
@@ -158,6 +209,55 @@ export function createNotes(ctx) {
|
|
|
158
209
|
}
|
|
159
210
|
if (card && n) return openEditor(n.id, card);
|
|
160
211
|
}
|
|
212
|
+
// Lliscar una targeta cap a qualsevol costat l'arxiva (o la desarxiva a «Arxivades»), com a Keep.
|
|
213
|
+
// Segueix el dit 1:1; si passa d'un terç de l'amplada o es llença ràpid, marxa; si no, torna al lloc.
|
|
214
|
+
let swipe = null, swipedAt = 0;
|
|
215
|
+
function bindSwipe(grid, sig) {
|
|
216
|
+
const reset = (card) => { card.style.transition = ''; card.style.transform = ''; card.style.opacity = ''; card.classList.remove('swiping'); };
|
|
217
|
+
grid.addEventListener('pointerdown', (e) => {
|
|
218
|
+
const card = e.target.closest('.nt-card');
|
|
219
|
+
if (!card || e.pointerType === 'mouse' || !e.isPrimary || view === 'trash' || swipe) return;
|
|
220
|
+
swipe = { card, id: card.dataset.id, x0: e.clientX, y0: e.clientY, dx: 0, lock: null, pid: e.pointerId, pts: [[e.timeStamp, e.clientX]] };
|
|
221
|
+
}, sig);
|
|
222
|
+
grid.addEventListener('pointermove', (e) => {
|
|
223
|
+
const g = swipe; if (!g || e.pointerId !== g.pid) return;
|
|
224
|
+
const dx = e.clientX - g.x0, dy = e.clientY - g.y0;
|
|
225
|
+
if (!g.lock) {
|
|
226
|
+
if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return;
|
|
227
|
+
if (Math.abs(dx) > Math.abs(dy) * 1.2) { g.lock = 'x'; g.card.setPointerCapture?.(e.pointerId); g.card.classList.add('swiping'); g.card.style.transition = 'none'; g.w = g.card.offsetWidth; }
|
|
228
|
+
else { swipe = null; return; }
|
|
229
|
+
}
|
|
230
|
+
g.dx = dx; g.pts.push([e.timeStamp, e.clientX]); if (g.pts.length > 5) g.pts.shift();
|
|
231
|
+
g.card.style.transform = `translateX(${dx}px) rotate(${dx / g.w * 3}deg)`;
|
|
232
|
+
g.card.style.opacity = String(Math.max(0.15, 1 - Math.abs(dx) / (g.w * 1.1)));
|
|
233
|
+
}, sig);
|
|
234
|
+
const end = (e) => {
|
|
235
|
+
const g = swipe; if (!g || e.pointerId !== g.pid) return;
|
|
236
|
+
swipe = null;
|
|
237
|
+
if (g.lock !== 'x') return;
|
|
238
|
+
swipedAt = Date.now();
|
|
239
|
+
const [t0, x0] = g.pts[0], [t1, x1] = g.pts[g.pts.length - 1], v = (x1 - x0) / Math.max(1, t1 - t0);
|
|
240
|
+
const n = notes.get(g.id), dir = Math.sign(Math.abs(v) > 0.45 ? v : g.dx);
|
|
241
|
+
const go = n && e.type === 'pointerup' && (Math.abs(g.dx) > g.w * 0.33 || (Math.abs(v) > 0.45 && Math.sign(v) === Math.sign(g.dx) && Math.abs(g.dx) > 24));
|
|
242
|
+
if (!go) {
|
|
243
|
+
g.card.style.transition = 'transform .32s cubic-bezier(.22,1,.36,1), opacity .2s';
|
|
244
|
+
g.card.style.transform = ''; g.card.style.opacity = '';
|
|
245
|
+
setTimeout(() => { reset(g.card); if (g.redraw) drawGrid(); }, 330);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
navigator.vibrate?.(8);
|
|
249
|
+
const was = { archived: n.archived, pinned: n.pinned };
|
|
250
|
+
const fly = reduce ? 0 : 200;
|
|
251
|
+
g.card.style.transition = `transform ${fly}ms cubic-bezier(.4,0,1,1), opacity ${fly}ms`;
|
|
252
|
+
g.card.style.transform = `translateX(${dir * (g.w + 60)}px) rotate(${dir * 6}deg)`; g.card.style.opacity = '0';
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
patch(n, { archived: !was.archived, pinned: false });
|
|
255
|
+
toastUndo(was.archived ? 'Desarxivada' : 'Arxivada', () => patch(n, was));
|
|
256
|
+
}, fly);
|
|
257
|
+
};
|
|
258
|
+
grid.addEventListener('pointerup', end, sig);
|
|
259
|
+
grid.addEventListener('pointercancel', end, sig);
|
|
260
|
+
}
|
|
161
261
|
// La targeta s'esvaeix abans de marxar de la llista; el toast permet desfer-ho.
|
|
162
262
|
function leave(card, fn, msg, undo) {
|
|
163
263
|
if (!reduce) { card.style.transition = 'opacity .16s, transform .16s'; card.style.opacity = '0'; card.style.transform = 'scale(.96)'; }
|
|
@@ -171,9 +271,11 @@ export function createNotes(ctx) {
|
|
|
171
271
|
b.onclick = (ev) => { ev.stopPropagation(); undo(); t.remove(); };
|
|
172
272
|
t.append(' ', b);
|
|
173
273
|
}
|
|
274
|
+
let inflight = 0;
|
|
174
275
|
async function patch(n, fields) {
|
|
175
276
|
Object.assign(n, fields); notes.set(n.id, n); drawAll();
|
|
176
|
-
|
|
277
|
+
inflight++;
|
|
278
|
+
try { upsert(await api(`/api/notes/${n.id}`, { method: 'PATCH', body: fields })); } catch (e) { toast(e.message, 4200); load(); } finally { inflight--; }
|
|
177
279
|
}
|
|
178
280
|
|
|
179
281
|
// ---------------------------------------------------------------- esdeveniments en directe (del flux d'nio)
|
|
@@ -182,30 +284,69 @@ export function createNotes(ctx) {
|
|
|
182
284
|
const n = msg.note, old = upsert(n); void old;
|
|
183
285
|
if (ed && ed.id === n.id) {
|
|
184
286
|
// Del servidor només entra el que no estem editant ara mateix: l'àudio, la transcripció i els canvis d'altres dispositius.
|
|
185
|
-
const keep = new Set(Object.keys(ed.dirty));
|
|
287
|
+
const keep = new Set([...Object.keys(ed.dirty), ...Object.keys(ed.savingFields)]);
|
|
186
288
|
const mine = ed.n; for (const k of Object.keys(n)) if (!keep.has(k)) mine[k] = n[k];
|
|
187
289
|
ed.n = mine; notes.set(n.id, mine); syncEditor();
|
|
188
290
|
}
|
|
189
291
|
if (root) { drawFilters(); drawGrid(); }
|
|
292
|
+
if (n.audio?.some((a) => a.state === 'transcribing')) schedule();
|
|
190
293
|
}
|
|
191
294
|
const reload = () => { if (root || loaded) load(); };
|
|
192
295
|
|
|
296
|
+
// Per si el flux en directe s'ha perdut (mòbil en segon pla, túnel que talla): es consulta l'ordinador
|
|
297
|
+
// cada poc mentre hi ha àudio transcrivint-se i, si no, de tant en tant. Només s'aplica el que ha canviat.
|
|
298
|
+
let pollT = 0, polling = false;
|
|
299
|
+
const busy = () => all().some((n) => n.audio.some((a) => a.state === 'transcribing')) || !!ed?.uploading;
|
|
300
|
+
async function refresh() {
|
|
301
|
+
if (polling || inflight || loading || !loaded || ctx.isDemo?.()) return; polling = true;
|
|
302
|
+
const mk = machineKey();
|
|
303
|
+
try {
|
|
304
|
+
const r = await api('/api/notes');
|
|
305
|
+
if (mk !== machineKey()) return;
|
|
306
|
+
const seen = new Set();
|
|
307
|
+
for (const n of r.notes) {
|
|
308
|
+
seen.add(n.id); const old = notes.get(n.id);
|
|
309
|
+
if (!old || old.updated !== n.updated || JSON.stringify(old.audio) !== JSON.stringify(n.audio)) onEvent({ type: 'note', note: n });
|
|
310
|
+
}
|
|
311
|
+
for (const id of [...notes.keys()]) if (!seen.has(id) && !(ed?.id === id && pendingSaves.has(ed))) onEvent({ type: 'note-gone', id });
|
|
312
|
+
} catch { /* ja es tornarà a provar */ } finally { polling = false; }
|
|
313
|
+
}
|
|
314
|
+
function schedule() {
|
|
315
|
+
clearTimeout(pollT);
|
|
316
|
+
if (!root && !ed) return;
|
|
317
|
+
pollT = setTimeout(async () => { if (!document.hidden) await refresh(); schedule(); }, busy() ? 1500 : 10000);
|
|
318
|
+
}
|
|
319
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden && (root || ed)) { refresh(); schedule(); } });
|
|
320
|
+
addEventListener('online', () => { if (root || ed) refresh(); });
|
|
321
|
+
|
|
193
322
|
// ---------------------------------------------------------------- editor
|
|
194
323
|
function openEditor(id, src, opts = {}) {
|
|
195
324
|
if (ed) return;
|
|
196
325
|
const n = id ? notes.get(id) : { id: null, title: '', text: '', items: opts.list ? [] : null, color: '', pinned: false, archived: false, trashed: 0, labels: [], audio: [] };
|
|
197
326
|
if (!n) return;
|
|
198
|
-
ed = { id, n, dirty: {}, timer: 0, chain: Promise.resolve(), rec: null, ai: null, src, pop: null };
|
|
327
|
+
ed = { id, n: structuredClone(n), machineKey: machineKey(), historyKey: opts.historyKey || uid(), dirty: {}, savingFields: {}, timer: 0, chain: Promise.resolve(true), rec: null, ai: null, src, pop: null };
|
|
328
|
+
if (!opts.fromHistory) history.pushState({ ...history.state, nioNote: { id, key: ed.historyKey } }, '', location.href);
|
|
199
329
|
const ov = document.createElement('div');
|
|
200
330
|
ov.className = 'nt-ov'; ov.innerHTML = `<div class="nt-ed" role="dialog" aria-modal="true" aria-label="Edita la nota" data-color="${n.color}"></div>`;
|
|
201
331
|
document.body.append(ov); ed.ov = ov; ed.card = ov.firstElementChild;
|
|
202
332
|
buildEditor();
|
|
203
333
|
ov.addEventListener('mousedown', (e) => { if (e.target === ov) closeEditor(); });
|
|
204
|
-
ov.addEventListener('keydown', (e) => {
|
|
334
|
+
ov.addEventListener('keydown', (e) => {
|
|
335
|
+
if (!ed) return;
|
|
336
|
+
if (e.key === 'Escape') { if (ed.pop) closePop(); else closeEditor(); }
|
|
337
|
+
else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) closeEditor();
|
|
338
|
+
else if (e.key === 'Tab') {
|
|
339
|
+
const controls = [...ov.querySelectorAll('button, input, textarea, select, [tabindex="0"]')].filter((el) => !el.disabled && el.getClientRects().length);
|
|
340
|
+
const first = controls[0], last = controls.at(-1);
|
|
341
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus(); }
|
|
342
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus(); }
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
$('[data-e="done"]', ed.card).focus({ preventScroll: true });
|
|
205
346
|
animateOpen(ed.card, src);
|
|
206
|
-
if (!n.title && !n.text && !n.items?.length && !opts.mic) setTimeout(() =>
|
|
347
|
+
if (!n.title && !n.text && !n.items?.length && !opts.mic && !ctx.isPhone()) setTimeout(() => ed && $('.nt-ed-title', ed.card)?.focus(), 60);
|
|
207
348
|
if (opts.list) addItem(null);
|
|
208
|
-
if (opts.mic)
|
|
349
|
+
if (opts.mic) toggleRec();
|
|
209
350
|
}
|
|
210
351
|
function animateOpen(card, src) {
|
|
211
352
|
if (reduce) return;
|
|
@@ -217,8 +358,21 @@ export function createNotes(ctx) {
|
|
|
217
358
|
card.animate([{ transform: `translate(${dx}px,${dy}px) scale(${sx},${sy})`, opacity: .6 }, { transform: 'none', opacity: 1 }], { duration: 300, easing: 'cubic-bezier(.22,1,.36,1)' });
|
|
218
359
|
ed.ov.animate([{ background: 'rgba(39,34,36,0)' }, { background: 'rgba(39,34,36,.32)' }], { duration: 300, easing: 'ease-out' });
|
|
219
360
|
}
|
|
220
|
-
async function closeEditor(instant) {
|
|
361
|
+
async function closeEditor(instant, fromHistory = false) {
|
|
221
362
|
const e = ed; if (!e) return;
|
|
363
|
+
if (!instant && !fromHistory && history.state?.nioNote) { history.back(); return; }
|
|
364
|
+
if (instant && history.state?.nioNote) { const { nioNote, ...rest } = history.state; history.replaceState(rest, '', location.href); }
|
|
365
|
+
if (!instant) {
|
|
366
|
+
if (e.closing) return;
|
|
367
|
+
e.closing = true;
|
|
368
|
+
const saved = await flush(e);
|
|
369
|
+
e.closing = false;
|
|
370
|
+
if (ed !== e) return;
|
|
371
|
+
if (!saved) {
|
|
372
|
+
if (fromHistory && root && location.hash.startsWith('#/notes')) history.pushState({ ...history.state, nioNote: { id: e.id, key: e.historyKey } }, '', location.href);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
222
376
|
ed = null; clearTimeout(e.timer); e.rec?.cancel?.(); e.audioEl?.pause();
|
|
223
377
|
const done = () => e.ov.remove();
|
|
224
378
|
if (instant || reduce) done(); else {
|
|
@@ -232,36 +386,61 @@ export function createNotes(ctx) {
|
|
|
232
386
|
await flush(e);
|
|
233
387
|
if (e.id && empty(e.n)) { notes.delete(e.id); api(`/api/notes/${e.id}?forever=1`, { method: 'DELETE' }).catch(() => {}); }
|
|
234
388
|
if (root) drawAll();
|
|
389
|
+
if (root) (e.src?.isConnected ? e.src.querySelector('button') || e.src : root.querySelector('[data-nt="new"]'))?.focus({ preventScroll: true });
|
|
235
390
|
}
|
|
236
391
|
|
|
237
392
|
// Desat: s'envia només el que ha canviat, en cua, mig segon després de l'última tecla.
|
|
238
393
|
function set(fields) {
|
|
239
394
|
Object.assign(ed.n, fields); Object.assign(ed.dirty, fields);
|
|
395
|
+
pendingSaves.add(ed); saveStatus(ed, 'Canvis pendents…');
|
|
240
396
|
ed.card.dataset.color = ed.n.color;
|
|
241
397
|
clearTimeout(ed.timer); ed.timer = setTimeout(flush, 450);
|
|
242
398
|
if (ed.id) notes.set(ed.id, ed.n);
|
|
243
399
|
if (root) { clearTimeout(set.t); set.t = setTimeout(() => { drawFilters(); drawGrid(); }, 200); }
|
|
244
400
|
}
|
|
245
401
|
function flush(e = ed, force = false) {
|
|
246
|
-
if (!e) return Promise.resolve();
|
|
402
|
+
if (!e) return Promise.resolve(true);
|
|
247
403
|
clearTimeout(e.timer);
|
|
248
404
|
e.chain = e.chain.then(async () => {
|
|
249
|
-
|
|
405
|
+
if (e.machineKey !== machineKey()) {
|
|
406
|
+
if ((!e.id && empty(e.n)) || (e.id && !Object.keys(e.dirty).length && !e.error)) return true;
|
|
407
|
+
e.error = true; pendingSaves.add(e); return false;
|
|
408
|
+
}
|
|
409
|
+
const fields = e.dirty; e.dirty = {}; e.savingFields = fields;
|
|
410
|
+
if (e.id && !Object.keys(fields).length) return !e.error;
|
|
411
|
+
saveStatus(e, 'Desant…');
|
|
250
412
|
try {
|
|
251
413
|
if (!e.id) {
|
|
252
|
-
if (!force && empty(e.n)) { e.
|
|
414
|
+
if (!force && empty(e.n)) { pendingSaves.delete(e); e.savingFields = {}; saveStatus(e, 'Nota buida'); return true; }
|
|
253
415
|
const { audio, id, ...body } = e.n; void audio; void id;
|
|
254
416
|
const r = await api('/api/notes', { method: 'POST', body }); e.id = r.id; Object.assign(e.n, { id: r.id, audio: r.audio, created: r.created, updated: r.updated }); notes.set(r.id, e.n);
|
|
255
|
-
|
|
256
|
-
|
|
417
|
+
historyNotes.set(e.historyKey, r.id);
|
|
418
|
+
if (ed === e && history.state?.nioNote) history.replaceState({ ...history.state, nioNote: { id: r.id, key: e.historyKey } }, '', location.href);
|
|
419
|
+
} else if (Object.keys(fields).length) { const r = await api(`/api/notes/${e.id}`, { method: 'PATCH', body: fields }); e.n.updated = r.updated; }
|
|
420
|
+
e.error = false; e.savingFields = {};
|
|
421
|
+
if (!Object.keys(e.dirty).length) pendingSaves.delete(e);
|
|
422
|
+
saveStatus(e, Object.keys(e.dirty).length ? 'Canvis pendents…' : 'Desat');
|
|
423
|
+
return true;
|
|
424
|
+
} catch (er) {
|
|
425
|
+
e.dirty = { ...fields, ...e.dirty }; e.savingFields = {}; e.error = true; pendingSaves.add(e);
|
|
426
|
+
saveStatus(e, 'No s’ha pogut desar', true);
|
|
427
|
+
toast(`No s’ha pogut desar: ${er.message}`, 4500);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
257
430
|
});
|
|
258
431
|
return e.chain;
|
|
259
432
|
}
|
|
433
|
+
function saveStatus(entry, text, error = false) {
|
|
434
|
+
const status = entry.card?.querySelector('#edSaveStatus'), retry = entry.card?.querySelector('[data-e="retry-save"]');
|
|
435
|
+
if (status) { status.textContent = text; status.classList.toggle('err', error); }
|
|
436
|
+
if (retry) retry.hidden = !error;
|
|
437
|
+
}
|
|
260
438
|
const ensureId = async () => { if (!ed.id) await flush(ed, true); return ed.id; };
|
|
261
439
|
|
|
262
440
|
function buildEditor() {
|
|
263
441
|
const n = ed.n, c = ed.card;
|
|
264
442
|
c.innerHTML = `
|
|
443
|
+
<div class="nt-ed-nav"><button class="nt-ib nt-back" type="button" data-e="done">${svg(I.back)}<span>Notes</span></button><div class="nt-save"><span id="edSaveStatus" role="status">Es desa automàticament</span><button type="button" class="nt-ib" data-e="retry-save" hidden>Torna a desar</button></div></div>
|
|
265
444
|
<div class="nt-ed-head"><input class="nt-ed-title" type="text" placeholder="Títol" value="${esc(n.title)}" aria-label="Títol" maxlength="200" autocomplete="off">
|
|
266
445
|
<button class="nt-ib ${n.pinned ? 'on' : ''}" type="button" data-e="pin" aria-label="Fixa la nota" aria-pressed="${n.pinned}">${svg(I.pin)}</button></div>
|
|
267
446
|
<div class="nt-ed-body" id="edBody"></div>
|
|
@@ -269,17 +448,25 @@ export function createNotes(ctx) {
|
|
|
269
448
|
<div class="nt-labels nt-ed-labels" id="edLabels"></div>
|
|
270
449
|
<div class="nt-ai" id="edAi" hidden></div>
|
|
271
450
|
<div class="nt-rec" id="edRec" hidden></div>
|
|
451
|
+
<div class="nt-more-scrim" id="edScrim" hidden></div>
|
|
452
|
+
<div class="nt-more-sheet" id="edMore" role="dialog" aria-label="Més opcions" hidden>
|
|
453
|
+
<div class="nt-more-grab" aria-hidden="true"></div>
|
|
454
|
+
<button type="button" data-e="archive">${n.archived ? 'Desarxiva' : 'Arxiva'}</button>
|
|
455
|
+
<button type="button" data-e="trash" class="danger">Envia a la paperera</button>
|
|
456
|
+
<button type="button" data-e="more-close" class="muted">Tanca</button>
|
|
457
|
+
</div>
|
|
272
458
|
<div class="nt-ed-foot" id="edFoot">
|
|
273
|
-
<div class="nt-tb">
|
|
459
|
+
<div class="nt-tb"><div class="nt-edit-tools">
|
|
274
460
|
<button class="nt-ib" type="button" data-e="mic" aria-label="Grava amb el micròfon" title="Grava (Whisper, a l’ordinador)">${svg(I.mic)}</button>
|
|
275
461
|
<button class="nt-ib" type="button" data-e="mode" id="edMode" title="Llista o text"></button>
|
|
276
462
|
<button class="nt-ib" type="button" data-e="color" aria-label="Color" title="Color">${svg(I.palette)}</button>
|
|
277
463
|
<button class="nt-ib" type="button" data-e="label" aria-label="Etiquetes" title="Etiquetes">${svg(I.tag)}</button>
|
|
278
464
|
<button class="nt-ib nt-ai-b" type="button" data-e="ai" aria-label="Assistent" title="Resumeix i organitza amb un model">${svg(I.spark)}<span>Assistent</span></button>
|
|
279
|
-
<
|
|
465
|
+
<button class="nt-ib nt-more-btn" type="button" data-e="more" aria-label="Més">${svg(I.more)}</button>
|
|
466
|
+
</div><div class="nt-note-tools">
|
|
280
467
|
<button class="nt-ib" type="button" data-e="archive" aria-label="Arxiva" title="Arxiva">${svg(I.archive)}</button>
|
|
281
468
|
<button class="nt-ib" type="button" data-e="trash" aria-label="Paperera" title="Paperera">${svg(I.trash)}</button>
|
|
282
|
-
<button class="btn ink nt-done" type="button" data-e="done">Fet</button>
|
|
469
|
+
<button class="btn ink nt-done" type="button" data-e="done">Fet</button></div>
|
|
283
470
|
</div>
|
|
284
471
|
</div>`;
|
|
285
472
|
const title = $('.nt-ed-title', c);
|
|
@@ -287,6 +474,31 @@ export function createNotes(ctx) {
|
|
|
287
474
|
title.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); ($('.nt-ed-text', c) || $('.nt-item input[type=text]', c))?.focus(); } });
|
|
288
475
|
c.addEventListener('click', onEditorClick);
|
|
289
476
|
drawBody(); drawAudio(); drawLabels(); drawMode();
|
|
477
|
+
bindMoreDrag($('#edMore', c), $('#edScrim', c));
|
|
478
|
+
}
|
|
479
|
+
// El panell «Més» segueix el dit cap avall i es tanca si baixa prou o es llença; si no, torna a pujar.
|
|
480
|
+
let moreDraggedAt = 0;
|
|
481
|
+
function bindMoreDrag(sheet, scrim) {
|
|
482
|
+
let g = null;
|
|
483
|
+
sheet.addEventListener('pointerdown', (e) => { if (e.pointerType === 'mouse') return; g = { y0: e.clientY, dy: 0, pid: e.pointerId, pts: [[e.timeStamp, e.clientY]], moved: false }; }, { passive: true });
|
|
484
|
+
sheet.addEventListener('pointermove', (e) => {
|
|
485
|
+
if (!g || e.pointerId !== g.pid) return;
|
|
486
|
+
const dy = e.clientY - g.y0;
|
|
487
|
+
if (!g.moved) { if (Math.abs(dy) < 6) return; g.moved = true; sheet.setPointerCapture?.(e.pointerId); sheet.style.transition = 'none'; scrim.style.transition = 'none'; }
|
|
488
|
+
g.dy = dy > 0 ? dy : dy / 4; // cap amunt, amb resistència
|
|
489
|
+
g.pts.push([e.timeStamp, e.clientY]); if (g.pts.length > 5) g.pts.shift();
|
|
490
|
+
sheet.style.transform = `translateY(${g.dy}px)`;
|
|
491
|
+
scrim.style.opacity = String(Math.max(0, 1 - Math.max(0, g.dy) / sheet.offsetHeight));
|
|
492
|
+
});
|
|
493
|
+
const end = (e) => {
|
|
494
|
+
if (!g || e.pointerId !== g.pid) return;
|
|
495
|
+
const d = g; g = null; if (!d.moved) return;
|
|
496
|
+
moreDraggedAt = Date.now();
|
|
497
|
+
const [t0, y0] = d.pts[0], [t1, y1] = d.pts[d.pts.length - 1], v = (y1 - y0) / Math.max(1, t1 - t0);
|
|
498
|
+
if (d.dy > sheet.offsetHeight * 0.3 || (v > 0.4 && d.dy > 12)) closeMore(true);
|
|
499
|
+
else { sheet.style.transition = ''; scrim.style.transition = ''; sheet.style.transform = ''; scrim.style.opacity = ''; }
|
|
500
|
+
};
|
|
501
|
+
sheet.addEventListener('pointerup', end); sheet.addEventListener('pointercancel', end);
|
|
290
502
|
}
|
|
291
503
|
function drawMode() { const b = $('#edMode', ed.card); if (b) { b.innerHTML = svg(ed.n.items ? I.text : I.list); b.setAttribute('aria-label', ed.n.items ? 'Passa a text' : 'Passa a llista'); } }
|
|
292
504
|
function drawBody() {
|
|
@@ -317,10 +529,12 @@ export function createNotes(ctx) {
|
|
|
317
529
|
}
|
|
318
530
|
function removeItem(id) { set({ items: ed.n.items.filter((x) => x.id !== id) }); drawBody(); }
|
|
319
531
|
function drawLabels() { const box = $('#edLabels', ed.card); box.innerHTML = ed.n.labels.map((l) => `<span class="nt-label">${esc(l)}<button type="button" data-e="unlabel" data-l="${esc(l)}" aria-label="Treu l’etiqueta ${esc(l)}">${svg(I.close)}</button></span>`).join(''); box.hidden = !ed.n.labels.length; }
|
|
532
|
+
// L'àudio acabat de pujar també surt a la llista (targeta «Transcrivint…»), sense esperar el flux.
|
|
533
|
+
function upsertAudio(note) { const cur = notes.get(note.id); if (cur && cur !== ed?.n) cur.audio = note.audio; if (root) drawGrid(); }
|
|
320
534
|
function drawAudio() {
|
|
321
535
|
const box = $('#edAudio', ed.card), list = ed.n.audio;
|
|
322
|
-
box.hidden = !list.length;
|
|
323
|
-
box.innerHTML = list.map((a) => `<div class="nt-au" data-a="${a.id}" data-st="${a.state}">
|
|
536
|
+
box.hidden = !list.length && !ed.uploading;
|
|
537
|
+
box.innerHTML = (ed.uploading ? '<div class="nt-au" data-st="transcribing"><span class="nt-au-s"><i class="nt-spin"></i>Enviant l’àudio a l’ordinador…</span></div>' : '') + list.map((a) => `<div class="nt-au" data-a="${a.id}" data-st="${a.state}">
|
|
324
538
|
<button type="button" class="nt-ib sm" data-e="play" aria-label="Escolta">${svg(I.play)}</button>
|
|
325
539
|
<span class="nt-au-d">${a.dur ? fmtDur(a.dur) : 'Àudio'}</span>
|
|
326
540
|
<span class="nt-au-s">${a.state === 'transcribing' ? '<i class="nt-spin"></i>Transcrivint amb Whisper…' : a.state === 'ok' ? `${svg(I.check)}Transcripció afegida` : `${esc(a.error || 'No s’ha pogut transcriure')}`}</span>
|
|
@@ -330,16 +544,24 @@ export function createNotes(ctx) {
|
|
|
330
544
|
function syncEditor() {
|
|
331
545
|
const n = ed.n, c = ed.card;
|
|
332
546
|
drawAudio(); drawLabels(); drawMode(); c.dataset.color = n.color;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
547
|
+
// El text que arriba (transcripció, un altre dispositiu) entra encara que el camp tingui el cursor,
|
|
548
|
+
// sempre que ara mateix no s'hi estigui escrivint; el cursor es queda on era.
|
|
549
|
+
const mine = (k) => k in ed.dirty || k in ed.savingFields;
|
|
550
|
+
const put = (el, v) => { const at = el.selectionStart, end = el.selectionEnd, foc = document.activeElement === el; el.value = v; if (foc) try { el.setSelectionRange(at, end); } catch { /* res */ } };
|
|
551
|
+
const ta = $('.nt-ed-text', c); if (ta && !mine('text') && ta.value !== n.text) { put(ta, n.text); grow(ta); }
|
|
552
|
+
const ti = $('.nt-ed-title', c); if (!mine('title') && ti.value !== n.title) put(ti, n.title);
|
|
553
|
+
if (n.items && !mine('items') && !c.contains(document.activeElement?.closest?.('.nt-item'))) drawBody();
|
|
336
554
|
if (!n.items && !ta) drawBody();
|
|
337
555
|
$('[data-e="pin"]', c).classList.toggle('on', n.pinned);
|
|
338
556
|
}
|
|
339
557
|
|
|
340
558
|
async function onEditorClick(ev) {
|
|
341
|
-
const b = ev.target.closest('[data-e]'), c = ed?.card; if (!b || !ed) { if (ed?.pop && !ev.target.closest('.nt-pop')) closePop(); return; }
|
|
559
|
+
const b = ev.target.closest('[data-e]'), c = ed?.card; if (!b || !ed) { if (ed?.pop && !ev.target.closest('.nt-pop')) closePop(); if (ed && !ev.target.closest('.nt-more-sheet')) closeMore(true); return; }
|
|
342
560
|
const a = b.dataset.e;
|
|
561
|
+
if (ev.target.closest('.nt-more-sheet') && Date.now() - moreDraggedAt < 350) return; // era un arrossegament, no un toc
|
|
562
|
+
if (a === 'more-close') return closeMore(true);
|
|
563
|
+
if (a !== 'more' && !ev.target.closest('.nt-more-sheet')) closeMore(true);
|
|
564
|
+
if (a === 'retry-save') { b.disabled = true; await flush(ed); b.disabled = false; return; }
|
|
343
565
|
if (!['color', 'label', 'ai', 'aiengine'].includes(a) && !ev.target.closest('.nt-pop')) closePop();
|
|
344
566
|
if (a === 'done') return closeEditor();
|
|
345
567
|
if (a === 'pin') { set({ pinned: !ed.n.pinned }); b.classList.toggle('on', ed.n.pinned); b.setAttribute('aria-pressed', ed.n.pinned); return; }
|
|
@@ -358,6 +580,7 @@ export function createNotes(ctx) {
|
|
|
358
580
|
if (a === 'color') return popColor(b);
|
|
359
581
|
if (a === 'label') return popLabel(b);
|
|
360
582
|
if (a === 'ai') return popAi(b);
|
|
583
|
+
if (a === 'more') return openMore();
|
|
361
584
|
if (a === 'mic') return toggleRec();
|
|
362
585
|
if (a === 'play') return play(b.closest('.nt-au').dataset.a, b);
|
|
363
586
|
if (a === 'voice') return ctx.voiceSheet();
|
|
@@ -366,12 +589,31 @@ export function createNotes(ctx) {
|
|
|
366
589
|
}
|
|
367
590
|
|
|
368
591
|
// ---------------------------------------------------------------- finestretes (color, etiquetes, assistent)
|
|
592
|
+
function openMore() {
|
|
593
|
+
if (!ed) return; closePop();
|
|
594
|
+
const s = $('#edMore', ed.card), sc = $('#edScrim', ed.card);
|
|
595
|
+
// Puja amb una transició (no una animació CSS) perquè el dit el pugui agafar mentre encara puja.
|
|
596
|
+
s.hidden = sc.hidden = false;
|
|
597
|
+
if (!reduce) { s.style.transition = sc.style.transition = 'none'; s.style.transform = 'translateY(100%)'; sc.style.opacity = '0'; void s.offsetHeight; }
|
|
598
|
+
s.style.transition = sc.style.transition = s.style.transform = sc.style.opacity = '';
|
|
599
|
+
$('button', s)?.focus({ preventScroll: true });
|
|
600
|
+
}
|
|
601
|
+
function closeMore(animated) {
|
|
602
|
+
const s = ed?.card?.querySelector('#edMore'), sc = ed?.card?.querySelector('#edScrim');
|
|
603
|
+
if (!s || s.hidden) return;
|
|
604
|
+
const hide = () => { s.hidden = sc.hidden = true; s.style.transition = sc.style.transition = s.style.transform = sc.style.opacity = ''; };
|
|
605
|
+
if (!animated || reduce) return hide();
|
|
606
|
+
s.style.transition = 'transform .22s cubic-bezier(.4,0,1,1)'; sc.style.transition = 'opacity .22s';
|
|
607
|
+
s.style.transform = 'translateY(105%)'; sc.style.opacity = '0';
|
|
608
|
+
setTimeout(hide, 220);
|
|
609
|
+
}
|
|
369
610
|
function closePop() { ed?.pop?.remove(); if (ed) ed.pop = null; }
|
|
370
611
|
function pop(anchor, html) {
|
|
371
612
|
closePop();
|
|
372
613
|
const p = document.createElement('div'); p.className = 'nt-pop'; p.innerHTML = html;
|
|
373
614
|
ed.card.append(p); ed.pop = p;
|
|
374
615
|
const a = anchor.getBoundingClientRect(), r = ed.card.getBoundingClientRect(), w = p.offsetWidth;
|
|
616
|
+
p.style.maxHeight = `${Math.max(80, a.top - Math.max(r.top, window.visualViewport?.offsetTop || 0) - 16)}px`;
|
|
375
617
|
p.style.left = `${Math.max(8, Math.min(a.left - r.left, r.width - w - 8))}px`; p.style.bottom = `${r.bottom - a.top + 8}px`;
|
|
376
618
|
return p;
|
|
377
619
|
}
|
|
@@ -413,10 +655,11 @@ export function createNotes(ctx) {
|
|
|
413
655
|
if (!ed) return;
|
|
414
656
|
try {
|
|
415
657
|
if (blob.size < 1500) throw new Error('No he sentit res. Prova-ho una altra vegada.');
|
|
658
|
+
ed.uploading = true; drawAudio();
|
|
416
659
|
const id = await ensureId(); if (!id) throw new Error('No s’ha pogut crear la nota');
|
|
417
660
|
const res = await ctx.nodeAudio(`/api/notes/${id}/audio?dur=${Math.round(dur)}`, { headers: { 'content-type': blob.type || 'audio/webm' }, body: blob });
|
|
418
|
-
const note = await res.json(); if (ed) {
|
|
419
|
-
} catch (e) { toast(e.message, 4500); }
|
|
661
|
+
const note = await res.json(); if (ed) { ed.uploading = false; ed.n.audio = note.audio; upsertAudio(note); drawAudio(); schedule(); }
|
|
662
|
+
} catch (e) { if (ed) { ed.uploading = false; drawAudio(); } toast(e.message, 4500); }
|
|
420
663
|
};
|
|
421
664
|
ed.rec = { stop: () => rec.stop(), cancel: () => { send = false; try { rec.stop(); } catch { cleanup(); } } };
|
|
422
665
|
rec.start(1000);
|
package/web/app/style.css
CHANGED
|
@@ -191,6 +191,11 @@ button.chip:hover { border-color: var(--fg); }
|
|
|
191
191
|
.session-card .last.err { color: var(--red); }
|
|
192
192
|
.session-card .last { color: var(--fg-muted); font-size: 14px; margin-top: 4px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
193
193
|
.session-card .meta { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; font-size: 12px; color: var(--fg-muted); }
|
|
194
|
+
#termSessions:not(:empty) { margin-bottom: 16px; }
|
|
195
|
+
.old-sessions { margin-top: 8px; }
|
|
196
|
+
.old-sessions > summary { cursor: pointer; padding: 8px 0; list-style: none; }
|
|
197
|
+
.old-sessions > summary::-webkit-details-marker { display: none; }
|
|
198
|
+
.old-sessions > .list { margin-top: 8px; }
|
|
194
199
|
.fab { position: fixed; right: 24px; bottom: calc(24px + var(--safe-b)); z-index: 15; display: inline-flex; align-items: center; gap: 8px; height: 50px; padding: 0 20px 0 16px; border: 0; border-radius: 16px; background: var(--fg); color: var(--bg); font-weight: 600; box-shadow: 0 14px 30px -14px color-mix(in srgb, var(--fg) 70%, transparent); transition: transform .15s; }
|
|
195
200
|
.fab:active { transform: scale(.97); }
|
|
196
201
|
|
|
@@ -744,3 +749,55 @@ body.chat-mode .demo-bar, body.board-mode .demo-bar { display: none; }
|
|
|
744
749
|
}
|
|
745
750
|
/* Referència per mesurar els missatges (offsetTop) dins la conversa. */
|
|
746
751
|
.feed { position: relative; }
|
|
752
|
+
|
|
753
|
+
/* Totes les seccions, amb controls còmodes de tocar. */
|
|
754
|
+
.more-sections { display: grid; gap: 22px; margin-top: 20px; }
|
|
755
|
+
.more-sections h3 { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--fg-muted); }
|
|
756
|
+
.more-sections section > div { display: grid; }
|
|
757
|
+
.more-link { display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 10px 12px; border-radius: 10px; text-decoration: none; }
|
|
758
|
+
.more-link > span:first-of-type { flex: 1; }
|
|
759
|
+
.more-link > .ico:last-child { width: 14px; color: var(--fg-muted); }
|
|
760
|
+
.more-link:hover, .more-link[aria-current="page"] { background: var(--bg-2); }
|
|
761
|
+
.voice-extras { border: 1px solid var(--border); border-radius: 12px; padding: 0 14px; }
|
|
762
|
+
.voice-extras summary { min-height: 48px; padding-block: 13px; cursor: pointer; font-weight: 600; }
|
|
763
|
+
.voice-extras > div { padding: 6px 0 16px; }
|
|
764
|
+
@media (max-width: 820px) { .tabbar > * { min-height: 48px; } .side nav a { min-height: 46px; } }
|
|
765
|
+
|
|
766
|
+
/* Llistes i formularis també han d’encaixar amb text llarg o el teclat obert. */
|
|
767
|
+
.side nav { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
768
|
+
.side > .brand, .side > .foot { flex-shrink: 0; }
|
|
769
|
+
:where(.panel, .card, .stack, .stack-lg, .grid-2, .grid-2 > *, label.f) { min-width: 0; }
|
|
770
|
+
:where(.top, .panel .body, .mem-list, .job, .sheet-in h2, .conn span) { overflow-wrap: anywhere; }
|
|
771
|
+
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
772
|
+
.conn span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
773
|
+
:where(button, a, input, textarea, select, summary):focus-visible { outline: 2px solid var(--fg); outline-offset: 3px; }
|
|
774
|
+
@media (max-width: 820px) {
|
|
775
|
+
dialog.sheet { max-height: min(92dvh, calc(var(--vvh, 100dvh) - 8px)); }
|
|
776
|
+
.btn, .btn.sm, .icon-btn, .seg button { min-height: 44px; }
|
|
777
|
+
input[type="text"], input[type="password"], input[type="url"], input[type="search"], select, textarea.field { font-size: 16px; }
|
|
778
|
+
.sheet-foot { bottom: calc(-22px - var(--safe-b)); }
|
|
779
|
+
}
|
|
780
|
+
@media (max-width: 520px) { .grid-2 { grid-template-columns: minmax(0, 1fr); } }
|
|
781
|
+
.sheet-in { position: relative; }
|
|
782
|
+
.sheet-close { position: absolute; top: 14px; right: 14px; z-index: 1; }
|
|
783
|
+
.sheet-in > h2 { padding-right: 42px; }
|
|
784
|
+
.form-error { color: var(--red); font-size: 13px; overflow-wrap: anywhere; }
|
|
785
|
+
dialog.sheet:has(#memForm) { width: min(780px, calc(100% - 32px)); }
|
|
786
|
+
@media (max-width: 820px) {
|
|
787
|
+
.sheet-close { top: 16px; right: 10px; }
|
|
788
|
+
dialog.sheet:has(#memForm) { width: 100%; }
|
|
789
|
+
}
|
|
790
|
+
@media (max-width: 520px) {
|
|
791
|
+
#jobWhen { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px; }
|
|
792
|
+
#jobWhen button { white-space: normal; padding: 6px 8px; }
|
|
793
|
+
}
|
|
794
|
+
@media (max-width: 820px) {
|
|
795
|
+
dialog.sheet { top: auto; bottom: var(--kb, 0px); margin: 0; }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/* Sessions obertes al terminal de l'ordinador */
|
|
799
|
+
.term-sessions { margin-bottom: 22px; }
|
|
800
|
+
.term-sessions h2.sec { margin: 0 0 8px; }
|
|
801
|
+
.term-card { width: 100%; text-align: left; font: inherit; color: inherit; cursor: pointer; border-style: dashed; }
|
|
802
|
+
.term-card:disabled { opacity: .6; cursor: progress; }
|
|
803
|
+
.term-go { margin-left: auto; display: inline-flex; align-items: center; gap: 2px; font-weight: 600; color: var(--fg); }
|
package/web/sw.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// nio · service worker: obre l'app encara que no hi hagi connexió i mostra els avisos.
|
|
2
|
-
const SHELL = 'nio-shell-
|
|
3
|
-
const FILES = ['/app', '/app/style.css', '/app/notes.css', '/app/app.js', '/app/notes.js', '/app/demo.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
|
|
2
|
+
const SHELL = 'nio-shell-v50';
|
|
3
|
+
const FILES = ['/app', '/app/style.css', '/app/notes.css', '/app/call.css', '/app/app.js', '/app/notes.js', '/app/call.js', '/app/demo.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
|
|
4
4
|
|
|
5
5
|
self.addEventListener('install', (e) => {
|
|
6
6
|
e.waitUntil(caches.open(SHELL).then((c) => c.addAll(FILES)).catch(() => {}));
|