primocode 7.18.0 → 8.0.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.
@@ -0,0 +1,504 @@
1
+ /* ==========================================================================
2
+ player.js — apresenta, reproduz com trilha, grava e imprime.
3
+ Serve tanto para /v/<id> quanto para o HTML autônomo exportado.
4
+ ========================================================================== */
5
+
6
+ (function () {
7
+ "use strict";
8
+
9
+ var palco = document.getElementById("palco");
10
+ if (!palco) return;
11
+
12
+ var arq = null, doc = {}, cenas = [], atual = 0;
13
+ var tocando = false, quadro = null, t0 = 0, decorrido = 0;
14
+ var trilha = null, gravador = null, pedacos = [], fluxoTela = null;
15
+ var ehVideo = false, transporte = null;
16
+
17
+ /* ------------------------------------------------------------- avisos -- */
18
+
19
+ function aviso(texto, ms) {
20
+ var a = document.querySelector(".aviso");
21
+ if (!a) { a = document.createElement("div"); a.className = "aviso"; document.body.appendChild(a); }
22
+ a.textContent = texto;
23
+ a.classList.add("on");
24
+ clearTimeout(a._t);
25
+ a._t = setTimeout(function () { a.classList.remove("on"); }, ms || 1800);
26
+ }
27
+
28
+ function mmss(s) {
29
+ s = Math.max(0, Math.floor(s || 0));
30
+ return String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
31
+ }
32
+
33
+ function duracaoDe(i) { return (cenas[i] && cenas[i].duracao) || 4; }
34
+ function total() { return cenas.reduce(function (s, _, i) { return s + duracaoDe(i); }, 0); }
35
+ function inicioDe(i) { var s = 0; for (var k = 0; k < i; k++) s += duracaoDe(k); return s; }
36
+
37
+ /* ----------------------------------------------------------- montagem -- */
38
+
39
+ function montar(a) {
40
+ arq = a;
41
+ doc = a.doc || {};
42
+ document.title = a.titulo || "PrimoCode";
43
+ palco.innerHTML = "";
44
+ ehVideo = a.ferramenta === "corte";
45
+
46
+ if (a.ferramenta === "prosa") return montarProsa();
47
+ if (a.ferramenta === "grade") return montarGrade();
48
+
49
+ cenas = doc.cenas || [];
50
+ cenas.forEach(function (c, i) {
51
+ var no = renderCena(c, doc, { animar: true });
52
+ no.style.position = "absolute";
53
+ no.style.inset = "0";
54
+ no.style.display = i === 0 ? "block" : "none";
55
+ palco.appendChild(no);
56
+ });
57
+ palco.style.cssText = "position:absolute;left:50%;top:50%;width:" +
58
+ (doc.largura || 1920) + "px;height:" + (doc.altura || 1080) + "px";
59
+
60
+ ajustar();
61
+ ir(0);
62
+
63
+ if (location.hash === "#imprimir") return setTimeout(imprimirTudo, 350);
64
+
65
+ if (ehVideo) {
66
+ montarTransporte();
67
+ prepararTrilha();
68
+ tocar();
69
+ }
70
+ }
71
+
72
+ function montarProsa() {
73
+ palco.style.cssText = "position:static";
74
+ document.body.style.overflow = "auto";
75
+ document.body.style.background = "#17171b";
76
+ var w = document.createElement("div");
77
+ w.style.cssText = "display:grid;place-items:center;padding:40px 20px";
78
+ w.appendChild(renderProsa(doc));
79
+ palco.appendChild(w);
80
+ }
81
+
82
+ function montarGrade() {
83
+ palco.style.cssText = "position:static";
84
+ document.body.style.overflow = "auto";
85
+ document.body.style.background = "#17171b";
86
+ var g = document.createElement("div");
87
+ g.style.cssText = "padding:26px";
88
+ (doc.abas || []).forEach(function (aba, i) {
89
+ var t = document.createElement("div");
90
+ t.style.cssText = "color:#8d8d9a;font:600 11px/1 system-ui;letter-spacing:.12em;" +
91
+ "text-transform:uppercase;margin:22px 0 10px";
92
+ t.textContent = aba.nome || "Planilha " + (i + 1);
93
+ g.appendChild(t);
94
+ var caixa = document.createElement("div");
95
+ caixa.style.cssText = "overflow:auto;border-radius:12px;border:1px solid rgba(255,255,255,.09)";
96
+ caixa.appendChild(renderGrade(doc, { aba: i }));
97
+ g.appendChild(caixa);
98
+ });
99
+ palco.appendChild(g);
100
+ }
101
+
102
+ function ajustar() {
103
+ if (!cenas.length) return;
104
+ var alturaUI = ehVideo ? 74 : 0;
105
+ var larguraUI = painel ? 360 : 0;
106
+ var e = Math.min((window.innerWidth - larguraUI) / (doc.largura || 1920),
107
+ (window.innerHeight - alturaUI) / (doc.altura || 1080));
108
+ palco.style.left = ((window.innerWidth - larguraUI) / 2) + "px";
109
+ palco.style.transform = "translate(-50%,-50%) scale(" + e + ")";
110
+ palco.style.marginTop = ehVideo ? (-alturaUI / 2) + "px" : "0";
111
+ }
112
+
113
+ /* ---------------------------------------------------------- navegação -- */
114
+
115
+ function ir(n, semRelogio) {
116
+ if (!cenas.length) return;
117
+ atual = Math.max(0, Math.min(cenas.length - 1, n));
118
+ Array.prototype.forEach.call(palco.children, function (no, i) {
119
+ no.style.display = i === atual ? "block" : "none";
120
+ if (i === atual) {
121
+ no.classList.remove("animar");
122
+ void no.offsetWidth; // reinicia entrada e transição
123
+ no.classList.add("animar");
124
+ contar(no);
125
+ }
126
+ });
127
+ narrar();
128
+ pintarApresentador();
129
+ if (!semRelogio) decorrido = inicioDe(atual);
130
+ pintar();
131
+ if (location.hash.indexOf("#imprimir") !== 0) {
132
+ history.replaceState(null, "", "#/" + (atual + 1));
133
+ }
134
+ }
135
+
136
+ function proxima() { if (atual < cenas.length - 1) ir(atual + 1); else if (ehVideo) fim(); }
137
+ function anterior() { ir(atual - 1); }
138
+
139
+ /* números que contam do zero até o valor ------------------------------- */
140
+
141
+ function contar(cena) {
142
+ Array.prototype.forEach.call(cena.querySelectorAll("b[data-contar]"), function (b) {
143
+ var alvo = parseFloat(b.getAttribute("data-contar"));
144
+ var molde = b.getAttribute("data-molde") || String(alvo);
145
+ var casas = (String(alvo).split(".")[1] || "").length;
146
+ var trecho = molde.match(/-?[\d.,]+/);
147
+ if (!trecho) return;
148
+ var inicio = performance.now(), dur = 1100;
149
+ (function passo(agora) {
150
+ var t = Math.min(1, (agora - inicio) / dur);
151
+ var suave = 1 - Math.pow(1 - t, 3);
152
+ var v = (alvo * suave).toFixed(casas);
153
+ b.textContent = molde.replace(trecho[0], String(v).replace(".", casas ? "," : ""));
154
+ if (t < 1) requestAnimationFrame(passo);
155
+ else b.textContent = molde;
156
+ })(inicio);
157
+ });
158
+ }
159
+
160
+ /* --------------------------------------------------------- reprodução -- */
161
+
162
+ function tocar() {
163
+ if (tocando || !cenas.length) return;
164
+ tocando = true;
165
+ t0 = performance.now() - decorrido * 1000;
166
+ if (trilha) { trilha.tocar(); }
167
+ laco();
168
+ atualizarBotao();
169
+ }
170
+
171
+ function pausar() {
172
+ tocando = false;
173
+ cancelAnimationFrame(quadro);
174
+ if (trilha) trilha.parar();
175
+ if (typeof Narracao !== "undefined") Narracao.parar();
176
+ if (vozGravada) { vozGravada.pause(); vozGravada = null; }
177
+ atualizarBotao();
178
+ }
179
+
180
+ function fim() {
181
+ pausar();
182
+ decorrido = total();
183
+ pintar();
184
+ aviso("fim", 1200);
185
+ if (gravador && gravador.state === "recording") setTimeout(pararGravacao, 600);
186
+ }
187
+
188
+ function laco() {
189
+ if (!tocando) return;
190
+ decorrido = (performance.now() - t0) / 1000;
191
+ var alvo = cenas.length - 1;
192
+ var acc = 0;
193
+ for (var i = 0; i < cenas.length; i++) {
194
+ acc += duracaoDe(i);
195
+ if (decorrido < acc) { alvo = i; break; }
196
+ }
197
+ if (decorrido >= total()) return fim();
198
+ if (alvo !== atual) ir(alvo, true);
199
+ pintar();
200
+ quadro = requestAnimationFrame(laco);
201
+ }
202
+
203
+ function buscar(segundos) {
204
+ decorrido = Math.max(0, Math.min(total() - 0.01, segundos));
205
+ var acc = 0, alvo = 0;
206
+ for (var i = 0; i < cenas.length; i++) {
207
+ acc += duracaoDe(i);
208
+ if (decorrido < acc) { alvo = i; break; }
209
+ alvo = i;
210
+ }
211
+ ir(alvo, true);
212
+ t0 = performance.now() - decorrido * 1000;
213
+ if (trilha && trilha.elemento) trilha.elemento.currentTime = (doc.trilha.inicio || 0) + decorrido;
214
+ pintar();
215
+ }
216
+
217
+ /* ---------------------------------------------------------- narração -- */
218
+
219
+ var vozGravada = null;
220
+
221
+ function narrar() {
222
+ if (typeof Narracao !== "undefined") Narracao.parar();
223
+ if (vozGravada) { vozGravada.pause(); vozGravada = null; }
224
+ if (mudo || doc.narracao === false) return;
225
+
226
+ var cena = cenas[atual] || {};
227
+
228
+ // voz gravada pelo usuário ganha da sintetizada, sempre
229
+ if (cena.narracaoAudio) {
230
+ vozGravada = new Audio(cena.narracaoAudio);
231
+ vozGravada.volume = (doc.voz && doc.voz.volume != null) ? doc.voz.volume : 1;
232
+ var p = vozGravada.play();
233
+ if (p && p.catch) p.catch(function () {});
234
+ return;
235
+ }
236
+
237
+ if (typeof Narracao === "undefined" || !Narracao.disponivel()) return;
238
+ if (cena.narracao) Narracao.falar(cena.narracao, doc.voz || {});
239
+ }
240
+
241
+ /* ----------------------------------------------------- apresentador --- */
242
+
243
+ var painel = null, cronometro = 0, tiquetaque = null;
244
+
245
+ function alternarApresentador() {
246
+ if (painel) {
247
+ painel.remove(); painel = null;
248
+ clearInterval(tiquetaque); tiquetaque = null;
249
+ ajustar();
250
+ return;
251
+ }
252
+ painel = document.createElement("div");
253
+ painel.className = "apresentador";
254
+ painel.innerHTML =
255
+ '<div><h4>Tempo</h4><div class="relogio" id="ap-relogio">00:00</div></div>' +
256
+ '<div><h4>Notas</h4><div class="corpo" id="ap-notas"></div></div>' +
257
+ '<div class="prox" id="ap-prox"></div>' +
258
+ '<div class="dica">→ avança · ← volta<br>S fecha · T zera o tempo<br>F tela cheia · P imprime</div>';
259
+ document.body.appendChild(painel);
260
+ tiquetaque = setInterval(function () {
261
+ cronometro++;
262
+ var r = document.getElementById("ap-relogio");
263
+ if (r) r.textContent = mmss(cronometro);
264
+ }, 1000);
265
+ pintarApresentador();
266
+ ajustar();
267
+ }
268
+
269
+ function pintarApresentador() {
270
+ if (!painel) return;
271
+ var notas = (cenas[atual] || {}).notas;
272
+ document.getElementById("ap-notas").innerHTML = notas
273
+ ? String(notas).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/\n/g, "<br>")
274
+ : '<span style="color:#5e6a77">— sem notas —</span>';
275
+ var prox = cenas[atual + 1];
276
+ var t = prox ? (prox.elementos || []).filter(function (e) {
277
+ return e.tipo === "texto" || e.tipo === "citacao";
278
+ })[0] : null;
279
+ document.getElementById("ap-prox").textContent = prox
280
+ ? "Próxima: " + ((t && t.texto) || "cena " + (atual + 2)).split("\n")[0]
281
+ : "Última cena";
282
+ }
283
+
284
+ /* ------------------------------------------------------------ trilha -- */
285
+
286
+ function prepararTrilha() {
287
+ var cfg = doc.trilha;
288
+ if (!cfg || (!cfg.src && cfg.tipo !== "gerada")) return;
289
+ if (typeof criarTrilha !== "function") return;
290
+ trilha = criarTrilha(cfg, function (msg) { aviso(msg, 3000); });
291
+ }
292
+
293
+ /* --------------------------------------------------------- transporte -- */
294
+
295
+ function montarTransporte() {
296
+ transporte = document.createElement("div");
297
+ transporte.className = "transporte";
298
+ transporte.innerHTML =
299
+ '<button class="tp-btn" id="tp-tocar" title="Tocar / pausar (K)"></button>' +
300
+ '<div class="tp-tempo" id="tp-agora">00:00</div>' +
301
+ '<div class="tp-barra" id="tp-barra"><div class="tp-cheio" id="tp-cheio"></div>' +
302
+ '<div class="tp-marcas" id="tp-marcas"></div></div>' +
303
+ '<div class="tp-tempo" id="tp-total">00:00</div>' +
304
+ '<button class="tp-btn pequeno" id="tp-som" title="Mudo (M)"></button>' +
305
+ '<input class="tp-vol" id="tp-vol" type="range" min="0" max="100" value="70">' +
306
+ '<button class="tp-btn texto" id="tp-gravar" title="Gravar em .webm">Gravar</button>';
307
+ document.body.appendChild(transporte);
308
+
309
+ document.getElementById("tp-tocar").onclick = function () { tocando ? pausar() : tocar(); };
310
+ document.getElementById("tp-som").onclick = alternarMudo;
311
+ document.getElementById("tp-gravar").onclick = alternarGravacao;
312
+ document.getElementById("tp-vol").oninput = function () {
313
+ if (trilha) trilha.volume(this.value / 100);
314
+ };
315
+
316
+ var barra = document.getElementById("tp-barra");
317
+ barra.onclick = function (ev) {
318
+ var r = barra.getBoundingClientRect();
319
+ buscar(((ev.clientX - r.left) / r.width) * total());
320
+ };
321
+
322
+ var marcas = document.getElementById("tp-marcas");
323
+ cenas.forEach(function (_, i) {
324
+ if (!i) return;
325
+ var m = document.createElement("i");
326
+ m.style.left = (inicioDe(i) / total() * 100) + "%";
327
+ marcas.appendChild(m);
328
+ });
329
+
330
+ document.getElementById("tp-total").textContent = mmss(total());
331
+ atualizarBotao();
332
+ }
333
+
334
+ function atualizarBotao() {
335
+ var b = document.getElementById("tp-tocar");
336
+ if (b) b.innerHTML = svgIcone(tocando ? "pausa" : "play");
337
+ var s = document.getElementById("tp-som");
338
+ if (s) s.innerHTML = svgIcone("olho");
339
+ }
340
+
341
+ var mudo = false;
342
+ function alternarMudo() {
343
+ mudo = !mudo;
344
+ if (trilha) trilha.volume(mudo ? 0 : (document.getElementById("tp-vol").value / 100));
345
+ aviso(mudo ? "som desligado" : "som ligado", 900);
346
+ }
347
+
348
+ function pintar() {
349
+ var c = document.getElementById("tp-cheio");
350
+ if (!c) return;
351
+ c.style.width = (decorrido / total() * 100) + "%";
352
+ document.getElementById("tp-agora").textContent = mmss(decorrido);
353
+ }
354
+
355
+ /* ---------------------------------------------------------- gravação --- */
356
+
357
+ function alternarGravacao() {
358
+ if (gravador && gravador.state === "recording") return pararGravacao();
359
+ iniciarGravacao();
360
+ }
361
+
362
+ function iniciarGravacao() {
363
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
364
+ return aviso("este navegador não grava a tela", 3000);
365
+ }
366
+ // audio:true deixa o usuário marcar "compartilhar áudio da aba" — é assim
367
+ // que a narração falada entra na gravação
368
+ navigator.mediaDevices.getDisplayMedia({
369
+ video: { frameRate: 30 }, audio: true,
370
+ }).then(function (fluxo) {
371
+ fluxoTela = fluxo;
372
+ var faixas = fluxo.getVideoTracks();
373
+ fluxo.getAudioTracks().forEach(function (f) { faixas.push(f); });
374
+ if (trilha && trilha.fluxo) {
375
+ trilha.fluxo.getAudioTracks().forEach(function (f) { faixas.push(f); });
376
+ }
377
+ var misto = new MediaStream(faixas);
378
+ pedacos = [];
379
+ var tipo = MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus")
380
+ ? "video/webm;codecs=vp9,opus" : "video/webm";
381
+ gravador = new MediaRecorder(misto, { mimeType: tipo, videoBitsPerSecond: 8000000 });
382
+ gravador.ondataavailable = function (ev) { if (ev.data.size) pedacos.push(ev.data); };
383
+ gravador.onstop = baixarGravacao;
384
+ fluxo.getVideoTracks()[0].onended = pararGravacao;
385
+
386
+ gravador.start();
387
+ document.getElementById("tp-gravar").classList.add("gravando");
388
+ document.getElementById("tp-gravar").textContent = "Parar";
389
+ buscar(0);
390
+ tocar();
391
+ aviso("gravando — o vídeo para sozinho no fim", 3000);
392
+ }).catch(function () { aviso("gravação cancelada", 1500); });
393
+ }
394
+
395
+ function pararGravacao() {
396
+ if (gravador && gravador.state === "recording") gravador.stop();
397
+ if (fluxoTela) fluxoTela.getTracks().forEach(function (f) { f.stop(); });
398
+ var b = document.getElementById("tp-gravar");
399
+ if (b) { b.classList.remove("gravando"); b.textContent = "Gravar"; }
400
+ }
401
+
402
+ function baixarGravacao() {
403
+ var blob = new Blob(pedacos, { type: "video/webm" });
404
+ var url = URL.createObjectURL(blob);
405
+ var a = document.createElement("a");
406
+ a.href = url;
407
+ a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() + ".webm";
408
+ a.click();
409
+ setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
410
+ aviso("vídeo salvo em .webm", 2600);
411
+ }
412
+
413
+ /* -------------------------------------------------------------- print -- */
414
+
415
+ function imprimirTudo() {
416
+ palco.style.cssText = "position:static;transform:none;width:auto;height:auto";
417
+ Array.prototype.forEach.call(palco.children, function (n) {
418
+ n.style.display = "block";
419
+ n.style.position = "relative";
420
+ n.style.inset = "auto";
421
+ n.classList.remove("animar");
422
+ });
423
+ var p = document.getElementById("progresso");
424
+ if (p) p.remove();
425
+ if (transporte) transporte.remove();
426
+ window.print();
427
+ }
428
+
429
+ /* ------------------------------------------------------------ eventos -- */
430
+
431
+ window.addEventListener("resize", ajustar);
432
+
433
+ document.addEventListener("keydown", function (ev) {
434
+ if (ev.metaKey || ev.ctrlKey || /input|textarea/i.test(ev.target.tagName)) return;
435
+ switch (ev.key) {
436
+ case "ArrowRight": case " ": case "PageDown":
437
+ ev.preventDefault(); if (ehVideo) pausar(); proxima(); break;
438
+ case "ArrowLeft": case "PageUp":
439
+ ev.preventDefault(); if (ehVideo) pausar(); anterior(); break;
440
+ case "Home": pausar(); ir(0); break;
441
+ case "End": pausar(); ir(cenas.length - 1); break;
442
+ case "k": case "K": tocando ? pausar() : tocar(); break;
443
+ case "m": case "M": alternarMudo(); break;
444
+ case "s": case "S": alternarApresentador(); break;
445
+ case "t": case "T": cronometro = 0; break;
446
+ case "n": case "N":
447
+ if (typeof Narracao !== "undefined" && Narracao.disponivel()) {
448
+ Narracao.falando() ? Narracao.parar() : narrar();
449
+ }
450
+ break;
451
+ case "f": case "F":
452
+ if (document.fullscreenElement) document.exitFullscreen();
453
+ else document.documentElement.requestFullscreen();
454
+ break;
455
+ case "p": case "P": window.print(); break;
456
+ case "Escape": pausar(); pararGravacao(); break;
457
+ }
458
+ });
459
+
460
+ var tx = null;
461
+ document.addEventListener("touchstart", function (e) { tx = e.touches[0].clientX; }, { passive: true });
462
+ document.addEventListener("touchend", function (e) {
463
+ if (tx == null) return;
464
+ var d = e.changedTouches[0].clientX - tx;
465
+ if (Math.abs(d) > 50) { pausar(); d < 0 ? proxima() : anterior(); }
466
+ tx = null;
467
+ }, { passive: true });
468
+
469
+ document.addEventListener("click", function (e) {
470
+ if (!cenas.length || e.target.closest("a,button,input,.transporte")) return;
471
+ if (ehVideo) { tocando ? pausar() : tocar(); return; }
472
+ e.clientX < window.innerWidth * 0.3 ? anterior() : proxima();
473
+ });
474
+
475
+ window.addEventListener("message", function (e) {
476
+ var m = e.data || {};
477
+ if (m.tipo === "arquivo") montar(m.arquivo);
478
+ if (m.tipo === "ir") { pausar(); ir(m.n); }
479
+ });
480
+
481
+ /* --------------------------------------------------------------- carga */
482
+
483
+ var embutido = document.getElementById("dados");
484
+ if (embutido) {
485
+ montar(JSON.parse(embutido.textContent));
486
+ } else {
487
+ var id = location.pathname.split("/").pop();
488
+ // lido antes de montar: ir() reescreve o hash e não deve pausar a reprodução
489
+ var inicio = parseInt((location.hash.match(/^#\/(\d+)/) || [])[1], 10);
490
+ fetch("/api/arquivos/" + id)
491
+ .then(function (r) { return r.json(); })
492
+ .then(function (a) {
493
+ if (a.erro) { palco.innerHTML = '<div style="color:#888;padding:40px">Arquivo não encontrado.</div>'; return; }
494
+ montar(a);
495
+ if (!isNaN(inicio) && inicio > 1) { pausar(); ir(inicio - 1); }
496
+ });
497
+ }
498
+
499
+ window.Player = {
500
+ ir: function (n) { ir(n - 1); }, proxima: proxima, anterior: anterior,
501
+ tocar: tocar, parar: pausar, buscar: buscar, gravar: alternarGravacao,
502
+ get cena() { return atual + 1; }, get total() { return cenas.length; },
503
+ };
504
+ })();