remove: eliminate file management and LittleFS-based filesystem, implement savegame management for JSON config slots with wear-leveling
This commit is contained in:
@@ -34,6 +34,11 @@ export interface NetworkConfig {
|
||||
max_wait: number
|
||||
}
|
||||
|
||||
export interface SaveInfo {
|
||||
idx: number,
|
||||
len: number,
|
||||
}
|
||||
|
||||
export interface FileList {
|
||||
total: number,
|
||||
used: number,
|
||||
|
||||
@@ -29,44 +29,10 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="subtitle">Files:</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Total Size</div>
|
||||
<div id="filetotalsize" class="filevalue"></div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Used Size</div>
|
||||
<div id="fileusedsize" class="filevalue"></div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Free Size</div>
|
||||
<div id="filefreesize" class="filevalue"></div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
|
||||
<div class="subtitle" >Upload:</div>
|
||||
</div>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double;">
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">
|
||||
File:
|
||||
</div>
|
||||
<input id="fileuploadfile" class="filevalue" type="file">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">
|
||||
Name:
|
||||
</div>
|
||||
<input id="fileuploadname" class="filevalue" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
|
||||
<button id="fileuploadbtn" class="subtitle">Upload</button>
|
||||
</div>
|
||||
<div class="subtitle">Save Slots:</div>
|
||||
<br>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
|
||||
<div class="subtitle">List:</div>
|
||||
</div>
|
||||
<div id="fileList" class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,96 +1,49 @@
|
||||
import {Controller} from "./main";
|
||||
import {FileInfo, FileList} from "./api";
|
||||
const regex = /[^a-zA-Z0-9_.]/g;
|
||||
|
||||
function sanitize(str:string){
|
||||
return str.replaceAll(regex, '_')
|
||||
}
|
||||
import {SaveInfo} from "./api";
|
||||
|
||||
export class FileView {
|
||||
readonly fileListView: HTMLElement;
|
||||
readonly controller: Controller;
|
||||
readonly filefreesize: HTMLElement;
|
||||
readonly filetotalsize: HTMLElement;
|
||||
readonly fileusedsize: HTMLElement;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
(document.getElementById("fileview") as HTMLElement).innerHTML = require('./fileview.html') as string;
|
||||
this.fileListView = document.getElementById("fileList") as HTMLElement
|
||||
this.filefreesize = document.getElementById("filefreesize") as HTMLElement
|
||||
this.filetotalsize = document.getElementById("filetotalsize") as HTMLElement
|
||||
this.fileusedsize = document.getElementById("fileusedsize") as HTMLElement
|
||||
|
||||
let fileuploadfile = document.getElementById("fileuploadfile") as HTMLInputElement
|
||||
let fileuploadname = document.getElementById("fileuploadname") as HTMLInputElement
|
||||
let fileuploadbtn = document.getElementById("fileuploadbtn") as HTMLInputElement
|
||||
fileuploadfile.onchange = () => {
|
||||
const selectedFile = fileuploadfile.files?.[0];
|
||||
if (selectedFile == null) {
|
||||
//TODO error dialog here
|
||||
return
|
||||
}
|
||||
|
||||
fileuploadname.value = sanitize(selectedFile.name)
|
||||
};
|
||||
|
||||
fileuploadname.onchange = () => {
|
||||
let input = fileuploadname.value
|
||||
let clean = sanitize(fileuploadname.value)
|
||||
if (input != clean){
|
||||
fileuploadname.value = clean
|
||||
}
|
||||
}
|
||||
|
||||
fileuploadbtn.onclick = () => {
|
||||
const selectedFile = fileuploadfile.files?.[0];
|
||||
if (selectedFile == null) {
|
||||
//TODO error dialog here
|
||||
return
|
||||
}
|
||||
controller.uploadFile(selectedFile, selectedFile.name)
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.fileListView = document.getElementById("fileList") as HTMLElement;
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
setFileList(fileList: FileList, public_url: string) {
|
||||
this.filetotalsize.innerText = Math.floor(fileList.total / 1024) + "kB"
|
||||
this.fileusedsize.innerText = Math.ceil(fileList.used / 1024) + "kB"
|
||||
this.filefreesize.innerText = Math.ceil((fileList.total - fileList.used) / 1024) + "kB"
|
||||
setSaveList(saves: SaveInfo[], public_url: string) {
|
||||
// Sort newest first (highest index = most recently written slot)
|
||||
const sorted = saves.slice().sort((a, b) => b.idx - a.idx);
|
||||
|
||||
//fast clear
|
||||
this.fileListView.textContent = ""
|
||||
for (let i = 0; i < fileList.files.length; i++) {
|
||||
let file = fileList.files[i]
|
||||
new FileEntry(this.controller, i, file, this.fileListView, public_url);
|
||||
this.fileListView.textContent = "";
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
new SaveEntry(this.controller, i, sorted[i], this.fileListView, public_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileEntry {
|
||||
class SaveEntry {
|
||||
view: HTMLElement;
|
||||
constructor(controller: Controller, fileid: number, fileinfo: FileInfo, parent: HTMLElement, public_url: string) {
|
||||
this.view = document.createElement("div") as HTMLElement
|
||||
parent.appendChild(this.view)
|
||||
this.view.classList.add("fileentryouter")
|
||||
constructor(controller: Controller, fileid: number, saveinfo: SaveInfo, parent: HTMLElement, public_url: string) {
|
||||
this.view = document.createElement("div") as HTMLElement;
|
||||
parent.appendChild(this.view);
|
||||
this.view.classList.add("fileentryouter");
|
||||
|
||||
const template = require('./fileviewentry.html') as string;
|
||||
this.view.innerHTML = template.replaceAll("${fileid}", String(fileid))
|
||||
this.view.innerHTML = template.replaceAll("${fileid}", String(fileid));
|
||||
|
||||
let name = document.getElementById("file_" + fileid + "_name") as HTMLElement;
|
||||
let size = document.getElementById("file_" + fileid + "_size") as HTMLElement;
|
||||
let deleteBtn = document.getElementById("file_" + fileid + "_delete") as HTMLButtonElement;
|
||||
deleteBtn.onclick = () => {
|
||||
controller.deleteFile(fileinfo.filename);
|
||||
}
|
||||
controller.deleteSlot(saveinfo.idx);
|
||||
};
|
||||
|
||||
let downloadBtn = document.getElementById("file_" + fileid + "_download") as HTMLAnchorElement;
|
||||
downloadBtn.href = public_url + "/file?filename=" + fileinfo.filename
|
||||
downloadBtn.download = fileinfo.filename
|
||||
downloadBtn.href = public_url + "/get_config?saveidx=" + saveinfo.idx;
|
||||
downloadBtn.download = "config_slot_" + saveinfo.idx + ".json";
|
||||
|
||||
name.innerText = fileinfo.filename;
|
||||
size.innerText = fileinfo.size.toString()
|
||||
name.innerText = "Slot " + saveinfo.idx;
|
||||
size.innerText = saveinfo.len + " bytes";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<div class="flexcontainer">
|
||||
<div id="file_${fileid}_name" class="filetitle">Name</div>
|
||||
<div id="file_${fileid}_name" class="filetitle">Slot</div>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Size</div>
|
||||
<div id = "file_${fileid}_size" class="filevalue"></div>
|
||||
<a id = "file_${fileid}_download" class="filevalue" target="_blank">Download</a>
|
||||
<button id = "file_${fileid}_delete" class="filevalue">Delete</button>
|
||||
<div id="file_${fileid}_size" class="filevalue"></div>
|
||||
<a id="file_${fileid}_download" class="filevalue" target="_blank">Download</a>
|
||||
<button id="file_${fileid}_delete" class="filevalue">Delete</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
SetTime, SSIDList, TankInfo,
|
||||
TestPump,
|
||||
VersionInfo,
|
||||
FileList, SolarState, PumpTestResult, Detection, CanPower
|
||||
SaveInfo, SolarState, PumpTestResult, Detection, CanPower
|
||||
} from "./api";
|
||||
import {SolarView} from "./solarview";
|
||||
import {toast} from "./toast";
|
||||
@@ -93,65 +93,36 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
async updateFileList(): Promise<void> {
|
||||
async updateSaveList(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(PUBLIC_URL + "/files");
|
||||
const response = await fetch(PUBLIC_URL + "/list_saves");
|
||||
const json = await response.json();
|
||||
const filelist = json as FileList;
|
||||
controller.fileview.setFileList(filelist, PUBLIC_URL);
|
||||
const saves = json as SaveInfo[];
|
||||
controller.fileview.setSaveList(saves, PUBLIC_URL);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
uploadFile(file: File, name: string) {
|
||||
let current = 0;
|
||||
let max = 100;
|
||||
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
|
||||
deleteSlot(idx: number) {
|
||||
controller.progressview.addIndeterminate("slot_delete", "Deleting slot " + idx);
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", event => {
|
||||
current = event.loaded / 1000;
|
||||
max = event.total / 1000;
|
||||
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("file_upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.addEventListener("error", () => {
|
||||
alert("Error upload")
|
||||
controller.progressview.removeProgress("file_upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
alert("abort upload")
|
||||
controller.progressview.removeProgress("file_upload")
|
||||
controller.updateFileList()
|
||||
}, false);
|
||||
ajax.open("POST", PUBLIC_URL + "/file?filename=" + name);
|
||||
ajax.send(file);
|
||||
}
|
||||
|
||||
deleteFile(name: string) {
|
||||
controller.progressview.addIndeterminate("file_delete", "Deleting " + name);
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.open("DELETE", PUBLIC_URL + "/file?filename=" + name);
|
||||
ajax.open("DELETE", PUBLIC_URL + "/delete_save?idx=" + idx);
|
||||
ajax.send();
|
||||
ajax.addEventListener("error", () => {
|
||||
controller.progressview.removeProgress("file_delete")
|
||||
alert("Error delete")
|
||||
controller.updateFileList()
|
||||
controller.progressview.removeProgress("slot_delete");
|
||||
alert("Error deleting slot");
|
||||
controller.updateSaveList();
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
controller.progressview.removeProgress("file_delete")
|
||||
alert("Error upload")
|
||||
controller.updateFileList()
|
||||
controller.progressview.removeProgress("slot_delete");
|
||||
alert("Aborted deleting slot");
|
||||
controller.updateSaveList();
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("file_delete")
|
||||
controller.updateFileList()
|
||||
controller.progressview.removeProgress("slot_delete");
|
||||
controller.updateSaveList();
|
||||
}, false);
|
||||
controller.updateFileList()
|
||||
}
|
||||
|
||||
async updateRTCData(): Promise<void> {
|
||||
@@ -668,7 +639,7 @@ const tasks = [
|
||||
{task: controller.updateSolarData, displayString: "Updating Solar Data"},
|
||||
{task: controller.downloadConfig, displayString: "Downloading Configuration"},
|
||||
{task: controller.version, displayString: "Fetching Version Information"},
|
||||
{task: controller.updateFileList, displayString: "Updating File List"},
|
||||
{task: controller.updateSaveList, displayString: "Updating Save Slots"},
|
||||
{task: controller.getBackupInfo, displayString: "Fetching Backup Information"},
|
||||
{task: controller.loadLogLocaleConfig, displayString: "Loading Log Localization Config"},
|
||||
{task: controller.loadTankInfo, displayString: "Loading Tank Information"},
|
||||
|
||||
Reference in New Issue
Block a user