glideshow 0.1.2__py3-none-any.whl
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.
- glideshow/__init__.py +15 -0
- glideshow/__main__.py +3 -0
- glideshow/agent.py +681 -0
- glideshow/build.py +54 -0
- glideshow/cli.py +320 -0
- glideshow/comments.py +35 -0
- glideshow/decks.py +230 -0
- glideshow/docs/agents.md +223 -0
- glideshow/docs/deck-format.md +125 -0
- glideshow/editor/.assetsignore +5 -0
- glideshow/editor/apple-touch-icon.png +0 -0
- glideshow/editor/data/font-metrics.json +1 -0
- glideshow/editor/data/fonts.json +1 -0
- glideshow/editor/data/icons.json +1 -0
- glideshow/editor/data/symbols.js +42 -0
- glideshow/editor/data/themes.js +15 -0
- glideshow/editor/deck/base.css +92 -0
- glideshow/editor/deck/present.js +222 -0
- glideshow/editor/deck/shapes.js +377 -0
- glideshow/editor/deck/template.html +37 -0
- glideshow/editor/editor.css +657 -0
- glideshow/editor/glideshow-1024.png +0 -0
- glideshow/editor/glideshow.svg +10 -0
- glideshow/editor/globals.d.ts +21 -0
- glideshow/editor/home.html +358 -0
- glideshow/editor/index.html +386 -0
- glideshow/editor/js/arrange.js +122 -0
- glideshow/editor/js/boot.js +66 -0
- glideshow/editor/js/boxes.js +25 -0
- glideshow/editor/js/comments.js +85 -0
- glideshow/editor/js/context-menus.js +93 -0
- glideshow/editor/js/dialogs.js +168 -0
- glideshow/editor/js/dock.js +22 -0
- glideshow/editor/js/draw.js +107 -0
- glideshow/editor/js/file.js +318 -0
- glideshow/editor/js/find.js +72 -0
- glideshow/editor/js/format-panel.js +101 -0
- glideshow/editor/js/geometry.js +153 -0
- glideshow/editor/js/guides.js +37 -0
- glideshow/editor/js/insert.js +73 -0
- glideshow/editor/js/keyboard.js +256 -0
- glideshow/editor/js/live.js +48 -0
- glideshow/editor/js/menubar.js +290 -0
- glideshow/editor/js/menus.js +93 -0
- glideshow/editor/js/motion.js +148 -0
- glideshow/editor/js/notes.js +63 -0
- glideshow/editor/js/objects.js +202 -0
- glideshow/editor/js/people.js +73 -0
- glideshow/editor/js/phone-comments.js +43 -0
- glideshow/editor/js/phone-edit.js +253 -0
- glideshow/editor/js/phone-home.js +51 -0
- glideshow/editor/js/phone-mode.js +73 -0
- glideshow/editor/js/phone-sheets.js +153 -0
- glideshow/editor/js/phone-touch.js +145 -0
- glideshow/editor/js/phone-view.js +150 -0
- glideshow/editor/js/pointer.js +407 -0
- glideshow/editor/js/render.js +353 -0
- glideshow/editor/js/shortcuts.js +67 -0
- glideshow/editor/js/slides.js +120 -0
- glideshow/editor/js/spacing.js +23 -0
- glideshow/editor/js/state.js +58 -0
- glideshow/editor/js/storage.js +250 -0
- glideshow/editor/js/text.js +366 -0
- glideshow/editor/js/theme-toggle.js +41 -0
- glideshow/editor/js/theme.js +173 -0
- glideshow/editor/js/toolbar-groups.js +109 -0
- glideshow/editor/js/toolbar.js +372 -0
- glideshow/editor/lib/browser-db.js +47 -0
- glideshow/editor/lib/library.js +612 -0
- glideshow/editor/lib/merge.js +174 -0
- glideshow/editor/lib/move-local.js +51 -0
- glideshow/editor/lib/onefile.js +108 -0
- glideshow/editor/lib/store-browser.js +75 -0
- glideshow/editor/lib/store-folder.js +109 -0
- glideshow/editor/local/sw.js +66 -0
- glideshow/editor/local/tsconfig.json +5 -0
- glideshow/editor/looks/glider.svg +9 -0
- glideshow/editor/looks/thermal-phone.css +144 -0
- glideshow/editor/looks/thermal.css +268 -0
- glideshow/editor/manifest.json +23 -0
- glideshow/editor/phone.css +345 -0
- glideshow/editor/share/share.css +124 -0
- glideshow/editor/share/share.js +256 -0
- glideshow/editor/share/speaker.html +88 -0
- glideshow/editor/tsconfig.json +6 -0
- glideshow/figure.py +33 -0
- glideshow/fonts.py +63 -0
- glideshow/gslides.py +1065 -0
- glideshow/history.py +210 -0
- glideshow/merge.py +210 -0
- glideshow/osutil.py +44 -0
- glideshow/pptx_export.py +207 -0
- glideshow/pptx_import.py +115 -0
- glideshow/publish.py +106 -0
- glideshow/render.py +288 -0
- glideshow/server.py +549 -0
- glideshow/service.py +65 -0
- glideshow/skills/glideshow/SKILL.md +85 -0
- glideshow-0.1.2.dist-info/METADATA +69 -0
- glideshow-0.1.2.dist-info/RECORD +103 -0
- glideshow-0.1.2.dist-info/WHEEL +4 -0
- glideshow-0.1.2.dist-info/entry_points.txt +2 -0
- glideshow-0.1.2.dist-info/licenses/LICENSE +662 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* Slide editor. The deck is deck.html: each <section> is a 1920×1080 slide whose children are
|
|
3
|
+
absolutely positioned .box elements. The editor owns box geometry and styling; anything
|
|
4
|
+
inside a box's .content is free HTML. Saves go through /api/save with the file's mtime, so an
|
|
5
|
+
agent editing deck.html at the same time is never silently overwritten. */
|
|
6
|
+
|
|
7
|
+
let W = 1920, H = 1080; // the page size: data-size on the deck's .slides (File → Page setup)
|
|
8
|
+
const THUMB_W = 158, CLIP = 'slides-boxes:', SLIDE_CLIP = 'glideshow-slides:';
|
|
9
|
+
/** @type {(s: string) => any} */
|
|
10
|
+
const $ = s => document.querySelector(s); // any: an element of whatever kind the selector names
|
|
11
|
+
// The server takes a POST only with X-Glideshow (other websites can't send it; see server.py `allowed`).
|
|
12
|
+
const _fetch = window.fetch.bind(window);
|
|
13
|
+
window.fetch = (u, o = {}) => _fetch(u, (o.method || 'GET').toUpperCase() === 'POST' ? { ...o, headers: { 'X-Glideshow': '1', ...(o.headers || {}) } } : o);
|
|
14
|
+
|
|
15
|
+
const stage = $('#ed-stage'), frame = $('#ed-frame'), canvas = $('#ed-canvas'), ui = $('#ed-ui'), guides = $('#ed-guides'), thumbs = $('#ed-thumbs');
|
|
16
|
+
|
|
17
|
+
let BASE_CSS = '', SHAPES_JS = '', PRESENT_JS = '';
|
|
18
|
+
let deckDoc = null, slides = [], cur = 0, sel = new Set(), editing = null, editStart = '', editFloor = 0, lastRange = null;
|
|
19
|
+
let mtime = null, scale = 1, busy = false, saving = false, saveTimer = null, pendingReload = false;
|
|
20
|
+
let hist = [], hptr = -1;
|
|
21
|
+
let remoteBy = ''; // who made the change coming in from elsewhere, when the live link says (glideshow.org; boot.js remoteVersion)
|
|
22
|
+
|
|
23
|
+
const uid = () => 'b' + Math.random().toString(36).slice(2, 8);
|
|
24
|
+
const boxes = (s = slides[cur]) => [...s.children].filter(e => e.classList.contains('box'));
|
|
25
|
+
const isGroup = b => b.classList.contains('group');
|
|
26
|
+
/* A group has no text of its own: text commands on it write to a detached stand-in, i.e. do nothing. */
|
|
27
|
+
const NO_CONTENT = document.createElement('div');
|
|
28
|
+
const content = b => b.querySelector(':scope > .content') || (isGroup(b) ? NO_CONTENT : null);
|
|
29
|
+
const kids = g => [...g.children].filter(e => e.classList.contains('box')); // a group's member boxes
|
|
30
|
+
/* The box a click lands on: a group as a whole, not one of its members (as Slides), unless the click is
|
|
31
|
+
inside the group the user has entered by double-clicking (`scope`), where members are picked one by one. */
|
|
32
|
+
let scope = null;
|
|
33
|
+
const parentBox = b => b?.parentElement?.closest('#ed-canvas .box') || null;
|
|
34
|
+
const topBox = el => { let b = el?.closest?.('#ed-canvas .box'); while (parentBox(b) && parentBox(b) !== scope) b = parentBox(b); return b; };
|
|
35
|
+
/* A group always hugs its members, as Slides: after members move or resize, its bounds follow them
|
|
36
|
+
(members are shifted so the group's top-left stays their origin); a group left empty is removed. */
|
|
37
|
+
function fitGroups(s = slides[cur]) {
|
|
38
|
+
for (const g of [...s.querySelectorAll('.box.group')].reverse()) { // innermost first
|
|
39
|
+
const ks = kids(g); if (!ks.length) { if (scope === g) scope = null; g.remove(); continue; }
|
|
40
|
+
const bb = bbox(ks.map(aabb)), o = geo(g);
|
|
41
|
+
if (Math.abs(bb.x) < .05 && Math.abs(bb.y) < .05 && Math.abs(bb.w - o.w) < .05 && Math.abs(bb.h - o.h) < .05) continue;
|
|
42
|
+
ks.forEach(k => { const q = geo(k); setGeo(k, { ...q, x: q.x - bb.x, y: q.y - bb.y }); });
|
|
43
|
+
Object.assign(g.style, { left: r1(o.x + bb.x) + 'px', top: r1(o.y + bb.y) + 'px', width: r1(bb.w) + 'px', height: r1(bb.h) + 'px' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const isLine = b => /^(line|arrowline|elbow|curve)$/.test(b.dataset.shape || '');
|
|
47
|
+
const isMedia = b => b.classList.contains('image') || b.classList.contains('video') || b.classList.contains('embed') || b.classList.contains('live'); // pictures, videos, web embeds: no text, aspect kept
|
|
48
|
+
const isStroke = b => isLine(b) || b.dataset.shape === 'path'; // lines and freehand paths: a stroke, no fill or text
|
|
49
|
+
const r1 = v => Math.round(v * 10) / 10;
|
|
50
|
+
const div = (cls) => { const d = document.createElement('div'); d.className = cls; return d; };
|
|
51
|
+
/* Status next to the title, worded as Slides. `icon` is a Material Symbols name (cloud states) or none. */
|
|
52
|
+
function setStatus(t, icon = '') {
|
|
53
|
+
if (!t && document.body.classList.contains('draft')) [t, icon] = ['Not saved: add a title', 'cloud_off']; // a draft keeps saying so
|
|
54
|
+
const el = $('#ed-status');
|
|
55
|
+
|
|
56
|
+
el.innerHTML = '<span class="icon"></span><span class="status-text"></span>';
|
|
57
|
+
el.firstChild.textContent = icon; el.lastChild.textContent = t;
|
|
58
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/* ---------- load / save / history ---------- */
|
|
2
|
+
|
|
3
|
+
function parseSlides(doc) { // attached lines are fitted to their shapes as each slide comes in (shapes.js), as the viewer does
|
|
4
|
+
const take = el => { const n = document.importNode(el, true); window.attachConnectors?.(n); return n; };
|
|
5
|
+
return [...doc.querySelectorAll('.slides > section')].flatMap(s => {
|
|
6
|
+
const kids = [...s.children].filter(c => c.tagName === 'SECTION');
|
|
7
|
+
if (!kids.length) return [take(s)];
|
|
8
|
+
const id = uid(); return kids.map(k => { const n = take(k); n.dataset.edStack = id; return n; });
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/* Each slide's id (data-sid on its <section>, kept in the deck), so edits made in two places merge slide by slide even
|
|
12
|
+
when slides were added, removed or moved (mergeSlides). A slide that comes without one (an older deck, an agent's)
|
|
13
|
+
gets one made from its place and content, the same in every editor that opens the same file; a new slide, or a
|
|
14
|
+
copy of one, gets a fresh one. */
|
|
15
|
+
const hashStr = t => { let h = 2166136261; for (let i = 0; i < t.length; i++) h = Math.imul(h ^ t.charCodeAt(i), 16777619); return (h >>> 0).toString(36); };
|
|
16
|
+
function ensureSids(fromFile = false) {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
slides.forEach((s, i) => {
|
|
19
|
+
let id = s.dataset.sid;
|
|
20
|
+
if (!id) id = fromFile ? 's' + hashStr(i + '\u0001' + s.outerHTML) : 's' + Math.random().toString(36).slice(2, 9);
|
|
21
|
+
while (seen.has(id)) id = 's' + Math.random().toString(36).slice(2, 9); // a copy keeps its original's id until here
|
|
22
|
+
if (s.dataset.sid !== id) s.dataset.sid = id;
|
|
23
|
+
seen.add(id);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const sidOf = h => (h.match(/^<section\b[^>]*\sdata-sid="([^"]+)"/) || [])[1] || '';
|
|
27
|
+
/* Three versions of the slides (as sectionHTML lists): `base`, the last one this editor saved or loaded; `mine`, what it
|
|
28
|
+
has now; `theirs`, what was saved elsewhere meanwhile. Merged slide by slide, and box by box in a slide both changed
|
|
29
|
+
(lib/merge.js, which a library online merges with too): { html, both }, both being the slides both changed. */
|
|
30
|
+
const mergeSlides = (base, mine, theirs) => /** @type {{ html: string[], both: number[], bothIds: string[] }} */ (glideMerge.mergeSlides(base, mine, theirs));
|
|
31
|
+
/* Where a merge kept this editor's version of something the other side changed too (a box, a slide's settings), theirs
|
|
32
|
+
is only in the version history: said in a bar over the slide that stays until dismissed (a status would be gone with
|
|
33
|
+
the save that follows), the slides named and lit in the filmstrip, Version history a click away. */
|
|
34
|
+
function mergeNote(ids, by = '') {
|
|
35
|
+
const at = ids.map(id => slides.findIndex(s => s.dataset.sid === id)).filter(i => i >= 0);
|
|
36
|
+
if (!at.length || !$('#ed-merge-bar')) return;
|
|
37
|
+
const n = at.map(i => i + 1), who = by || 'an edit made elsewhere';
|
|
38
|
+
$('#ed-merge-what').textContent = `You and ${who} changed the same thing on slide${n.length > 1 ? 's' : ''} ${n.join(', ')}. Yours was kept; theirs is in Version history.`;
|
|
39
|
+
$('#ed-merge-bar').hidden = false;
|
|
40
|
+
if (!at.includes(cur)) go(at[0]);
|
|
41
|
+
requestAnimationFrame(() => { for (const i of at) thumbs.children[i]?.animate([{ boxShadow: '0 0 0 3px #d93025' }, { boxShadow: '0 0 0 3px #d93025', offset: .8 }, { boxShadow: '0 0 0 0 transparent' }], { duration: 4000 }); });
|
|
42
|
+
}
|
|
43
|
+
/* Saved elsewhere while this editor has changes of its own: take theirs, merge mine in (mergeSlides), save the result. */
|
|
44
|
+
async function mergeIn() {
|
|
45
|
+
const base = savedSnap, mine = slides.map(s => sectionHTML(s)), sid = slides[cur]?.dataset.sid, myTitle = $('#ed-title').value, baseTitle = deckDoc?.title;
|
|
46
|
+
await load(true, true);
|
|
47
|
+
const theirs = slides.map(s => sectionHTML(s)), { html, both, bothIds } = mergeSlides(base || [], mine, theirs);
|
|
48
|
+
if (myTitle && myTitle !== baseTitle && $('#ed-title').value === baseTitle) $('#ed-title').value = myTitle; // a title changed here, not there
|
|
49
|
+
if (html.join('\0') !== theirs.join('\0') || $('#ed-title').value !== deckDoc.title) {
|
|
50
|
+
restore({ html, cur: Math.max(0, html.findIndex(h => sidOf(h) === sid)) }); pushHistory();
|
|
51
|
+
}
|
|
52
|
+
const by = remoteBy;
|
|
53
|
+
setStatus(`Merged your changes with an edit ${by ? 'by ' + by : 'made elsewhere'}`, 'merge');
|
|
54
|
+
if (both.length) mergeNote(bothIds, by);
|
|
55
|
+
}
|
|
56
|
+
/* On glideshow.org a change someone else saved is not a step of this editor's undo: it goes into every state the undo
|
|
57
|
+
history holds (each merged with it, as mergeSlides), so ⌘Z takes back only this editor's own changes and keeps
|
|
58
|
+
theirs. A state that would undo theirs (one of this editor's later steps changed a slide they changed too, or removed)
|
|
59
|
+
is marked, and undo stops there and says so rather than overwrite them. On the Mac a change from disk stays a step
|
|
60
|
+
of its own, so an agent's edit can still be taken back with ⌘Z. */
|
|
61
|
+
const rebaseUndo = () => !!window.GLIDESHOW_CLOUD && !(/** @type {any} */ (window.GLIDESHOW_CLOUD).local);
|
|
62
|
+
function rebaseHistory(base, theirs, by = '') {
|
|
63
|
+
const same = (a, b) => a === b || (a !== undefined && b !== undefined && normHTML(a) === normHTML(b));
|
|
64
|
+
const B = new Map(base.map(h => [sidOf(h), h])), T = new Map(theirs.map(h => [sidOf(h), h]));
|
|
65
|
+
const touched = [...B.keys()].filter(id => !same(B.get(id), T.get(id))); // slides they changed or removed
|
|
66
|
+
hist.forEach((st, i) => {
|
|
67
|
+
const M = new Map(st.html.map(h => [sidOf(h), h])), clash = i === hptr ? [] : touched.filter(id => !same(M.get(id), B.get(id)));
|
|
68
|
+
st.html = mergeSlides(base, st.html, theirs).html;
|
|
69
|
+
if (clash.length) { st.clash = [...new Set([...(st.clash || []), ...clash])]; st.by = by || st.by || ''; }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/* Undo or redo to a state that would overwrite someone else's change: not done, and said why. */
|
|
73
|
+
function overwrites(to) {
|
|
74
|
+
if (!to?.clash?.length) return false;
|
|
75
|
+
const at = to.clash.map(id => slides.findIndex(s => s.dataset.sid === id) + 1).filter(n => n > 0);
|
|
76
|
+
setStatus(`Can't undo past this: ${to.by || 'someone else'} has since changed ${at.length ? `slide${at.length > 1 ? 's' : ''} ${at.join(', ')}` : 'a slide it would change'}`, 'block');
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const unsaved = () => !!savedSnap && slides.map(s => sectionHTML(s)).join('\0') !== savedSnap.join('\0');
|
|
80
|
+
|
|
81
|
+
async function load(keep, outside = false) {
|
|
82
|
+
const was = outside ? slides.map(s => sectionHTML(s)).join('\0') : '';
|
|
83
|
+
const j = await (await fetch('/api/deck', { cache: 'no-store' })).json();
|
|
84
|
+
// changed here while the deck was on its way: not replaced; the change's own save meets the newer version and merges with it
|
|
85
|
+
if (outside && slides.map(s => sectionHTML(s)).join('\0') !== was) { pendingReload = true; return; }
|
|
86
|
+
const ids = new Set([...sel].map(b => b.id)), before = outside ? slides.map(s => sectionHTML(s)) : null;
|
|
87
|
+
mtime = j.mtime; pendingReload = false; editing = null;
|
|
88
|
+
if (outside && j.files && fileVers) { // online: only the files that changed are fetched again (render.js fresh)
|
|
89
|
+
for (const [p, ver] of Object.entries(j.files)) if (fileVers[p] && fileVers[p] !== ver) {
|
|
90
|
+
fileGen.set(p, (fileGen.get(p) || 0) + 1);
|
|
91
|
+
for (const k of [...gifStills.keys()]) if (fileOf(k) === p) gifStills.delete(k);
|
|
92
|
+
for (const k of [...videoStills.keys()]) if (fileOf(k) === p) videoStills.delete(k);
|
|
93
|
+
}
|
|
94
|
+
thumbEpoch++;
|
|
95
|
+
} else if (outside) { gifStills.clear(); videoStills.clear(); imgGen++; thumbEpoch++; } // pictures and videos rewritten beside the deck are fetched again (render.js)
|
|
96
|
+
if (j.files) fileVers = j.files;
|
|
97
|
+
deckDoc = new DOMParser().parseFromString(j.html, 'text/html'); savedCode = deckCode(j.html); deckId = j.id || deckId;
|
|
98
|
+
applySize();
|
|
99
|
+
$('#ed-deck-theme-live').textContent = deckDoc.querySelector('#deck-theme')?.textContent || '';
|
|
100
|
+
syncEditorFonts();
|
|
101
|
+
// reveal.js stacks (a <section> of <section>s) become consecutive slides marked data-ed-stack (editor only; regrouped on save)
|
|
102
|
+
slides = parseSlides(deckDoc);
|
|
103
|
+
if (!slides.length) slides.push(blankSlide());
|
|
104
|
+
slides.forEach(s => s.querySelectorAll('.box').forEach(b => { if (!b.id) b.id = uid(); }));
|
|
105
|
+
ensureSids(true);
|
|
106
|
+
cur = keep ? Math.min(cur, slides.length - 1) : 0;
|
|
107
|
+
sel = new Set(keep ? boxes().filter(b => ids.has(b.id)) : []);
|
|
108
|
+
$('#ed-title').value = deckDoc.title || 'Untitled deck';
|
|
109
|
+
$('#ed-date').value = deckDoc.head.querySelector('meta[name="date"]')?.content || '';
|
|
110
|
+
|
|
111
|
+
isDraft = !!j.draft; // untitled and not saved to the library until it has a title (server: decks.py new_draft)
|
|
112
|
+
document.body.classList.toggle('draft', isDraft);
|
|
113
|
+
if (isDraft) { $('#ed-title').value = ''; $('#ed-title').placeholder = 'Untitled presentation'; draftStatus(); }
|
|
114
|
+
render(); drawGuides();
|
|
115
|
+
// a change from disk is one undo step: ⌘Z takes it back (and saves that); on glideshow.org it goes into the undo history instead
|
|
116
|
+
if (outside && rebaseUndo() && savedSnap && hist.length) rebaseHistory(savedSnap, slides.map(s => sectionHTML(s)), remoteBy); else pushHistory();
|
|
117
|
+
savedSnap = slides.map(s => sectionHTML(s));
|
|
118
|
+
if (before) showOutsideChanges(before);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cleanBox(b) {
|
|
122
|
+
const c = b.cloneNode(true);
|
|
123
|
+
c.querySelectorAll('svg.shape').forEach(e => e.remove());
|
|
124
|
+
unrenderMath(c); // the deck keeps the equation source
|
|
125
|
+
c.querySelectorAll('[contenteditable], [spellcheck]').forEach(e => { e.removeAttribute('contenteditable'); e.removeAttribute('spellcheck'); });
|
|
126
|
+
[c, ...c.querySelectorAll('.box')].forEach(e => ['--shx', '--shy', '--shb', '--shc', '--fit', '--ct', '--cr', '--cb', '--cl', '--tx-l', '--tx-t', '--tx-r', '--tx-b'].forEach(k => e.style.removeProperty(k))); // derived; shapes.js recomputes them
|
|
127
|
+
c.querySelectorAll('[data-ph]').forEach(e => e.removeAttribute('data-ph')); // the editor's copy of the placeholder prompt
|
|
128
|
+
c.querySelectorAll('[src*="gsv="], [poster*="gsv="]').forEach(e => ['src', 'poster'].forEach(a => { const v = e.getAttribute(a); if (v) e.setAttribute(a, plainSrc(v)); })); // the editor's fetch-again mark (render.js)
|
|
129
|
+
c.querySelectorAll('video[data-ed-poster]').forEach(v => { v.removeAttribute('poster'); v.removeAttribute('data-ed-poster'); }); // the editor's first frame for a video without a poster
|
|
130
|
+
c.classList.remove('on'); // stray toolbar state that older editors leaked onto boxes with data-valign
|
|
131
|
+
if (!c.getAttribute('style')?.trim()) c.removeAttribute('style');
|
|
132
|
+
return c;
|
|
133
|
+
}
|
|
134
|
+
function sectionHTML(s, save) {
|
|
135
|
+
const shell = s.cloneNode(false);
|
|
136
|
+
if (save) shell.removeAttribute('data-ed-stack');
|
|
137
|
+
const open = shell.outerHTML.slice(0, -'</section>'.length);
|
|
138
|
+
const inner = [...s.children].map(e => ' ' + (e.classList.contains('box') ? cleanBox(e) : e).outerHTML).join('\n');
|
|
139
|
+
return `${open}\n${inner}\n</section>`;
|
|
140
|
+
}
|
|
141
|
+
function deckHTML() {
|
|
142
|
+
const d = deckDoc.cloneNode(true);
|
|
143
|
+
d.title = $('#ed-title').value.trim() || 'Untitled deck';
|
|
144
|
+
let meta = d.head.querySelector('meta[name="date"]'); // the deck's date: <meta name="date" content="YYYY-MM-DD">
|
|
145
|
+
if ($('#ed-date').value) { if (!meta) { meta = d.createElement('meta'); meta.name = 'date'; d.head.querySelector('title').after(meta); } meta.content = $('#ed-date').value; }
|
|
146
|
+
else meta?.remove();
|
|
147
|
+
const put = (id, tag, text, parent) => {
|
|
148
|
+
let e = d.getElementById(id);
|
|
149
|
+
if (!e) { e = d.createElement(tag); e.id = id; parent.append(e); }
|
|
150
|
+
e.textContent = '\n' + text.trim() + '\n';
|
|
151
|
+
};
|
|
152
|
+
put('deck-base', 'style', BASE_CSS, d.head);
|
|
153
|
+
put('deck-shapes', 'script', SHAPES_JS, d.body);
|
|
154
|
+
put('deck-present', 'script', PRESENT_JS, d.body);
|
|
155
|
+
d.querySelector('.slides').innerHTML = '\n' + stacks(themeStash?.slides).map(g => g.length > 1
|
|
156
|
+
? '<section>\n' + g.map(s => sectionHTML(s, true)).join('\n\n') + '\n</section>' : sectionHTML(g[0], true)).join('\n\n') + '\n';
|
|
157
|
+
return '<!doctype html>\n' + d.documentElement.outerHTML + '\n';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* The slideshow's own code a deck carries (base.css, shapes.js, present.js: deckHTML writes this editor's copies on every
|
|
161
|
+
save), as last saved or loaded. A deck last saved by an older editor, or written by an agent since, runs that older
|
|
162
|
+
code when presented: a slideshow from the editor saves it first when it differs (arrange.js present), so a fix to the
|
|
163
|
+
slideshow (2026-10-22: videos left paused coming back to their slide) reaches it at once. */
|
|
164
|
+
let savedCode = '', deckId = ''; // deckId: this deck's files are also /library/<id>/… (slides copied to another deck, keyboard.js)
|
|
165
|
+
const deckCode = (/** @type {string} */ html) => ['deck-base', 'deck-shapes', 'deck-present']
|
|
166
|
+
.map(id => (html.match(new RegExp(`<(?:style|script) id="${id}">([\\s\\S]*?)</(?:style|script)>`)) || [])[1]?.trim() || '').join('\u0000');
|
|
167
|
+
const currentCode = () => [BASE_CSS, SHAPES_JS, PRESENT_JS].map(t => (t || '').trim()).join('\u0000');
|
|
168
|
+
|
|
169
|
+
function scheduleSave() {
|
|
170
|
+
if (themeStash || histView) return; // Edit theme's stand-in slides and history previews are never the deck // Edit theme: the stand-in slides are never the deck
|
|
171
|
+
clearTimeout(saveTimer);
|
|
172
|
+
setStatus('Saving…', 'sync');
|
|
173
|
+
saveTimer = setTimeout(save, 400);
|
|
174
|
+
}
|
|
175
|
+
let isDraft = false, savedSnap = null; // savedSnap: each slide's HTML as last saved or loaded (what a clash merges against)
|
|
176
|
+
const normHTML = h => h.replace(/\sid="[^"]*"/g, ''); // ids the editor gives id-less boxes differ on every load
|
|
177
|
+
const draftStatus = () => { setStatus('Not saved: add a title', 'cloud_off'); $('#ed-status').dataset.tip = 'Untitled presentations are not kept. Give it a title to save it to your library.'; };
|
|
178
|
+
const draftEdited = () => isDraft && hptr > 0; // something was done in it, so leaving would lose work
|
|
179
|
+
async function save() {
|
|
180
|
+
|
|
181
|
+
if (saving) { saveTimer = setTimeout(save, 200); return; }
|
|
182
|
+
saveTimer = null; saving = true;
|
|
183
|
+
try {
|
|
184
|
+
const snap = slides.map(s => sectionHTML(s));
|
|
185
|
+
const html = deckHTML(), collab = /** @type {any} */ (window.GLIDESHOW_CLOUD)?.collab;
|
|
186
|
+
// a collaborator's save is their slides (and title): the server puts them, cleaned, into the owner's deck (cloud/share.js),
|
|
187
|
+
// keeping the owner's own copy of each slide left as it was in the version this save starts from (cloud/clean.js keepUntouched)
|
|
188
|
+
const base = new Map((savedSnap || []).map((h, i) => [sidOf(h), { h, i }]));
|
|
189
|
+
const r = await fetch('/api/save', { method: 'POST', body: JSON.stringify({ html, mtime, ...(collab ? { slides: new DOMParser().parseFromString(html, 'text/html').querySelector('.slides')?.innerHTML || '',
|
|
190
|
+
same: snap.flatMap(h => { const b = base.get(sidOf(h)); return b && b.h === h ? [{ sid: sidOf(h), at: b.i }] : []; }), count: savedSnap?.length ?? -1 } : {}) }) });
|
|
191
|
+
const j = await r.json();
|
|
192
|
+
if (r.status === 409) { // saved elsewhere meanwhile (an agent, another editor): merged slide by slide, then saved
|
|
193
|
+
saving = false;
|
|
194
|
+
await mergeIn();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
mtime = j.mtime; savedSnap = snap; savedCode = deckCode(html);
|
|
198
|
+
if (presenting()) refreshPresent();
|
|
199
|
+
if (j.moved) { isDraft = false; deckPath = j.moved; $('#ed-title').placeholder = ''; document.body.classList.remove('draft'); if (window.GLIDESHOW_CLOUD && !/** @type {any} */ (window.GLIDESHOW_CLOUD).local) live(); } // (the live link follows the deck it became)
|
|
200
|
+
|
|
201
|
+
if (isDraft) { draftStatus(); saving = false; return; }
|
|
202
|
+
setStatus('All changes saved', 'cloud_done');
|
|
203
|
+
$('#ed-status').dataset.tip = 'Saved to disk at ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
204
|
+
} catch {
|
|
205
|
+
setStatus('Save failed: is the server running?', 'cloud_off');
|
|
206
|
+
}
|
|
207
|
+
saving = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Slides grouped into reveal.js stacks: consecutive slides with the same data-ed-stack form one vertical stack. */
|
|
211
|
+
function stacks(list = slides) {
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const s of list) { const last = out.at(-1); if (last && s.dataset.edStack && last[0].dataset.edStack === s.dataset.edStack) last.push(s); else out.push([s]); }
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
const isSub = i => i > 0 && slides[i].dataset.edStack && slides[i - 1].dataset.edStack === slides[i].dataset.edStack;
|
|
217
|
+
/* A slide's position as reveal.js counts it: [h, v] (0-based), and back. */
|
|
218
|
+
function hv(i) { let h = -1, v = 0; for (let j = 0; j <= i; j++) { if (isSub(j)) v++; else { h++; v = 0; } } return [h, v]; }
|
|
219
|
+
function indexOf(h, v = 0) { for (let i = 0; i < slides.length; i++) { const [a, b] = hv(i); if (a === h && b === v) return i; } return Math.min(slides.length - 1, Math.max(0, h)); }
|
|
220
|
+
const snapshot = () => ({ html: slides.map(s => sectionHTML(s)), cur });
|
|
221
|
+
function pushHistory() {
|
|
222
|
+
if (themeStash || histView) return false;
|
|
223
|
+
ensureSids();
|
|
224
|
+
const s = snapshot(), top = hist[hptr];
|
|
225
|
+
if (top && top.html.join('\0') === s.html.join('\0')) { top.cur = s.cur; return false; }
|
|
226
|
+
hist = hist.slice(0, hptr + 1);
|
|
227
|
+
hist.push(s);
|
|
228
|
+
if (hist.length > 300) hist.shift();
|
|
229
|
+
hptr = hist.length - 1;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
function commit() {
|
|
233
|
+
fitGroups(); followConnectors(); showAINotes();
|
|
234
|
+
if (pushHistory()) scheduleSave();
|
|
235
|
+
refreshFind();
|
|
236
|
+
renderThumbs(); updateToolbar();
|
|
237
|
+
}
|
|
238
|
+
// `at` is the slide to show: the slide where the undone/redone change was made, as in Slides.
|
|
239
|
+
function restore(s, at = s.cur) {
|
|
240
|
+
const ids = new Set([...sel].map(b => b.id));
|
|
241
|
+
const t = document.createElement('template'), same = new Map(); // slides the step didn't change stay the same elements,
|
|
242
|
+
slides.forEach(x => { const h = sectionHTML(x); same.set(h, [...(same.get(h) || []), x]); }); // so their thumbnails aren't redrawn
|
|
243
|
+
slides = s.html.map(h => { const kept = same.get(h)?.shift(); if (kept) return kept; t.innerHTML = h; return t.content.firstElementChild; });
|
|
244
|
+
cur = Math.min(at, slides.length - 1);
|
|
245
|
+
sel = new Set(boxes().filter(b => ids.has(b.id)));
|
|
246
|
+
render(); scheduleSave();
|
|
247
|
+
}
|
|
248
|
+
function undo() { stopEdit(); if (hptr > 0 && !overwrites(hist[hptr - 1])) { const at = hist[hptr].cur; restore(hist[--hptr], at); } }
|
|
249
|
+
function redo() { stopEdit(); if (hptr < hist.length - 1 && !overwrites(hist[hptr + 1])) restore(hist[++hptr]); }
|
|
250
|
+
function maybeReload() { if (pendingReload && !busy && !editing) { pendingReload = false; unsaved() || saveTimer ? (clearTimeout(saveTimer), saveTimer = null, mergeIn()) : load(true, true); } } // with changes of its own, merged (mergeIn)
|