junebug12851/pokered-save-editor

View on GitHub
electron/SaveFile.js

Summary

Maintainability
A
0 mins
Test Coverage
const BluePromise = require("bluebird");
const fs = require("fs");
const electron = require("electron");
const { dialog } = electron;
const fs2 = BluePromise.promisifyAll(fs);
const _ = require("lodash");
 
module.exports = class SaveFile {
constructor(app) {
this.app = app;
 
// Init empty path and data
this.onPathChange();
this.onDataChange();
 
this.app.on("menu-clearRecentDocs", this.clearRecentDocs.bind(this));
this.app.on("menu-openRecentDocs", this.openRecentDocs.bind(this));
this.app.on("menu-open", this.openFile.bind(this));
this.app.on("menu-reopen", this.reOpenFile.bind(this));
this.app.on("menu-new", this.closeFile.bind(this));
this.app.on("menu-save", this.saveFile.bind(this));
this.app.on("menu-saveAs", this.saveAsFile.bind(this));
this.app.on("menu-saveAsCopy", this.saveAsCopyFile.bind(this));
this.app.on("menu-wipe", this.wipeUnusedSpace.bind(this));
 
this.app.on("ipcFrom-dataUpdate", this.onDataChange.bind(this));
 
this.app.on("window-ready", this.onWindowReady.bind(this));
}
 
onWindowReady() {
this.onPathChange(this.filePath);
}
 
onPathChange(path = "") {
if (path !== "")
this.addRecentDocument(path);
 
this.filePath = path;
this.app.emit("ipcTo", "pathChange", path);
}
 
/**
* This should be the only method to be called to change data
* @param {Uint8Array} data New data to apply
* @param {boolean} fromRender Is this data from the render?
* @param {boolean} internalOnly Should this data silently change?
*/
onDataChange(data = new Uint8Array(0x8000), fromRender = false, internalOnly = false) {
this.fileData = data;
 
if (!fromRender)
this.app.emit("ipcTo", "dataChange", data, internalOnly);
 
if (fromRender && this.pendingSave)
this._writeSaveFile();
}
 
// Adds a file to the list of recent documents which is persistent
// Only 10 unique non-duplicate files are kept meaning each entry will be
// a different file, the samefile will remain in the same slot
// They are added to the top, oldest at the bottom
// They are accessible via CommandOrControl+Shift+# <0-9>
addRecentDocument(path) {
let recentDocs = this.app.store.get('recentDocs', []);
recentDocs.unshift(path);
recentDocs = _.uniq(recentDocs);
if (recentDocs.length > 10)
recentDocs.length = 10;
this.app.store.set('recentDocs', recentDocs);
}
 
openRecentDocs(index) {
let recentDocs = this.app.store.get('recentDocs', []);
this.readSaveFile(recentDocs[index]);
}
 
// Handles Open File Dialog
// We want this to use es6 async/await and since it never throws an error
// we can't use Bluebird so we need to promisfy it manually
openOpenFileDialog(title) {
return new Promise((resolve) => {
dialog.showOpenDialog({
title,
buttonLabel: "Open",
filters: [
{ name: 'SAV Files', extensions: ['sav'] },
{ name: 'All Files', extensions: ['*'] },
],
properties: [
"openFile",
"treatPackageAsDirectory",
],
}, (files) => {
resolve(files);
});
});
}
 
// Handles Save File Dialog
// We want this to use es6 async/await and since it never throws an error
// we can't use Bluebird so we need to promisfy it manually
openSaveFileDialog(title) {
return new Promise((resolve) => {
dialog.showSaveDialog({
title,
buttonLabel: "Save",
filters: [
{ name: 'SAV Files', extensions: ['sav'] },
{ name: 'All Files', extensions: ['*'] },
],
}, (file) => {
resolve(file);
});
});
}
 
// Handles loading file into memory
async readSaveFile(filePath) {
// Read file
const data = await fs2.readFileAsync(filePath);
 
this.onDataChange(data);
this.onPathChange(filePath);
}
 
// Cache and save requested data update
async _writeSaveFile() {
await fs2.writeFileAsync(this.pendingSave, this.fileData);
this.pendingSave = null;
}
 
// Request data update
async writeSaveFile(_filePath = this.filePath) {
this.pendingSave = _filePath;
this.app.emit("ipcTo", "dataUpdate");
}
 
clearRecentDocs() {
this.app.store.set('recentDocs', []);
}
 
// Initiates an open file dialog to open save file
async openFile() {
const fileNames = await this.openOpenFileDialog("Open Save File");
 
if (fileNames === undefined) {
return;
}
 
const filePath = fileNames[0];
await this.readSaveFile(filePath);
}
 
// Reloads file from disk erasing unsaved changes, if no open file is
// present just resets buffer
async reOpenFile() {
// If theres no open file then reload an empty array
if (this.filePath === "") {
this.onDataChange();
return;
}
 
await this.readSaveFile(this.filePath);
}
 
// Closes file in memory and erases buffer
closeFile() {
this.onDataChange();
this.onPathChange();
}
 
// Save file
async saveFile() {
if (this.filePath === "") {
await this.saveAsFile();
return;
}
 
await this.writeSaveFile();
}
 
// Save file as...
async saveAsFile() {
const fileName = await this.openSaveFileDialog("Save File As...");
 
if (fileName === undefined) {
return;
}
 
await this.saveFile();
this.onPathChange(fileName);
}
 
// Save copy of file
async saveAsCopyFile() {
const fileName = await this.openSaveFileDialog("Save Copy As...");
 
if (fileName === undefined) {
return;
}
 
await this.writeSaveFile(fileName);
}
 
// This erases the raw internal data completely leaving the file all zeroes
// Normally the expanded copy will overwrite only the used bytes and leave
// everything else as-is however with this method the expanded copy will
// still do the same but will be writing back to a clean slate thus blasting
// away all unused values
wipeUnusedSpace() {
// Mark internal only to be true so that the render doesn't treat it like
// a new file, all existing data is kept
this.onDataChange(undefined, false, true);
}
}