karlkoder-playground/src/project_save_handler.js

196 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}