Refactoring: cleaning up main.ts file
This commit is contained in:
126
src/API.ts
126
src/API.ts
@ -11,7 +11,11 @@ import { SoundEvent } from "./classes/SoundEvent";
|
|||||||
import { MidiEvent } from "./classes/MidiEvent";
|
import { MidiEvent } from "./classes/MidiEvent";
|
||||||
import { LRUCache } from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import { InputOptions, Player } from "./classes/ZPlayer";
|
import { InputOptions, Player } from "./classes/ZPlayer";
|
||||||
import { template_universes } from "./AppSettings";
|
import {
|
||||||
|
loadUniverse,
|
||||||
|
openUniverseModal,
|
||||||
|
template_universes,
|
||||||
|
} from "./FileManagement";
|
||||||
import {
|
import {
|
||||||
samples,
|
samples,
|
||||||
initAudioOnFirstClick,
|
initAudioOnFirstClick,
|
||||||
@ -71,8 +75,8 @@ export class UserAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadUniverseFromInterface = (universe: string) => {
|
_loadUniverseFromInterface = (universe: string) => {
|
||||||
this.app.loadUniverse(universe as string);
|
loadUniverse(this.app, universe as string);
|
||||||
this.app.openBuffersModal();
|
openUniverseModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
_deleteUniverseFromInterface = (universe: string) => {
|
_deleteUniverseFromInterface = (universe: string) => {
|
||||||
@ -141,11 +145,11 @@ export class UserAPI {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
clearTimeout(this.errorTimeoutID);
|
clearTimeout(this.errorTimeoutID);
|
||||||
clearTimeout(this.printTimeoutID);
|
clearTimeout(this.printTimeoutID);
|
||||||
this.app.error_line.innerHTML = error as string;
|
this.app.interface.error_line.innerHTML = error as string;
|
||||||
this.app.error_line.style.color = "color-red-800";
|
this.app.interface.error_line.style.color = "color-red-800";
|
||||||
this.app.error_line.classList.remove("hidden");
|
this.app.interface.error_line.classList.remove("hidden");
|
||||||
this.errorTimeoutID = setTimeout(
|
this.errorTimeoutID = setTimeout(
|
||||||
() => this.app.error_line.classList.add("hidden"),
|
() => this.app.interface.error_line.classList.add("hidden"),
|
||||||
2000
|
2000
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -154,11 +158,11 @@ export class UserAPI {
|
|||||||
console.log(message);
|
console.log(message);
|
||||||
clearTimeout(this.printTimeoutID);
|
clearTimeout(this.printTimeoutID);
|
||||||
clearTimeout(this.errorTimeoutID);
|
clearTimeout(this.errorTimeoutID);
|
||||||
this.app.error_line.innerHTML = message as string;
|
this.app.interface.error_line.innerHTML = message as string;
|
||||||
this.app.error_line.style.color = "white";
|
this.app.interface.error_line.style.color = "white";
|
||||||
this.app.error_line.classList.remove("hidden");
|
this.app.interface.error_line.classList.remove("hidden");
|
||||||
this.printTimeoutID = setTimeout(
|
this.printTimeoutID = setTimeout(
|
||||||
() => this.app.error_line.classList.add("hidden"),
|
() => this.app.interface.error_line.classList.add("hidden"),
|
||||||
4000
|
4000
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -591,8 +595,9 @@ export class UserAPI {
|
|||||||
root: number | string,
|
root: number | string,
|
||||||
scale: number | string,
|
scale: number | string,
|
||||||
channel: number = 0,
|
channel: number = 0,
|
||||||
port: number | string = (this.MidiConnection.currentOutputIndex || 0),
|
port: number | string = this.MidiConnection.currentOutputIndex || 0,
|
||||||
soundOff: boolean = false): void => {
|
soundOff: boolean = false
|
||||||
|
): void => {
|
||||||
/**
|
/**
|
||||||
* Sends given scale to midi output for visual aid
|
* Sends given scale to midi output for visual aid
|
||||||
*/
|
*/
|
||||||
@ -600,47 +605,53 @@ export class UserAPI {
|
|||||||
this.hide_scale(root, scale, channel, port);
|
this.hide_scale(root, scale, channel, port);
|
||||||
const scaleNotes = getAllScaleNotes(scale, root);
|
const scaleNotes = getAllScaleNotes(scale, root);
|
||||||
// Send each scale note to current midi out
|
// Send each scale note to current midi out
|
||||||
scaleNotes.forEach(note => {
|
scaleNotes.forEach((note) => {
|
||||||
this.MidiConnection.sendMidiOn(note, channel, 1, port);
|
this.MidiConnection.sendMidiOn(note, channel, 1, port);
|
||||||
if (soundOff) this.MidiConnection.sendAllSoundOff(channel, port);
|
if (soundOff) this.MidiConnection.sendAllSoundOff(channel, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scale_aid = scale;
|
this.scale_aid = scale;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public hide_scale = (
|
public hide_scale = (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
root: number | string = 0,
|
root: number | string = 0,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
scale: number | string = 0,
|
scale: number | string = 0,
|
||||||
channel: number = 0,
|
channel: number = 0,
|
||||||
port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => {
|
port: number | string = this.MidiConnection.currentOutputIndex || 0
|
||||||
|
): void => {
|
||||||
/**
|
/**
|
||||||
* Hides all notes by sending all notes off to midi output
|
* Hides all notes by sending all notes off to midi output
|
||||||
*/
|
*/
|
||||||
const allNotes = Array.from(Array(128).keys());
|
const allNotes = Array.from(Array(128).keys());
|
||||||
// Send each scale note to current midi out
|
// Send each scale note to current midi out
|
||||||
allNotes.forEach(note => {
|
allNotes.forEach((note) => {
|
||||||
this.MidiConnection.sendMidiOff(note, channel, port);
|
this.MidiConnection.sendMidiOff(note, channel, port);
|
||||||
});
|
});
|
||||||
this.scale_aid = undefined;
|
this.scale_aid = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
}
|
midi_notes_off = (
|
||||||
|
channel: number = 0,
|
||||||
midi_notes_off = (channel: number = 0, port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => {
|
port: number | string = this.MidiConnection.currentOutputIndex || 0
|
||||||
|
): void => {
|
||||||
/**
|
/**
|
||||||
* Sends all notes off to midi output
|
* Sends all notes off to midi output
|
||||||
*/
|
*/
|
||||||
this.MidiConnection.sendAllNotesOff(channel, port);
|
this.MidiConnection.sendAllNotesOff(channel, port);
|
||||||
}
|
};
|
||||||
|
|
||||||
midi_sound_off = (channel: number = 0, port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => {
|
midi_sound_off = (
|
||||||
|
channel: number = 0,
|
||||||
|
port: number | string = this.MidiConnection.currentOutputIndex || 0
|
||||||
|
): void => {
|
||||||
/**
|
/**
|
||||||
* Sends all sound off to midi output
|
* Sends all sound off to midi output
|
||||||
*/
|
*/
|
||||||
this.MidiConnection.sendAllSoundOff(channel, port);
|
this.MidiConnection.sendAllSoundOff(channel, port);
|
||||||
}
|
};
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// Ziffers related functions
|
// Ziffers related functions
|
||||||
@ -1255,7 +1266,6 @@ export class UserAPI {
|
|||||||
|
|
||||||
denominator = this.meter;
|
denominator = this.meter;
|
||||||
|
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// Fill
|
// Fill
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@ -1276,7 +1286,9 @@ export class UserAPI {
|
|||||||
const nArray = Array.isArray(n) ? n : [n];
|
const nArray = Array.isArray(n) ? n : [n];
|
||||||
const results: boolean[] = nArray.map(
|
const results: boolean[] = nArray.map(
|
||||||
(value) =>
|
(value) =>
|
||||||
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % Math.floor(value * this.ppqn()) === 0
|
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) %
|
||||||
|
Math.floor(value * this.ppqn()) ===
|
||||||
|
0
|
||||||
);
|
);
|
||||||
return results.some((value) => value === true);
|
return results.some((value) => value === true);
|
||||||
};
|
};
|
||||||
@ -1284,17 +1296,19 @@ export class UserAPI {
|
|||||||
|
|
||||||
public bar = (n: number | number[] = 1, nudge: number = 0): boolean => {
|
public bar = (n: number | number[] = 1, nudge: number = 0): boolean => {
|
||||||
/**
|
/**
|
||||||
* Determine if the current pulse is on a specified bar, with optional nudge.
|
* Determine if the current pulse is on a specified bar, with optional nudge.
|
||||||
* @param n Single bar multiplier or array of bar multipliers
|
* @param n Single bar multiplier or array of bar multipliers
|
||||||
* @param nudge Offset in bars to nudge the bar forward or backward
|
* @param nudge Offset in bars to nudge the bar forward or backward
|
||||||
* @returns True if the current pulse is on one of the specified bars (considering nudge), false otherwise
|
* @returns True if the current pulse is on one of the specified bars (considering nudge), false otherwise
|
||||||
*/
|
*/
|
||||||
const nArray = Array.isArray(n) ? n : [n];
|
const nArray = Array.isArray(n) ? n : [n];
|
||||||
const barLength = this.app.clock.time_signature[1] * this.ppqn();
|
const barLength = this.app.clock.time_signature[1] * this.ppqn();
|
||||||
const nudgeInPulses = Math.floor(nudge * barLength);
|
const nudgeInPulses = Math.floor(nudge * barLength);
|
||||||
const results: boolean[] = nArray.map(
|
const results: boolean[] = nArray.map(
|
||||||
(value) =>
|
(value) =>
|
||||||
(this.app.clock.pulses_since_origin - nudgeInPulses) % Math.floor(value * barLength) === 0
|
(this.app.clock.pulses_since_origin - nudgeInPulses) %
|
||||||
|
Math.floor(value * barLength) ===
|
||||||
|
0
|
||||||
);
|
);
|
||||||
return results.some((value) => value === true);
|
return results.some((value) => value === true);
|
||||||
};
|
};
|
||||||
@ -1302,11 +1316,11 @@ export class UserAPI {
|
|||||||
|
|
||||||
public pulse = (n: number | number[] = 1, nudge: number = 0): boolean => {
|
public pulse = (n: number | number[] = 1, nudge: number = 0): boolean => {
|
||||||
/**
|
/**
|
||||||
* Determine if the current pulse is on a specified pulse count, with optional nudge.
|
* Determine if the current pulse is on a specified pulse count, with optional nudge.
|
||||||
* @param n Single pulse count or array of pulse counts
|
* @param n Single pulse count or array of pulse counts
|
||||||
* @param nudge Offset in pulses to nudge the pulse forward or backward
|
* @param nudge Offset in pulses to nudge the pulse forward or backward
|
||||||
* @returns True if the current pulse is on one of the specified pulse counts (considering nudge), false otherwise
|
* @returns True if the current pulse is on one of the specified pulse counts (considering nudge), false otherwise
|
||||||
*/
|
*/
|
||||||
const nArray = Array.isArray(n) ? n : [n];
|
const nArray = Array.isArray(n) ? n : [n];
|
||||||
const results: boolean[] = nArray.map(
|
const results: boolean[] = nArray.map(
|
||||||
(value) => (this.app.clock.pulses_since_origin - nudge) % value === 0
|
(value) => (this.app.clock.pulses_since_origin - nudge) % value === 0
|
||||||
@ -1318,11 +1332,10 @@ export class UserAPI {
|
|||||||
public tick = (tick: number | number[], offset: number = 0): boolean => {
|
public tick = (tick: number | number[], offset: number = 0): boolean => {
|
||||||
const nArray = Array.isArray(tick) ? tick : [tick];
|
const nArray = Array.isArray(tick) ? tick : [tick];
|
||||||
const results: boolean[] = nArray.map(
|
const results: boolean[] = nArray.map(
|
||||||
(value) => (this.app.clock.time_position.pulse === value + offset)
|
(value) => this.app.clock.time_position.pulse === value + offset
|
||||||
);
|
);
|
||||||
return results.some((value) => value === true)
|
return results.some((value) => value === true);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// Modulo based time filters
|
// Modulo based time filters
|
||||||
@ -1582,7 +1595,8 @@ export class UserAPI {
|
|||||||
* @returns A sine wave between -1 and 1
|
* @returns A sine wave between -1 and 1
|
||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
(Math.sin(this.app.clock.ctx.currentTime * Math.PI * 2 * freq) + offset) * times
|
(Math.sin(this.app.clock.ctx.currentTime * Math.PI * 2 * freq) + offset) *
|
||||||
|
times
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1595,7 +1609,7 @@ export class UserAPI {
|
|||||||
* @returns A sine wave between 0 and 1
|
* @returns A sine wave between 0 and 1
|
||||||
* @see sine
|
* @see sine
|
||||||
*/
|
*/
|
||||||
return ((this.sine(freq, offset) + 1) / 2) * times
|
return ((this.sine(freq, offset) + 1) / 2) * times;
|
||||||
};
|
};
|
||||||
|
|
||||||
saw = (freq: number = 1, times: number = 1, offset: number = 0): number => {
|
saw = (freq: number = 1, times: number = 1, offset: number = 0): number => {
|
||||||
@ -1610,7 +1624,9 @@ export class UserAPI {
|
|||||||
* @see sine
|
* @see sine
|
||||||
* @see noise
|
* @see noise
|
||||||
*/
|
*/
|
||||||
return (((this.app.clock.ctx.currentTime * freq) % 1) * 2 - 1 + offset) * times;
|
return (
|
||||||
|
(((this.app.clock.ctx.currentTime * freq) % 1) * 2 - 1 + offset) * times
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
usaw = (freq: number = 1, times: number = 1, offset: number = 0): number => {
|
usaw = (freq: number = 1, times: number = 1, offset: number = 0): number => {
|
||||||
@ -1625,7 +1641,11 @@ export class UserAPI {
|
|||||||
return ((this.saw(freq, offset) + 1) / 2) * times;
|
return ((this.saw(freq, offset) + 1) / 2) * times;
|
||||||
};
|
};
|
||||||
|
|
||||||
triangle = (freq: number = 1, times: number = 1, offset: number = 0): number => {
|
triangle = (
|
||||||
|
freq: number = 1,
|
||||||
|
times: number = 1,
|
||||||
|
offset: number = 0
|
||||||
|
): number => {
|
||||||
/**
|
/**
|
||||||
* Returns a triangle wave between -1 and 1.
|
* Returns a triangle wave between -1 and 1.
|
||||||
*
|
*
|
||||||
@ -1638,7 +1658,11 @@ export class UserAPI {
|
|||||||
return (Math.abs(this.saw(freq, offset)) * 2 - 1) * times;
|
return (Math.abs(this.saw(freq, offset)) * 2 - 1) * times;
|
||||||
};
|
};
|
||||||
|
|
||||||
utriangle = (freq: number = 1, times: number = 1, offset: number = 0): number => {
|
utriangle = (
|
||||||
|
freq: number = 1,
|
||||||
|
times: number = 1,
|
||||||
|
offset: number = 0
|
||||||
|
): number => {
|
||||||
/**
|
/**
|
||||||
* Returns a triangle wave between 0 and 1.
|
* Returns a triangle wave between 0 and 1.
|
||||||
*
|
*
|
||||||
@ -1740,15 +1764,17 @@ export class UserAPI {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public range = (
|
public range = (
|
||||||
inputY: number, yMin: number,
|
inputY: number,
|
||||||
yMax: number, xMin: number,
|
yMin: number,
|
||||||
xMax: number): number => {
|
yMax: number,
|
||||||
|
xMin: number,
|
||||||
|
xMax: number
|
||||||
|
): number => {
|
||||||
const percent = (inputY - yMin) / (yMax - yMin);
|
const percent = (inputY - yMin) / (yMax - yMin);
|
||||||
const outputX = percent * (xMax - xMin) + xMin;
|
const outputX = percent * (xMax - xMin) + xMin;
|
||||||
return outputX;
|
return outputX;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
limit = (value: number, min: number, max: number): number => {
|
limit = (value: number, min: number, max: number): number => {
|
||||||
/**
|
/**
|
||||||
* Limits a value between a minimum and a maximum.
|
* Limits a value between a minimum and a maximum.
|
||||||
|
|||||||
@ -20,6 +20,18 @@ import { reference } from "./documentation/reference";
|
|||||||
import { synths } from "./documentation/synths";
|
import { synths } from "./documentation/synths";
|
||||||
import { bonus } from "./documentation/bonus";
|
import { bonus } from "./documentation/bonus";
|
||||||
|
|
||||||
|
// Setting up the Markdown converter with syntax highlighting
|
||||||
|
import showdown from "showdown";
|
||||||
|
import showdownHighlight from "showdown-highlight";
|
||||||
|
showdown.setFlavor("github");
|
||||||
|
import { classMap } from "./DomElements";
|
||||||
|
const bindings = Object.keys(classMap).map((key) => ({
|
||||||
|
type: "output",
|
||||||
|
regex: new RegExp(`<${key}([^>]*)>`, "g"),
|
||||||
|
//@ts-ignore
|
||||||
|
replace: (match, p1) => `<${key} class="${classMap[key]}" ${p1}>`,
|
||||||
|
}));
|
||||||
|
|
||||||
export const key_shortcut = (shortcut: string): string => {
|
export const key_shortcut = (shortcut: string): string => {
|
||||||
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">${shortcut}</kbd>`;
|
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">${shortcut}</kbd>`;
|
||||||
};
|
};
|
||||||
@ -38,8 +50,8 @@ export const makeExampleFactory = (application: Editor): Function => {
|
|||||||
<details ${open ? "open" : ""}>
|
<details ${open ? "open" : ""}>
|
||||||
<summary >${description}
|
<summary >${description}
|
||||||
<button class="py-1 align-top text-base rounded-lg pl-2 pr-2 hover:bg-green-700 bg-green-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
|
<button class="py-1 align-top text-base rounded-lg pl-2 pr-2 hover:bg-green-700 bg-green-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
|
||||||
<button class="py-1 text-base rounded-lg pl-2 pr-2 hover:bg-neutral-600 bg-neutral-500 inline-block pl-2" onclick="app.api._stopDocExample()">⏸️ Pause</button>
|
<button class="py-1 text-base rounded-lg pr-2 hover:bg-neutral-600 bg-neutral-500 inline-block pl-2" onclick="app.api._stopDocExample()">⏸️ Pause</button>
|
||||||
<button class="py-1 text-base rounded-lg pl-2 pr-2 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
|
<button class="py-1 text-base rounded-lg pr-2 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
|
||||||
</summary>
|
</summary>
|
||||||
\`\`\`javascript
|
\`\`\`javascript
|
||||||
${code}
|
${code}
|
||||||
@ -77,3 +89,37 @@ export const documentation_factory = (application: Editor) => {
|
|||||||
about: about(),
|
about: about(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const showDocumentation = (app: Editor) => {
|
||||||
|
if (document.getElementById("app")?.classList.contains("hidden")) {
|
||||||
|
document.getElementById("app")?.classList.remove("hidden");
|
||||||
|
document.getElementById("documentation")?.classList.add("hidden");
|
||||||
|
app.exampleIsPlaying = false;
|
||||||
|
} else {
|
||||||
|
document.getElementById("app")?.classList.add("hidden");
|
||||||
|
document.getElementById("documentation")?.classList.remove("hidden");
|
||||||
|
// Load and convert Markdown content from the documentation file
|
||||||
|
updateDocumentationContent(app);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hideDocumentation = () => {
|
||||||
|
if (document.getElementById("app")?.classList.contains("hidden")) {
|
||||||
|
document.getElementById("app")?.classList.remove("hidden");
|
||||||
|
document.getElementById("documentation")?.classList.add("hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDocumentationContent = (app: Editor) => {
|
||||||
|
const converter = new showdown.Converter({
|
||||||
|
emoji: true,
|
||||||
|
moreStyling: true,
|
||||||
|
backslashEscapesHTMLTags: true,
|
||||||
|
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
|
||||||
|
});
|
||||||
|
const converted_markdown = converter.makeHtml(
|
||||||
|
app.docs[app.currentDocumentationPane]
|
||||||
|
);
|
||||||
|
document.getElementById("documentation-content")!.innerHTML =
|
||||||
|
converted_markdown;
|
||||||
|
};
|
||||||
|
|||||||
83
src/DomElements.ts
Normal file
83
src/DomElements.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
export type ElementMap = {
|
||||||
|
[key: string]:
|
||||||
|
| HTMLElement
|
||||||
|
| HTMLButtonElement
|
||||||
|
| HTMLDivElement
|
||||||
|
| HTMLInputElement
|
||||||
|
| HTMLSelectElement
|
||||||
|
| HTMLCanvasElement
|
||||||
|
| HTMLFormElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const singleElements = {
|
||||||
|
topos_logo: "topos-logo",
|
||||||
|
fill_viewer: "fillviewer",
|
||||||
|
load_universe_button: "load-universe-button",
|
||||||
|
download_universe_button: "download-universes",
|
||||||
|
upload_universe_button: "upload-universes",
|
||||||
|
destroy_universes_button: "destroy-universes",
|
||||||
|
documentation_button: "doc-button-1",
|
||||||
|
eval_button: "eval-button-1",
|
||||||
|
local_button: "local-button",
|
||||||
|
global_button: "global-button",
|
||||||
|
init_button: "init-button",
|
||||||
|
note_button: "note-button",
|
||||||
|
settings_button: "settings-button",
|
||||||
|
close_settings_button: "close-settings-button",
|
||||||
|
close_universes_button: "close-universes-button",
|
||||||
|
universe_viewer: "universe-viewer",
|
||||||
|
buffer_modal: "modal-buffers",
|
||||||
|
buffer_search: "buffer-search",
|
||||||
|
universe_creator: "universe-creator",
|
||||||
|
local_script_tabs: "local-script-tabs",
|
||||||
|
font_size_input: "font-size-input",
|
||||||
|
font_family_selector: "font-family",
|
||||||
|
vim_mode_checkbox: "vim-mode",
|
||||||
|
line_numbers_checkbox: "show-line-numbers",
|
||||||
|
time_position_checkbox: "show-time-position",
|
||||||
|
tips_checkbox: "show-tips",
|
||||||
|
midi_clock_checkbox: "send-midi-clock",
|
||||||
|
midi_channels_scripts: "midi-channels-scripts",
|
||||||
|
midi_clock_ppqn: "midi-clock-ppqn-input",
|
||||||
|
load_demo_songs: "load-demo-songs",
|
||||||
|
normal_mode_button: "normal-mode",
|
||||||
|
vim_mode_button: "vim-mode",
|
||||||
|
share_button: "share-button",
|
||||||
|
audio_nudge_range: "audio_nudge",
|
||||||
|
dough_nudge_range: "dough_nudge",
|
||||||
|
error_line: "error_line",
|
||||||
|
hydra_canvas: "hydra-bg",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buttonGroups = {
|
||||||
|
play_buttons: ["play-button-1"],
|
||||||
|
stop_buttons: ["stop-button-1"],
|
||||||
|
clear_buttons: ["clear-button-1"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const classMap = {
|
||||||
|
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
|
||||||
|
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
|
||||||
|
h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-700 rounded-lg py-2 px-2 lg:mt-16",
|
||||||
|
ul: "text-underline pl-6",
|
||||||
|
li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal",
|
||||||
|
p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal",
|
||||||
|
warning:
|
||||||
|
"animate-pulse lg:text-2xl font-bold text-rose-600 lg:mx-6 mx-2 my-4 leading-normal",
|
||||||
|
a: "lg:text-2xl text-base text-orange-300",
|
||||||
|
code: "lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden",
|
||||||
|
icode:
|
||||||
|
"lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
|
||||||
|
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
|
||||||
|
blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4",
|
||||||
|
details:
|
||||||
|
"lg:mx-12 py-2 px-6 lg:text-2xl text-white rounded-lg bg-neutral-600",
|
||||||
|
summary: "font-semibold text-xl",
|
||||||
|
table:
|
||||||
|
"justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse",
|
||||||
|
thead:
|
||||||
|
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
|
||||||
|
th: "",
|
||||||
|
td: "",
|
||||||
|
tr: "",
|
||||||
|
};
|
||||||
@ -1,5 +1,9 @@
|
|||||||
|
import { Prec } from "@codemirror/state";
|
||||||
|
import { indentWithTab } from "@codemirror/commands";
|
||||||
import {
|
import {
|
||||||
keymap,
|
keymap,
|
||||||
|
ViewUpdate,
|
||||||
|
lineNumbers,
|
||||||
highlightSpecialChars,
|
highlightSpecialChars,
|
||||||
drawSelection,
|
drawSelection,
|
||||||
highlightActiveLine,
|
highlightActiveLine,
|
||||||
@ -9,6 +13,7 @@ import {
|
|||||||
highlightActiveLineGutter,
|
highlightActiveLineGutter,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
import { Extension, EditorState } from "@codemirror/state";
|
import { Extension, EditorState } from "@codemirror/state";
|
||||||
|
import { vim } from "@replit/codemirror-vim";
|
||||||
import {
|
import {
|
||||||
defaultHighlightStyle,
|
defaultHighlightStyle,
|
||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
@ -23,6 +28,12 @@ import {
|
|||||||
closeBracketsKeymap,
|
closeBracketsKeymap,
|
||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
import { lintKeymap } from "@codemirror/lint";
|
import { lintKeymap } from "@codemirror/lint";
|
||||||
|
import { Compartment } from "@codemirror/state";
|
||||||
|
import { Editor } from "./main";
|
||||||
|
import { EditorView } from "codemirror";
|
||||||
|
import { toposTheme } from "./themes/toposTheme";
|
||||||
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
|
import { inlineHoveringTips } from "./documentation/inlineHelp";
|
||||||
|
|
||||||
export const editorSetup: Extension = (() => [
|
export const editorSetup: Extension = (() => [
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
@ -47,3 +58,63 @@ export const editorSetup: Extension = (() => [
|
|||||||
...lintKeymap,
|
...lintKeymap,
|
||||||
]),
|
]),
|
||||||
])();
|
])();
|
||||||
|
|
||||||
|
export const installEditor = (app: Editor) => {
|
||||||
|
app.vimModeCompartment = new Compartment();
|
||||||
|
app.hoveringCompartment = new Compartment();
|
||||||
|
app.withLineNumbers = new Compartment();
|
||||||
|
app.chosenLanguage = new Compartment();
|
||||||
|
app.fontSize = new Compartment();
|
||||||
|
const vimPlugin = app.settings.vimMode ? vim() : [];
|
||||||
|
const lines = app.settings.line_numbers ? lineNumbers() : [];
|
||||||
|
|
||||||
|
const fontModif = EditorView.theme({
|
||||||
|
"&": {
|
||||||
|
fontSize: `${app.settings.font_size}px`,
|
||||||
|
},
|
||||||
|
$content: {
|
||||||
|
fontFamily: `${app.settings.font}, Menlo, Monaco, Lucida Console, monospace`,
|
||||||
|
fontSize: `${app.settings.font_size}px`,
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
fontSize: `${app.settings.font_size}px`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.editorExtensions = [
|
||||||
|
app.vimModeCompartment.of(vimPlugin),
|
||||||
|
app.withLineNumbers.of(lines),
|
||||||
|
app.fontSize.of(fontModif),
|
||||||
|
app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []),
|
||||||
|
editorSetup,
|
||||||
|
toposTheme,
|
||||||
|
app.chosenLanguage.of(javascript()),
|
||||||
|
EditorView.updateListener.of((v: ViewUpdate) => {
|
||||||
|
v;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
app.dynamicPlugins = new Compartment();
|
||||||
|
app.state = EditorState.create({
|
||||||
|
extensions: [
|
||||||
|
...app.editorExtensions,
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
app.dynamicPlugins.of(app.userPlugins),
|
||||||
|
Prec.highest(
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
key: "Ctrl-Enter",
|
||||||
|
run: () => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
keymap.of([indentWithTab]),
|
||||||
|
],
|
||||||
|
doc: app.universes[app.selected_universe].global.candidate,
|
||||||
|
});
|
||||||
|
app.view = new EditorView({
|
||||||
|
parent: document.getElementById("editor") as HTMLElement,
|
||||||
|
state: app.state,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Editor } from "./main";
|
import type { Editor } from "./main";
|
||||||
import type { File } from "./AppSettings";
|
import type { File } from "./FileManagement";
|
||||||
|
|
||||||
const delay = (ms: number) =>
|
const delay = (ms: number) =>
|
||||||
new Promise((_, reject) =>
|
new Promise((_, reject) =>
|
||||||
@ -24,7 +24,7 @@ const tryCatchWrapper = (
|
|||||||
).call(application.api);
|
).call(application.api);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
application.error_line.innerHTML = error as string;
|
application.interface.error_line.innerHTML = error as string;
|
||||||
console.log(error);
|
console.log(error);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ export const tryEvaluate = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
application.error_line.innerHTML = error as string;
|
application.interface.error_line.innerHTML = error as string;
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -91,7 +91,7 @@ export const evaluate = async (
|
|||||||
]);
|
]);
|
||||||
if (code.evaluations) code.evaluations++;
|
if (code.evaluations) code.evaluations++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
application.error_line.innerHTML = error as string;
|
application.interface.error_line.innerHTML = error as string;
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { tutorial_universe } from "./universes/tutorial";
|
import { tutorial_universe } from "./universes/tutorial";
|
||||||
|
import { gzipSync, decompressSync, strFromU8 } from "fflate";
|
||||||
|
import { examples } from "./examples/excerpts";
|
||||||
|
import { type Editor } from "./main";
|
||||||
|
import { uniqueNamesGenerator, colors, animals } from "unique-names-generator";
|
||||||
|
import { tryEvaluate } from "./Evaluator";
|
||||||
export type Universes = { [key: string]: Universe };
|
export type Universes = { [key: string]: Universe };
|
||||||
|
|
||||||
export interface Universe {
|
export interface Universe {
|
||||||
@ -62,9 +66,9 @@ export interface Settings {
|
|||||||
tips: boolean;
|
tips: boolean;
|
||||||
send_clock: boolean;
|
send_clock: boolean;
|
||||||
midi_channels_scripts: boolean;
|
midi_channels_scripts: boolean;
|
||||||
midi_clock_input: string|undefined;
|
midi_clock_input: string | undefined;
|
||||||
midi_clock_ppqn: number;
|
midi_clock_ppqn: number;
|
||||||
default_midi_input: string|undefined;
|
default_midi_input: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const template_universe = {
|
export const template_universe = {
|
||||||
@ -139,8 +143,8 @@ export class AppSettings {
|
|||||||
public tips: boolean = true;
|
public tips: boolean = true;
|
||||||
public send_clock: boolean = false;
|
public send_clock: boolean = false;
|
||||||
public midi_channels_scripts: boolean = true;
|
public midi_channels_scripts: boolean = true;
|
||||||
public midi_clock_input: string|undefined = undefined;
|
public midi_clock_input: string | undefined = undefined;
|
||||||
public default_midi_input: string|undefined = undefined;
|
public default_midi_input: string | undefined = undefined;
|
||||||
public midi_clock_ppqn: number = 24;
|
public midi_clock_ppqn: number = 24;
|
||||||
public load_demo_songs: boolean = true;
|
public load_demo_songs: boolean = true;
|
||||||
|
|
||||||
@ -225,3 +229,137 @@ export class AppSettings {
|
|||||||
localStorage.setItem("topos", JSON.stringify(this.data));
|
localStorage.setItem("topos", JSON.stringify(this.data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initializeSelectedUniverse = (app: Editor): void => {
|
||||||
|
/**
|
||||||
|
* Initializes the selected universe. If there is no selected universe, it
|
||||||
|
* will create a new one. If there is a selected universe, it will load it.
|
||||||
|
*
|
||||||
|
* @param app - The main application
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
if (app.settings.load_demo_songs) {
|
||||||
|
let random_example = examples[Math.floor(Math.random() * examples.length)];
|
||||||
|
app.selected_universe = "Welcome";
|
||||||
|
app.universes[app.selected_universe].global.committed = random_example;
|
||||||
|
app.universes[app.selected_universe].global.candidate = random_example;
|
||||||
|
} else {
|
||||||
|
app.selected_universe = app.settings.selected_universe;
|
||||||
|
if (app.universes[app.selected_universe] === undefined)
|
||||||
|
app.universes[app.selected_universe] = structuredClone(template_universe);
|
||||||
|
}
|
||||||
|
app.interface.universe_viewer.innerHTML = `Topos: ${app.selected_universe}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emptyUrl = () => {
|
||||||
|
window.history.replaceState({}, document.title, "/");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const share = async (app: Editor) => {
|
||||||
|
async function bufferToBase64(buffer: Uint8Array) {
|
||||||
|
const base64url: string = await new Promise((r) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => r(reader.result as string);
|
||||||
|
reader.readAsDataURL(new Blob([buffer]));
|
||||||
|
});
|
||||||
|
return base64url.slice(base64url.indexOf(",") + 1);
|
||||||
|
}
|
||||||
|
let data = JSON.stringify({
|
||||||
|
universe: app.settings.universes[app.selected_universe],
|
||||||
|
});
|
||||||
|
let encoded_data = gzipSync(new TextEncoder().encode(data), { level: 9 });
|
||||||
|
const hashed_table = await bufferToBase64(encoded_data);
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("universe", hashed_table);
|
||||||
|
window.history.replaceState({}, "", url.toString());
|
||||||
|
// Copy the text inside the text field
|
||||||
|
navigator.clipboard.writeText(url.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadUniverserFromUrl = (app: Editor): void => {
|
||||||
|
/**
|
||||||
|
* Loads a universe from the URL bar.
|
||||||
|
* @param app - The main application
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
// Loading from URL bar
|
||||||
|
let url = new URLSearchParams(window.location.search);
|
||||||
|
if (url !== undefined) {
|
||||||
|
let new_universe;
|
||||||
|
if (url !== null) {
|
||||||
|
const universeParam = url.get("universe");
|
||||||
|
if (universeParam !== null) {
|
||||||
|
let data = Uint8Array.from(atob(universeParam), (c) => c.charCodeAt(0));
|
||||||
|
new_universe = JSON.parse(strFromU8(decompressSync(data)));
|
||||||
|
const randomName: string = uniqueNamesGenerator({
|
||||||
|
length: 2,
|
||||||
|
separator: "_",
|
||||||
|
dictionaries: [colors, animals],
|
||||||
|
});
|
||||||
|
loadUniverse(app, randomName, new_universe["universe"]);
|
||||||
|
emptyUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadUniverse = (
|
||||||
|
app: Editor,
|
||||||
|
universeName: string,
|
||||||
|
universe: Universe = template_universe
|
||||||
|
): void => {
|
||||||
|
console.log(universeName, universe);
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
|
||||||
|
// Getting the new universe name and moving on
|
||||||
|
let selectedUniverse = universeName.trim();
|
||||||
|
if (app.universes[selectedUniverse] === undefined) {
|
||||||
|
app.settings.universes[selectedUniverse] = universe;
|
||||||
|
app.universes[selectedUniverse] = universe;
|
||||||
|
}
|
||||||
|
app.selected_universe = selectedUniverse;
|
||||||
|
app.settings.selected_universe = app.selected_universe;
|
||||||
|
app.interface.universe_viewer.innerHTML = `Topos: ${selectedUniverse}`;
|
||||||
|
|
||||||
|
// Updating the editor View to reflect the selected universe
|
||||||
|
app.updateEditorView();
|
||||||
|
|
||||||
|
// Evaluating the initialisation script for the selected universe
|
||||||
|
tryEvaluate(app, app.universes[app.selected_universe.toString()].init);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openUniverseModal = (): void => {
|
||||||
|
// If the modal is hidden, unhide it and hide the editor
|
||||||
|
if (
|
||||||
|
document.getElementById("modal-buffers")!.classList.contains("invisible")
|
||||||
|
) {
|
||||||
|
document.getElementById("editor")!.classList.add("invisible");
|
||||||
|
document.getElementById("modal-buffers")!.classList.remove("invisible");
|
||||||
|
document.getElementById("buffer-search")!.focus();
|
||||||
|
} else {
|
||||||
|
closeUniverseModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeUniverseModal = (): void => {
|
||||||
|
// @ts-ignore
|
||||||
|
document.getElementById("buffer-search")!.value = "";
|
||||||
|
document.getElementById("editor")!.classList.remove("invisible");
|
||||||
|
document.getElementById("modal-buffers")!.classList.add("invisible");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openSettingsModal = (): void => {
|
||||||
|
if (
|
||||||
|
document.getElementById("modal-settings")!.classList.contains("invisible")
|
||||||
|
) {
|
||||||
|
document.getElementById("editor")!.classList.add("invisible");
|
||||||
|
document.getElementById("modal-settings")!.classList.remove("invisible");
|
||||||
|
} else {
|
||||||
|
closeSettingsModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeSettingsModal = (): void => {
|
||||||
|
document.getElementById("editor")!.classList.remove("invisible");
|
||||||
|
document.getElementById("modal-settings")!.classList.add("invisible");
|
||||||
|
};
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import { UserAPI } from "../API";
|
import { UserAPI } from "../API";
|
||||||
import { AppSettings } from "../AppSettings";
|
import { AppSettings } from "../FileManagement";
|
||||||
|
|
||||||
export type MidiNoteEvent = {
|
export type MidiNoteEvent = {
|
||||||
note: number;
|
note: number;
|
||||||
velocity: number;
|
velocity: number;
|
||||||
channel: number;
|
channel: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type MidiCCEvent = {
|
export type MidiCCEvent = {
|
||||||
control: number;
|
control: number;
|
||||||
value: number;
|
value: number;
|
||||||
channel: number;
|
channel: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class MidiConnection {
|
export class MidiConnection {
|
||||||
/**
|
/**
|
||||||
@ -45,7 +45,8 @@ export class MidiConnection {
|
|||||||
public lastNote: MidiNoteEvent | undefined = undefined;
|
public lastNote: MidiNoteEvent | undefined = undefined;
|
||||||
public lastCC: { [control: number]: number } = {};
|
public lastCC: { [control: number]: number } = {};
|
||||||
public lastNoteInChannel: { [channel: number]: MidiNoteEvent } = {};
|
public lastNoteInChannel: { [channel: number]: MidiNoteEvent } = {};
|
||||||
public lastCCInChannel: { [channel: number]: { [control: number]: number } } = {};
|
public lastCCInChannel: { [channel: number]: { [control: number]: number } } =
|
||||||
|
{};
|
||||||
|
|
||||||
/* MIDI clock stuff */
|
/* MIDI clock stuff */
|
||||||
private midiClockInputIndex: number | undefined = undefined;
|
private midiClockInputIndex: number | undefined = undefined;
|
||||||
@ -173,8 +174,12 @@ export class MidiConnection {
|
|||||||
* Updates the MIDI clock input select element with the available MIDI inputs.
|
* Updates the MIDI clock input select element with the available MIDI inputs.
|
||||||
*/
|
*/
|
||||||
if (this.midiInputs.length > 0) {
|
if (this.midiInputs.length > 0) {
|
||||||
const midiClockSelect = document.getElementById("midi-clock-input") as HTMLSelectElement;
|
const midiClockSelect = document.getElementById(
|
||||||
const midiInputSelect = document.getElementById("default-midi-input") as HTMLSelectElement;
|
"midi-clock-input"
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
const midiInputSelect = document.getElementById(
|
||||||
|
"default-midi-input"
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
|
||||||
midiClockSelect.innerHTML = "";
|
midiClockSelect.innerHTML = "";
|
||||||
midiInputSelect.innerHTML = "";
|
midiInputSelect.innerHTML = "";
|
||||||
@ -201,7 +206,9 @@ export class MidiConnection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.settings.midi_clock_input) {
|
if (this.settings.midi_clock_input) {
|
||||||
const clockMidiInputIndex = this.getMidiInputIndex(this.settings.midi_clock_input);
|
const clockMidiInputIndex = this.getMidiInputIndex(
|
||||||
|
this.settings.midi_clock_input
|
||||||
|
);
|
||||||
midiClockSelect.value = clockMidiInputIndex.toString();
|
midiClockSelect.value = clockMidiInputIndex.toString();
|
||||||
if (clockMidiInputIndex > 0) {
|
if (clockMidiInputIndex > 0) {
|
||||||
this.midiClockInput = this.midiInputs[clockMidiInputIndex];
|
this.midiClockInput = this.midiInputs[clockMidiInputIndex];
|
||||||
@ -212,7 +219,9 @@ export class MidiConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.default_midi_input) {
|
if (this.settings.default_midi_input) {
|
||||||
const defaultMidiInputIndex = this.getMidiInputIndex(this.settings.default_midi_input);
|
const defaultMidiInputIndex = this.getMidiInputIndex(
|
||||||
|
this.settings.default_midi_input
|
||||||
|
);
|
||||||
midiInputSelect.value = defaultMidiInputIndex.toString();
|
midiInputSelect.value = defaultMidiInputIndex.toString();
|
||||||
if (defaultMidiInputIndex > 0) {
|
if (defaultMidiInputIndex > 0) {
|
||||||
this.currentInputIndex = defaultMidiInputIndex;
|
this.currentInputIndex = defaultMidiInputIndex;
|
||||||
@ -226,16 +235,25 @@ export class MidiConnection {
|
|||||||
midiClockSelect.addEventListener("change", (event) => {
|
midiClockSelect.addEventListener("change", (event) => {
|
||||||
const value = (event.target as HTMLSelectElement).value;
|
const value = (event.target as HTMLSelectElement).value;
|
||||||
if (value === "-1") {
|
if (value === "-1") {
|
||||||
if (this.midiClockInput && this.midiClockInputIndex != this.currentInputIndex) this.midiClockInput.onmidimessage = null;
|
if (
|
||||||
|
this.midiClockInput &&
|
||||||
|
this.midiClockInputIndex != this.currentInputIndex
|
||||||
|
)
|
||||||
|
this.midiClockInput.onmidimessage = null;
|
||||||
this.midiClockInput = undefined;
|
this.midiClockInput = undefined;
|
||||||
this.settings.midi_clock_input = undefined;
|
this.settings.midi_clock_input = undefined;
|
||||||
} else {
|
} else {
|
||||||
const clockInputIndex = parseInt(value);
|
const clockInputIndex = parseInt(value);
|
||||||
this.midiClockInputIndex = clockInputIndex;
|
this.midiClockInputIndex = clockInputIndex;
|
||||||
if (this.midiClockInput && this.midiClockInputIndex != this.currentInputIndex) this.midiClockInput.onmidimessage = null;
|
if (
|
||||||
|
this.midiClockInput &&
|
||||||
|
this.midiClockInputIndex != this.currentInputIndex
|
||||||
|
)
|
||||||
|
this.midiClockInput.onmidimessage = null;
|
||||||
this.midiClockInput = this.midiInputs[clockInputIndex];
|
this.midiClockInput = this.midiInputs[clockInputIndex];
|
||||||
this.registerMidiInputListener(clockInputIndex);
|
this.registerMidiInputListener(clockInputIndex);
|
||||||
this.settings.midi_clock_input = this.midiClockInput.name || undefined;
|
this.settings.midi_clock_input =
|
||||||
|
this.midiClockInput.name || undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -243,17 +261,25 @@ export class MidiConnection {
|
|||||||
midiInputSelect.addEventListener("change", (event) => {
|
midiInputSelect.addEventListener("change", (event) => {
|
||||||
const value = (event.target as HTMLSelectElement).value;
|
const value = (event.target as HTMLSelectElement).value;
|
||||||
if (value === "-1") {
|
if (value === "-1") {
|
||||||
if (this.currentInputIndex && this.currentInputIndex != this.midiClockInputIndex) this.unregisterMidiInputListener(this.currentInputIndex);
|
if (
|
||||||
|
this.currentInputIndex &&
|
||||||
|
this.currentInputIndex != this.midiClockInputIndex
|
||||||
|
)
|
||||||
|
this.unregisterMidiInputListener(this.currentInputIndex);
|
||||||
this.currentInputIndex = undefined;
|
this.currentInputIndex = undefined;
|
||||||
this.settings.default_midi_input = undefined;
|
this.settings.default_midi_input = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (this.currentInputIndex && this.currentInputIndex != this.midiClockInputIndex) this.unregisterMidiInputListener(this.currentInputIndex);
|
if (
|
||||||
|
this.currentInputIndex &&
|
||||||
|
this.currentInputIndex != this.midiClockInputIndex
|
||||||
|
)
|
||||||
|
this.unregisterMidiInputListener(this.currentInputIndex);
|
||||||
this.currentInputIndex = parseInt(value);
|
this.currentInputIndex = parseInt(value);
|
||||||
this.registerMidiInputListener(this.currentInputIndex);
|
this.registerMidiInputListener(this.currentInputIndex);
|
||||||
this.settings.default_midi_input = this.midiInputs[this.currentInputIndex].name || undefined;
|
this.settings.default_midi_input =
|
||||||
|
this.midiInputs[this.currentInputIndex].name || undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,36 +316,60 @@ export class MidiConnection {
|
|||||||
}
|
}
|
||||||
/* DEFAULT MIDI INPUT */
|
/* DEFAULT MIDI INPUT */
|
||||||
if (input.name === this.settings.default_midi_input) {
|
if (input.name === this.settings.default_midi_input) {
|
||||||
|
|
||||||
// If message is one of note ons
|
// If message is one of note ons
|
||||||
if (message.data[0] >= 0x90 && message.data[0] <= 0x9F) {
|
if (message.data[0] >= 0x90 && message.data[0] <= 0x9f) {
|
||||||
const channel = message.data[0] - 0x90 + 1;
|
const channel = message.data[0] - 0x90 + 1;
|
||||||
const note = message.data[1];
|
const note = message.data[1];
|
||||||
const velocity = message.data[2];
|
const velocity = message.data[2];
|
||||||
|
|
||||||
this.lastNote = { note, velocity, channel, timestamp: event.timeStamp };
|
this.lastNote = {
|
||||||
this.lastNoteInChannel[channel] = { note, velocity, channel, timestamp: event.timeStamp };
|
note,
|
||||||
|
velocity,
|
||||||
|
channel,
|
||||||
|
timestamp: event.timeStamp,
|
||||||
|
};
|
||||||
|
this.lastNoteInChannel[channel] = {
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
channel,
|
||||||
|
timestamp: event.timeStamp,
|
||||||
|
};
|
||||||
|
|
||||||
if (this.settings.midi_channels_scripts) this.api.script(channel);
|
if (this.settings.midi_channels_scripts) this.api.script(channel);
|
||||||
|
|
||||||
this.pushToMidiInputBuffer({ note, velocity, channel, timestamp: event.timeStamp });
|
this.pushToMidiInputBuffer({
|
||||||
this.activeNotes.push({ note, velocity, channel, timestamp: event.timeStamp });
|
note,
|
||||||
|
velocity,
|
||||||
|
channel,
|
||||||
|
timestamp: event.timeStamp,
|
||||||
|
});
|
||||||
|
this.activeNotes.push({
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
channel,
|
||||||
|
timestamp: event.timeStamp,
|
||||||
|
});
|
||||||
|
|
||||||
const sticky = this.removeFromStickyNotes(note, channel);
|
const sticky = this.removeFromStickyNotes(note, channel);
|
||||||
if (!sticky) this.stickyNotes.push({ note, velocity, channel, timestamp: event.timeStamp });
|
if (!sticky)
|
||||||
|
this.stickyNotes.push({
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
channel,
|
||||||
|
timestamp: event.timeStamp,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If note off
|
// If note off
|
||||||
if (message.data[0] >= 0x80 && message.data[0] <= 0x8F) {
|
if (message.data[0] >= 0x80 && message.data[0] <= 0x8f) {
|
||||||
const channel = message.data[0] - 0x80 + 1;
|
const channel = message.data[0] - 0x80 + 1;
|
||||||
const note = message.data[1];
|
const note = message.data[1];
|
||||||
this.removeFromActiveNotes(note, channel);
|
this.removeFromActiveNotes(note, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If message is one of CCs
|
// If message is one of CCs
|
||||||
if (message.data[0] >= 0xB0 && message.data[0] <= 0xBF) {
|
if (message.data[0] >= 0xb0 && message.data[0] <= 0xbf) {
|
||||||
|
const channel = message.data[0] - 0xb0 + 1;
|
||||||
const channel = message.data[0] - 0xB0 + 1;
|
|
||||||
const control = message.data[1];
|
const control = message.data[1];
|
||||||
const value = message.data[2];
|
const value = message.data[2];
|
||||||
|
|
||||||
@ -333,13 +383,15 @@ export class MidiConnection {
|
|||||||
|
|
||||||
//console.log(`CC: ${control} VALUE: ${value} CHANNEL: ${channel}`);
|
//console.log(`CC: ${control} VALUE: ${value} CHANNEL: ${channel}`);
|
||||||
|
|
||||||
this.pushToMidiCCBuffer({ control, value, channel, timestamp: event.timeStamp });
|
this.pushToMidiCCBuffer({
|
||||||
|
control,
|
||||||
|
value,
|
||||||
|
channel,
|
||||||
|
timestamp: event.timeStamp,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -347,16 +399,22 @@ export class MidiConnection {
|
|||||||
/* Methods for handling active midi notes */
|
/* Methods for handling active midi notes */
|
||||||
|
|
||||||
public removeFromActiveNotes(note: number, channel: number): void {
|
public removeFromActiveNotes(note: number, channel: number): void {
|
||||||
const index = this.activeNotes.findIndex((e) => e.note === note && e.channel === channel);
|
const index = this.activeNotes.findIndex(
|
||||||
|
(e) => e.note === note && e.channel === channel
|
||||||
|
);
|
||||||
if (index >= 0) this.activeNotes.splice(index, 1);
|
if (index >= 0) this.activeNotes.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFromStickyNotes(note: number, channel: number): boolean {
|
public removeFromStickyNotes(note: number, channel: number): boolean {
|
||||||
const index = this.stickyNotes.findIndex((e) => e.note === note && e.channel === channel);
|
const index = this.stickyNotes.findIndex(
|
||||||
|
(e) => e.note === note && e.channel === channel
|
||||||
|
);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.stickyNotes.splice(index, 1);
|
this.stickyNotes.splice(index, 1);
|
||||||
return true;
|
return true;
|
||||||
} else { return false; }
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public stickyNotesFromChannel(channel: number): MidiNoteEvent[] {
|
public stickyNotesFromChannel(channel: number): MidiNoteEvent[] {
|
||||||
@ -425,7 +483,6 @@ export class MidiConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public onMidiClock(timestamp: number): void {
|
public onMidiClock(timestamp: number): void {
|
||||||
/**
|
/**
|
||||||
* Called when a MIDI clock message is received.
|
* Called when a MIDI clock message is received.
|
||||||
@ -434,7 +491,6 @@ export class MidiConnection {
|
|||||||
this.clockTicks += 1;
|
this.clockTicks += 1;
|
||||||
|
|
||||||
if (this.lastTimestamp > 0) {
|
if (this.lastTimestamp > 0) {
|
||||||
|
|
||||||
if (this.lastTimestamp === timestamp) {
|
if (this.lastTimestamp === timestamp) {
|
||||||
// This is error handling for odd MIDI clock messages with the same timestamp
|
// This is error handling for odd MIDI clock messages with the same timestamp
|
||||||
this.clockErrorCount += 1;
|
this.clockErrorCount += 1;
|
||||||
@ -452,12 +508,13 @@ export class MidiConnection {
|
|||||||
this.skipOnError = this.settings.midi_clock_ppqn / 4;
|
this.skipOnError = this.settings.midi_clock_ppqn / 4;
|
||||||
timestamp = 0; // timestamp 0 == lastTimestamp 0
|
timestamp = 0; // timestamp 0 == lastTimestamp 0
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
this.midiClockDelta = timestamp - this.lastTimestamp;
|
this.midiClockDelta = timestamp - this.lastTimestamp;
|
||||||
this.lastBPM = 60 * (1000 / this.midiClockDelta / this.settings.midi_clock_ppqn);
|
this.lastBPM =
|
||||||
|
60 * (1000 / this.midiClockDelta / this.settings.midi_clock_ppqn);
|
||||||
|
|
||||||
this.clockBuffer.push(this.lastBPM);
|
this.clockBuffer.push(this.lastBPM);
|
||||||
if (this.clockBuffer.length > this.clockBufferLength) this.clockBuffer.shift();
|
if (this.clockBuffer.length > this.clockBufferLength)
|
||||||
|
this.clockBuffer.shift();
|
||||||
|
|
||||||
const estimatedBPM = this.estimatedBPM();
|
const estimatedBPM = this.estimatedBPM();
|
||||||
if (estimatedBPM !== this.roundedBPM) {
|
if (estimatedBPM !== this.roundedBPM) {
|
||||||
@ -465,13 +522,11 @@ export class MidiConnection {
|
|||||||
this.api.bpm(estimatedBPM);
|
this.api.bpm(estimatedBPM);
|
||||||
this.roundedBPM = estimatedBPM;
|
this.roundedBPM = estimatedBPM;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastTimestamp = timestamp;
|
this.lastTimestamp = timestamp;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public estimatedBPM(): number {
|
public estimatedBPM(): number {
|
||||||
@ -523,7 +578,8 @@ export class MidiConnection {
|
|||||||
if (typeof output === "number") {
|
if (typeof output === "number") {
|
||||||
if (output < 0 || output >= this.midiOutputs.length) {
|
if (output < 0 || output >= this.midiOutputs.length) {
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid MIDI output index. Index must be in the range 0-${this.midiOutputs.length - 1
|
`Invalid MIDI output index. Index must be in the range 0-${
|
||||||
|
this.midiOutputs.length - 1
|
||||||
}.`
|
}.`
|
||||||
);
|
);
|
||||||
return this.currentOutputIndex;
|
return this.currentOutputIndex;
|
||||||
@ -552,7 +608,8 @@ export class MidiConnection {
|
|||||||
if (typeof input === "number") {
|
if (typeof input === "number") {
|
||||||
if (input < 0 || input >= this.midiInputs.length) {
|
if (input < 0 || input >= this.midiInputs.length) {
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid MIDI input index. Index must be in the range 0-${this.midiInputs.length - 1
|
`Invalid MIDI input index. Index must be in the range 0-${
|
||||||
|
this.midiInputs.length - 1
|
||||||
}.`
|
}.`
|
||||||
);
|
);
|
||||||
return -1;
|
return -1;
|
||||||
@ -625,26 +682,35 @@ export class MidiConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendMidiOn(note: number, channel: number, velocity: number, port: number | string = this.currentOutputIndex) {
|
public sendMidiOn(
|
||||||
/**
|
note: number,
|
||||||
|
channel: number,
|
||||||
|
velocity: number,
|
||||||
|
port: number | string = this.currentOutputIndex
|
||||||
|
) {
|
||||||
|
/**
|
||||||
* Sending Midi Note on message
|
* Sending Midi Note on message
|
||||||
*/
|
*/
|
||||||
if(typeof port === "string") port = this.getMidiOutputIndex(port);
|
if (typeof port === "string") port = this.getMidiOutputIndex(port);
|
||||||
const output = this.midiOutputs[port];
|
const output = this.midiOutputs[port];
|
||||||
note = Math.min(Math.max(note, 0), 127);
|
note = Math.min(Math.max(note, 0), 127);
|
||||||
if (output) {
|
if (output) {
|
||||||
const noteOnMessage = [0x90 + channel, note, velocity];
|
const noteOnMessage = [0x90 + channel, note, velocity];
|
||||||
output.send(noteOnMessage);
|
output.send(noteOnMessage);
|
||||||
} else {
|
} else {
|
||||||
console.error("MIDI output not available.");
|
console.error("MIDI output not available.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMidiOff(note: number, channel: number, port: number | string = this.currentOutputIndex) {
|
sendMidiOff(
|
||||||
|
note: number,
|
||||||
|
channel: number,
|
||||||
|
port: number | string = this.currentOutputIndex
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Sending Midi Note off message
|
* Sending Midi Note off message
|
||||||
*/
|
*/
|
||||||
if(typeof port === "string") port = this.getMidiOutputIndex(port);
|
if (typeof port === "string") port = this.getMidiOutputIndex(port);
|
||||||
const output = this.midiOutputs[port];
|
const output = this.midiOutputs[port];
|
||||||
note = Math.min(Math.max(note, 0), 127);
|
note = Math.min(Math.max(note, 0), 127);
|
||||||
if (output) {
|
if (output) {
|
||||||
@ -655,11 +721,14 @@ export class MidiConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAllNotesOff(channel: number, port: number | string = this.currentOutputIndex) {
|
sendAllNotesOff(
|
||||||
|
channel: number,
|
||||||
|
port: number | string = this.currentOutputIndex
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Sending Midi Note off message
|
* Sending Midi Note off message
|
||||||
*/
|
*/
|
||||||
if(typeof port === "string") port = this.getMidiOutputIndex(port);
|
if (typeof port === "string") port = this.getMidiOutputIndex(port);
|
||||||
const output = this.midiOutputs[port];
|
const output = this.midiOutputs[port];
|
||||||
if (output) {
|
if (output) {
|
||||||
const noteOffMessage = [0xb0 + channel, 123, 0];
|
const noteOffMessage = [0xb0 + channel, 123, 0];
|
||||||
@ -669,11 +738,14 @@ export class MidiConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAllSoundOff(channel: number, port: number | string = this.currentOutputIndex) {
|
sendAllSoundOff(
|
||||||
|
channel: number,
|
||||||
|
port: number | string = this.currentOutputIndex
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Sending all sound off
|
* Sending all sound off
|
||||||
*/
|
*/
|
||||||
if(typeof port === "string") port = this.getMidiOutputIndex(port);
|
if (typeof port === "string") port = this.getMidiOutputIndex(port);
|
||||||
const output = this.midiOutputs[port];
|
const output = this.midiOutputs[port];
|
||||||
if (output) {
|
if (output) {
|
||||||
const noteOffMessage = [0xb0 + channel, 120, 0];
|
const noteOffMessage = [0xb0 + channel, 120, 0];
|
||||||
|
|||||||
434
src/InterfaceLogic.ts
Normal file
434
src/InterfaceLogic.ts
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
import { EditorView } from "codemirror";
|
||||||
|
import { vim } from "@replit/codemirror-vim";
|
||||||
|
import { type Editor } from "./main";
|
||||||
|
import {
|
||||||
|
documentation_factory,
|
||||||
|
hideDocumentation,
|
||||||
|
showDocumentation,
|
||||||
|
updateDocumentationContent,
|
||||||
|
} from "./Documentation";
|
||||||
|
import {
|
||||||
|
type Universe,
|
||||||
|
template_universe,
|
||||||
|
template_universes,
|
||||||
|
loadUniverse,
|
||||||
|
emptyUrl,
|
||||||
|
share,
|
||||||
|
closeUniverseModal,
|
||||||
|
openUniverseModal,
|
||||||
|
} from "./FileManagement";
|
||||||
|
import { loadSamples } from "./API";
|
||||||
|
import { tryEvaluate } from "./Evaluator";
|
||||||
|
import { inlineHoveringTips } from "./documentation/inlineHelp";
|
||||||
|
import { lineNumbers } from "@codemirror/view";
|
||||||
|
|
||||||
|
export const installInterfaceLogic = (app: Editor) => {
|
||||||
|
(app.interface.line_numbers_checkbox as HTMLInputElement).checked =
|
||||||
|
app.settings.line_numbers;
|
||||||
|
(app.interface.time_position_checkbox as HTMLInputElement).checked =
|
||||||
|
app.settings.time_position;
|
||||||
|
(app.interface.tips_checkbox as HTMLInputElement).checked = app.settings.tips;
|
||||||
|
(app.interface.midi_clock_checkbox as HTMLInputElement).checked =
|
||||||
|
app.settings.send_clock;
|
||||||
|
(app.interface.midi_channels_scripts as HTMLInputElement).checked =
|
||||||
|
app.settings.midi_channels_scripts;
|
||||||
|
(app.interface.midi_clock_ppqn as HTMLInputElement).value =
|
||||||
|
app.settings.midi_clock_ppqn.toString();
|
||||||
|
if (!app.settings.time_position) {
|
||||||
|
(app.interface.timeviewer as HTMLElement).classList.add("hidden");
|
||||||
|
}
|
||||||
|
(app.interface.load_demo_songs as HTMLInputElement).checked =
|
||||||
|
app.settings.load_demo_songs;
|
||||||
|
|
||||||
|
const tabs = document.querySelectorAll('[id^="tab-"]');
|
||||||
|
// Iterate over the tabs with an index
|
||||||
|
for (let i = 0; i < tabs.length; i++) {
|
||||||
|
tabs[i].addEventListener("click", (event) => {
|
||||||
|
// Updating the CSS accordingly
|
||||||
|
tabs[i].classList.add("bg-orange-300");
|
||||||
|
for (let j = 0; j < tabs.length; j++) {
|
||||||
|
if (j != i) tabs[j].classList.remove("bg-orange-300");
|
||||||
|
}
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
|
||||||
|
let tab = event.target as HTMLElement;
|
||||||
|
let tab_id = tab.id.split("-")[1];
|
||||||
|
app.local_index = parseInt(tab_id);
|
||||||
|
app.updateEditorView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.interface.topos_logo.addEventListener("click", () => {
|
||||||
|
hideDocumentation();
|
||||||
|
app.updateKnownUniversesView();
|
||||||
|
openUniverseModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.buttonElements.play_buttons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
if (app.isPlaying) {
|
||||||
|
app.setButtonHighlighting("pause", true);
|
||||||
|
app.isPlaying = !app.isPlaying;
|
||||||
|
app.clock.pause();
|
||||||
|
app.api.MidiConnection.sendStopMessage();
|
||||||
|
} else {
|
||||||
|
app.setButtonHighlighting("play", true);
|
||||||
|
app.isPlaying = !app.isPlaying;
|
||||||
|
app.clock.start();
|
||||||
|
app.api.MidiConnection.sendStartMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.buttonElements.clear_buttons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
app.setButtonHighlighting("clear", true);
|
||||||
|
if (confirm("Do you want to reset the current universe?")) {
|
||||||
|
app.universes[app.selected_universe] =
|
||||||
|
structuredClone(template_universe);
|
||||||
|
app.updateEditorView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.documentation_button.addEventListener("click", () => {
|
||||||
|
showDocumentation(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.destroy_universes_button.addEventListener("click", () => {
|
||||||
|
if (confirm("Do you want to destroy all universes?")) {
|
||||||
|
app.universes = {
|
||||||
|
...template_universes,
|
||||||
|
};
|
||||||
|
app.updateKnownUniversesView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.audio_nudge_range.addEventListener("input", () => {
|
||||||
|
app.clock.nudge = parseInt(
|
||||||
|
(app.interface.audio_nudge_range as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.dough_nudge_range.addEventListener("input", () => {
|
||||||
|
app.dough_nudge = parseInt(
|
||||||
|
(app.interface.dough_nudge_range as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.upload_universe_button.addEventListener("click", () => {
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.accept = ".json";
|
||||||
|
|
||||||
|
fileInput.addEventListener("change", (event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file, "UTF-8");
|
||||||
|
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = JSON.parse(evt.target!.result as string);
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
app.universes[key] = value as Universe;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = (evt) => {
|
||||||
|
console.error("An error occurred reading the file:", evt);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
fileInput.click();
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.download_universe_button.addEventListener("click", () => {
|
||||||
|
// Trigger save of the universe before downloading
|
||||||
|
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
|
||||||
|
|
||||||
|
// Generate a file name based on timestamp
|
||||||
|
let fileName = `topos-universes-${Date.now()}.json`;
|
||||||
|
|
||||||
|
// Create Blob and Object URL
|
||||||
|
const blob = new Blob([JSON.stringify(app.settings.universes)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create a temporary anchor and trigger download
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
// Revoke the Object URL to free resources
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.load_universe_button.addEventListener("click", () => {
|
||||||
|
let query = (app.interface.buffer_search as HTMLInputElement).value;
|
||||||
|
if (query.length > 2 && query.length < 20 && !query.includes(" ")) {
|
||||||
|
loadUniverse(app, query);
|
||||||
|
app.settings.selected_universe = query;
|
||||||
|
(app.interface.buffer_search as HTMLInputElement).value = "";
|
||||||
|
closeUniverseModal();
|
||||||
|
app.view.focus();
|
||||||
|
emptyUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.eval_button.addEventListener("click", () => {
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
app.flashBackground("#404040", 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.buttonElements.stop_buttons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
app.setButtonHighlighting("stop", true);
|
||||||
|
app.isPlaying = false;
|
||||||
|
app.clock.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.local_button.addEventListener("click", () =>
|
||||||
|
app.changeModeFromInterface("local")
|
||||||
|
);
|
||||||
|
app.interface.global_button.addEventListener("click", () =>
|
||||||
|
app.changeModeFromInterface("global")
|
||||||
|
);
|
||||||
|
app.interface.init_button.addEventListener("click", () =>
|
||||||
|
app.changeModeFromInterface("init")
|
||||||
|
);
|
||||||
|
app.interface.note_button.addEventListener("click", () =>
|
||||||
|
app.changeModeFromInterface("notes")
|
||||||
|
);
|
||||||
|
|
||||||
|
app.interface.font_family_selector.addEventListener("change", () => {
|
||||||
|
//@ts-ignore
|
||||||
|
let new_font = (app.interface.font_family_selector as HTMLSelectElement)
|
||||||
|
.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.font_size_input.addEventListener("input", () => {
|
||||||
|
let new_value: string | number = (
|
||||||
|
app.interface.font_size_input as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
app.settings.font_size = parseInt(new_value);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.settings_button.addEventListener("click", () => {
|
||||||
|
// Populate the font family selector
|
||||||
|
const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement;
|
||||||
|
doughNudgeRange.value = app.dough_nudge.toString();
|
||||||
|
// @ts-ignore
|
||||||
|
const doughNumber = document.getElementById(
|
||||||
|
"doughnumber"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
doughNumber.value = app.dough_nudge.toString();
|
||||||
|
(app.interface.font_family_selector as HTMLSelectElement).value =
|
||||||
|
app.settings.font;
|
||||||
|
|
||||||
|
if (app.settings.font_size === null) {
|
||||||
|
app.settings.font_size = 12;
|
||||||
|
}
|
||||||
|
const fontSizeInput = app.interface.font_size_input as HTMLInputElement;
|
||||||
|
fontSizeInput.value = app.settings.font_size.toString();
|
||||||
|
|
||||||
|
// Get the right value to update graphical widgets
|
||||||
|
const lineNumbersCheckbox = app.interface
|
||||||
|
.line_numbers_checkbox as HTMLInputElement;
|
||||||
|
lineNumbersCheckbox.checked = app.settings.line_numbers;
|
||||||
|
const timePositionCheckbox = app.interface
|
||||||
|
.time_position_checkbox as HTMLInputElement;
|
||||||
|
timePositionCheckbox.checked = app.settings.time_position;
|
||||||
|
const tipsCheckbox = app.interface.tips_checkbox as HTMLInputElement;
|
||||||
|
tipsCheckbox.checked = app.settings.tips;
|
||||||
|
const midiClockCheckbox = app.interface
|
||||||
|
.midi_clock_checkbox as HTMLInputElement;
|
||||||
|
midiClockCheckbox.checked = app.settings.send_clock;
|
||||||
|
const midiChannelsScripts = app.interface
|
||||||
|
.midi_channels_scripts as HTMLInputElement;
|
||||||
|
midiChannelsScripts.checked = app.settings.midi_channels_scripts;
|
||||||
|
const midiClockPpqn = app.interface.midi_clock_ppqn as HTMLInputElement;
|
||||||
|
midiClockPpqn.value = app.settings.midi_clock_ppqn.toString();
|
||||||
|
const loadDemoSongs = app.interface.load_demo_songs as HTMLInputElement;
|
||||||
|
loadDemoSongs.checked = app.settings.load_demo_songs;
|
||||||
|
const vimModeCheckbox = app.interface.vim_mode_checkbox as HTMLInputElement;
|
||||||
|
vimModeCheckbox.checked = app.settings.vimMode;
|
||||||
|
|
||||||
|
let modal_settings = document.getElementById("modal-settings");
|
||||||
|
let editor = document.getElementById("editor");
|
||||||
|
modal_settings?.classList.remove("invisible");
|
||||||
|
|
||||||
|
editor?.classList.add("invisible");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.close_settings_button.addEventListener("click", () => {
|
||||||
|
let modal_settings = document.getElementById("modal-settings");
|
||||||
|
let editor = document.getElementById("editor");
|
||||||
|
modal_settings?.classList.add("invisible");
|
||||||
|
editor?.classList.remove("invisible");
|
||||||
|
// Update the font size once again
|
||||||
|
app.view.dispatch({
|
||||||
|
effects: app.fontSize.reconfigure(
|
||||||
|
EditorView.theme({
|
||||||
|
"&": { fontSize: app.settings.font_size + "px" },
|
||||||
|
"&content": {
|
||||||
|
fontFamily: app.settings.font,
|
||||||
|
fontSize: app.settings.font_size + "px",
|
||||||
|
},
|
||||||
|
".cm-gutters": { fontSize: app.settings.font_size + "px" },
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.close_universes_button.addEventListener("click", () => {
|
||||||
|
openUniverseModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.share_button.addEventListener("click", async () => {
|
||||||
|
// trigger a manual save
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
app.currentFile().committed = app.view.state.doc.toString();
|
||||||
|
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
|
||||||
|
// encode as a blob!
|
||||||
|
await share(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.vim_mode_checkbox.addEventListener("change", () => {
|
||||||
|
let checked = (app.interface.vim_mode_checkbox as HTMLInputElement).checked
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
app.settings.vimMode = checked;
|
||||||
|
app.view.dispatch({
|
||||||
|
effects: app.vimModeCompartment.reconfigure(checked ? vim() : []),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.line_numbers_checkbox.addEventListener("change", () => {
|
||||||
|
let lineNumbersCheckbox = app.interface
|
||||||
|
.line_numbers_checkbox as HTMLInputElement;
|
||||||
|
let checked = lineNumbersCheckbox.checked ? true : false;
|
||||||
|
app.settings.line_numbers = checked;
|
||||||
|
app.view.dispatch({
|
||||||
|
effects: app.withLineNumbers.reconfigure(checked ? [lineNumbers()] : []),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.time_position_checkbox.addEventListener("change", () => {
|
||||||
|
let timeviewer = document.getElementById("timeviewer") as HTMLElement;
|
||||||
|
let checked = (app.interface.time_position_checkbox as HTMLInputElement)
|
||||||
|
.checked
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
app.settings.time_position = checked;
|
||||||
|
checked
|
||||||
|
? timeviewer.classList.remove("hidden")
|
||||||
|
: timeviewer.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.tips_checkbox.addEventListener("change", () => {
|
||||||
|
let checked = (app.interface.tips_checkbox as HTMLInputElement).checked
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
app.settings.tips = checked;
|
||||||
|
app.view.dispatch({
|
||||||
|
effects: app.hoveringCompartment.reconfigure(
|
||||||
|
checked ? inlineHoveringTips : []
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.midi_clock_checkbox.addEventListener("change", () => {
|
||||||
|
let checked = (app.interface.midi_clock_checkbox as HTMLInputElement)
|
||||||
|
.checked
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
app.settings.send_clock = checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.midi_channels_scripts.addEventListener("change", () => {
|
||||||
|
let checked = (app.interface.midi_channels_scripts as HTMLInputElement)
|
||||||
|
.checked
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
app.settings.midi_channels_scripts = checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.midi_clock_ppqn.addEventListener("change", () => {
|
||||||
|
let value = parseInt(
|
||||||
|
(app.interface.midi_clock_ppqn as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
app.settings.midi_clock_ppqn = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.load_demo_songs.addEventListener("change", () => {
|
||||||
|
let checked = (app.interface.load_demo_songs as HTMLInputElement).checked
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
app.settings.load_demo_songs = checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.interface.universe_creator.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let data = new FormData(app.interface.universe_creator as HTMLFormElement);
|
||||||
|
let universeName = data.get("universe") as string | null;
|
||||||
|
|
||||||
|
if (universeName) {
|
||||||
|
if (universeName.length > 2 && universeName.length < 20) {
|
||||||
|
loadUniverse(app, universeName);
|
||||||
|
app.settings.selected_universe = universeName;
|
||||||
|
(app.interface.buffer_search as HTMLInputElement).value = "";
|
||||||
|
closeUniverseModal();
|
||||||
|
app.view.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tryEvaluate(app, app.universes[app.selected_universe.toString()].init);
|
||||||
|
|
||||||
|
[
|
||||||
|
"introduction",
|
||||||
|
"interface",
|
||||||
|
"interaction",
|
||||||
|
"code",
|
||||||
|
"time",
|
||||||
|
"sound",
|
||||||
|
"samples",
|
||||||
|
"synths",
|
||||||
|
"chaining",
|
||||||
|
"patterns",
|
||||||
|
"ziffers",
|
||||||
|
"midi",
|
||||||
|
"functions",
|
||||||
|
"lfos",
|
||||||
|
"probabilities",
|
||||||
|
"variables",
|
||||||
|
// "reference",
|
||||||
|
"shortcuts",
|
||||||
|
"about",
|
||||||
|
"bonus",
|
||||||
|
].forEach((e) => {
|
||||||
|
let name = `docs_` + e;
|
||||||
|
document.getElementById(name)!.addEventListener("click", async () => {
|
||||||
|
if (name !== "docs_samples") {
|
||||||
|
app.currentDocumentationPane = e;
|
||||||
|
updateDocumentationContent(app);
|
||||||
|
} else {
|
||||||
|
console.log("Loading samples!");
|
||||||
|
await loadSamples().then(() => {
|
||||||
|
app.docs = documentation_factory(app);
|
||||||
|
app.currentDocumentationPane = e;
|
||||||
|
updateDocumentationContent(app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
160
src/KeyActions.ts
Normal file
160
src/KeyActions.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { type Editor } from "./main";
|
||||||
|
import { vim } from "@replit/codemirror-vim";
|
||||||
|
import { tryEvaluate } from "./Evaluator";
|
||||||
|
import { hideDocumentation, showDocumentation } from "./Documentation";
|
||||||
|
import { openSettingsModal, openUniverseModal } from "./FileManagement";
|
||||||
|
|
||||||
|
export const registerFillKeys = (app: Editor) => {
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.altKey) {
|
||||||
|
app.fill = true;
|
||||||
|
app.interface.fill_viewer.classList.remove("invisible");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keyup", (event) => {
|
||||||
|
if (event.key === "Alt") {
|
||||||
|
app.fill = false;
|
||||||
|
app.interface.fill_viewer.classList.add("invisible");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerOnKeyDown = (app: Editor) => {
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "s") {
|
||||||
|
event.preventDefault();
|
||||||
|
app.setButtonHighlighting("stop", true);
|
||||||
|
app.clock.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "p") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (app.isPlaying) {
|
||||||
|
app.isPlaying = false;
|
||||||
|
app.setButtonHighlighting("pause", true);
|
||||||
|
app.clock.pause();
|
||||||
|
} else {
|
||||||
|
app.isPlaying = true;
|
||||||
|
app.setButtonHighlighting("play", true);
|
||||||
|
app.clock.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl + Shift + V: Vim Mode
|
||||||
|
if (
|
||||||
|
(event.key === "v" || event.key === "V") &&
|
||||||
|
event.ctrlKey &&
|
||||||
|
event.shiftKey
|
||||||
|
) {
|
||||||
|
app.settings.vimMode = !app.settings.vimMode;
|
||||||
|
event.preventDefault();
|
||||||
|
app.userPlugins = app.settings.vimMode ? [] : [vim()];
|
||||||
|
app.view.dispatch({
|
||||||
|
effects: app.dynamicPlugins.reconfigure(app.userPlugins),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl + Enter or Return: Evaluate
|
||||||
|
if ((event.key === "Enter" || event.key === "Return") && event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
app.flashBackground("#404040", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate (bis)
|
||||||
|
if (
|
||||||
|
(event.key === "Enter" && event.shiftKey) ||
|
||||||
|
(event.key === "e" && event.ctrlKey)
|
||||||
|
) {
|
||||||
|
event.preventDefault(); // Prevents the addition of a new line
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
app.flashBackground("#404040", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force evaluation
|
||||||
|
if (event.key === "Enter" && event.shiftKey && event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
tryEvaluate(app, app.currentFile());
|
||||||
|
app.flashBackground("#404040", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// app is the modal to switch between universes
|
||||||
|
if (event.ctrlKey && event.key === "b") {
|
||||||
|
event.preventDefault();
|
||||||
|
hideDocumentation();
|
||||||
|
app.updateKnownUniversesView();
|
||||||
|
openUniverseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// app is the modal that opens up the settings
|
||||||
|
if (event.shiftKey && event.key === "Escape") {
|
||||||
|
openSettingsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "l") {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("local");
|
||||||
|
hideDocumentation();
|
||||||
|
app.view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "n") {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("notes");
|
||||||
|
hideDocumentation();
|
||||||
|
app.view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "g") {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("global");
|
||||||
|
hideDocumentation();
|
||||||
|
app.view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "i") {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("init");
|
||||||
|
hideDocumentation();
|
||||||
|
app.changeToLocalBuffer(0);
|
||||||
|
app.view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && event.key === "d") {
|
||||||
|
event.preventDefault();
|
||||||
|
showDocumentation(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
[112, 113, 114, 115, 116, 117, 118, 119, 120].forEach((keycode, index) => {
|
||||||
|
if (event.keyCode === keycode) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.api.script(keycode - 111);
|
||||||
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("local");
|
||||||
|
app.changeToLocalBuffer(index);
|
||||||
|
hideDocumentation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event.keyCode == 121) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("global");
|
||||||
|
hideDocumentation();
|
||||||
|
}
|
||||||
|
if (event.keyCode == 122) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.changeModeFromInterface("init");
|
||||||
|
hideDocumentation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
38
src/WindowBehavior.ts
Normal file
38
src/WindowBehavior.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { type Editor } from "./main";
|
||||||
|
|
||||||
|
export const installWindowBehaviors = (
|
||||||
|
app: Editor,
|
||||||
|
window: Window,
|
||||||
|
preventMultipleTabs: boolean = false
|
||||||
|
) => {
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
event.preventDefault();
|
||||||
|
// Iterate over all local files and set the candidate to the committed
|
||||||
|
app.currentFile().candidate = app.view.state.doc.toString();
|
||||||
|
app.currentFile().committed = app.view.state.doc.toString();
|
||||||
|
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
|
||||||
|
app.clock.stop();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (preventMultipleTabs) {
|
||||||
|
localStorage.openpages = Date.now();
|
||||||
|
window.addEventListener(
|
||||||
|
"storage",
|
||||||
|
function (e) {
|
||||||
|
if (e.key == "openpages") {
|
||||||
|
// Listen if anybody else is opening the same page!
|
||||||
|
localStorage.page_available = Date.now();
|
||||||
|
}
|
||||||
|
if (e.key == "page_available") {
|
||||||
|
document.getElementById("all")!.classList.add("invisible");
|
||||||
|
alert(
|
||||||
|
"Topos is already opened in another tab. Close this tab now to prevent data loss."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
1269
src/main.ts
1269
src/main.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user