196 lines
5.5 KiB
JavaScript
196 lines
5.5 KiB
JavaScript
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;
|
||
}
|
||
}
|