import { promptUpload } from "./prompt_upload.js"; import { slugify } from "./utils.js"; import { HtmlExporter } from "./html_exporter.js"; import { KarlkoderCodec } from "./karlkoder_codec.js"; export class ProjectSaveHandler { constructor(editor, assetEditor, assetProvider, sessionSaveHandler, projectName) { this.editor = editor; this.assetEditor = assetEditor; this.sessionSaveHandler = sessionSaveHandler; this.htmlExporter = new HtmlExporter(assetProvider); this.projectName = projectName; this.fileHandles = {}; // Used for Chromium file picker this.saveName = null; this.isSaved = true; editor.addEventListener("change", () => { this.isSaved = false; }); assetEditor.addChangeListener(() => { this.isSaved = false; }); } async showLoadFilePrompt() { const files = await promptUpload(".karlkode", false); if (files.length === 0) { return; } if (files.length > 1) { throw new Error( `unreachable: something went wrong ! files.length > 1 : files.length = ${files.length}`, ); } await this.loadFromFile(files[0]); } async loadFromFile(file) { if ( !this.isSaved && !confirm( "Your project has not been saved. Loading a new project will override your current one. Continue?", ) ) { return; } const items = KarlkoderCodec.de( await fetch(URL.createObjectURL(file)).then((x) => x.bytes()), ); const assets = items .filter((x) => x.tag === "asset") .map((x) => { delete x.tag; return x; }); const code = items.find((x) => x.tag === "code"); delete code.tag; await this.assetEditor.importAssets( assets.map(({ name, mime, content }) => { const file = new File([content], name, { type: mime }); return { name, file }; }), ); const dec = new TextDecoder(); this.sessionSaveHandler.saveEditorCode(dec.decode(code.content)); this.sessionSaveHandler.saveProjectName(code.name); this.isSaved = true; this.fileHandles = {}; } async saveFile() { const saved = await this.downloadFile( "project", this.getSaveFileName(), await KarlkoderCodec.en( this.projectName.value, this.editor.getValue(), await this.assetEditor.getAssets(), ), ".karlkode", "application/x-karlkode", ); this.isSaved = saved; if (globalThis.chrome) { this.showSavedStatus(saved, saved ? "Saved" : "Not saved"); } } async exportProject() { const html = await this.htmlExporter.export(this.projectName.value, this.editor.getValue()); const exported = await this.downloadFile( "export", this.getSaveFileName(), html, ".html", "text/html", ); if (globalThis.chrome) { this.showSavedStatus(exported, exported ? "Exported" : "Not exported"); } } getSaveFileName() { if (this.saveName) { return this.saveName; } return slugify(this.projectName.value) || "project"; } showSavedStatus(success, status) { const saveStatus = document.querySelector("#save-status"); saveStatus.textContent = (success ? "✓ " : "× ") + status; saveStatus.style.backgroundColor = success ? "#43A047" : "#E53935"; saveStatus.style.transition = ""; saveStatus.style.opacity = 1; setTimeout(() => { saveStatus.style.transition = "opacity 1s ease-out"; saveStatus.style.opacity = 0; }, 1500); } async downloadFile(id, name, content, extension, mime) { if ("showSaveFilePicker" in globalThis) { try { this.fileHandles[id] ??= await globalThis.showSaveFilePicker({ id: "karlkoder", startIn: "documents", suggestedName: name + extension, types: [{ accept: { [mime]: [extension], }, }], }); const stream = await this.fileHandles[id].createWritable(); await stream.write(content); await stream.close(); return true; } catch (e) { // Throws AbortError or NotAllowedError on cancel console.log("file picker error", e); this.fileHandles[id] = null; // Ask for new file handle next time return false; } } const filename = prompt("Filename?", name); if (!filename) { return false; } this.saveName = filename; const blob = new Blob([content], { type: mime }); const url = URL.createObjectURL(blob); const element = document.createElement("a"); element.href = url; element.download = filename.endsWith(extension) ? filename : filename + extension; element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); return true; } }