Merge branch 'main' into clockwork
This commit is contained in:
41
index.html
41
index.html
@ -104,30 +104,41 @@
|
|||||||
|
|
||||||
<div id="documentation" class="hidden">
|
<div id="documentation" class="hidden">
|
||||||
<div id="documentation-page" class="flex flex-row">
|
<div id="documentation-page" class="flex flex-row">
|
||||||
<aside class="h-fit p-1 lg:p-8 bg-neutral-900 text-white">
|
<aside class="h-fit p-1 lg:p-6 bg-neutral-900 text-white">
|
||||||
<nav class="space-y-0 text-xl sm:text-sm">
|
<nav class="text-xl sm:text-sm overflow-y-scroll">
|
||||||
<div class="space-y-2">
|
<div class="">
|
||||||
<h2 class="font-semibold text-gray-400">Basics</h2>
|
<h2 class="font-semibold lg:text-xl text-gray-400">Basics</h2>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<p rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Introduction </p>
|
<p rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Introduction </p>
|
||||||
<p rel="noopener noreferrer" id="docs_interface" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interface</p>
|
<p rel="noopener noreferrer" id="docs_interface" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interface</p>
|
||||||
<p rel="noopener noreferrer" id="docs_code" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Code</p>
|
<p rel="noopener noreferrer" id="docs_shortcuts" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Keyboard</p>
|
||||||
<p rel="noopener noreferrer" id="docs_time" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time</p>
|
<p rel="noopener noreferrer" id="docs_code" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Coding</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">Sound</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h2 class="font-semibold lg:text-xl pb-1 pt-1 text-gray-400">Learning</h2>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p rel="noopener noreferrer" id="docs_time" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time/Rhythm</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>
|
||||||
<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_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_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>
|
<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>
|
||||||
<p rel="noopener noreferrer" id="docs_chaining" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Chaining</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h2 class="font-semibold lg:text-xl pb-1 pt-1 text-gray-400">Patterns</h2>
|
||||||
|
<div class="flex flex-col">
|
||||||
<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_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_ziffers" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Ziffers</p>
|
<p rel="noopener noreferrer" id="docs_chaining" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Chaining</p>
|
||||||
<p rel="noopener noreferrer" id="docs_functions" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Functions</p>
|
<p rel="noopener noreferrer" id="docs_functions" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Functions</p>
|
||||||
<p rel="noopener noreferrer" id="docs_shortcuts" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Shortcuts</p>
|
<p rel="noopener noreferrer" id="docs_ziffers" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Ziffers</p>
|
||||||
|
<!--
|
||||||
<p rel="noopener noreferrer" id="docs_reference" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Reference</p>
|
<p rel="noopener noreferrer" id="docs_reference" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Reference</p>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h2 class="font-semibold text-gray-400">More</h2>
|
<h2 class="font-semibold lg:text-xl text-gray-400">More</h2>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
|
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +157,7 @@
|
|||||||
<p class="text-semibold text-2xl pb-4">Known universes</p>
|
<p class="text-semibold text-2xl pb-4">Known universes</p>
|
||||||
<p id="existing-universes" class="text-xl"></p>
|
<p id="existing-universes" class="text-xl"></p>
|
||||||
<div id="disclaimer" class="pb-4">
|
<div id="disclaimer" class="pb-4">
|
||||||
<form>
|
<form id="universe-creator">
|
||||||
<label for="search" class="mb-2 text-sm font-medium text-gray-900 sr-only text-white">Search</label>
|
<label for="search" class="mb-2 text-sm font-medium text-gray-900 sr-only text-white">Search</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
@ -154,8 +165,8 @@
|
|||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-800 outline-0 rounded-lg bg-gray-800 text-white" placeholder="Buffer..." required>
|
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-800 outline-0 rounded-lg bg-gray-800 text-white" placeholder="Buffer..." required>
|
||||||
<button id="load-universe-button" class="text-black absolute right-2.5 bottom-2.5 bg-white hover:bg-white focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
|
<button id="load-universe-button" class="text-black absolute right-2.5 bottom-2.5 bg-white hover:bg-white focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-2 flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 border-spacing-y-4">
|
<div class="mt-2 flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 border-spacing-y-4">
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"astring": "^1.8.6",
|
"astring": "^1.8.6",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"fflate": "^0.8.0",
|
||||||
"lru-cache": "^10.0.1",
|
"lru-cache": "^10.0.1",
|
||||||
"marked": "^7.0.3",
|
"marked": "^7.0.3",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
|
|||||||
@ -1900,9 +1900,8 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|
|||||||
|
|
||||||
| Shortcut | Key | Description |
|
| Shortcut | Key | Description |
|
||||||
|----------|-------|------------------------------------------------------------|
|
|----------|-------|------------------------------------------------------------|
|
||||||
|**Start** transport|${key_shortcut("Ctrl + P")}|Start audio playback|
|
|**Start/Pause** transport|${key_shortcut("Ctrl + P")}|Start or pause audio playback|
|
||||||
|**Pause** the transport |${key_shortcut("Ctrl + S")}|Pause audio playback|
|
|**Stop** the transport |${key_shortcut("Ctrl + S")}|Stop and rewind audio playback|
|
||||||
|**Rewind** the transport|${key_shortcut("Ctrl + R")}|Rewind audio playback|
|
|
||||||
|
|
||||||
## Moving in the interface
|
## Moving in the interface
|
||||||
|
|
||||||
|
|||||||
82
src/main.ts
82
src/main.ts
@ -27,6 +27,8 @@ import {
|
|||||||
template_universes,
|
template_universes,
|
||||||
} from "./AppSettings";
|
} from "./AppSettings";
|
||||||
import { tryEvaluate } from "./Evaluator";
|
import { tryEvaluate } from "./Evaluator";
|
||||||
|
// @ts-ignore
|
||||||
|
import { gzipSync, decompressSync, strFromU8 } from 'fflate';
|
||||||
|
|
||||||
// Importing showdown and setting up the markdown converter
|
// Importing showdown and setting up the markdown converter
|
||||||
import showdown from "showdown";
|
import showdown from "showdown";
|
||||||
@ -149,6 +151,9 @@ export class Editor {
|
|||||||
buffer_search: HTMLInputElement = document.getElementById(
|
buffer_search: HTMLInputElement = document.getElementById(
|
||||||
"buffer-search"
|
"buffer-search"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
universe_creator: HTMLFormElement = document.getElementById(
|
||||||
|
"universe-creator"
|
||||||
|
) as HTMLFormElement;
|
||||||
|
|
||||||
// Local script tabs
|
// Local script tabs
|
||||||
local_script_tabs: HTMLDivElement = document.getElementById(
|
local_script_tabs: HTMLDivElement = document.getElementById(
|
||||||
@ -259,19 +264,13 @@ export class Editor {
|
|||||||
// Application event listeners
|
// Application event listeners
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
|
|
||||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
window.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||||
// TAB should do nothing
|
|
||||||
if (event.key === "Tab") {
|
if (event.key === "Tab") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.ctrlKey && event.key === "s") {
|
if (event.ctrlKey && event.key === "s") {
|
||||||
event.preventDefault();
|
|
||||||
this.setButtonHighlighting("pause", true);
|
|
||||||
this.clock.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.ctrlKey && event.key === "r") {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.setButtonHighlighting("stop", true);
|
this.setButtonHighlighting("stop", true);
|
||||||
this.clock.stop();
|
this.clock.stop();
|
||||||
@ -279,8 +278,15 @@ export class Editor {
|
|||||||
|
|
||||||
if (event.ctrlKey && event.key === "p") {
|
if (event.ctrlKey && event.key === "p") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.setButtonHighlighting("play", true);
|
if (this.isPlaying) {
|
||||||
this.clock.start();
|
this.isPlaying = false;
|
||||||
|
this.setButtonHighlighting("pause", true);
|
||||||
|
this.clock.pause();
|
||||||
|
} else {
|
||||||
|
this.isPlaying = true;
|
||||||
|
this.setButtonHighlighting("play", true);
|
||||||
|
this.clock.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl + Shift + V: Vim Mode
|
// Ctrl + Shift + V: Vim Mode
|
||||||
@ -316,6 +322,7 @@ export class Editor {
|
|||||||
|
|
||||||
// This is the modal to switch between universes
|
// This is the modal to switch between universes
|
||||||
if (event.ctrlKey && event.key === "b") {
|
if (event.ctrlKey && event.key === "b") {
|
||||||
|
event.preventDefault();
|
||||||
this.hideDocumentation();
|
this.hideDocumentation();
|
||||||
this.updateKnownUniversesView();
|
this.updateKnownUniversesView();
|
||||||
this.openBuffersModal();
|
this.openBuffersModal();
|
||||||
@ -365,8 +372,10 @@ export class Editor {
|
|||||||
if (event.keyCode === keycode) {
|
if (event.keyCode === keycode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
this.api.script(keycode - 111);
|
this.api.script(keycode - 111);
|
||||||
} else {
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
this.changeModeFromInterface("local");
|
this.changeModeFromInterface("local");
|
||||||
this.changeToLocalBuffer(index);
|
this.changeToLocalBuffer(index);
|
||||||
this.hideDocumentation();
|
this.hideDocumentation();
|
||||||
@ -376,10 +385,12 @@ export class Editor {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (event.keyCode == 121) {
|
if (event.keyCode == 121) {
|
||||||
|
event.preventDefault();
|
||||||
this.changeModeFromInterface("global");
|
this.changeModeFromInterface("global");
|
||||||
this.hideDocumentation();
|
this.hideDocumentation();
|
||||||
}
|
}
|
||||||
if (event.keyCode == 122) {
|
if (event.keyCode == 122) {
|
||||||
|
event.preventDefault();
|
||||||
this.changeModeFromInterface("init");
|
this.changeModeFromInterface("init");
|
||||||
this.hideDocumentation();
|
this.hideDocumentation();
|
||||||
}
|
}
|
||||||
@ -522,13 +533,13 @@ export class Editor {
|
|||||||
this.settings.font_size = parseInt(new_value);
|
this.settings.font_size = parseInt(new_value);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.share_button.addEventListener("click", () => {
|
this.share_button.addEventListener("click", async () => {
|
||||||
// trigger a manual save
|
// trigger a manual save
|
||||||
this.currentFile().candidate = app.view.state.doc.toString();
|
this.currentFile().candidate = app.view.state.doc.toString();
|
||||||
this.currentFile().committed = app.view.state.doc.toString();
|
this.currentFile().committed = app.view.state.doc.toString();
|
||||||
this.settings.saveApplicationToLocalStorage(app.universes, app.settings);
|
this.settings.saveApplicationToLocalStorage(app.universes, app.settings);
|
||||||
// encode as a blob!
|
// encode as a blob!
|
||||||
this.share();
|
await this.share();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.normal_mode_button.addEventListener("click", () => {
|
this.normal_mode_button.addEventListener("click", () => {
|
||||||
@ -553,18 +564,24 @@ export class Editor {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.buffer_search.addEventListener("keydown", (event) => {
|
this.universe_creator.addEventListener("submit", (event) => {
|
||||||
if (event.key === "Enter") {
|
|
||||||
let query = this.buffer_search.value;
|
event.preventDefault();
|
||||||
if (query.length > 2 && query.length < 20) {
|
|
||||||
this.loadUniverse(query);
|
let data = new FormData(this.universe_creator);
|
||||||
this.settings.selected_universe = query;
|
let universeName = data.get("universe") as string|null;
|
||||||
|
|
||||||
|
if(universeName){
|
||||||
|
if (universeName.length > 2 && universeName.length < 20) {
|
||||||
|
this.loadUniverse(universeName);
|
||||||
|
this.settings.selected_universe = universeName;
|
||||||
this.buffer_search.value = "";
|
this.buffer_search.value = "";
|
||||||
this.closeBuffersModal();
|
this.closeBuffersModal();
|
||||||
this.view.focus();
|
this.view.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tryEvaluate(this, this.universes[this.selected_universe.toString()].init);
|
tryEvaluate(this, this.universes[this.selected_universe.toString()].init);
|
||||||
|
|
||||||
[
|
[
|
||||||
@ -580,7 +597,7 @@ export class Editor {
|
|||||||
"ziffers",
|
"ziffers",
|
||||||
"midi",
|
"midi",
|
||||||
"functions",
|
"functions",
|
||||||
"reference",
|
// "reference",
|
||||||
"shortcuts",
|
"shortcuts",
|
||||||
"about",
|
"about",
|
||||||
].forEach((e) => {
|
].forEach((e) => {
|
||||||
@ -639,7 +656,8 @@ export class Editor {
|
|||||||
if (url !== null) {
|
if (url !== null) {
|
||||||
const universeParam = url.get("universe");
|
const universeParam = url.get("universe");
|
||||||
if (universeParam !== null) {
|
if (universeParam !== null) {
|
||||||
new_universe = JSON.parse(atob(universeParam));
|
let data = Uint8Array.from(atob(universeParam), c => c.charCodeAt(0))
|
||||||
|
new_universe = JSON.parse(strFromU8(decompressSync(data)));
|
||||||
const randomName: string = uniqueNamesGenerator({
|
const randomName: string = uniqueNamesGenerator({
|
||||||
length: 2, separator: '_',
|
length: 2, separator: '_',
|
||||||
dictionaries: [colors, animals],
|
dictionaries: [colors, animals],
|
||||||
@ -692,12 +710,24 @@ export class Editor {
|
|||||||
existing_universes!.innerHTML = final_html;
|
existing_universes!.innerHTML = final_html;
|
||||||
}
|
}
|
||||||
|
|
||||||
share() {
|
async share() {
|
||||||
const hashed_table = btoa(
|
|
||||||
JSON.stringify({
|
async function bufferToBase64(buffer:Uint8Array) {
|
||||||
universe: this.settings.universes[this.selected_universe],
|
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: this.settings.universes[this.selected_universe],
|
||||||
|
});
|
||||||
|
let encoded_data = gzipSync(new TextEncoder().encode(data));
|
||||||
|
// TODO make this async
|
||||||
|
// TODO maybe try with compression level 9
|
||||||
|
const hashed_table = await bufferToBase64(encoded_data);
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set("universe", hashed_table);
|
url.searchParams.set("universe", hashed_table);
|
||||||
window.history.replaceState({}, "", url.toString());
|
window.history.replaceState({}, "", url.toString());
|
||||||
|
|||||||
@ -767,6 +767,11 @@ fastq@^1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
reusify "^1.0.4"
|
||||||
|
|
||||||
|
fflate@^0.8.0:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.0.tgz#f93ad1dcbe695a25ae378cf2386624969a7cda32"
|
||||||
|
integrity sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
fill-range@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||||
|
|||||||
Reference in New Issue
Block a user