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 { LRUCache } from "lru-cache";
|
||||
import { InputOptions, Player } from "./classes/ZPlayer";
|
||||
import { template_universes } from "./AppSettings";
|
||||
import {
|
||||
loadUniverse,
|
||||
openUniverseModal,
|
||||
template_universes,
|
||||
} from "./FileManagement";
|
||||
import {
|
||||
samples,
|
||||
initAudioOnFirstClick,
|
||||
@ -71,8 +75,8 @@ export class UserAPI {
|
||||
}
|
||||
|
||||
_loadUniverseFromInterface = (universe: string) => {
|
||||
this.app.loadUniverse(universe as string);
|
||||
this.app.openBuffersModal();
|
||||
loadUniverse(this.app, universe as string);
|
||||
openUniverseModal();
|
||||
};
|
||||
|
||||
_deleteUniverseFromInterface = (universe: string) => {
|
||||
@ -141,11 +145,11 @@ export class UserAPI {
|
||||
console.log(error);
|
||||
clearTimeout(this.errorTimeoutID);
|
||||
clearTimeout(this.printTimeoutID);
|
||||
this.app.error_line.innerHTML = error as string;
|
||||
this.app.error_line.style.color = "color-red-800";
|
||||
this.app.error_line.classList.remove("hidden");
|
||||
this.app.interface.error_line.innerHTML = error as string;
|
||||
this.app.interface.error_line.style.color = "color-red-800";
|
||||
this.app.interface.error_line.classList.remove("hidden");
|
||||
this.errorTimeoutID = setTimeout(
|
||||
() => this.app.error_line.classList.add("hidden"),
|
||||
() => this.app.interface.error_line.classList.add("hidden"),
|
||||
2000
|
||||
);
|
||||
};
|
||||
@ -154,11 +158,11 @@ export class UserAPI {
|
||||
console.log(message);
|
||||
clearTimeout(this.printTimeoutID);
|
||||
clearTimeout(this.errorTimeoutID);
|
||||
this.app.error_line.innerHTML = message as string;
|
||||
this.app.error_line.style.color = "white";
|
||||
this.app.error_line.classList.remove("hidden");
|
||||
this.app.interface.error_line.innerHTML = message as string;
|
||||
this.app.interface.error_line.style.color = "white";
|
||||
this.app.interface.error_line.classList.remove("hidden");
|
||||
this.printTimeoutID = setTimeout(
|
||||
() => this.app.error_line.classList.add("hidden"),
|
||||
() => this.app.interface.error_line.classList.add("hidden"),
|
||||
4000
|
||||
);
|
||||
};
|
||||
@ -591,8 +595,9 @@ export class UserAPI {
|
||||
root: number | string,
|
||||
scale: number | string,
|
||||
channel: number = 0,
|
||||
port: number | string = (this.MidiConnection.currentOutputIndex || 0),
|
||||
soundOff: boolean = false): void => {
|
||||
port: number | string = this.MidiConnection.currentOutputIndex || 0,
|
||||
soundOff: boolean = false
|
||||
): void => {
|
||||
/**
|
||||
* Sends given scale to midi output for visual aid
|
||||
*/
|
||||
@ -600,47 +605,53 @@ export class UserAPI {
|
||||
this.hide_scale(root, scale, channel, port);
|
||||
const scaleNotes = getAllScaleNotes(scale, root);
|
||||
// Send each scale note to current midi out
|
||||
scaleNotes.forEach(note => {
|
||||
scaleNotes.forEach((note) => {
|
||||
this.MidiConnection.sendMidiOn(note, channel, 1, port);
|
||||
if (soundOff) this.MidiConnection.sendAllSoundOff(channel, port);
|
||||
});
|
||||
|
||||
this.scale_aid = scale;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public hide_scale = (
|
||||
// @ts-ignore
|
||||
root: number | string = 0,
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
scale: number | string = 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
|
||||
*/
|
||||
const allNotes = Array.from(Array(128).keys());
|
||||
// Send each scale note to current midi out
|
||||
allNotes.forEach(note => {
|
||||
allNotes.forEach((note) => {
|
||||
this.MidiConnection.sendMidiOff(note, channel, port);
|
||||
});
|
||||
this.scale_aid = undefined;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
midi_notes_off = (channel: number = 0, port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => {
|
||||
midi_notes_off = (
|
||||
channel: number = 0,
|
||||
port: number | string = this.MidiConnection.currentOutputIndex || 0
|
||||
): void => {
|
||||
/**
|
||||
* Sends all notes off to midi output
|
||||
*/
|
||||
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
|
||||
*/
|
||||
this.MidiConnection.sendAllSoundOff(channel, port);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================
|
||||
// Ziffers related functions
|
||||
@ -1255,7 +1266,6 @@ export class UserAPI {
|
||||
|
||||
denominator = this.meter;
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Fill
|
||||
// =============================================================
|
||||
@ -1276,7 +1286,9 @@ export class UserAPI {
|
||||
const nArray = Array.isArray(n) ? n : [n];
|
||||
const results: boolean[] = nArray.map(
|
||||
(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);
|
||||
};
|
||||
@ -1284,17 +1296,19 @@ export class UserAPI {
|
||||
|
||||
public bar = (n: number | number[] = 1, nudge: number = 0): boolean => {
|
||||
/**
|
||||
* Determine if the current pulse is on a specified bar, with optional nudge.
|
||||
* @param n Single bar multiplier or array of bar multipliers
|
||||
* @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
|
||||
*/
|
||||
* Determine if the current pulse is on a specified bar, with optional nudge.
|
||||
* @param n Single bar multiplier or array of bar multipliers
|
||||
* @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
|
||||
*/
|
||||
const nArray = Array.isArray(n) ? n : [n];
|
||||
const barLength = this.app.clock.time_signature[1] * this.ppqn();
|
||||
const nudgeInPulses = Math.floor(nudge * barLength);
|
||||
const results: boolean[] = nArray.map(
|
||||
(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);
|
||||
};
|
||||
@ -1302,11 +1316,11 @@ export class UserAPI {
|
||||
|
||||
public pulse = (n: number | number[] = 1, nudge: number = 0): boolean => {
|
||||
/**
|
||||
* 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 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
|
||||
*/
|
||||
* 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 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
|
||||
*/
|
||||
const nArray = Array.isArray(n) ? n : [n];
|
||||
const results: boolean[] = nArray.map(
|
||||
(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 => {
|
||||
const nArray = Array.isArray(tick) ? tick : [tick];
|
||||
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
|
||||
@ -1582,7 +1595,8 @@ export class UserAPI {
|
||||
* @returns A sine wave between -1 and 1
|
||||
*/
|
||||
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
|
||||
* @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 => {
|
||||
@ -1610,7 +1624,9 @@ export class UserAPI {
|
||||
* @see sine
|
||||
* @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 => {
|
||||
@ -1625,7 +1641,11 @@ export class UserAPI {
|
||||
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.
|
||||
*
|
||||
@ -1638,7 +1658,11 @@ export class UserAPI {
|
||||
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.
|
||||
*
|
||||
@ -1740,15 +1764,17 @@ export class UserAPI {
|
||||
};
|
||||
|
||||
public range = (
|
||||
inputY: number, yMin: number,
|
||||
yMax: number, xMin: number,
|
||||
xMax: number): number => {
|
||||
inputY: number,
|
||||
yMin: number,
|
||||
yMax: number,
|
||||
xMin: number,
|
||||
xMax: number
|
||||
): number => {
|
||||
const percent = (inputY - yMin) / (yMax - yMin);
|
||||
const outputX = percent * (xMax - xMin) + xMin;
|
||||
return outputX;
|
||||
};
|
||||
|
||||
|
||||
limit = (value: number, min: number, max: number): number => {
|
||||
/**
|
||||
* Limits a value between a minimum and a maximum.
|
||||
|
||||
@ -20,6 +20,18 @@ import { reference } from "./documentation/reference";
|
||||
import { synths } from "./documentation/synths";
|
||||
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 => {
|
||||
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" : ""}>
|
||||
<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 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 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-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-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
|
||||
</summary>
|
||||
\`\`\`javascript
|
||||
${code}
|
||||
@ -77,3 +89,37 @@ export const documentation_factory = (application: Editor) => {
|
||||
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 {
|
||||
keymap,
|
||||
ViewUpdate,
|
||||
lineNumbers,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
@ -9,6 +13,7 @@ import {
|
||||
highlightActiveLineGutter,
|
||||
} from "@codemirror/view";
|
||||
import { Extension, EditorState } from "@codemirror/state";
|
||||
import { vim } from "@replit/codemirror-vim";
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
@ -23,6 +28,12 @@ import {
|
||||
closeBracketsKeymap,
|
||||
} from "@codemirror/autocomplete";
|
||||
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 = (() => [
|
||||
highlightActiveLineGutter(),
|
||||
@ -47,3 +58,63 @@ export const editorSetup: Extension = (() => [
|
||||
...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 { File } from "./AppSettings";
|
||||
import type { File } from "./FileManagement";
|
||||
|
||||
const delay = (ms: number) =>
|
||||
new Promise((_, reject) =>
|
||||
@ -24,7 +24,7 @@ const tryCatchWrapper = (
|
||||
).call(application.api);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
application.error_line.innerHTML = error as string;
|
||||
application.interface.error_line.innerHTML = error as string;
|
||||
console.log(error);
|
||||
resolve(false);
|
||||
}
|
||||
@ -74,7 +74,7 @@ export const tryEvaluate = async (
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
application.error_line.innerHTML = error as string;
|
||||
application.interface.error_line.innerHTML = error as string;
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
@ -91,7 +91,7 @@ export const evaluate = async (
|
||||
]);
|
||||
if (code.evaluations) code.evaluations++;
|
||||
} catch (error) {
|
||||
application.error_line.innerHTML = error as string;
|
||||
application.interface.error_line.innerHTML = error as string;
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
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 interface Universe {
|
||||
@ -62,9 +66,9 @@ export interface Settings {
|
||||
tips: boolean;
|
||||
send_clock: boolean;
|
||||
midi_channels_scripts: boolean;
|
||||
midi_clock_input: string|undefined;
|
||||
midi_clock_input: string | undefined;
|
||||
midi_clock_ppqn: number;
|
||||
default_midi_input: string|undefined;
|
||||
default_midi_input: string | undefined;
|
||||
}
|
||||
|
||||
export const template_universe = {
|
||||
@ -139,8 +143,8 @@ export class AppSettings {
|
||||
public tips: boolean = true;
|
||||
public send_clock: boolean = false;
|
||||
public midi_channels_scripts: boolean = true;
|
||||
public midi_clock_input: string|undefined = undefined;
|
||||
public default_midi_input: string|undefined = undefined;
|
||||
public midi_clock_input: string | undefined = undefined;
|
||||
public default_midi_input: string | undefined = undefined;
|
||||
public midi_clock_ppqn: number = 24;
|
||||
public load_demo_songs: boolean = true;
|
||||
|
||||
@ -225,3 +229,137 @@ export class AppSettings {
|
||||
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 { AppSettings } from "../AppSettings";
|
||||
import { AppSettings } from "../FileManagement";
|
||||
|
||||
export type MidiNoteEvent = {
|
||||
note: number;
|
||||
velocity: number;
|
||||
channel: number;
|
||||
timestamp: number;
|
||||
}
|
||||
};
|
||||
|
||||
export type MidiCCEvent = {
|
||||
control: number;
|
||||
value: number;
|
||||
channel: number;
|
||||
timestamp: number;
|
||||
}
|
||||
};
|
||||
|
||||
export class MidiConnection {
|
||||
/**
|
||||
@ -45,7 +45,8 @@ export class MidiConnection {
|
||||
public lastNote: MidiNoteEvent | undefined = undefined;
|
||||
public lastCC: { [control: number]: number } = {};
|
||||
public lastNoteInChannel: { [channel: number]: MidiNoteEvent } = {};
|
||||
public lastCCInChannel: { [channel: number]: { [control: number]: number } } = {};
|
||||
public lastCCInChannel: { [channel: number]: { [control: number]: number } } =
|
||||
{};
|
||||
|
||||
/* MIDI clock stuff */
|
||||
private midiClockInputIndex: number | undefined = undefined;
|
||||
@ -173,8 +174,12 @@ export class MidiConnection {
|
||||
* Updates the MIDI clock input select element with the available MIDI inputs.
|
||||
*/
|
||||
if (this.midiInputs.length > 0) {
|
||||
const midiClockSelect = document.getElementById("midi-clock-input") as HTMLSelectElement;
|
||||
const midiInputSelect = document.getElementById("default-midi-input") as HTMLSelectElement;
|
||||
const midiClockSelect = document.getElementById(
|
||||
"midi-clock-input"
|
||||
) as HTMLSelectElement;
|
||||
const midiInputSelect = document.getElementById(
|
||||
"default-midi-input"
|
||||
) as HTMLSelectElement;
|
||||
|
||||
midiClockSelect.innerHTML = "";
|
||||
midiInputSelect.innerHTML = "";
|
||||
@ -201,7 +206,9 @@ export class MidiConnection {
|
||||
});
|
||||
|
||||
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();
|
||||
if (clockMidiInputIndex > 0) {
|
||||
this.midiClockInput = this.midiInputs[clockMidiInputIndex];
|
||||
@ -212,7 +219,9 @@ export class MidiConnection {
|
||||
}
|
||||
|
||||
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();
|
||||
if (defaultMidiInputIndex > 0) {
|
||||
this.currentInputIndex = defaultMidiInputIndex;
|
||||
@ -226,16 +235,25 @@ export class MidiConnection {
|
||||
midiClockSelect.addEventListener("change", (event) => {
|
||||
const value = (event.target as HTMLSelectElement).value;
|
||||
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.settings.midi_clock_input = undefined;
|
||||
} else {
|
||||
const clockInputIndex = parseInt(value);
|
||||
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.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) => {
|
||||
const value = (event.target as HTMLSelectElement).value;
|
||||
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.settings.default_midi_input = undefined;
|
||||
} 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.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 */
|
||||
if (input.name === this.settings.default_midi_input) {
|
||||
|
||||
// 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 note = message.data[1];
|
||||
const velocity = message.data[2];
|
||||
|
||||
this.lastNote = { note, velocity, channel, timestamp: event.timeStamp };
|
||||
this.lastNoteInChannel[channel] = { note, velocity, channel, timestamp: event.timeStamp };
|
||||
this.lastNote = {
|
||||
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);
|
||||
|
||||
this.pushToMidiInputBuffer({ note, velocity, channel, timestamp: event.timeStamp });
|
||||
this.activeNotes.push({ note, velocity, channel, timestamp: event.timeStamp });
|
||||
this.pushToMidiInputBuffer({
|
||||
note,
|
||||
velocity,
|
||||
channel,
|
||||
timestamp: event.timeStamp,
|
||||
});
|
||||
this.activeNotes.push({
|
||||
note,
|
||||
velocity,
|
||||
channel,
|
||||
timestamp: event.timeStamp,
|
||||
});
|
||||
|
||||
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 (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 note = message.data[1];
|
||||
this.removeFromActiveNotes(note, channel);
|
||||
}
|
||||
|
||||
// If message is one of CCs
|
||||
if (message.data[0] >= 0xB0 && message.data[0] <= 0xBF) {
|
||||
|
||||
const channel = message.data[0] - 0xB0 + 1;
|
||||
if (message.data[0] >= 0xb0 && message.data[0] <= 0xbf) {
|
||||
const channel = message.data[0] - 0xb0 + 1;
|
||||
const control = message.data[1];
|
||||
const value = message.data[2];
|
||||
|
||||
@ -333,13 +383,15 @@ export class MidiConnection {
|
||||
|
||||
//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 */
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
this.stickyNotes.splice(index, 1);
|
||||
return true;
|
||||
} else { return false; }
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public stickyNotesFromChannel(channel: number): MidiNoteEvent[] {
|
||||
@ -425,7 +483,6 @@ export class MidiConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public onMidiClock(timestamp: number): void {
|
||||
/**
|
||||
* Called when a MIDI clock message is received.
|
||||
@ -434,7 +491,6 @@ export class MidiConnection {
|
||||
this.clockTicks += 1;
|
||||
|
||||
if (this.lastTimestamp > 0) {
|
||||
|
||||
if (this.lastTimestamp === timestamp) {
|
||||
// This is error handling for odd MIDI clock messages with the same timestamp
|
||||
this.clockErrorCount += 1;
|
||||
@ -452,12 +508,13 @@ export class MidiConnection {
|
||||
this.skipOnError = this.settings.midi_clock_ppqn / 4;
|
||||
timestamp = 0; // timestamp 0 == lastTimestamp 0
|
||||
} else {
|
||||
|
||||
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);
|
||||
if (this.clockBuffer.length > this.clockBufferLength) this.clockBuffer.shift();
|
||||
if (this.clockBuffer.length > this.clockBufferLength)
|
||||
this.clockBuffer.shift();
|
||||
|
||||
const estimatedBPM = this.estimatedBPM();
|
||||
if (estimatedBPM !== this.roundedBPM) {
|
||||
@ -465,13 +522,11 @@ export class MidiConnection {
|
||||
this.api.bpm(estimatedBPM);
|
||||
this.roundedBPM = estimatedBPM;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lastTimestamp = timestamp;
|
||||
|
||||
}
|
||||
|
||||
public estimatedBPM(): number {
|
||||
@ -523,7 +578,8 @@ export class MidiConnection {
|
||||
if (typeof output === "number") {
|
||||
if (output < 0 || output >= this.midiOutputs.length) {
|
||||
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;
|
||||
@ -552,7 +608,8 @@ export class MidiConnection {
|
||||
if (typeof input === "number") {
|
||||
if (input < 0 || input >= this.midiInputs.length) {
|
||||
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;
|
||||
@ -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
|
||||
*/
|
||||
if(typeof port === "string") port = this.getMidiOutputIndex(port);
|
||||
if (typeof port === "string") port = this.getMidiOutputIndex(port);
|
||||
const output = this.midiOutputs[port];
|
||||
note = Math.min(Math.max(note, 0), 127);
|
||||
if (output) {
|
||||
const noteOnMessage = [0x90 + channel, note, velocity];
|
||||
output.send(noteOnMessage);
|
||||
} else {
|
||||
} else {
|
||||
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];
|
||||
note = Math.min(Math.max(note, 0), 127);
|
||||
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];
|
||||
if (output) {
|
||||
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];
|
||||
if (output) {
|
||||
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