aboutsummaryrefslogtreecommitdiffstats
path: root/src/index.html
diff options
context:
space:
mode:
Diffstat (limited to 'src/index.html')
-rw-r--r--src/index.html126
1 files changed, 61 insertions, 65 deletions
diff --git a/src/index.html b/src/index.html
index cbcc7cd..7f5a476 100644
--- a/src/index.html
+++ b/src/index.html
@@ -116,7 +116,7 @@ questions?</textarea>
<button type="button" onclick="App.downloadSent()">download for local sent usage</button>
<button type="button" onclick="App.exportPDF()">export .pdf</button>
</div>
- <p class="hint">F5 to present · Esc to exit · ← → h l j k space enter to navigate · images are local to this browser</p>
+ <p class="hint">F5 to present · Esc to exit · ← → h l j k space enter to navigate · images upload to server and preload before presenting</p>
</main>
<!-- presentation overlay -->
@@ -179,9 +179,7 @@ questions?</textarea>
},
init() {
- this.restoreImageState();
this.restoreState();
- this.ensureBundledImageInLocalStore();
this.loadFonts();
this.bindEvents();
this.updateColors();
@@ -448,7 +446,27 @@ questions?</textarea>
return this.defaultBundledImage.url;
}
- return canonicalName;
+ return 'uploads/' + encodeURIComponent(canonicalName);
+ },
+
+ listSlideImageUrls(slides) {
+ const urls = new Set();
+ for (const slide of slides) {
+ if (!slide || !slide.img) continue;
+ const src = this.resolveSlideImageSrc(slide.img);
+ if (src) urls.add(src);
+ }
+ return [...urls];
+ },
+
+ async preloadSlideImages(slides) {
+ const urls = this.listSlideImageUrls(slides);
+ await Promise.all(urls.map(src => new Promise((resolve) => {
+ const img = new Image();
+ img.onload = () => resolve();
+ img.onerror = () => resolve();
+ img.src = src;
+ })));
},
buildSentFontStack(primaryFamily) {
@@ -548,9 +566,6 @@ questions?</textarea>
input.addEventListener('input', () => {
localStorage.setItem('sent-web-content', input.value);
});
- input.addEventListener('blur', () => {
- this.pruneUnusedLocalImages(input.value);
- });
input.addEventListener('keydown', e => this.handleEditorKeydown(e));
document.addEventListener('keydown', e => this.handleKeydown(e));
@@ -695,9 +710,8 @@ questions?</textarea>
},
// presentation controls
- startPresentation() {
+ async startPresentation() {
const text = document.getElementById('input').value;
- this.pruneUnusedLocalImages(text);
this.slides = this.parseSent(text);
if (this.slides.length === 0) {
@@ -705,6 +719,22 @@ questions?</textarea>
return;
}
+ const btn = document.getElementById('btn-present');
+ const originalBtnText = btn ? btn.textContent : 'present';
+ if (btn) {
+ btn.disabled = true;
+ btn.textContent = 'preloading images...';
+ }
+
+ try {
+ await this.preloadSlideImages(this.slides);
+ } finally {
+ if (btn) {
+ btn.disabled = false;
+ btn.textContent = originalBtnText;
+ }
+ }
+
this.idx = 0;
this.presenting = true;
document.getElementById('presentation').classList.add('active');
@@ -898,68 +928,48 @@ questions?</textarea>
return;
}
- const rateCheck = this.canStoreUpload();
- if (!rateCheck.ok) {
- status.textContent = `error: ${rateCheck.error}`;
- done();
- return;
- }
-
- status.textContent = 'storing locally...';
+ status.textContent = 'uploading...';
try {
- const ta = document.getElementById('input');
- this.pruneUnusedLocalImages(ta.value);
-
- if (Object.keys(this.imageStore).length >= this.uploadPolicy.maxImages) {
- status.textContent = `error: local image limit reached (${this.uploadPolicy.maxImages})`;
- return;
- }
+ const fd = new FormData();
+ fd.append('image', file);
- const filename = this.makeLocalImageName(file, ext);
- const dataUrl = await this.fileToDataUrl(file);
+ const res = await fetch('/upload', {
+ method: 'POST',
+ body: fd,
+ });
- const nextStore = {
- ...this.imageStore,
- [filename]: {
- dataUrl,
- mime: file.type,
- bytes: file.size,
- createdAt: Date.now(),
- },
- };
+ let data = {};
+ try {
+ data = await res.json();
+ } catch (_) {}
- const nextTotalBytes = this.imageStoreBytes(nextStore);
- if (nextTotalBytes > this.uploadPolicy.maxTotalBytes) {
- status.textContent = `error: local quota exceeded (max ${this.formatBytes(this.uploadPolicy.maxTotalBytes)} total)`;
+ if (!res.ok || data.error) {
+ const msg = data.error || `upload failed (${res.status})`;
+ status.textContent = `error: ${msg}`;
return;
}
- this.imageStore = nextStore;
- this.saveImageStore();
-
- this.uploadTimestamps.push(Date.now());
- this.pruneUploadTimestamps();
- this.saveUploadRateState();
+ if (!data.filename) {
+ status.textContent = 'error: invalid upload response';
+ return;
+ }
+ const ta = document.getElementById('input');
const pos = ta.selectionStart;
const txt = ta.value;
- const ins = `\n@${filename}\n`;
+ const ins = `\n@${data.filename}\n`;
ta.value = txt.substring(0, pos) + ins + txt.substring(pos);
ta.selectionStart = ta.selectionEnd = pos + ins.length;
ta.focus();
localStorage.setItem('sent-web-content', ta.value);
- status.textContent = `stored: ${filename}`;
+ status.textContent = `photo inserted`;
setTimeout(() => {
status.textContent = '';
}, 3000);
} catch (err) {
- if (err && err.name === 'QuotaExceededError') {
- status.textContent = 'error: browser storage quota reached';
- } else {
- status.textContent = 'error: failed to store image locally';
- }
+ status.textContent = 'upload failed';
console.error(err);
} finally {
done();
@@ -969,20 +979,6 @@ questions?</textarea>
// download .sent file for local usage (base64-encoded to preserve unicode and avoid filename issues)
downloadSent() {
const text = document.getElementById('input').value;
- let localRefs = 0;
- for (const name of this.referencedImageNames(text)) {
- if (this.imageStore[name]) {
- localRefs += 1;
- }
- }
-
- if (localRefs > 0) {
- const ok = window.confirm(
- `this deck references ${localRefs} locally stored image(s). the .sent file will not include image binaries. continue?`
- );
- if (!ok) return;
- }
-
const blob = new Blob([text], {
type: 'text/plain'
});