Add self-contained shareable HTML export for YouTube recaps
New 'Share page (HTML)' entry in the Export menu generates a single standalone .html file: the recap record inlined as JSON plus a small baked-in renderer that reproduces the embedded YouTube player and the expandable timestamped summaries. The recipient opens it with no account and no calls back to the server. On mobile it hands the file to the native share sheet (navigator.share with files); on desktop or where unsupported it downloads. YouTube only by design — podcasts have no portable audio and are rejected with a toast. The generator runs inside index.html's own <script>, so closing tags are split and inlined JSON escapes '<' to avoid premature script termination. Ship as 0.2.160.
This commit is contained in:
@@ -11092,6 +11092,175 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Self-contained shareable HTML page (YouTube recaps) ──────────
|
||||
// Builds ONE standalone .html document: the recap record inlined as
|
||||
// JSON + a tiny embedded renderer that reproduces the YouTube embed
|
||||
// and the expandable timestamped summaries, with no account and no
|
||||
// calls back to this server. The recipient just opens the file. Only
|
||||
// external load is YouTube's own iframe/API (inherent to embedding a
|
||||
// video). YouTube only — podcasts have no portable audio to ship.
|
||||
//
|
||||
// Style + markup are copied from the live .chunk renderer so the
|
||||
// shared page matches the logged-in view. NOTE: this string is built
|
||||
// inside index.html's own <script>, so the closing script tags below
|
||||
// are written as `<\/script>` and the inlined JSON escapes `<` to
|
||||
// `<` — both prevent the HTML parser from ending this script
|
||||
// early. Don't "simplify" either away.
|
||||
const SHARE_PAGE_CSS =
|
||||
'*{box-sizing:border-box}' +
|
||||
'body{margin:0;background:#0b1120;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.5}' +
|
||||
'.wrap{max-width:860px;margin:0 auto;padding:20px 16px 64px}' +
|
||||
'.rc-head h1{font-size:20px;font-weight:680;color:#f1f5f9;margin:0 0 6px;line-height:1.3}' +
|
||||
'.rc-meta{font-size:12px;color:#64748b;margin:0 0 16px}' +
|
||||
'.rc-meta a{color:#818cf8;text-decoration:none}' +
|
||||
'.rc-meta a:hover{text-decoration:underline}' +
|
||||
'.rc-video{position:relative;width:100%;aspect-ratio:16/9;background:#000;border-radius:12px;overflow:hidden;margin-bottom:16px}' +
|
||||
'.rc-video iframe{position:absolute;inset:0;width:100%;height:100%;border:0}' +
|
||||
'.rc-toolbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;gap:10px}' +
|
||||
'.rc-count{font-size:12px;color:#64748b}' +
|
||||
'.rc-expand{font-size:12px;color:#818cf8;background:none;border:1px solid #334155;border-radius:6px;padding:5px 10px;cursor:pointer}' +
|
||||
'.rc-expand:hover{border-color:#818cf8}' +
|
||||
'.chunk{background:#111827;border:1px solid #1e293b;border-radius:10px;margin-bottom:6px;overflow:hidden;transition:all 0.2s}' +
|
||||
'.chunk:hover{border-color:#334155}' +
|
||||
'.chunk.expanded{border-color:#818cf8;background:#0f172a;box-shadow:0 2px 16px rgba(129,140,248,0.06)}' +
|
||||
'.chunk.now-playing{border-color:#22c55e}' +
|
||||
'.chunk-header{width:100%;padding:10px 14px;background:none;border:none;text-align:left;display:flex;gap:10px;align-items:flex-start;color:inherit}' +
|
||||
'.chunk-play-btn{width:26px;height:26px;border-radius:7px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:#1e293b;color:#64748b;border:none;cursor:pointer;transition:all 0.15s;font-size:12px;margin-top:1px}' +
|
||||
'.chunk-play-btn:hover{background:#818cf8;color:#fff}' +
|
||||
'.chunk.now-playing .chunk-play-btn{background:#22c55e;color:#fff}' +
|
||||
'.chunk-info{flex:1;min-width:0}' +
|
||||
'.chunk-title-row{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}' +
|
||||
'.chunk-title{font-size:13px;font-weight:650;color:#f1f5f9;line-height:1.3}' +
|
||||
'.chunk-time{font-size:10px;color:#475569;font-weight:500;font-family:"SF Mono",Menlo,monospace;white-space:nowrap}' +
|
||||
'.chunk-summary{margin:3px 0 0;font-size:12px;line-height:1.5;color:#94a3b8}' +
|
||||
'.chunk-arrow{font-size:22px;color:#475569;transition:transform 0.2s;flex-shrink:0;padding:4px 8px;border-radius:6px;line-height:1}' +
|
||||
'.chunk-arrow:hover{background:#1e293b;color:#818cf8}' +
|
||||
'.chunk.expanded .chunk-arrow{transform:rotate(180deg);color:#818cf8}' +
|
||||
'.chunk-body{max-height:0;overflow:hidden;transition:max-height 0.4s ease}' +
|
||||
'.chunk.expanded .chunk-body{max-height:15000px}' +
|
||||
'.chunk-body-inner{padding:12px 14px 12px 52px;border-top:1px solid #1e293b}' +
|
||||
'.transcript-line{display:flex;gap:10px;align-items:flex-start;padding:5px 8px;cursor:pointer;border-radius:6px;transition:background 0.15s;border:none;background:none;width:100%;text-align:left;color:inherit}' +
|
||||
'.transcript-line:hover{background:rgba(129,140,248,0.06)}' +
|
||||
'.ts-badge{font-size:11px;font-family:"SF Mono",Menlo,monospace;color:#818cf8;min-width:52px;padding-top:2px;font-weight:500;white-space:nowrap;flex-shrink:0}' +
|
||||
'.transcript-text{font-size:13px;line-height:1.55;color:#cbd5e1}' +
|
||||
'.rc-spk{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background:#1e293b;color:#a5b4fc;font-size:10px;font-weight:600;flex-shrink:0;margin-top:1px}' +
|
||||
'.rc-foot{margin-top:28px;text-align:center;font-size:11px;color:#475569}' +
|
||||
'.rc-foot a{color:#64748b;text-decoration:none}' +
|
||||
'@media (max-width:640px){.wrap{padding:14px 10px 48px}.chunk-title{font-size:12px}.chunk-summary{font-size:11px}.transcript-text{font-size:12px}.chunk-body-inner{padding-left:14px}}';
|
||||
|
||||
// The renderer that ships INSIDE the shared file. Plain ES5-ish, no
|
||||
// backticks and no ${...} (it lives inside this template-built string).
|
||||
const SHARE_PAGE_JS =
|
||||
'var DATA=JSON.parse(document.getElementById("recap-data").textContent);' +
|
||||
'function esc(s){return String(s==null?"":s).replace(/[&<>"\\u0027]/g,function(c){return{"&":"&","<":"<",">":">","\\u0022":""","\\u0027":"'"}[c];});}' +
|
||||
'function ft(sec){sec=Math.floor(sec||0);var h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=sec%60;function p(n){return(n<10?"0":"")+n;}return h>0?h+":"+p(m)+":"+p(s):m+":"+p(s);}' +
|
||||
'function spk(e){if(!e.speaker||e.speaker==="Speaker_Unknown")return"";var nm=DATA.speakerNames||{};var full=(nm[e.speaker]&&String(nm[e.speaker]).trim())||e.speaker.replace("Speaker_","Speaker ");var mt=e.speaker.match(/Speaker_([A-Z0-9]+)/);var lt=mt?mt[1]:"?";return\'<span class="rc-spk" title="\'+esc(full)+\'">\'+esc(lt)+\'</span>\';}' +
|
||||
'function line(e){return\'<button class="transcript-line" onclick="seekTo(\'+Math.floor(e.offset)+\')"><span class="ts-badge">\\u25B6 \'+ft(e.offset)+\'</span>\'+spk(e)+\'<span class="transcript-text">\'+esc(e.text)+\'</span></button>\';}' +
|
||||
'function renderChunk(ch,i){var ents=ch.entries||[];var last=ents.length?ents[ents.length-1]:null;var endT=ft(last?(last.offset+(last.duration||0)):ch.startTime);' +
|
||||
'var body=ents.map(line).join("");' +
|
||||
'return\'<div class="chunk" id="rc-chunk-\'+i+\'"><div class="chunk-header">\'+' +
|
||||
'\'<button class="chunk-play-btn" onclick="event.stopPropagation();seekTo(\'+Math.floor(ch.startTime)+\');hl(\'+i+\')"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 19,12 5,21"></polygon></svg></button>\'+' +
|
||||
'\'<div class="chunk-info" onclick="toggleChunk(\'+i+\')" style="cursor:pointer"><div class="chunk-title-row"><span class="chunk-title">\'+esc(ch.title)+\'</span><span class="chunk-time">\'+ft(ch.startTime)+\' \\u2013 \'+endT+\'</span></div><p class="chunk-summary">\'+esc(ch.summary)+\'</p></div>\'+' +
|
||||
'\'<div class="chunk-arrow" onclick="toggleChunk(\'+i+\')" style="cursor:pointer">\\u25BE</div></div>\'+' +
|
||||
'\'<div class="chunk-body"><div class="chunk-body-inner">\'+body+\'</div></div></div>\';}' +
|
||||
'function toggleChunk(i){var el=document.getElementById("rc-chunk-"+i);if(el)el.classList.toggle("expanded");}' +
|
||||
'function hl(i){var ns=document.querySelectorAll(".chunk.now-playing");for(var k=0;k<ns.length;k++)ns[k].classList.remove("now-playing");var el=document.getElementById("rc-chunk-"+i);if(el)el.classList.add("now-playing");}' +
|
||||
'var allExp=false;function toggleAll(){allExp=!allExp;var cs=document.querySelectorAll(".chunk");for(var k=0;k<cs.length;k++)cs[k].classList.toggle("expanded",allExp);document.getElementById("rc-expand").textContent=allExp?"Collapse all":"Expand all";}' +
|
||||
'var player=null,ytReady=false;' +
|
||||
'function onYouTubeIframeAPIReady(){player=new YT.Player("rc-player",{width:"100%",height:"100%",videoId:DATA.videoId,playerVars:{playsinline:1,rel:0},events:{onReady:function(){ytReady=true;}}});}' +
|
||||
'function seekTo(sec){if(ytReady&&player&&player.seekTo){player.seekTo(sec,true);player.playVideo();window.scrollTo({top:0,behavior:"smooth"});}else{window.open("https://www.youtube.com/watch?v="+DATA.videoId+"&t="+Math.floor(sec),"_blank");}}' +
|
||||
'var chunks=DATA.chunks||[];document.getElementById("rc-chunks").innerHTML=chunks.map(renderChunk).join("");' +
|
||||
'document.getElementById("rc-count").textContent=chunks.length+(chunks.length===1?" topic":" topics");';
|
||||
|
||||
function buildSharePageHtml(record) {
|
||||
const title = record.title || "Untitled";
|
||||
// Escape `<` so the inlined JSON can never close this <script>.
|
||||
const dataJson = JSON.stringify(record).replace(/</g, "\\u003c");
|
||||
const srcUrl = "https://www.youtube.com/watch?v=" + encodeURIComponent(record.videoId || "");
|
||||
const dateLine = record.uploadDate ? (" · " + escHtml(String(record.uploadDate))) : "";
|
||||
return '<!DOCTYPE html>\n' +
|
||||
'<html lang="en"><head>\n' +
|
||||
'<meta charset="utf-8">\n' +
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
|
||||
'<meta name="robots" content="noindex">\n' +
|
||||
'<title>' + escHtml(title) + ' — Recap</title>\n' +
|
||||
'<style>' + SHARE_PAGE_CSS + '</style>\n' +
|
||||
'</head><body>\n' +
|
||||
'<div class="wrap">\n' +
|
||||
' <div class="rc-head"><h1>' + escHtml(title) + '</h1>\n' +
|
||||
' <p class="rc-meta"><a href="' + srcUrl + '" target="_blank" rel="noopener">Watch on YouTube ↗</a>' + dateLine + '</p></div>\n' +
|
||||
' <div class="rc-video"><div id="rc-player"></div></div>\n' +
|
||||
' <div class="rc-toolbar"><span class="rc-count" id="rc-count"></span><button class="rc-expand" id="rc-expand" onclick="toggleAll()">Expand all</button></div>\n' +
|
||||
' <div id="rc-chunks"></div>\n' +
|
||||
' <div class="rc-foot">Generated with <a href="https://recaps.cc" target="_blank" rel="noopener">Recaps</a></div>\n' +
|
||||
'</div>\n' +
|
||||
'<script type="application/json" id="recap-data">' + dataJson + '<\/script>\n' +
|
||||
'<script>' + SHARE_PAGE_JS + '<\/script>\n' +
|
||||
'<script src="https://www.youtube.com/iframe_api"><\/script>\n' +
|
||||
'</body></html>';
|
||||
}
|
||||
|
||||
// Hand the generated file to the OS share sheet when supported
|
||||
// (mobile → Messages / Mail / AirDrop), else download it. canShare
|
||||
// with files must be tested in the click gesture; an await before
|
||||
// share() can void the gesture on iOS, which we catch → download.
|
||||
async function shareOrDownloadHtml(record) {
|
||||
const html = buildSharePageHtml(record);
|
||||
const filename = safeExportFilename(record.title) + ".html";
|
||||
try {
|
||||
if (navigator.canShare) {
|
||||
const file = new File([html], filename, { type: "text/html" });
|
||||
if (navigator.canShare({ files: [file] })) {
|
||||
await navigator.share({ files: [file], title: record.title || "Recap" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e && e.name === "AbortError") return; // user dismissed the sheet
|
||||
// any other failure → fall through to a plain download
|
||||
}
|
||||
downloadTextFile(filename, "text/html;charset=utf-8", html);
|
||||
showToast("Share page saved", "🔗");
|
||||
}
|
||||
|
||||
function shareRecordFromState() {
|
||||
return {
|
||||
title: state.videoTitle || "Untitled",
|
||||
videoId: state.videoId,
|
||||
type: state.currentType,
|
||||
url: state.url || ("https://www.youtube.com/watch?v=" + state.videoId),
|
||||
uploadDate: state.videoUploadDate || null,
|
||||
chunks: state.chunks,
|
||||
speakers: state.speakers || null,
|
||||
speakerNames: state.speakerNames || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function exportCurrentSharePage() {
|
||||
if (!state.chunks.length) return;
|
||||
if ((state.currentType || "youtube") !== "youtube" || !state.videoId) {
|
||||
showToast("Share page is available for YouTube recaps only", "⚠", 3500);
|
||||
return;
|
||||
}
|
||||
await shareOrDownloadHtml(shareRecordFromState());
|
||||
}
|
||||
|
||||
async function exportSessionSharePage(sessionId) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/history/${sessionId}`);
|
||||
if (!res.ok) throw new Error("Server error " + res.status);
|
||||
const data = await res.json();
|
||||
if (!data.chunks || data.chunks.length === 0) { showToast("No data to export", "⚠", 3000); return; }
|
||||
if ((data.type || "youtube") !== "youtube" || !data.videoId) {
|
||||
showToast("Share page is available for YouTube recaps only", "⚠", 3500);
|
||||
return;
|
||||
}
|
||||
await shareOrDownloadHtml(data);
|
||||
} catch (e) {
|
||||
showToast("Export failed: " + e.message, "✕", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export menu popover ──────────────────────────────────────────
|
||||
// Shared 3-option menu (PDF / Markdown / JSON) used by the main
|
||||
// view's Export button AND the per-row export button in the
|
||||
@@ -11114,6 +11283,7 @@
|
||||
"border-radius:8px; padding:4px; box-shadow:0 8px 24px rgba(0,0,0,0.6); " +
|
||||
"min-width:170px;";
|
||||
const opts = [
|
||||
{ icon: "🔗", label: "Share page (HTML)", run: () => scope === "current" ? exportCurrentSharePage() : exportSessionSharePage(scope) },
|
||||
{ icon: "📄", label: "Export PDF", run: () => scope === "current" ? exportCurrentPDF() : exportSessionPDF(scope) },
|
||||
{ icon: "📝", label: "Export Markdown", run: () => scope === "current" ? exportCurrentMarkdown() : exportSessionMarkdown(scope) },
|
||||
{ icon: "{ }", label: "Export JSON", run: () => scope === "current" ? exportCurrentJSON() : exportSessionJSON(scope) },
|
||||
|
||||
@@ -178,8 +178,9 @@ import { v_0_2_156 } from './v0.2.156'
|
||||
import { v_0_2_157 } from './v0.2.157'
|
||||
import { v_0_2_158 } from './v0.2.158'
|
||||
import { v_0_2_159 } from './v0.2.159'
|
||||
import { v_0_2_160 } from './v0.2.160'
|
||||
|
||||
export const versionGraph = VersionGraph.of({
|
||||
current: v_0_2_159,
|
||||
other: [v_0_2_158, v_0_2_157, v_0_2_156, v_0_2_155, v_0_2_154, v_0_2_153, v_0_2_152, v_0_2_151, v_0_2_150, v_0_2_149, v_0_2_148, v_0_2_147, v_0_2_146, v_0_2_145, v_0_2_144, v_0_2_143, v_0_2_142, v_0_2_141, v_0_2_140, v_0_2_139, v_0_2_138, v_0_2_137, v_0_2_136, v_0_2_135, v_0_2_134, v_0_2_133, v_0_2_132, v_0_2_131, v_0_2_130, v_0_2_129, v_0_2_128, v_0_2_127, v_0_2_126, v_0_2_125, v_0_2_124, v_0_2_123, v_0_2_122, v_0_2_121, v_0_2_120, v_0_2_119, v_0_2_118, v_0_2_117, v_0_2_116, v_0_2_115, v_0_2_114, v_0_2_113, v_0_2_112, v_0_2_111, v_0_2_110, v_0_2_109, v_0_2_108, v_0_2_107, v_0_2_106, v_0_2_105, v_0_2_104, v_0_2_103, v_0_2_102, v_0_2_101, v_0_2_100, v_0_2_99, v_0_2_98, v_0_2_97, v_0_2_96, v_0_2_95, v_0_2_94, v_0_2_93, v_0_2_92, v_0_2_91, v_0_2_90, v_0_2_89, v_0_2_88, v_0_2_87, v_0_2_86, v_0_2_85, v_0_2_84, v_0_2_83, v_0_2_82, v_0_2_81, v_0_2_80, v_0_2_79, v_0_2_78, v_0_2_77, v_0_2_76, v_0_2_75, v_0_2_74, v_0_2_73, v_0_2_72, v_0_2_71, v_0_2_70, v_0_2_69, v_0_2_68, v_0_2_67, v_0_2_66, v_0_2_65, v_0_2_64, v_0_2_63, v_0_2_62, v_0_2_61, v_0_2_60, v_0_2_59, v_0_2_58, v_0_2_57, v_0_2_56, v_0_2_55, v_0_2_54, v_0_2_53, v_0_2_52, v_0_2_51, v_0_2_50, v_0_2_49, v_0_2_48, v_0_2_47, v_0_2_46, v_0_2_45, v_0_2_44, v_0_2_43, v_0_2_42, v_0_2_41, v_0_2_40, v_0_2_39, v_0_2_38, v_0_2_37, v_0_2_36, v_0_2_35, v_0_2_34, v_0_2_33, v_0_2_32, v_0_2_31, v_0_2_30, v_0_2_29, v_0_2_28, v_0_2_27, v_0_2_26, v_0_2_25, v_0_2_24, v_0_2_23, v_0_2_22, v_0_2_21, v_0_2_20, v_0_2_19, v_0_2_18, v_0_2_17, v_0_2_16, v_0_2_15, v_0_2_14, v_0_2_13, v_0_2_12, v_0_2_11, v_0_2_10, v_0_2_9, v_0_2_8, v_0_2_7, v_0_2_6, v_0_2_5, v_0_2_4, v_0_2_3, v_0_2_2, v_0_2_1, v_0_2_0, v_0_1_18, v_0_1_17, v_0_1_16, v_0_1_15, v_0_1_14, v_0_1_13, v_0_1_12, v_0_1_11, v_0_1_10, v_0_1_9, v_0_1_8, v_0_1_7, v_0_1_6, v_0_1_5, v_0_1_4, v_0_1_3, v_0_1_2, v_0_1_1, v_0_1_0],
|
||||
current: v_0_2_160,
|
||||
other: [v_0_2_159, v_0_2_158, v_0_2_157, v_0_2_156, v_0_2_155, v_0_2_154, v_0_2_153, v_0_2_152, v_0_2_151, v_0_2_150, v_0_2_149, v_0_2_148, v_0_2_147, v_0_2_146, v_0_2_145, v_0_2_144, v_0_2_143, v_0_2_142, v_0_2_141, v_0_2_140, v_0_2_139, v_0_2_138, v_0_2_137, v_0_2_136, v_0_2_135, v_0_2_134, v_0_2_133, v_0_2_132, v_0_2_131, v_0_2_130, v_0_2_129, v_0_2_128, v_0_2_127, v_0_2_126, v_0_2_125, v_0_2_124, v_0_2_123, v_0_2_122, v_0_2_121, v_0_2_120, v_0_2_119, v_0_2_118, v_0_2_117, v_0_2_116, v_0_2_115, v_0_2_114, v_0_2_113, v_0_2_112, v_0_2_111, v_0_2_110, v_0_2_109, v_0_2_108, v_0_2_107, v_0_2_106, v_0_2_105, v_0_2_104, v_0_2_103, v_0_2_102, v_0_2_101, v_0_2_100, v_0_2_99, v_0_2_98, v_0_2_97, v_0_2_96, v_0_2_95, v_0_2_94, v_0_2_93, v_0_2_92, v_0_2_91, v_0_2_90, v_0_2_89, v_0_2_88, v_0_2_87, v_0_2_86, v_0_2_85, v_0_2_84, v_0_2_83, v_0_2_82, v_0_2_81, v_0_2_80, v_0_2_79, v_0_2_78, v_0_2_77, v_0_2_76, v_0_2_75, v_0_2_74, v_0_2_73, v_0_2_72, v_0_2_71, v_0_2_70, v_0_2_69, v_0_2_68, v_0_2_67, v_0_2_66, v_0_2_65, v_0_2_64, v_0_2_63, v_0_2_62, v_0_2_61, v_0_2_60, v_0_2_59, v_0_2_58, v_0_2_57, v_0_2_56, v_0_2_55, v_0_2_54, v_0_2_53, v_0_2_52, v_0_2_51, v_0_2_50, v_0_2_49, v_0_2_48, v_0_2_47, v_0_2_46, v_0_2_45, v_0_2_44, v_0_2_43, v_0_2_42, v_0_2_41, v_0_2_40, v_0_2_39, v_0_2_38, v_0_2_37, v_0_2_36, v_0_2_35, v_0_2_34, v_0_2_33, v_0_2_32, v_0_2_31, v_0_2_30, v_0_2_29, v_0_2_28, v_0_2_27, v_0_2_26, v_0_2_25, v_0_2_24, v_0_2_23, v_0_2_22, v_0_2_21, v_0_2_20, v_0_2_19, v_0_2_18, v_0_2_17, v_0_2_16, v_0_2_15, v_0_2_14, v_0_2_13, v_0_2_12, v_0_2_11, v_0_2_10, v_0_2_9, v_0_2_8, v_0_2_7, v_0_2_6, v_0_2_5, v_0_2_4, v_0_2_3, v_0_2_2, v_0_2_1, v_0_2_0, v_0_1_18, v_0_1_17, v_0_1_16, v_0_1_15, v_0_1_14, v_0_1_13, v_0_1_12, v_0_1_11, v_0_1_10, v_0_1_9, v_0_1_8, v_0_1_7, v_0_1_6, v_0_1_5, v_0_1_4, v_0_1_3, v_0_1_2, v_0_1_1, v_0_1_0],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { VersionInfo } from '@start9labs/start-sdk'
|
||||
|
||||
export const v_0_2_160 = VersionInfo.of({
|
||||
version: '0.2.160:0',
|
||||
releaseNotes: {
|
||||
en_US:
|
||||
'New: Share page (HTML) export for YouTube recaps. The Export menu now offers a self-contained .html file with the embedded video and expandable timestamped summaries baked in — send it to anyone and they can open it with no account. On mobile it opens the native share sheet; on desktop it downloads.',
|
||||
},
|
||||
migrations: {
|
||||
up: async ({ effects }) => {},
|
||||
down: async ({ effects }) => {},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user