0.10.2-24 Laout
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Dialog } from 'primereact/dialog';
|
import { Dialog } from 'primereact/dialog';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
|
|
||||||
import blurayIndicatorIcon from '../assets/media-bluray.svg';
|
import blurayIndicatorIcon from '../assets/media-bluray.svg';
|
||||||
import discIndicatorIcon from '../assets/media-disc.svg';
|
import discIndicatorIcon from '../assets/media-disc.svg';
|
||||||
import otherIndicatorIcon from '../assets/media-other.svg';
|
import otherIndicatorIcon from '../assets/media-other.svg';
|
||||||
@@ -118,6 +117,55 @@ function shellQuote(value) {
|
|||||||
return `'${raw.replace(/'/g, `'"'"'`)}'`;
|
return `'${raw.replace(/'/g, `'"'"'`)}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackLang(value) {
|
||||||
|
const raw = String(value || '').trim().toLowerCase();
|
||||||
|
if (!raw) return 'und';
|
||||||
|
const map = { en: 'en', eng: 'en', de: 'de', deu: 'de', ger: 'de', tr: 'tr', tur: 'tr', fr: 'fr', fra: 'fr', fre: 'fr', es: 'es', spa: 'es', it: 'it', ita: 'it' };
|
||||||
|
if (map[raw]) return map[raw];
|
||||||
|
return raw.length >= 2 ? raw.slice(0, 2) : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackCodec(type, value, hint = null) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
const merged = `${raw} ${String(hint || '')}`.toLowerCase();
|
||||||
|
if (!raw) return '-';
|
||||||
|
if (type === 'subtitle') return merged.includes('pgs') ? 'PGS' : raw.toUpperCase();
|
||||||
|
if (merged.includes('dts-hd ma') || merged.includes('dts hd ma')) return 'DTS-HD MA';
|
||||||
|
if (merged.includes('dts-hd')) return 'DTS-HD';
|
||||||
|
if (merged.includes('dts') || merged.includes('dca')) return 'DTS';
|
||||||
|
if (merged.includes('truehd')) return 'TRUEHD';
|
||||||
|
if (merged.includes('e-ac-3') || merged.includes('eac3') || merged.includes('dd+')) return 'E-AC-3';
|
||||||
|
if (merged.includes('ac-3') || merged.includes('ac3') || merged.includes('dolby digital')) return 'AC-3';
|
||||||
|
return raw.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackChLayout(rawValue) {
|
||||||
|
const raw = String(rawValue || '').trim().toLowerCase();
|
||||||
|
if (!raw) return null;
|
||||||
|
if (raw.includes('7.1')) return '8ch | Surround 7.1';
|
||||||
|
if (raw.includes('5.1')) return '6ch | Surround 5.1';
|
||||||
|
if (raw.includes('stereo') || raw.includes('2.0') || raw.includes('downmix')) return '2ch | Stereo';
|
||||||
|
if (raw.includes('mono') || raw.includes('1.0')) return '1ch | Mono';
|
||||||
|
const nMatch = raw.match(/^([\d.]+)$/);
|
||||||
|
if (nMatch) {
|
||||||
|
const n = Number(nMatch[1]);
|
||||||
|
if (Number.isFinite(n) && n > 0) return `${Math.trunc(n)}ch`;
|
||||||
|
}
|
||||||
|
const chMatch = raw.match(/(\d+)\s*ch/);
|
||||||
|
if (chMatch) return `${chMatch[1]}ch`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackSizeFormat(bytes) {
|
||||||
|
const value = Number(bytes || 0);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return '-';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
let size = value;
|
||||||
|
let i = 0;
|
||||||
|
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
|
||||||
|
return `${size.toFixed(2)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildExecutedHandBrakeCommand(handbrakeInfo) {
|
function buildExecutedHandBrakeCommand(handbrakeInfo) {
|
||||||
const cmd = String(handbrakeInfo?.cmd || '').trim();
|
const cmd = String(handbrakeInfo?.cmd || '').trim();
|
||||||
const args = Array.isArray(handbrakeInfo?.args) ? handbrakeInfo.args : [];
|
const args = Array.isArray(handbrakeInfo?.args) ? handbrakeInfo.args : [];
|
||||||
@@ -546,35 +594,9 @@ export default function JobDetailDialog({
|
|||||||
|| configuredSelection.postScriptIds.length > 0
|
|| configuredSelection.postScriptIds.length > 0
|
||||||
|| configuredSelection.preChainIds.length > 0
|
|| configuredSelection.preChainIds.length > 0
|
||||||
|| configuredSelection.postChainIds.length > 0;
|
|| configuredSelection.postChainIds.length > 0;
|
||||||
const reviewPreEncodeItems = [
|
|
||||||
...configuredSelection.preScriptIds.map((id) => ({ type: 'script', id })),
|
|
||||||
...configuredSelection.preChainIds.map((id) => ({ type: 'chain', id }))
|
|
||||||
];
|
|
||||||
const reviewPostEncodeItems = [
|
|
||||||
...configuredSelection.postScriptIds.map((id) => ({ type: 'script', id })),
|
|
||||||
...configuredSelection.postChainIds.map((id) => ({ type: 'chain', id }))
|
|
||||||
];
|
|
||||||
const encodePlanUserPreset = job?.encodePlan?.userPreset && typeof job.encodePlan.userPreset === 'object'
|
const encodePlanUserPreset = job?.encodePlan?.userPreset && typeof job.encodePlan.userPreset === 'object'
|
||||||
? job.encodePlan.userPreset
|
? job.encodePlan.userPreset
|
||||||
: null;
|
: null;
|
||||||
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
|
|
||||||
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
|
|
||||||
const encodePlanManualTrackSelection = job?.encodePlan?.manualTrackSelection
|
|
||||||
&& typeof job.encodePlan.manualTrackSelection === 'object'
|
|
||||||
? job.encodePlan.manualTrackSelection
|
|
||||||
: null;
|
|
||||||
const reviewTrackSelectionByTitle = encodePlanManualTrackSelection?.titleId != null
|
|
||||||
? {
|
|
||||||
[encodePlanManualTrackSelection.titleId]: {
|
|
||||||
audioTrackIds: Array.isArray(encodePlanManualTrackSelection.audioTrackIds)
|
|
||||||
? encodePlanManualTrackSelection.audioTrackIds
|
|
||||||
: [],
|
|
||||||
subtitleTrackIds: Array.isArray(encodePlanManualTrackSelection.subtitleTrackIds)
|
|
||||||
? encodePlanManualTrackSelection.subtitleTrackIds
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
const reviewSelectedEncodeTitleId = job?.encodePlan?.encodeInputTitleId ?? null;
|
const reviewSelectedEncodeTitleId = job?.encodePlan?.encodeInputTitleId ?? null;
|
||||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||||
const canDownloadRaw = Boolean(job?.raw_path && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
const canDownloadRaw = Boolean(job?.raw_path && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||||
@@ -851,25 +873,67 @@ export default function JobDetailDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Titel-Liste */}
|
{/* Spurauswahl – read-only */}
|
||||||
{!isCd && !isAudiobook && job.encodePlan ? (
|
{job.encodePlan && Array.isArray(job.encodePlan.titles) && job.encodePlan.titles.length > 0 ? (
|
||||||
<section className="job-meta-block job-meta-block-full">
|
<div className="spurauswahl-block">
|
||||||
<h4>Spurauswahl</h4>
|
<div className="spurauswahl-label">Spurauswahl</div>
|
||||||
<MediaInfoReviewPanel
|
{job.encodePlan.titles
|
||||||
review={job.encodePlan}
|
.filter((t) => t.selectedForEncode || t.encodeInput || String(t.id) === String(reviewSelectedEncodeTitleId))
|
||||||
commandOutputPath={job.output_path || null}
|
.map((title) => {
|
||||||
selectedEncodeTitleId={reviewSelectedEncodeTitleId}
|
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
||||||
trackSelectionByTitle={reviewTrackSelectionByTitle}
|
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||||
availableScripts={configuredSelection.scriptCatalog}
|
return (
|
||||||
availableChains={configuredSelection.chainCatalog}
|
<div key={title.id} className="track-title-block">
|
||||||
preEncodeItems={reviewPreEncodeItems}
|
<div className="track-title-row">
|
||||||
postEncodeItems={reviewPostEncodeItems}
|
<span>#{title.id} | {title.fileName} | {title.durationMinutes != null ? `${Number(title.durationMinutes).toFixed(2)} min` : '-'} | {trackSizeFormat(title.sizeBytes)}</span>
|
||||||
userPresets={reviewUserPresets}
|
{title.encodeInput ? <span className="track-input-badge">Encode-Input</span> : null}
|
||||||
selectedUserPresetId={Number.isFinite(encodePlanUserPresetId) && encodePlanUserPresetId > 0
|
</div>
|
||||||
? Math.trunc(encodePlanUserPresetId)
|
{audioTracks.length > 0 ? (
|
||||||
: null}
|
<div className="track-group">
|
||||||
/>
|
<div className="track-group-label">Tonspuren (Titel #{title.id})</div>
|
||||||
</section>
|
{audioTracks.map((track) => {
|
||||||
|
const lang = trackLang(track.language || track.languageLabel);
|
||||||
|
const codec = trackCodec('audio', track.format, track.description || track.title);
|
||||||
|
const chLayout = trackChLayout(track.channels);
|
||||||
|
let displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||||
|
if (chLayout) displayText += ` | ${chLayout}`;
|
||||||
|
const actionInfo = track.selectedForEncode
|
||||||
|
? (String(track.encodePreviewSummary || track.encodeActionSummary || '').trim() || 'Copy')
|
||||||
|
: 'Nicht übernommen';
|
||||||
|
return (
|
||||||
|
<div key={track.id} className="track-item">
|
||||||
|
<span>{displayText}</span>
|
||||||
|
<small className="track-action-note">Encode: {actionInfo}</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{subtitleTracks.length > 0 ? (
|
||||||
|
<div className="track-group">
|
||||||
|
<div className="track-group-label">Subtitles (Titel #{title.id})</div>
|
||||||
|
{subtitleTracks.map((track) => {
|
||||||
|
const lang = trackLang(track.language || track.languageLabel);
|
||||||
|
const codec = trackCodec('subtitle', track.format);
|
||||||
|
const displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||||
|
const rawAction = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
|
||||||
|
const actionInfo = track.selectedForEncode
|
||||||
|
? (/^nicht übernommen$/i.test(rawAction) ? 'Übernehmen' : (rawAction || 'Übernehmen'))
|
||||||
|
: 'Nicht übernommen';
|
||||||
|
return (
|
||||||
|
<div key={track.id} className="track-item">
|
||||||
|
<span>{displayText}</span>
|
||||||
|
<small className="track-action-note">Encode: {actionInfo}</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -866,11 +866,15 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
|
|
||||||
const renderPoster = (row, className = 'history-dv-poster') => {
|
const renderPoster = (row, className = 'history-dv-poster') => {
|
||||||
const mediaMeta = resolveMediaTypeMeta(row);
|
const mediaMeta = resolveMediaTypeMeta(row);
|
||||||
|
const isAudioSquare = ['cd', 'audiobook'].includes(mediaMeta.mediaType);
|
||||||
|
const imgClass = className === 'history-dv-poster-grid' && isAudioSquare
|
||||||
|
? 'history-dv-poster-grid-audio'
|
||||||
|
: className;
|
||||||
const title = row?.title || row?.detected_title || 'Poster';
|
const title = row?.title || row?.detected_title || 'Poster';
|
||||||
if (row?.poster_url && row.poster_url !== 'N/A') {
|
if (row?.poster_url && row.poster_url !== 'N/A') {
|
||||||
return <img src={row.poster_url} alt={title} className={className} loading="lazy" />;
|
return <img src={row.poster_url} alt={title} className={imgClass} loading="lazy" />;
|
||||||
}
|
}
|
||||||
return <div className="history-dv-poster-fallback">{['cd', 'audiobook'].includes(mediaMeta.mediaType) ? 'Kein Cover' : 'Kein Poster'}</div>;
|
return <div className="history-dv-poster-fallback">{isAudioSquare ? 'Kein Cover' : 'Kein Poster'}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPresenceChip = (label, available) => (
|
const renderPresenceChip = (label, available) => (
|
||||||
|
|||||||
@@ -2334,10 +2334,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-poster,
|
.history-dv-poster,
|
||||||
.history-dv-poster-grid {
|
.history-dv-poster-grid,
|
||||||
|
.history-dv-poster-grid-audio {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 0.35rem;
|
border-radius: 0.35rem;
|
||||||
border: 1px solid #cba266;
|
border: 1px solid #cba266;
|
||||||
background: #f6ebd6;
|
background: #f6ebd6;
|
||||||
@@ -2345,10 +2345,27 @@ body {
|
|||||||
|
|
||||||
.history-dv-poster {
|
.history-dv-poster {
|
||||||
height: 88px;
|
height: 88px;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-poster-grid {
|
.history-dv-poster-grid {
|
||||||
height: 164px;
|
height: 164px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-dv-poster-grid-audio {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-dv-grid-poster-wrap {
|
||||||
|
width: min(120px, 100%);
|
||||||
|
height: 164px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-poster-fallback {
|
.history-dv-poster-fallback {
|
||||||
@@ -2380,11 +2397,6 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-grid-poster-wrap {
|
|
||||||
width: min(120px, 100%);
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-dv-grid-status-overlay {
|
.history-dv-grid-status-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
@@ -3023,6 +3035,68 @@ body {
|
|||||||
color: var(--rip-muted);
|
color: var(--rip-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spurauswahl-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spurauswahl-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title-block {
|
||||||
|
border: 1px solid var(--rip-border);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
background: var(--rip-panel-soft);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-input-badge {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
background: var(--rip-accent-soft, #e8f4ea);
|
||||||
|
color: var(--rip-success, #2d7a3a);
|
||||||
|
border: 1px solid var(--rip-success, #2d7a3a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
border-left: 2px solid var(--rip-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-group-label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
.media-title-block {
|
.media-title-block {
|
||||||
border: 1px solid var(--rip-border);
|
border: 1px solid var(--rip-border);
|
||||||
border-radius: 0.45rem;
|
border-radius: 0.45rem;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.2-23",
|
"version": "0.10.2-24",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
|
|||||||
Reference in New Issue
Block a user