Skip to content

Commit

Permalink
Added song recovery feature to file menu.
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnesky committed Mar 27, 2020
1 parent b83ef12 commit 17c5e9f
Show file tree
Hide file tree
Showing 15 changed files with 427 additions and 118 deletions.
2 changes: 1 addition & 1 deletion editor/BeatsPerBarPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ namespace beepbox {
private _saveChanges = (): void => {
window.localStorage.setItem("beatCountStrategy", this._conversionStrategySelect.value);
this._doc.prompt = null;
this._doc.record(new ChangeBeatsPerBar(this._doc, BeatsPerBarPrompt._validate(this._beatsStepper), this._conversionStrategySelect.value), "replace");
this._doc.record(new ChangeBeatsPerBar(this._doc, BeatsPerBarPrompt._validate(this._beatsStepper), this._conversionStrategySelect.value), StateChangeType.replace);
}
}
}
4 changes: 2 additions & 2 deletions editor/ChannelSettingsPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ namespace beepbox {

private static _validateNumber(event: Event): void {
const input: HTMLInputElement = <HTMLInputElement>event.target;
input.value = String(this._validate(input));
input.value = String(ChannelSettingsPrompt._validate(input));
}

private static _validate(input: HTMLInputElement): number {
Expand All @@ -122,7 +122,7 @@ namespace beepbox {
group.append(new ChangeInstrumentsPerChannel(this._doc, ChannelSettingsPrompt._validate(this._instrumentsStepper)));
group.append(new ChangeChannelCount(this._doc, ChannelSettingsPrompt._validate(this._pitchChannelStepper), ChannelSettingsPrompt._validate(this._drumChannelStepper)));
this._doc.prompt = null;
this._doc.record(group, "replace");
this._doc.record(group, StateChangeType.replace);
}
}
}
2 changes: 1 addition & 1 deletion editor/EditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace beepbox {
export const isMobile: boolean = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|android|ipad|playbook|silk/i.test(navigator.userAgent);

export class EditorConfig {
public static readonly version: string = "3.0.8";
public static readonly version: string = "3.0.9";
public static readonly versionDisplayName: string = "BeepBox " + EditorConfig.version;
public static readonly presetCategories: DictionaryArray<PresetCategory> = toNameMap([
{name: "Custom Instruments", presets: <DictionaryArray<Preset>> toNameMap([
Expand Down
4 changes: 2 additions & 2 deletions editor/ImportPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ namespace beepbox {
reader.addEventListener("load", (event: Event): void => {
this._doc.prompt = null;
this._doc.goBackToStart();
this._doc.record(new ChangeSong(this._doc, <string>reader.result), "replace");
this._doc.record(new ChangeSong(this._doc, <string>reader.result), StateChangeType.replace, true);
});
reader.readAsText(file);
} else if (extension == "midi" || extension == "mid") {
Expand Down Expand Up @@ -858,7 +858,7 @@ namespace beepbox {
this._doc.goBackToStart();
for (const channel of this._doc.song.channels) channel.muted = false;
this._doc.prompt = null;
this._doc.record(new ChangeImportMidi(this._doc), "replace");
this._doc.record(new ChangeImportMidi(this._doc), StateChangeType.replace, true);
}
}
}
2 changes: 1 addition & 1 deletion editor/MoveNotesSidewaysPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ namespace beepbox {
private _saveChanges = (): void => {
window.localStorage.setItem("moveNotesSidewaysStrategy", this._conversionStrategySelect.value);
this._doc.prompt = null;
this._doc.record(new ChangeMoveNotesSideways(this._doc, +this._beatsStepper.value, this._conversionStrategySelect.value), "replace");
this._doc.record(new ChangeMoveNotesSideways(this._doc, +this._beatsStepper.value, this._conversionStrategySelect.value), StateChangeType.replace);
}
}
}
4 changes: 2 additions & 2 deletions editor/OctaveScrollBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,12 @@ namespace beepbox {
if (this._mouseY < this._barBottom - this._barHeight * 0.5) {
if (currentOctave < Config.scrollableOctaves) {
this._change = new ChangeOctave(this._doc, oldValue, currentOctave + 1);
this._doc.record(this._change, canReplaceLastChange ? "replace" : "push");
this._doc.record(this._change, canReplaceLastChange ? StateChangeType.replace : StateChangeType.push);
}
} else {
if (currentOctave > 0) {
this._change = new ChangeOctave(this._doc, oldValue, currentOctave - 1);
this._doc.record(this._change, canReplaceLastChange ? "replace" : "push");
this._doc.record(this._change, canReplaceLastChange ? StateChangeType.replace : StateChangeType.push);
}
}
}
Expand Down
71 changes: 47 additions & 24 deletions editor/SongDocument.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (C) 2020 John Nesky, distributed under the MIT license.

/// <reference path="../synth/synth.ts" />
/// <reference path="SongRecovery.ts" />
/// <reference path="EditorConfig.ts" />
/// <reference path="ChangeNotifier.ts" />

Expand All @@ -10,10 +11,16 @@ namespace beepbox {
sequenceNumber: number;
bar: number;
channel: number;
recoveryUid: string;
prompt: string | null;
}

type StateChangeType = "replace" | "push" | "jump";
export const enum StateChangeType {
replace = 0,
push,
jump,
length,
}

export class SongDocument {
public song: Song;
Expand All @@ -39,9 +46,12 @@ namespace beepbox {
public prompt: string | null = null;

private static readonly _maximumUndoHistory: number = 100;
private _recovery: SongRecovery = new SongRecovery();
private _recoveryUid: string;
private _recentChange: Change | null = null;
private _sequenceNumber: number = 0;
private _stateChangeType: StateChangeType = "replace";
private _stateChangeType: StateChangeType = StateChangeType.replace;
private _recordedNewSong: boolean = false;
private _barFromCurrentState: number = 0;
private _channelFromCurrentState: number = 0;
private _waitingToUpdateState: boolean = false;
Expand Down Expand Up @@ -86,16 +96,18 @@ namespace beepbox {
let state: HistoryState | null = this._getHistoryState();
if (state == null) {
// When the page is first loaded, indicate that undo is NOT possible.
state = {canUndo: false, sequenceNumber: 0, bar: 0, channel: 0, prompt: null};
state = {canUndo: false, sequenceNumber: 0, bar: 0, channel: 0, recoveryUid: generateUid(), prompt: null};
}
if (state.recoveryUid == undefined) state.recoveryUid = generateUid();
this._replaceState(state, songString);
window.addEventListener("hashchange", this._whenUrlHashChanged);
window.addEventListener("hashchange", this._whenHistoryStateChanged);
window.addEventListener("popstate", this._whenHistoryStateChanged);

this.bar = state.bar;
this.channel = state.channel;
this._barFromCurrentState = state.bar;
this._channelFromCurrentState = state.channel;
this._recoveryUid = state.recoveryUid;
this.barScrollPos = Math.max(0, this.bar - (this.trackVisibleBars - 6));
this.prompt = state.prompt;

Expand Down Expand Up @@ -187,27 +199,27 @@ namespace beepbox {
}
}

private _whenUrlHashChanged = (): void => {
private _whenHistoryStateChanged = (): void => {
if (window.history.state == null && window.location.hash != "") {
// The user changed the hash directly.
this._sequenceNumber++;
const state: HistoryState = {canUndo: true, sequenceNumber: this._sequenceNumber, bar: this.bar, channel: this.channel, prompt: this.prompt};
this._resetSongRecoveryUid();
const state: HistoryState = {canUndo: true, sequenceNumber: this._sequenceNumber, bar: this.bar, channel: this.channel, recoveryUid: this._recoveryUid, prompt: null};
new ChangeSong(this, window.location.hash);
this.prompt = state.prompt;
if (this.displayBrowserUrl) {
this._replaceState(state, this.song.toBase64String());
} else {
this._pushState(state, this.song.toBase64String());
}
this.forgetLastChange();
this.notifier.notifyWatchers();
return;
}
}

private _whenHistoryStateChanged = (): void => {

const state: HistoryState | null = this._getHistoryState();
if (state == null) throw new Error("History state is null.");

// We're listening for both hashchanged and popstate, which often fire together.
// Abort if we've already handled the current state.
if (state.sequenceNumber == this._sequenceNumber) return;

Expand All @@ -226,6 +238,7 @@ namespace beepbox {

this._barFromCurrentState = state.bar;
this._channelFromCurrentState = state.channel;
this._recoveryUid = state.recoveryUid;

//this.barScrollPos = Math.min(this.bar, Math.max(this.bar - (this.trackVisibleBars - 1), this.barScrollPos));

Expand All @@ -240,48 +253,58 @@ namespace beepbox {
private _updateHistoryState = (): void => {
this._waitingToUpdateState = false;
const hash: string = this.song.toBase64String();
if (this._stateChangeType == "push") {
if (this._stateChangeType == StateChangeType.push) {
this._sequenceNumber++;
} else if (this._stateChangeType == "jump") {
} else if (this._stateChangeType >= StateChangeType.jump) {
this._sequenceNumber += 2;
}
let state: HistoryState = {canUndo: true, sequenceNumber: this._sequenceNumber, bar: this.bar, channel: this.channel, prompt: this.prompt};
if (this._stateChangeType == "push" || this._stateChangeType == "jump") {
this._pushState(state, hash);
if (this._recordedNewSong) {
this._resetSongRecoveryUid();
} else {
this._recovery.saveVersion(this._recoveryUid, hash);
}
let state: HistoryState = {canUndo: true, sequenceNumber: this._sequenceNumber, bar: this.bar, channel: this.channel, recoveryUid: this._recoveryUid, prompt: this.prompt};
if (this._stateChangeType == StateChangeType.replace) {
this._replaceState(state, hash);
} else {
this._pushState(state, hash);
}
this._barFromCurrentState = state.bar;
this._channelFromCurrentState = state.channel;
this._stateChangeType = "replace";
this._stateChangeType = StateChangeType.replace;
this._recordedNewSong = false;
}

public record(change: Change, stateChangeType: StateChangeType = "push"): void {
public record(change: Change, stateChangeType: StateChangeType = StateChangeType.push, newSong: boolean = false): void {
if (change.isNoop()) {
this._recentChange = null;
if (stateChangeType == "replace") {
if (stateChangeType == StateChangeType.replace) {
this._back();
}
} else {
change.commit();
this._recentChange = change;
if (stateChangeType == "push" && this._stateChangeType == "replace") {
this._stateChangeType = stateChangeType;
} else if (stateChangeType == "jump") {
this._stateChangeType = stateChangeType;
}
if (this._stateChangeType < stateChangeType) this._stateChangeType = stateChangeType;
this._recordedNewSong = this._recordedNewSong || newSong;
if (!this._waitingToUpdateState) {
// Defer updating the url/history until all sequenced changes have
// committed and the interface has rendered the latest changes to
// improve perceived responsiveness.
window.requestAnimationFrame(this._updateHistoryState);
this._waitingToUpdateState = true;
}
}
}

private _resetSongRecoveryUid(): void {
this._recoveryUid = generateUid();
}

public openPrompt(prompt: string): void {
this.prompt = prompt;
const hash: string = this.song.toBase64String();
this._sequenceNumber++;
const state = {canUndo: true, sequenceNumber: this._sequenceNumber, bar: this.bar, channel: this.channel, prompt: this.prompt};
const state = {canUndo: true, sequenceNumber: this._sequenceNumber, bar: this.bar, channel: this.channel, recoveryUid: this._recoveryUid, prompt: this.prompt};
this._pushState(state, hash);
}

Expand Down
2 changes: 1 addition & 1 deletion editor/SongDurationPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ namespace beepbox {
const group: ChangeGroup = new ChangeGroup();
group.append(new ChangeBarCount(this._doc, SongDurationPrompt._validate(this._barsStepper), this._positionSelect.value == "beginning"));
this._doc.prompt = null;
this._doc.record(group, "replace");
this._doc.record(group, StateChangeType.replace);
}
}
}
14 changes: 11 additions & 3 deletions editor/SongEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
/// <reference path="ChannelSettingsPrompt.ts" />
/// <reference path="ExportPrompt.ts" />
/// <reference path="ImportPrompt.ts" />
/// <reference path="SongRecoveryPrompt.ts" />

namespace beepbox {
const {button, div, input, select, span, optgroup, option} = HTML;
Expand Down Expand Up @@ -134,6 +135,7 @@ namespace beepbox {
option({value: "shareUrl"}, "⤳ Share Song URL"),
option({value: "viewPlayer"}, "▶ View in Song Player"),
option({value: "copyEmbed"}, "⎘ Copy HTML Embed Code"),
option({value: "songRecovery"}, "⚠ Recover Recent Song..."),
);
private readonly _editMenu: HTMLSelectElement = select({style: "width: 100%;"},
option({selected: true, disabled: true, hidden: false}, "Edit"), // todo: "hidden" should be true but looks wrong on mac chrome, adds checkmark next to first visible option. :(
Expand Down Expand Up @@ -526,6 +528,9 @@ namespace beepbox {
case "import":
this.prompt = new ImportPrompt(this._doc);
break;
case "songRecovery":
this.prompt = new SongRecoveryPrompt(this._doc);
break;
case "barCount":
this.prompt = new SongDurationPrompt(this._doc);
break;
Expand Down Expand Up @@ -1170,17 +1175,17 @@ namespace beepbox {
case "new":
this._doc.goBackToStart();
for (const channel of this._doc.song.channels) channel.muted = false;
this._doc.record(new ChangeSong(this._doc, ""));
this._doc.record(new ChangeSong(this._doc, ""), StateChangeType.push, true);
break;
case "export":
this._openPrompt("export");
break;
case "import":
this._openPrompt("import");
break;
case "copyUrl": {
case "copyUrl":
this._copyTextToClipboard(new URL("#" + this._doc.song.toBase64String(), location.href).href);
} break;
break;
case "shareUrl":
(<any>navigator).share({ url: new URL("#" + this._doc.song.toBase64String(), location.href).href });
break;
Expand All @@ -1190,6 +1195,9 @@ namespace beepbox {
case "copyEmbed":
this._copyTextToClipboard(`<iframe width="384" height="60" style="border: none;" src="${new URL("player/#song=" + this._doc.song.toBase64String(), location.href).href}"></iframe>`);
break;
case "songRecovery":
this._openPrompt("songRecovery");
break;
}
this._fileMenu.selectedIndex = 0;
}
Expand Down
Loading

0 comments on commit 17c5e9f

Please sign in to comment.