Nice Work
This commit is contained in:
Binary file not shown.
Binary file not shown.
+41
-31
@@ -567,7 +567,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateFolder() {
|
async function handleCreateFolder() {
|
||||||
if (!newFolderName.trim()) {
|
if (!uploadAfterCreate && !newFolderName.trim()) {
|
||||||
addToast('error', 'Ordnername fehlt.');
|
addToast('error', 'Ordnername fehlt.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -575,23 +575,33 @@ export default function App() {
|
|||||||
addToast('error', 'Bitte Audiodateien auswaehlen.');
|
addToast('error', 'Bitte Audiodateien auswaehlen.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await createMediaFolder(currentPath, newFolderName.trim());
|
|
||||||
if (!response.ok) {
|
const trimmedName = newFolderName.trim();
|
||||||
addToast('error', response.data.detail || 'Ordner anlegen fehlgeschlagen.');
|
const needsFolder = uploadAfterCreate && !trimmedName && !currentPath;
|
||||||
|
if (needsFolder) {
|
||||||
|
addToast('error', 'Bitte zuerst einen Ordner anlegen.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const createdPath = currentPath
|
|
||||||
? `${currentPath}/${newFolderName.trim()}`
|
let targetPath = currentPath;
|
||||||
: newFolderName.trim();
|
if (trimmedName) {
|
||||||
|
const response = await createMediaFolder(currentPath, trimmedName);
|
||||||
|
if (!response.ok) {
|
||||||
|
addToast('error', response.data.detail || 'Ordner anlegen fehlgeschlagen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetPath = currentPath ? `${currentPath}/${trimmedName}` : trimmedName;
|
||||||
|
addToast('success', 'Ordner angelegt.');
|
||||||
|
await handleMediaRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
addToast('success', 'Ordner angelegt.');
|
|
||||||
await handleMediaRefresh();
|
|
||||||
if (uploadAfterCreate) {
|
if (uploadAfterCreate) {
|
||||||
setActiveUploadLabel(`Upload: ${pendingUploadFiles.length} Datei(en)`);
|
setActiveUploadLabel(`Upload: ${pendingUploadFiles.length} Datei(en)`);
|
||||||
setUploadInProgress(true);
|
setUploadInProgress(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
const uploadResponse = await uploadMedia(
|
const uploadResponse = await uploadMedia(
|
||||||
createdPath,
|
targetPath,
|
||||||
pendingUploadFiles,
|
pendingUploadFiles,
|
||||||
(percent) => setUploadProgress(percent)
|
(percent) => setUploadProgress(percent)
|
||||||
);
|
);
|
||||||
@@ -604,14 +614,16 @@ export default function App() {
|
|||||||
setActiveUploadLabel('');
|
setActiveUploadLabel('');
|
||||||
setUploadAfterCreate(false);
|
setUploadAfterCreate(false);
|
||||||
setPendingUploadFiles([]);
|
setPendingUploadFiles([]);
|
||||||
setCurrentPath(createdPath);
|
if (targetPath) {
|
||||||
|
setCurrentPath(targetPath);
|
||||||
|
}
|
||||||
addToast('success', 'Upload abgeschlossen.');
|
addToast('success', 'Upload abgeschlossen.');
|
||||||
await handleMediaRefresh();
|
await handleMediaRefresh();
|
||||||
setActiveModal('');
|
setActiveModal('');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!uploadAfterCreate) {
|
|
||||||
setActiveModal('');
|
setActiveModal('');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRename() {
|
async function handleRename() {
|
||||||
@@ -1442,11 +1454,15 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
{activeModal === 'new-folder' && (
|
{activeModal === 'new-folder' && (
|
||||||
<>
|
<>
|
||||||
<h3>Neuen Ordner anlegen</h3>
|
<h3>{uploadAfterCreate ? 'Upload vorbereiten' : 'Neuen Ordner anlegen'}</h3>
|
||||||
<input
|
<input
|
||||||
value={newFolderName}
|
value={newFolderName}
|
||||||
onChange={(event) => setNewFolderName(event.target.value)}
|
onChange={(event) => setNewFolderName(event.target.value)}
|
||||||
placeholder="Ordnername"
|
placeholder={
|
||||||
|
uploadAfterCreate
|
||||||
|
? 'Optionaler Ordnername (leer = aktueller Ordner)'
|
||||||
|
: 'Ordnername'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{uploadAfterCreate && (
|
{uploadAfterCreate && (
|
||||||
<>
|
<>
|
||||||
@@ -1509,7 +1525,9 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="muted">Upload startet direkt nach dem Anlegen.</p>
|
<p className="muted">
|
||||||
|
Upload startet direkt. Ohne Ordnername landet er im aktuellen Ordner.
|
||||||
|
</p>
|
||||||
{uploadInProgress && (
|
{uploadInProgress && (
|
||||||
<div className="upload-progress">
|
<div className="upload-progress">
|
||||||
<div style={{ width: `${uploadProgress}%` }} />
|
<div style={{ width: `${uploadProgress}%` }} />
|
||||||
@@ -1894,23 +1912,15 @@ export default function App() {
|
|||||||
className="icon-button upload"
|
className="icon-button upload"
|
||||||
title="Upload"
|
title="Upload"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (!currentPath) {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
setUploadAfterCreate(true);
|
||||||
addToast('error', 'Bitte zuerst einen Ordner anlegen.');
|
setPendingUploadFiles([]);
|
||||||
setUploadAfterCreate(true);
|
setNewFolderName('');
|
||||||
setPendingUploadFiles([]);
|
setActiveModal('new-folder');
|
||||||
setActiveModal('new-folder');
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UploadSimple size={16} />
|
<UploadSimple size={16} />
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
onChange={handleUpload}
|
|
||||||
accept="audio/*"
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+27
-27
@@ -1,6 +1,6 @@
|
|||||||
const defaultHost =
|
const defaultHost =
|
||||||
typeof window !== 'undefined' && window.location ? window.location.hostname : '127.0.0.1';
|
typeof window !== 'undefined' && window.location ? window.location.hostname : '127.0.0.1';
|
||||||
const API_BASE = import.meta.env.VITE_BACKEND_URL || `http://${defaultHost}:5001`;
|
const API_BASE = '/api';
|
||||||
|
|
||||||
async function readJson(response) {
|
async function readJson(response) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -12,12 +12,12 @@ async function readJson(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getBoxes() {
|
export async function getBoxes() {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes`);
|
const response = await fetch(`${API_BASE}/boxes`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pairBox(boxId) {
|
export async function pairBox(boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/pair`, {
|
const response = await fetch(`${API_BASE}/boxes/pair`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ box_id: boxId }),
|
body: JSON.stringify({ box_id: boxId }),
|
||||||
@@ -26,17 +26,17 @@ export async function pairBox(boxId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getStatus(boxId) {
|
export async function getStatus(boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/status`);
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/status`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMediaTree() {
|
export async function getMediaTree() {
|
||||||
const response = await fetch(`${API_BASE}/api/media-tree`);
|
const response = await fetch(`${API_BASE}/media-tree`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMediaFolder(parentPath, name) {
|
export async function createMediaFolder(parentPath, name) {
|
||||||
const response = await fetch(`${API_BASE}/api/media/folder`, {
|
const response = await fetch(`${API_BASE}/media/folder`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ parent_path: parentPath, name }),
|
body: JSON.stringify({ parent_path: parentPath, name }),
|
||||||
@@ -45,7 +45,7 @@ export async function createMediaFolder(parentPath, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function renameMedia(path, name) {
|
export async function renameMedia(path, name) {
|
||||||
const response = await fetch(`${API_BASE}/api/media/rename`, {
|
const response = await fetch(`${API_BASE}/media/rename`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ path, name }),
|
body: JSON.stringify({ path, name }),
|
||||||
@@ -54,7 +54,7 @@ export async function renameMedia(path, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function moveMedia(path, targetParent) {
|
export async function moveMedia(path, targetParent) {
|
||||||
const response = await fetch(`${API_BASE}/api/media/move`, {
|
const response = await fetch(`${API_BASE}/media/move`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ path, target_parent: targetParent }),
|
body: JSON.stringify({ path, target_parent: targetParent }),
|
||||||
@@ -63,7 +63,7 @@ export async function moveMedia(path, targetParent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMedia(path) {
|
export async function deleteMedia(path) {
|
||||||
const response = await fetch(`${API_BASE}/api/media/delete`, {
|
const response = await fetch(`${API_BASE}/media/delete`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ path }),
|
body: JSON.stringify({ path }),
|
||||||
@@ -80,7 +80,7 @@ export function uploadMedia(targetPath, files, onProgress) {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.open('POST', `${API_BASE}/api/media/upload`);
|
request.open('POST', `${API_BASE}/media/upload`);
|
||||||
request.upload.onprogress = (event) => {
|
request.upload.onprogress = (event) => {
|
||||||
if (!event.lengthComputable || !onProgress) return;
|
if (!event.lengthComputable || !onProgress) return;
|
||||||
const percent = Math.round((event.loaded / event.total) * 100);
|
const percent = Math.round((event.loaded / event.total) * 100);
|
||||||
@@ -106,12 +106,12 @@ export function uploadMedia(targetPath, files, onProgress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getTags() {
|
export async function getTags() {
|
||||||
const response = await fetch(`${API_BASE}/api/tags`);
|
const response = await fetch(`${API_BASE}/tags`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateTag(label) {
|
export async function generateTag(label) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/generate`, {
|
const response = await fetch(`${API_BASE}/tags/generate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ label }),
|
body: JSON.stringify({ label }),
|
||||||
@@ -120,7 +120,7 @@ export async function generateTag(label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function claimTag(uid, label) {
|
export async function claimTag(uid, label) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/claim`, {
|
const response = await fetch(`${API_BASE}/tags/claim`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ uid, label }),
|
body: JSON.stringify({ uid, label }),
|
||||||
@@ -129,7 +129,7 @@ export async function claimTag(uid, label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function markTagWritten(uid) {
|
export async function markTagWritten(uid) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/write`, {
|
const response = await fetch(`${API_BASE}/tags/${uid}/write`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
@@ -137,22 +137,22 @@ export async function markTagWritten(uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getBoxTags(boxId) {
|
export async function getBoxTags(boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/tags`);
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/tags`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBoxLocalTags(boxId) {
|
export async function getBoxLocalTags(boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/local-tags`);
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/local-tags`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTagBlocks(boxId) {
|
export async function getTagBlocks(boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/tag-blocks`);
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/tag-blocks`);
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTagBlock(boxId, uid, blocked) {
|
export async function setTagBlock(boxId, uid, blocked) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/tag-blocks`, {
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/tag-blocks`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ uid, blocked }),
|
body: JSON.stringify({ uid, blocked }),
|
||||||
@@ -161,7 +161,7 @@ export async function setTagBlock(boxId, uid, blocked) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setBoxAlias(boxId, alias) {
|
export async function setBoxAlias(boxId, alias) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/alias`, {
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/alias`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ alias }),
|
body: JSON.stringify({ alias }),
|
||||||
@@ -170,7 +170,7 @@ export async function setBoxAlias(boxId, alias) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setTagAlias(uid, alias) {
|
export async function setTagAlias(uid, alias) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/alias`, {
|
const response = await fetch(`${API_BASE}/tags/${uid}/alias`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ alias }),
|
body: JSON.stringify({ alias }),
|
||||||
@@ -179,7 +179,7 @@ export async function setTagAlias(uid, alias) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function assignTag(uid, boxId) {
|
export async function assignTag(uid, boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/assign`, {
|
const response = await fetch(`${API_BASE}/tags/${uid}/assign`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ box_id: boxId }),
|
body: JSON.stringify({ box_id: boxId }),
|
||||||
@@ -188,7 +188,7 @@ export async function assignTag(uid, boxId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function unassignTag(uid, boxId) {
|
export async function unassignTag(uid, boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/unassign`, {
|
const response = await fetch(`${API_BASE}/tags/${uid}/unassign`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ box_id: boxId }),
|
body: JSON.stringify({ box_id: boxId }),
|
||||||
@@ -197,14 +197,14 @@ export async function unassignTag(uid, boxId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTag(uid) {
|
export async function deleteTag(uid) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/${uid}`, {
|
const response = await fetch(`${API_BASE}/tags/${uid}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
return readJson(response);
|
return readJson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTagMedia(uid, mediaPath) {
|
export async function setTagMedia(uid, mediaPath) {
|
||||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/media`, {
|
const response = await fetch(`${API_BASE}/tags/${uid}/media`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ media_path: mediaPath }),
|
body: JSON.stringify({ media_path: mediaPath }),
|
||||||
@@ -213,7 +213,7 @@ export async function setTagMedia(uid, mediaPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function pullTagFromBox(boxId, uid, targetFolder) {
|
export async function pullTagFromBox(boxId, uid, targetFolder) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/pull-tag`, {
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/pull-tag`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ uid, target_folder: targetFolder }),
|
body: JSON.stringify({ uid, target_folder: targetFolder }),
|
||||||
@@ -222,7 +222,7 @@ export async function pullTagFromBox(boxId, uid, targetFolder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendCommand(boxId, command, payload = {}) {
|
export async function sendCommand(boxId, command, payload = {}) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/command`, {
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/command`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ command, payload }),
|
body: JSON.stringify({ command, payload }),
|
||||||
@@ -231,7 +231,7 @@ export async function sendCommand(boxId, command, payload = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function unpairBox(boxId) {
|
export async function unpairBox(boxId) {
|
||||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/unpair`, {
|
const response = await fetch(`${API_BASE}/boxes/${boxId}/unpair`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [react()],
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
server: {
|
const rawTarget = env.VITE_BACKEND_URL || 'http://127.0.0.1:5001';
|
||||||
host: true,
|
const apiTarget = rawTarget.replace(/\/api\/?$/, '');
|
||||||
port: 5174,
|
|
||||||
strictPort: true,
|
return {
|
||||||
},
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 5174,
|
||||||
|
strictPort: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: apiTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user