refactor some of the documentation about samples
This commit is contained in:
@ -159,6 +159,14 @@
|
||||
</details>
|
||||
<p rel="noopener noreferrer" id="docs_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Patterns</p>
|
||||
<p rel="noopener noreferrer" id="docs_sound" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Audio Engine</p>
|
||||
<details class="space-y-2" open=false>
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Samples</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_sample_list" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Samples List</p>
|
||||
<p rel="noopener noreferrer" id="docs_loading_samples" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Loading Samples</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<p rel="noopener noreferrer" id="docs_samples" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Samples</p>
|
||||
<p rel="noopener noreferrer" id="docs_synths" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synths</p>
|
||||
<p rel="noopener noreferrer" id="docs_midi" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">MIDI</p>
|
||||
|
||||
8
samples
Normal file
8
samples
Normal file
@ -0,0 +1,8 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const loading_samples = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `# Loading samples`
|
||||
}
|
||||
14
src/API.ts
14
src/API.ts
@ -41,16 +41,16 @@ interface ControlChange {
|
||||
export async function loadSamples() {
|
||||
return Promise.all([
|
||||
initAudioOnFirstClick(),
|
||||
samples("github:tidalcycles/Dirt-Samples/master").then(() =>
|
||||
samples("github:tidalcycles/Dirt-Samples/master", undefined, { tag: "Tidal" }).then(() =>
|
||||
registerSynthSounds()
|
||||
),
|
||||
registerZZFXSounds(),
|
||||
samples("github:Bubobubobubobubo/Dough-Fox/main"),
|
||||
samples("github:Bubobubobubobubo/Dough-Samples/main"),
|
||||
samples("github:Bubobubobubobubo/Dough-Amiga/main"),
|
||||
samples("github:Bubobubobubobubo/Dough-Amen/main"),
|
||||
samples("github:Bubobubobubobubo/Dough-Waveforms/main"),
|
||||
samples(drums, "github:ritchse/tidal-drum-machines/main/machines/")
|
||||
samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", { tag: "Machines" }),
|
||||
samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, { tag: "FoxDot" }),
|
||||
samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, { tag: "Pack" }),
|
||||
samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, { tag: "Amiga" }),
|
||||
samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, { tag: "Amen" }),
|
||||
samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, { tag: "Waveforms" }),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { type Editor } from "./main";
|
||||
// Basics
|
||||
import { introduction } from "./documentation/basics/welcome";
|
||||
import { loading_samples } from "./documentation/samples/loading_samples";
|
||||
import { sample_banks } from "./documentation/samples/sample_banks";
|
||||
import { sample_list } from "./documentation/samples/sample_list";
|
||||
import { software_interface } from "./documentation/basics/interface";
|
||||
import { shortcuts } from "./documentation/basics/keyboard";
|
||||
import { code } from "./documentation/basics/code";
|
||||
@ -94,6 +97,9 @@ export const documentation_factory = (application: Editor) => {
|
||||
oscilloscope: oscilloscope(application),
|
||||
synchronisation: synchronisation(application),
|
||||
bonus: bonus(application),
|
||||
sample_list: sample_list(application),
|
||||
sample_banks: sample_banks(application),
|
||||
loading_samples: loading_samples(application),
|
||||
about: about(),
|
||||
};
|
||||
};
|
||||
|
||||
@ -506,6 +506,8 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
"about",
|
||||
"bonus",
|
||||
"oscilloscope",
|
||||
"sample_list",
|
||||
"loading_samples",
|
||||
].forEach((e) => {
|
||||
let name = `docs_` + e;
|
||||
document.getElementById(name)!.addEventListener("click", async () => {
|
||||
|
||||
@ -1,47 +1,6 @@
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const samples_to_markdown = (application: Editor) => {
|
||||
let samples = application.api._all_samples();
|
||||
let markdownList = "";
|
||||
let keys = Object.keys(samples);
|
||||
let i = -1;
|
||||
while (i++ < keys.length - 1) {
|
||||
//@ts-ignore
|
||||
if (!samples[keys[i]].data) continue;
|
||||
//@ts-ignore
|
||||
if (!samples[keys[i]].data.samples) continue;
|
||||
//markdownList += `**${keys[i]}** (_${
|
||||
// //@ts-ignore
|
||||
// samples[keys[i]].data.samples.length
|
||||
//}_) `;
|
||||
//
|
||||
|
||||
// Adding new examples for each sample folder!
|
||||
const codeId = `sampleExample${i}`;
|
||||
application.api.codeExamples[
|
||||
codeId
|
||||
] = `sound("${keys[i]}").n(irand(1, 5)).end(1).out()`;
|
||||
// @ts-ignore
|
||||
const howMany = samples[keys[i]].data.samples.length;
|
||||
|
||||
markdownList += `
|
||||
<button
|
||||
class="hover:bg-neutral-500 inline px-4 py-2 bg-neutral-700 text-orange-300 text-xl"
|
||||
onclick="app.api._playDocExampleOnce(app.api.codeExamples['${codeId}'])"
|
||||
>
|
||||
${keys[i]}
|
||||
<b class="text-white">(${howMany})</b>
|
||||
</button>`;
|
||||
}
|
||||
return markdownList;
|
||||
};
|
||||
|
||||
export const injectAvailableSamples = (application: Editor): string => {
|
||||
let generatedPage = samples_to_markdown(application);
|
||||
return generatedPage;
|
||||
};
|
||||
|
||||
export const samples = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
@ -49,50 +8,5 @@ export const samples = (application: Editor): string => {
|
||||
|
||||
Audio samples are dynamically loaded from the web. By default, Topos is providing some samples coming from the classic [Dirt-Samples](https://github.com/tidalcycles/Dirt-Samples) but also from the [Topos-Samples](https://github.com/Bubobubobubobubo/Topos-Samples) repository. You can contribute to the latter if you want to share your samples with the community! For each sample folder, we are indicating how many of them are available in parentheses. The samples starting with <ic>ST</ic> are coming from [a wonderful collection](https://archive.org/details/AmigaSoundtrackerSamplePacksst-xx) of Ultimate Tracker Amiga audio samples released by Karsten Obarski. They are very high-pitched as was usual in the tracker era. Pitch them down using <ic>.speed(0.5)</ic>.
|
||||
|
||||
## Available audio samples
|
||||
|
||||
<b class="flex lg:pl-6 lg:pr-6 text-bold mb-8">Samples can take a few seconds to load. Please wait if you are not hearing anything. Lower your volume, take it slow. Some sounds might be harsh.</b>
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${injectAvailableSamples(application)}
|
||||
</div>
|
||||
|
||||
# Loading custom samples
|
||||
|
||||
Topos is exposing the <ic>samples</ic> function that you can use to load your own set of samples. Samples are loaded on-the-fly from the web. Topos is a web application living in the browser. It is running in a sandboxed environment. Thus, it cannot have access to the files stored on your local system. Loading samples requires building a _map_ of the audio files, where a name is associated to a specific file:
|
||||
|
||||
${makeExample(
|
||||
"Loading samples from a map",
|
||||
`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');`,
|
||||
true
|
||||
)}
|
||||
|
||||
This example is loading two samples from each folder declared in the original repository (in the <ic>strudel.json</ic> file). You can then play with them using the syntax you are already used to:
|
||||
|
||||
${makeExample(
|
||||
"Playing with the loaded samples",
|
||||
`rhythm(.5, 5, 8)::sound('bd').n(ir(1,2)).end(1).out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
Internally, Topos is loading samples using a different technique where sample maps are directly taken from the previously mentioned <ic>strudel.json</ic> file that lives in each repository:
|
||||
|
||||
${makeExample(
|
||||
"This is how Topos is loading its own samples",
|
||||
`
|
||||
// Visit the concerned repos and search for 'strudel.json'
|
||||
samples("github:tidalcycles/Dirt-Samples/master");
|
||||
samples("github:Bubobubobubobubo/Dough-Samples/main");
|
||||
samples("github:Bubobubobubobubo/Dough-Amiga/main");
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
To learn more about the audio sample loading mechanism, please refer to [this page](https://strudel.tidalcycles.org/learn/samples) written by Felix Roos who has implemented the sample loading mechanism. The API is absolutely identic in Topos!
|
||||
|
||||
`;
|
||||
};
|
||||
|
||||
65
src/documentation/samples/loading_samples.ts
Normal file
65
src/documentation/samples/loading_samples.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const loading_samples = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `# Loading custom samples
|
||||
|
||||
|
||||
Topos is exposing the <ic>samples</ic> function that you can use to load your own set of samples.
|
||||
|
||||
Samples are loaded on-the-fly from the web. Topos is a web application living in the browser. It is running in a sandboxed environment. Thus, it cannot have access to the files stored on your local system. Loading samples requires building a _map_ of the audio files, where a name is associated to a specific file:
|
||||
|
||||
${makeExample(
|
||||
"Loading samples from a map",
|
||||
`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');`,
|
||||
true
|
||||
)}
|
||||
|
||||
This example is loading two samples from each folder declared in the original repository (in the <ic>strudel.json</ic> file). You can then play with them using the syntax you are already used to:
|
||||
|
||||
${makeExample(
|
||||
"Playing with the loaded samples",
|
||||
`rhythm(.5, 5, 8)::sound('bd').n(ir(1,2)).end(1).out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
Internally, Topos is loading samples using a different technique where sample maps are directly taken from the previously mentioned <ic>strudel.json</ic> file that lives in each repository:
|
||||
|
||||
${makeExample(
|
||||
"This is how Topos is loading its own samples",
|
||||
`
|
||||
// Visit the concerned repos and search for 'strudel.json'
|
||||
samples("github:tidalcycles/Dirt-Samples/master");
|
||||
samples("github:Bubobubobubobubo/Dough-Samples/main");
|
||||
samples("github:Bubobubobubobubo/Dough-Amiga/main");
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
To learn more about the audio sample loading mechanism, please refer to [this page](https://strudel.tidalcycles.org/learn/samples) written by Felix Roos who has implemented the sample loading mechanism. The API is absolutely identic in Topos!
|
||||
|
||||
# Loading sounds using Shabda
|
||||
|
||||
You can load samples coming from [Freesound](https://freesound.org/) using the [Shabda](https://shabda.ndre.gr/) API. To do so, study the following example:
|
||||
|
||||
${makeExample(
|
||||
"Loading samples from shabda",
|
||||
`
|
||||
// Prepend the sample you want with 'shabda:'
|
||||
samples("shabda:ocean")
|
||||
|
||||
// Use the sound without 'shabda:'
|
||||
beat(1)::sound('ocean').clip(1).out()
|
||||
`, true
|
||||
)}
|
||||
|
||||
You can also use the <ic>.n</ic> attribute like usual to load a different sample.
|
||||
`
|
||||
}
|
||||
13
src/documentation/samples/sample_banks.ts
Normal file
13
src/documentation/samples/sample_banks.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const sample_banks = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `# Sample Banks
|
||||
|
||||
There is a <ic>bank</ic> attribute that can help you to sort audio samples from large collections.
|
||||
|
||||
**AJKPercusyn**, **AkaiLinn**, **AkaiMPC60**, **AkaiXR10**, **AlesisHR16**, **AlesisSR16**, **BossDR110**, **BossDR220**, **BossDR55**, **BossDR550**, **BossDR660**, **CasioRZ1**, **CasioSK1**, **CasioVL1**, **DoepferMS404**, **EmuDrumulator**, **EmuModular**, **EmuSP12**, **KorgDDM110**, **KorgKPR77**, **KorgKR55**, **KorgKRZ**, **KorgM1**, **KorgMinipops**, **KorgPoly800**, **KorgT3**, **Linn9000**, **LinnDrum**, **LinnLM1**, **LinnLM2**, **MFB512**, **MPC1000**, **MoogConcertMateMG1**, **OberheimDMX**, **RhodesPolaris**, **RhythmAce**, **RolandCompurhythm1000**, **RolandCompurhythm78**, **RolandCompurhythm8000**, **RolandD110**, **RolandD70**, **RolandDDR30**, **RolandJD990**, **RolandMC202**, **RolandMC303**, **RolandMT32**, **RolandR8**, **RolandS50**, **RolandSH09**, **RolandSystem100**, **RolandTR505**, **RolandTR606**, **RolandTR626**, **RolandTR707**, **RolandTR727**, **RolandTR808**, **RolandTR909**, **SakataDPM48**, **SequentialCircuitsDrumtracks**, **SequentialCircuitsTom**, **SergeModular**, **SimmonsSDS400**, **SimmonsSDS5**, **SoundmastersR88**, **UnivoxMicroRhythmer12**, **ViscoSpaceDrum**, **XdrumLM8953**, **YamahaRM50**, **YamahaRX21**, **YamahaRX5**, **YamahaRY30**, **YamahaTG33**.
|
||||
`
|
||||
}
|
||||
134
src/documentation/samples/sample_list.ts
Normal file
134
src/documentation/samples/sample_list.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const samples_to_markdown = (application: Editor, tag_filter?: string) => {
|
||||
let samples = application.api._all_samples();
|
||||
let markdownList = "";
|
||||
let keys = Object.keys(samples);
|
||||
let i = -1;
|
||||
while (i++ < keys.length - 1) {
|
||||
//@ts-ignore
|
||||
if (!samples[keys[i]].data) continue;
|
||||
//@ts-ignore
|
||||
if (!samples[keys[i]].data.samples) continue;
|
||||
// @ts-ignore
|
||||
if (samples[keys[i]].data.tag !== tag_filter) continue;
|
||||
//markdownList += `**${keys[i]}** (_${
|
||||
// //@ts-ignore
|
||||
// samples[keys[i]].data.samples.length
|
||||
//}_) `;
|
||||
//
|
||||
|
||||
// Adding new examples for each sample folder!
|
||||
const codeId = `sampleExample${i}`;
|
||||
application.api.codeExamples[
|
||||
codeId
|
||||
] = `sound("${keys[i]}").n(irand(1, 5)).end(1).out()`;
|
||||
// @ts-ignore
|
||||
const howMany = samples[keys[i]].data.samples.length;
|
||||
|
||||
markdownList += `
|
||||
<button
|
||||
class="hover:bg-neutral-500 inline px-4 py-2 bg-neutral-700 text-orange-300 text-xl"
|
||||
onclick="app.api._playDocExampleOnce(app.api.codeExamples['${codeId}'])"
|
||||
>
|
||||
${keys[i]}
|
||||
<b class="text-white">(${howMany})</b>
|
||||
</button>`;
|
||||
}
|
||||
return markdownList;
|
||||
};
|
||||
|
||||
export const injectAllSamples = (application: Editor): string => {
|
||||
let generatedPage = samples_to_markdown(application, "Topos");
|
||||
return generatedPage;
|
||||
};
|
||||
|
||||
|
||||
export const injectDrumMachineSamples = (application: Editor): string => {
|
||||
let generatedPage = samples_to_markdown(application, "Machines");
|
||||
return generatedPage;
|
||||
};
|
||||
|
||||
|
||||
export const sample_list = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Available audio samples
|
||||
|
||||
On this page, you will find an exhaustive list of all the samples currently loaded by default by the system. Samples are sorted by **sample packs**. I am gradually adding more of them.
|
||||
|
||||
## Waveforms
|
||||
|
||||
A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with <ic>wt_</ic> will be looped. Look at this demo:
|
||||
|
||||
${makeExample("Wavetable synthesis made easy :)", `
|
||||
beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out()
|
||||
`, true)}
|
||||
|
||||
|
||||
Pick one folder and spend some time exploring it. There is a lot of different waveforms.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Waveforms")}
|
||||
</div>
|
||||
|
||||
## Drum machines sample pack
|
||||
|
||||
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the <ic>.bank()</ic> parameter like so:
|
||||
|
||||
${makeExample(
|
||||
"Using a classic drum machine", `
|
||||
beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out()
|
||||
`, true)}
|
||||
|
||||
Here is the complete list of available machines:
|
||||
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Machines")}
|
||||
</div>
|
||||
|
||||
## FoxDot sample pack
|
||||
|
||||
The default sample pack used by Ryan Kirkbride's [FoxDot](https://github.com/Qirky/FoxDot). It is a nice curated sample pack that covers all the basic sounds you could want.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "FoxDot")}
|
||||
</div>
|
||||
|
||||
## Amiga sample pack
|
||||
|
||||
This set of audio samples is taken from [this wonderful collection](https://archive.org/details/AmigaSoundtrackerSamplePacksst-xx) of **Ultimate Tracker Amiga samples**. They were initially made by Karsten Obarski. These files were processed: pitched down one octave, gain down 6db. The audio has been processed with [SoX](https://github.com/chirlu/sox). The script used to do so is also included in this repository.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Amiga")}
|
||||
</div>
|
||||
|
||||
## Amen break sample pack
|
||||
|
||||
A collection of many different amen breaks. Use <ic>.stretch()</ic> to play with these:
|
||||
|
||||
${makeExample(
|
||||
"Stretching an amen break", `
|
||||
beat(4)::sound('amen1').stretch(4).out()
|
||||
`, true,
|
||||
)}
|
||||
|
||||
The stretch should be adapted based on the length of each amen break.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Amen")}
|
||||
</div>
|
||||
|
||||
## TidalCycles sample library
|
||||
|
||||
Many live coders are expecting to find the Tidal sample library wherever they go, so here it is :)
|
||||
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Tidal")}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@ -159,8 +159,9 @@ export class Editor {
|
||||
let pre_loading = async () => {
|
||||
await loadSamples();
|
||||
};
|
||||
pre_loading();
|
||||
this.docs = documentation_factory(this);
|
||||
pre_loading().then(() => {
|
||||
this.docs = documentation_factory(this);
|
||||
});
|
||||
|
||||
// ================================================================================
|
||||
// Application event listeners
|
||||
@ -484,7 +485,7 @@ export class Editor {
|
||||
console.log("Hydra loaded successfully");
|
||||
this.initializeHydra();
|
||||
};
|
||||
script.onerror = function () {
|
||||
script.onerror = function() {
|
||||
console.error("Error loading Hydra script");
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
|
||||
Reference in New Issue
Block a user