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">
|
<button id="load-button" class="secondary">
|
||||||
📄 Load
|
📄 Load
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<span id="save-status"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="export-button">
|
<button id="export-button">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { promptUpload } from "./prompt_upload.js";
|
import { promptUpload } from "./prompt_upload.js";
|
||||||
import { downloadFile, slugify } from "./utils.js";
|
import { slugify } from "./utils.js";
|
||||||
import { HtmlExporter } from "./html_exporter.js";
|
import { HtmlExporter } from "./html_exporter.js";
|
||||||
import { KarlkoderCodec } from "./karlkoder_codec.js";
|
import { KarlkoderCodec } from "./karlkoder_codec.js";
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ export class ProjectSaveHandler {
|
|||||||
|
|
||||||
this.htmlExporter = new HtmlExporter(assetProvider);
|
this.htmlExporter = new HtmlExporter(assetProvider);
|
||||||
|
|
||||||
|
this.fileHandles = {}; // Used for Chromium file picker
|
||||||
this.saveName = null;
|
this.saveName = null;
|
||||||
this.isSaved = true;
|
this.isSaved = true;
|
||||||
|
|
||||||
@ -78,36 +79,39 @@ export class ProjectSaveHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveFile() {
|
async saveFile() {
|
||||||
const filename = prompt("Filename?", this.getSaveFileName());
|
const saved = await this.downloadFile(
|
||||||
if (!filename) {
|
"project",
|
||||||
return;
|
this.getSaveFileName(),
|
||||||
}
|
|
||||||
|
|
||||||
this.saveName = filename;
|
|
||||||
this.isSaved = true;
|
|
||||||
|
|
||||||
downloadFile(
|
|
||||||
filename,
|
|
||||||
await KarlkoderCodec.en(
|
await KarlkoderCodec.en(
|
||||||
this.projectName.value,
|
this.projectName.value,
|
||||||
this.editor.getValue(),
|
this.editor.getValue(),
|
||||||
await this.assetEditor.getAssets(),
|
await this.assetEditor.getAssets(),
|
||||||
),
|
),
|
||||||
".karlkode",
|
".karlkode",
|
||||||
|
"application/x-karlkode",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.isSaved = saved;
|
||||||
|
|
||||||
|
if (globalThis.chrome) {
|
||||||
|
this.showSavedStatus(saved, saved ? "Saved" : "Not saved");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportProject() {
|
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());
|
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() {
|
getSaveFileName() {
|
||||||
@ -117,4 +121,72 @@ export class ProjectSaveHandler {
|
|||||||
|
|
||||||
return slugify(this.projectName.value) || "project";
|
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) {
|
export function slugify(text) {
|
||||||
return text
|
return text
|
||||||
.split(/\W+/)
|
.split(/\W+/)
|
||||||
|
39
style.css
39
style.css
@ -221,32 +221,6 @@ div#buttons button {
|
|||||||
box-shadow: none;
|
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 {
|
button {
|
||||||
background-color: #087aaf;
|
background-color: #087aaf;
|
||||||
border: none;
|
border: none;
|
||||||
@ -262,6 +236,10 @@ button:hover {
|
|||||||
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #06577e;
|
||||||
|
}
|
||||||
|
|
||||||
button.secondary {
|
button.secondary {
|
||||||
background-color: #616161;
|
background-color: #616161;
|
||||||
}
|
}
|
||||||
@ -415,6 +393,7 @@ footer {
|
|||||||
|
|
||||||
#project-header-left {
|
#project-header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,6 +414,14 @@ footer {
|
|||||||
outline: none;
|
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 {
|
#export-button {
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user