use chromium save file picker when available
This commit is contained in:
parent
f2b5a8d257
commit
d4dcd9bc28
@ -71,6 +71,8 @@
|
||||
<button id="load-button" class="secondary">
|
||||
📄 Load
|
||||
</button>
|
||||
|
||||
<span id="save-status"></span>
|
||||
</div>
|
||||
|
||||
<button id="export-button">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { promptUpload } from "./prompt_upload.js";
|
||||
import { downloadFile, slugify } from "./utils.js";
|
||||
import { slugify } from "./utils.js";
|
||||
import { HtmlExporter } from "./html_exporter.js";
|
||||
import { KarlkoderCodec } from "./karlkoder_codec.js";
|
||||
|
||||
@ -11,6 +11,7 @@ export class ProjectSaveHandler {
|
||||
|
||||
this.htmlExporter = new HtmlExporter(assetProvider);
|
||||
|
||||
this.fileHandles = {}; // Used for Chromium file picker
|
||||
this.saveName = null;
|
||||
this.isSaved = true;
|
||||
|
||||
@ -78,36 +79,39 @@ export class ProjectSaveHandler {
|
||||
}
|
||||
|
||||
async saveFile() {
|
||||
const filename = prompt("Filename?", this.getSaveFileName());
|
||||
if (!filename) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveName = filename;
|
||||
this.isSaved = true;
|
||||
|
||||
downloadFile(
|
||||
filename,
|
||||
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 filename = prompt("Filename?", this.getSaveFileName());
|
||||
if (!filename) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveName = filename;
|
||||
|
||||
const html = await this.htmlExporter.export(this.projectName.value, this.editor.getValue());
|
||||
|
||||
downloadFile(filename, html, ".html", "text/html");
|
||||
const exported = await this.downloadFile(
|
||||
"export",
|
||||
this.getSaveFileName(),
|
||||
html,
|
||||
".html",
|
||||
"text/html",
|
||||
);
|
||||
|
||||
if (globalThis.chrome) {
|
||||
this.showSavedStatus(exported, exported ? "Exported" : "Not exported");
|
||||
}
|
||||
}
|
||||
|
||||
getSaveFileName() {
|
||||
@ -117,4 +121,72 @@ export class ProjectSaveHandler {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
17
src/utils.js
17
src/utils.js
@ -1,20 +1,3 @@
|
||||
export function downloadFile(filename, content, extension, mime) {
|
||||
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);
|
||||
}
|
||||
|
||||
export function slugify(text) {
|
||||
return text
|
||||
.split(/\W+/)
|
||||
|
39
style.css
39
style.css
@ -221,32 +221,6 @@ div#buttons button {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
padding: 0.3rem 0.6rem;
|
||||
color: #424242;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-option:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #087aaf;
|
||||
border: none;
|
||||
@ -262,6 +236,10 @@ button:hover {
|
||||
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #06577e;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background-color: #616161;
|
||||
}
|
||||
@ -415,6 +393,7 @@ footer {
|
||||
|
||||
#project-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@ -435,6 +414,14 @@ footer {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#save-status {
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.125rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
#export-button {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user