blob: 3bc341f3e3ce476ff9a278b173480207cc544aeb [file] [log] [blame] [raw]
// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import $ from 'jquery';
import _ from 'underscore';
import {saveAs} from 'file-saver';
import {Alert} from './alert.js';
import {ga} from '../analytics.js';
import * as local from '../local.js';
import {Language} from '../../types/languages.interfaces.js';
import {unwrap, unwrapString} from '../assert.js';
const history = require('../history');
type PopulateItem = {name: string; load: () => void; delete?: () => void; overwrite?: () => void};
export class LoadSave {
private modal: JQuery | null = null;
private alertSystem: Alert;
private onLoadCallback: (...any) => void = _.identity;
private editorText = '';
private extension = '.txt';
private base: string;
private currentLanguage: Language | null;
constructor() {
this.alertSystem = new Alert();
this.alertSystem.prefixMessage = 'Load-Saver';
this.base = window.httpRoot;
this.fetchBuiltins()
.then(() => {})
.catch(() => {});
}
public static getLocalFiles(): Record<string, string> {
return JSON.parse(local.get('files', '{}'));
}
public static setLocalFile(name: string, file: string) {
const files = LoadSave.getLocalFiles();
files[name] = file;
local.set('files', JSON.stringify(files));
}
public static removeLocalFile(name: string) {
const files = LoadSave.getLocalFiles();
if (name in files) {
delete files[name];
}
local.set('files', JSON.stringify(files));
}
private async fetchBuiltins(): Promise<Record<string, any>[]> {
return new Promise<Record<string, any>[]>(resolve => {
$.getJSON(window.location.origin + this.base + 'source/builtin/list', resolve);
});
}
public initializeIfNeeded() {
if (this.modal === null || this.modal.length === 0) {
this.modal = $('#load-save');
this.modal.find('.local-file').on('change', e => this.onLocalFile(e));
this.modal.find('.save-button').on('click', () => this.onSaveToBrowserStorage());
this.modal.find('.save-file').on('click', () => this.onSaveToFile());
}
}
private onLoad(data: string, name?: string) {
this.onLoadCallback(data, name);
}
private doLoad(element) {
$.getJSON(
window.location.origin + this.base + 'source/builtin/load/' + element.lang + '/' + element.file,
response => this.onLoad(response.file),
);
this.modal?.modal('hide');
}
private static populate(root: JQuery, list: PopulateItem[]) {
root.find('li:not(.template)').remove();
const template = root.find('.template');
for (const elem of list) {
const clone = template.clone();
clone.removeClass('template').appendTo(root).find('a').text(elem.name).on('click', elem.load);
const deleteButton = clone.find('button.delete');
if (elem.delete !== undefined) {
deleteButton.on('click', () => elem.delete?.());
}
const overwriteButton = clone.find('button.overwrite');
if (elem.overwrite !== undefined) {
overwriteButton.on('click', () => elem.overwrite?.());
}
}
}
private async populateBuiltins() {
const builtins = (await this.fetchBuiltins()).filter(entry => this.currentLanguage?.id === entry.lang);
return LoadSave.populate(
unwrap(this.modal).find('.examples'),
builtins.map(elem => {
return {
name: elem.name,
load: () => this.doLoad(elem),
};
}),
);
}
private populateLocalStorage() {
const files = LoadSave.getLocalFiles();
const keys = Object.keys(files);
LoadSave.populate(
unwrap(this.modal).find('.local-storage'),
keys.map(name => {
const data = files[name];
return {
name: name,
load: () => {
this.onLoad(data);
this.modal?.modal('hide');
},
delete: () => {
this.alertSystem.ask(
`Delete ${_.escape(name)}?`,
`Do you want to delete '${_.escape(name)}'?`,
{
yes: () => {
LoadSave.removeLocalFile(name);
this.populateLocalStorage();
},
},
);
},
overwrite: () => {
this.alertSystem.ask(
`Overwrite ${_.escape(name)}?`,
`Do you want to overwrite '${_.escape(name)}'?`,
{
yes: () => {
LoadSave.setLocalFile(name, this.editorText);
this.populateLocalStorage();
},
},
);
},
};
}),
);
}
private populateLocalHistory() {
LoadSave.populate(
unwrap(this.modal).find('.local-history'),
history.sources(unwrap(this.currentLanguage).id).map(data => {
const dt = new Date(data.dt).toString();
return {
name: dt.replace(/\s\(.*\)/, ''),
load: () => {
this.onLoad(data.source);
this.modal?.modal('hide');
},
};
}),
);
}
// From https://developers.google.com/web/updates/2014/08/Easier-ArrayBuffer-String-conversion-with-the-Encoding-API
private static ab2str(buf) {
const dataView = new DataView(buf);
// The TextDecoder interface is documented at http://encoding.spec.whatwg.org/#interface-textdecoder
const decoder = new TextDecoder('utf-8');
return decoder.decode(dataView);
}
private onLocalFile(event: JQuery.ChangeEvent) {
const files = event.target.files;
if (files.length !== 0) {
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
let result: string;
if (reader.result instanceof ArrayBuffer) {
result = LoadSave.ab2str(reader.result);
} else {
result = reader.result ?? '';
}
this.onLoad(result, file.name);
};
reader.readAsText(file);
}
this.modal?.modal('hide');
}
public run(onLoad, editorText, currentLanguage: Language) {
this.initializeIfNeeded();
this.populateLocalStorage();
this.setMinimalOptions(editorText, currentLanguage);
this.populateLocalHistory();
this.onLoadCallback = onLoad;
unwrap(this.modal).find('.local-file').attr('accept', currentLanguage.extensions.join(','));
this.populateBuiltins().then(() => this.modal?.modal());
ga.proxy('send', {
hitType: 'event',
eventCategory: 'OpenModalPane',
eventAction: 'LoadSave',
});
}
private onSaveToBrowserStorage() {
const saveNameValue = unwrapString(this.modal?.find('.save-name').val());
if (!saveNameValue) {
this.alertSystem.alert('Save name', 'Invalid save name', {isError: true});
return;
}
const name = `${saveNameValue} (${this.currentLanguage?.name ?? ''})`;
const doneCallback = () => {
LoadSave.setLocalFile(name, this.editorText);
};
if (name in LoadSave.getLocalFiles()) {
this.modal?.modal('hide');
this.alertSystem.ask(
'Replace current?',
`Do you want to replace the existing saved file '${_.escape(name)}'?`,
{yes: doneCallback},
);
} else {
doneCallback();
this.modal?.modal('hide');
}
}
setMinimalOptions(editorText: string, currentLanguage: Language) {
this.editorText = editorText;
this.currentLanguage = currentLanguage;
this.extension = currentLanguage.extensions[0] || '.txt';
}
onSaveToFile(fileEditor?: string) {
try {
const fileLang = this.currentLanguage?.name ?? '';
const name = fileLang && fileEditor !== undefined ? fileLang + ' Editor #' + fileEditor + ' ' : '';
saveAs(
new Blob([this.editorText], {type: 'text/plain;charset=utf-8'}),
'Compiler Explorer ' + name + 'Code' + this.extension,
);
return true;
} catch (e) {
this.alertSystem.notify('Error while saving your code. Use the clipboard instead.', {
group: 'savelocalerror',
alertClass: 'notification-error',
dismissTime: 5000,
});
return false;
}
}
}