11
This commit is contained in:
@@ -1,8 +1,24 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
SUBTITLE_FONT_OPTIONS,
|
||||
buildSubtitlePreviewBoxStyle,
|
||||
} from "../../config/subtitleStylePresets.js";
|
||||
|
||||
const config = defineModel("config", { type: Object, required: true });
|
||||
|
||||
const importInputRef = ref(null);
|
||||
const newGroupName = ref("");
|
||||
const editingGroupIndex = ref(-1);
|
||||
const keywordInputs = ref({});
|
||||
|
||||
const renderModeOptions = [
|
||||
{ label: "内联字幕", value: "inline-emphasis" },
|
||||
{ label: "浮动特效", value: "floating" },
|
||||
];
|
||||
|
||||
const builtinGroupNames = new Set(["行动词", "情感词", "描述词", "重点词/成语词"]);
|
||||
|
||||
const enableKeywordEffects = computed({
|
||||
get: () => config.value.enableKeywordEffects !== false,
|
||||
set: (v) => {
|
||||
@@ -17,7 +33,128 @@ const keywordRenderMode = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const groups = computed(() => config.value.keywordGroupsStyles || []);
|
||||
const groups = computed({
|
||||
get: () => config.value.keywordGroupsStyles || [],
|
||||
set: (value) => {
|
||||
config.value.keywordGroupsStyles = value;
|
||||
},
|
||||
});
|
||||
|
||||
function ensureGroups() {
|
||||
if (!Array.isArray(config.value.keywordGroupsStyles)) {
|
||||
config.value.keywordGroupsStyles = [];
|
||||
}
|
||||
return config.value.keywordGroupsStyles;
|
||||
}
|
||||
|
||||
function ensureStyleOverride(group) {
|
||||
if (!group.styleOverride) {
|
||||
group.styleOverride = {
|
||||
fontName: config.value.subtitleStyle?.fontName || "胡晓波男神体",
|
||||
fontSize: Math.max(Number(config.value.subtitleStyle?.fontSize) || 24, 24),
|
||||
fontColor: group.color || "#FFFF00",
|
||||
outlineColor: "#000000",
|
||||
outlineWidth: 2,
|
||||
};
|
||||
}
|
||||
if (!group.color) group.color = group.styleOverride.fontColor || "#1890ff";
|
||||
return group.styleOverride;
|
||||
}
|
||||
|
||||
function createGroup() {
|
||||
const name = newGroupName.value.trim();
|
||||
if (!name) return;
|
||||
const list = ensureGroups();
|
||||
list.push({
|
||||
groupName: name,
|
||||
description: `模板分组${name}`,
|
||||
keywords: [],
|
||||
styleOverride: {
|
||||
fontName: config.value.subtitleStyle?.fontName || "胡晓波男神体",
|
||||
fontSize: 24,
|
||||
fontColor: "#FFFF00",
|
||||
outlineColor: "#000000",
|
||||
outlineWidth: 2,
|
||||
},
|
||||
effectId: null,
|
||||
effectDuration: null,
|
||||
color: "#1890ff",
|
||||
soundEffectId: null,
|
||||
soundEffectConfig: null,
|
||||
});
|
||||
newGroupName.value = "";
|
||||
editingGroupIndex.value = list.length - 1;
|
||||
}
|
||||
|
||||
function removeGroup(index) {
|
||||
const group = groups.value[index];
|
||||
if (!group || builtinGroupNames.has(group.groupName)) return;
|
||||
if (!window.confirm(`确定删除分组「${group.groupName || `分组 ${index + 1}`}」?`)) {
|
||||
return;
|
||||
}
|
||||
groups.value.splice(index, 1);
|
||||
if (editingGroupIndex.value === index) editingGroupIndex.value = -1;
|
||||
}
|
||||
|
||||
function groupKeywords(group) {
|
||||
return group.keywords || group.words || [];
|
||||
}
|
||||
|
||||
function addKeyword(group, index) {
|
||||
const word = String(keywordInputs.value[index] || "").trim();
|
||||
if (!word) return;
|
||||
if (!Array.isArray(group.keywords)) group.keywords = groupKeywords(group).slice();
|
||||
if (!group.keywords.includes(word)) group.keywords.push(word);
|
||||
keywordInputs.value[index] = "";
|
||||
}
|
||||
|
||||
function removeKeyword(group, keyword) {
|
||||
group.keywords = groupKeywords(group).filter((item) => item !== keyword);
|
||||
}
|
||||
|
||||
function groupPreviewStyle(group) {
|
||||
return buildSubtitlePreviewBoxStyle({
|
||||
...(config.value.subtitleStyle || {}),
|
||||
...(group.styleOverride || {}),
|
||||
});
|
||||
}
|
||||
|
||||
function exportGroups() {
|
||||
const blob = new Blob(
|
||||
[
|
||||
JSON.stringify(
|
||||
{
|
||||
version: "1.0.0",
|
||||
exportedAt: new Date().toISOString(),
|
||||
keywordGroupsStyles: groups.value,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
],
|
||||
{ type: "application/json" },
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "keyword-groups-config.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function importGroups(event) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
const data = JSON.parse(await file.text());
|
||||
const list = data.keywordGroupsStyles || data.groups || data;
|
||||
if (!Array.isArray(list)) {
|
||||
window.alert("无效的关键词分组配置");
|
||||
return;
|
||||
}
|
||||
groups.value = list;
|
||||
editingGroupIndex.value = -1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,10 +186,7 @@ const groups = computed(() => config.value.keywordGroupsStyles || []);
|
||||
</div>
|
||||
<SelectButton
|
||||
v-model="keywordRenderMode"
|
||||
:options="[
|
||||
{ label: '内联字幕', value: 'inline-emphasis' },
|
||||
{ label: '浮动特效', value: 'floating' },
|
||||
]"
|
||||
:options="renderModeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
size="small"
|
||||
@@ -80,23 +214,181 @@ const groups = computed(() => config.value.keywordGroupsStyles || []);
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-600/40 bg-slate-900/40 p-3">
|
||||
<h6 class="mb-3 text-sm font-medium text-slate-200">关键词分组({{ groups.length }})</h6>
|
||||
<div class="max-h-48 space-y-2 overflow-y-auto">
|
||||
<h6 class="mb-3 text-sm font-medium text-slate-200">关键词分组管理</h6>
|
||||
|
||||
<div class="mb-4 rounded bg-slate-800/60 p-3">
|
||||
<div class="flex gap-2">
|
||||
<InputText
|
||||
v-model="newGroupName"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
placeholder="输入分组名称"
|
||||
@keydown.enter="createGroup"
|
||||
/>
|
||||
<Button label="创建分组" size="small" @click="createGroup" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium text-slate-200">已有分组 ({{ groups.length }})</div>
|
||||
|
||||
<div
|
||||
v-for="(group, i) in groups"
|
||||
:key="i"
|
||||
class="rounded border border-slate-600/30 bg-slate-800/50 p-2 text-sm"
|
||||
:style="{ borderLeftWidth: '4px', borderLeftColor: group.color || '#3b82f6' }"
|
||||
:key="`${group.groupName || 'group'}-${i}`"
|
||||
class="rounded-lg border border-slate-600/40 bg-slate-800/50 p-3"
|
||||
:style="{ borderLeftWidth: '4px', borderLeftColor: group.color || '#1890ff' }"
|
||||
>
|
||||
<div class="font-medium text-slate-200">{{ group.groupName || `分组 ${i + 1}` }}</div>
|
||||
<div v-if="group.styleOverride?.fontName" class="mt-1 text-xs text-slate-400">
|
||||
强调字体:{{ group.styleOverride.fontName }} ·
|
||||
{{ group.styleOverride.fontColor || group.color }}
|
||||
<div class="mb-2 flex items-start justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 font-medium text-slate-100">
|
||||
<span>{{ group.groupName || `分组 ${i + 1}` }}</span>
|
||||
</div>
|
||||
<InputText
|
||||
v-model="group.description"
|
||||
size="small"
|
||||
class="mt-2 w-full"
|
||||
placeholder="分组描述"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<Button
|
||||
:label="editingGroupIndex === i ? '收起样式' : '编辑样式'"
|
||||
size="small"
|
||||
text
|
||||
@click="
|
||||
ensureStyleOverride(group);
|
||||
editingGroupIndex = editingGroupIndex === i ? -1 : i;
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
v-if="!builtinGroupNames.has(group.groupName)"
|
||||
label="删除"
|
||||
size="small"
|
||||
text
|
||||
severity="danger"
|
||||
@click="removeGroup(i)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="mb-1 text-xs text-slate-400">关键词 ({{ groupKeywords(group).length }})</div>
|
||||
<div v-if="groupKeywords(group).length" class="flex flex-wrap gap-1">
|
||||
<Tag
|
||||
v-for="keyword in groupKeywords(group)"
|
||||
:key="keyword"
|
||||
:value="keyword"
|
||||
rounded
|
||||
:style="{
|
||||
backgroundColor: group.color || '#1890ff',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
}"
|
||||
class="cursor-pointer"
|
||||
@click="removeKeyword(group, keyword)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="text-xs text-slate-500">暂无关键词</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<InputText
|
||||
v-model="keywordInputs[i]"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
placeholder="添加关键词"
|
||||
@keydown.enter="addKeyword(group, i)"
|
||||
/>
|
||||
<Button label="添加" size="small" severity="secondary" @click="addKeyword(group, i)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editingGroupIndex === i"
|
||||
class="mt-3 rounded-lg border border-slate-600/40 bg-white p-3 text-slate-800"
|
||||
>
|
||||
<div class="mb-3 text-sm font-medium">分组样式</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="col-span-2">
|
||||
<label class="mb-1 block text-xs text-slate-500">字体</label>
|
||||
<Select
|
||||
v-model="group.styleOverride.fontName"
|
||||
:options="SUBTITLE_FONT_OPTIONS"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-500">字体大小</label>
|
||||
<InputNumber
|
||||
v-model="group.styleOverride.fontSize"
|
||||
:min="12"
|
||||
:max="120"
|
||||
class="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-500">标签颜色</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="group.color" type="color" class="h-8 w-10" />
|
||||
<InputText v-model="group.color" size="small" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-500">文字颜色</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="group.styleOverride.fontColor" type="color" class="h-8 w-10" />
|
||||
<InputText v-model="group.styleOverride.fontColor" size="small" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-500">描边颜色</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="group.styleOverride.outlineColor" type="color" class="h-8 w-10" />
|
||||
<InputText v-model="group.styleOverride.outlineColor" size="small" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="mb-1 block text-xs text-slate-500">描边宽度</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Slider
|
||||
v-model="group.styleOverride.outlineWidth"
|
||||
:min="0"
|
||||
:max="10"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="w-10 text-right font-mono text-sm">
|
||||
{{ group.styleOverride.outlineWidth }}px
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 rounded bg-slate-800 p-3 text-center">
|
||||
<span :style="groupPreviewStyle(group)">示例关键词</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-slate-500">分组样式编辑与关键词标记将在后续版本开放</p>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<Button label="导出配置" size="small" severity="secondary" @click="exportGroups" />
|
||||
<Button
|
||||
label="导入配置"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="importInputRef?.click()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="importInputRef"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
class="hidden"
|
||||
@change="importGroups"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -58,6 +58,13 @@ const noBackground = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const shadowBlurPreview = computed({
|
||||
get: () => subtitleStyle.value?.shadowBlur ?? 4,
|
||||
set: (value) => {
|
||||
if (subtitleStyle.value) subtitleStyle.value.shadowBlur = value;
|
||||
},
|
||||
});
|
||||
|
||||
const previewStyle = computed(() =>
|
||||
buildSubtitlePreviewBoxStyle(subtitleStyle.value),
|
||||
);
|
||||
@@ -189,6 +196,24 @@ function applyPreset(preset) {
|
||||
<InputText v-model="subtitleStyle.shadowColor" size="small" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="mb-2 block text-sm text-slate-400">阴影模糊(仅预览有效)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Slider
|
||||
v-model="shadowBlurPreview"
|
||||
:min="0"
|
||||
:max="20"
|
||||
class="flex-1"
|
||||
disabled
|
||||
/>
|
||||
<span class="w-10 text-right font-mono text-sm text-slate-400">
|
||||
{{ shadowBlurPreview }}px
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-slate-400">
|
||||
注:FFmpeg 不支持阴影模糊,实际视频中仅显示阴影偏移效果
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="col-span-2">
|
||||
<label class="mb-2 block text-sm text-slate-600">背景颜色</label>
|
||||
|
||||
@@ -20,6 +20,13 @@ const enableBackground = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const backgroundOpacityPercent = computed({
|
||||
get: () => Math.round((Number(line.value.backgroundOpacity) || 0) * 100),
|
||||
set: (value) => {
|
||||
line.value.backgroundOpacity = Math.max(0, Math.min(100, Number(value) || 0)) / 100;
|
||||
},
|
||||
});
|
||||
|
||||
const enableShadow = computed({
|
||||
get: () => (line.value.shadowOffset ?? 0) > 0,
|
||||
set: (on) => {
|
||||
@@ -44,7 +51,7 @@ const previewStyle = computed(() => {
|
||||
}
|
||||
return {
|
||||
fontFamily: line.value.fontName ? `'${line.value.fontName}'` : "优设标题黑",
|
||||
fontSize: `${Math.min(Number(line.value.fontSize) || 80, 48)}px`,
|
||||
fontSize: `${Number(line.value.fontSize) || 80}px`,
|
||||
color: line.value.fontColor || "#fff",
|
||||
backgroundColor:
|
||||
enableBackground.value && line.value.backgroundColor !== "transparent"
|
||||
@@ -52,6 +59,8 @@ const previewStyle = computed(() => {
|
||||
: "transparent",
|
||||
textShadow: shadows.join(", ") || "none",
|
||||
padding: "8px 16px",
|
||||
display: "inline-block",
|
||||
lineHeight: 1.12,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -108,6 +117,22 @@ const previewStyle = computed(() => {
|
||||
<Checkbox v-model="enableBackground" binary />
|
||||
启用背景
|
||||
</label>
|
||||
<div v-if="enableBackground" class="mt-2 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-500">背景颜色</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="line.backgroundColor" type="color" class="h-8 w-10" />
|
||||
<InputText v-model="line.backgroundColor" size="small" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-500">背景透明度</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Slider v-model="backgroundOpacityPercent" :min="0" :max="100" class="flex-1" />
|
||||
<span class="w-10 text-right text-xs">{{ backgroundOpacityPercent }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -116,17 +141,26 @@ const previewStyle = computed(() => {
|
||||
<Checkbox v-model="enableShadow" binary />
|
||||
启用阴影
|
||||
</label>
|
||||
<div v-if="enableShadow" class="flex items-center gap-2">
|
||||
<label class="text-xs text-slate-500">阴影偏移</label>
|
||||
<Slider v-model="line.shadowOffset" :min="1" :max="10" class="flex-1" />
|
||||
<input v-model="line.shadowColor" type="color" class="h-8 w-10" />
|
||||
<div v-if="enableShadow" class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-slate-500">阴影偏移</label>
|
||||
<Slider v-model="line.shadowOffset" :min="1" :max="10" class="flex-1" />
|
||||
<span class="w-8 text-right text-xs">{{ line.shadowOffset }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-slate-500">阴影颜色</label>
|
||||
<input v-model="line.shadowColor" type="color" class="h-8 w-10" />
|
||||
<InputText v-model="line.shadowColor" size="small" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 text-sm font-medium text-slate-700">效果预览</div>
|
||||
<div class="rounded bg-slate-100 py-4 text-center" :style="previewStyle">
|
||||
{{ line.text || "标题文字" }}
|
||||
<div class="overflow-hidden rounded bg-slate-100 py-4 text-center">
|
||||
<div :style="previewStyle">
|
||||
{{ line.text || "标题文字" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import TitleLineStyleEditor from "./TitleLineStyleEditor.vue";
|
||||
import { syncTitleLinesFromReference } from "../../utils/subtitleTemplateDraft.js";
|
||||
|
||||
@@ -37,6 +37,19 @@ function reloadFromReference() {
|
||||
}
|
||||
|
||||
const linePreviews = computed(() => titleConfig.value?.lines || []);
|
||||
|
||||
const maxCharMarks = [8, 12, 15, 20];
|
||||
|
||||
function markLeft(value) {
|
||||
return `${((value - 8) / (20 - 8)) * 100}%`;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => titleConfig.value?.maxCharsPerLine,
|
||||
() => {
|
||||
reloadFromReference();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -57,36 +70,48 @@ const linePreviews = computed(() => titleConfig.value?.lines || []);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-400">每行最大字符数</label>
|
||||
<InputNumber
|
||||
v-model="titleConfig.maxCharsPerLine"
|
||||
:min="8"
|
||||
:max="20"
|
||||
size="small"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-28 shrink-0 text-sm text-slate-300">每行最大字符数:</div>
|
||||
<div class="relative flex-1 pb-5">
|
||||
<Slider
|
||||
v-model="titleConfig.maxCharsPerLine"
|
||||
:min="8"
|
||||
:max="20"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="pointer-events-none absolute inset-x-0 top-6 text-xs text-slate-500">
|
||||
<span
|
||||
v-for="mark in maxCharMarks"
|
||||
:key="mark"
|
||||
class="absolute -translate-x-1/2"
|
||||
:style="{ left: markLeft(mark) }"
|
||||
>
|
||||
{{ mark }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-400">行间距(%)</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-28 shrink-0 text-sm text-slate-300">行间距(%):</div>
|
||||
<InputNumber
|
||||
v-model="titleConfig.lineSpacingPercent"
|
||||
:min="3"
|
||||
:max="15"
|
||||
size="small"
|
||||
class="w-full"
|
||||
class="w-32"
|
||||
/>
|
||||
<span class="text-xs text-slate-500">相对于视频高度</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs text-slate-400">顶部边距(%)</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-28 shrink-0 text-sm text-slate-300">顶部边距(%):</div>
|
||||
<InputNumber
|
||||
v-model="titleConfig.topMarginPercent"
|
||||
:min="2"
|
||||
:max="20"
|
||||
size="small"
|
||||
class="w-full"
|
||||
class="w-32"
|
||||
/>
|
||||
<span class="text-xs text-slate-500">相对于视频高度</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user