mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Add NSFW browse control functionality - Done
This commit is contained in:
@@ -11,6 +11,8 @@ from ..utils.file_utils import load_metadata, get_file_info
|
|||||||
from .lora_cache import LoraCache
|
from .lora_cache import LoraCache
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
from .lora_hash_index import LoraHashIndex
|
from .lora_hash_index import LoraHashIndex
|
||||||
|
from .settings_manager import settings
|
||||||
|
from ..utils.constants import NSFW_LEVELS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -196,6 +198,13 @@ class LoraScanner:
|
|||||||
# Get the base data set
|
# Get the base data set
|
||||||
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name
|
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name
|
||||||
|
|
||||||
|
# Apply SFW filtering if enabled
|
||||||
|
if settings.get('show_only_sfw', False):
|
||||||
|
filtered_data = [
|
||||||
|
item for item in filtered_data
|
||||||
|
if not item.get('preview_nsfw_level') or item.get('preview_nsfw_level') < NSFW_LEVELS['R']
|
||||||
|
]
|
||||||
|
|
||||||
# Apply folder filtering
|
# Apply folder filtering
|
||||||
if folder is not None:
|
if folder is not None:
|
||||||
if recursive:
|
if recursive:
|
||||||
|
|||||||
8
py/utils/constants.py
Normal file
8
py/utils/constants.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
NSFW_LEVELS = {
|
||||||
|
"PG": 1,
|
||||||
|
"PG13": 2,
|
||||||
|
"R": 4,
|
||||||
|
"X": 8,
|
||||||
|
"XXX": 16,
|
||||||
|
"Blocked": 32, # Probably not actually visible through the API without being logged in on model owner account?
|
||||||
|
}
|
||||||
@@ -262,6 +262,83 @@
|
|||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NSFW Level Selector */
|
||||||
|
.nsfw-level-selector {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: var(--z-modal);
|
||||||
|
width: 300px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw-level-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw-level-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-nsfw-selector {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-nsfw-selector:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-level {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw-level-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw-level-btn {
|
||||||
|
flex: 1 0 calc(33% - 8px);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw-level-btn:hover {
|
||||||
|
background: var(--lora-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw-level-btn.active {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile optimizations */
|
/* Mobile optimizations */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.selected-thumbnails-strip {
|
.selected-thumbnails-strip {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { refreshSingleLoraMetadata } from '../api/loraApi.js';
|
import { refreshSingleLoraMetadata } from '../api/loraApi.js';
|
||||||
|
import { showToast, getNSFWLevelName } from '../utils/uiHelpers.js';
|
||||||
|
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||||
|
|
||||||
export class LoraContextMenu {
|
export class LoraContextMenu {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.menu = document.getElementById('loraContextMenu');
|
this.menu = document.getElementById('loraContextMenu');
|
||||||
this.currentCard = null;
|
this.currentCard = null;
|
||||||
|
this.nsfwSelector = document.getElementById('nsfwLevelSelector');
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +61,274 @@ export class LoraContextMenu {
|
|||||||
case 'refresh-metadata':
|
case 'refresh-metadata':
|
||||||
refreshSingleLoraMetadata(this.currentCard.dataset.filepath);
|
refreshSingleLoraMetadata(this.currentCard.dataset.filepath);
|
||||||
break;
|
break;
|
||||||
|
case 'set-nsfw':
|
||||||
|
this.showNSFWLevelSelector(null, null, this.currentCard);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hideMenu();
|
this.hideMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize NSFW Level Selector events
|
||||||
|
this.initNSFWSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
initNSFWSelector() {
|
||||||
|
// Close button
|
||||||
|
const closeBtn = this.nsfwSelector.querySelector('.close-nsfw-selector');
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
this.nsfwSelector.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Level buttons
|
||||||
|
const levelButtons = this.nsfwSelector.querySelectorAll('.nsfw-level-btn');
|
||||||
|
levelButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const level = parseInt(btn.dataset.level);
|
||||||
|
const filePath = this.nsfwSelector.dataset.cardPath;
|
||||||
|
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.saveModelMetadata(filePath, { preview_nsfw_level: level });
|
||||||
|
|
||||||
|
// Update card data
|
||||||
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
|
if (card) {
|
||||||
|
let metaData = {};
|
||||||
|
try {
|
||||||
|
metaData = JSON.parse(card.dataset.meta || '{}');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing metadata:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
metaData.preview_nsfw_level = level;
|
||||||
|
card.dataset.meta = JSON.stringify(metaData);
|
||||||
|
card.dataset.nsfwLevel = level.toString();
|
||||||
|
|
||||||
|
// Apply blur effect immediately
|
||||||
|
this.updateCardBlurEffect(card, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Content rating set to ${getNSFWLevelName(level)}`, 'success');
|
||||||
|
this.nsfwSelector.style.display = 'none';
|
||||||
|
} catch (error) {
|
||||||
|
showToast(`Failed to set content rating: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (this.nsfwSelector.style.display === 'block' &&
|
||||||
|
!this.nsfwSelector.contains(e.target) &&
|
||||||
|
!e.target.closest('.context-menu-item[data-action="set-nsfw"]')) {
|
||||||
|
this.nsfwSelector.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveModelMetadata(filePath, data) {
|
||||||
|
const response = await fetch('/loras/api/save-metadata', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath,
|
||||||
|
...data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCardBlurEffect(card, level) {
|
||||||
|
// Get user settings for blur threshold
|
||||||
|
const blurThreshold = parseInt(localStorage.getItem('nsfwBlurLevel') || '4');
|
||||||
|
|
||||||
|
// Get card preview container
|
||||||
|
const previewContainer = card.querySelector('.card-preview');
|
||||||
|
if (!previewContainer) return;
|
||||||
|
|
||||||
|
// Get preview media element
|
||||||
|
const previewMedia = previewContainer.querySelector('img') || previewContainer.querySelector('video');
|
||||||
|
if (!previewMedia) return;
|
||||||
|
|
||||||
|
// Check if blur should be applied
|
||||||
|
if (level >= blurThreshold) {
|
||||||
|
// Add blur class to the preview container
|
||||||
|
previewContainer.classList.add('blurred');
|
||||||
|
|
||||||
|
// Get or create the NSFW overlay
|
||||||
|
let nsfwOverlay = previewContainer.querySelector('.nsfw-overlay');
|
||||||
|
if (!nsfwOverlay) {
|
||||||
|
// Create new overlay
|
||||||
|
nsfwOverlay = document.createElement('div');
|
||||||
|
nsfwOverlay.className = 'nsfw-overlay';
|
||||||
|
|
||||||
|
// Create and configure the warning content
|
||||||
|
const warningContent = document.createElement('div');
|
||||||
|
warningContent.className = 'nsfw-warning';
|
||||||
|
|
||||||
|
// Determine NSFW warning text based on level
|
||||||
|
let nsfwText = "Mature Content";
|
||||||
|
if (level >= NSFW_LEVELS.XXX) {
|
||||||
|
nsfwText = "XXX-rated Content";
|
||||||
|
} else if (level >= NSFW_LEVELS.X) {
|
||||||
|
nsfwText = "X-rated Content";
|
||||||
|
} else if (level >= NSFW_LEVELS.R) {
|
||||||
|
nsfwText = "R-rated Content";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add warning text and show button
|
||||||
|
warningContent.innerHTML = `
|
||||||
|
<p>${nsfwText}</p>
|
||||||
|
<button class="show-content-btn">Show</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add click event to the show button
|
||||||
|
const showBtn = warningContent.querySelector('.show-content-btn');
|
||||||
|
showBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
previewContainer.classList.remove('blurred');
|
||||||
|
nsfwOverlay.style.display = 'none';
|
||||||
|
|
||||||
|
// Update toggle button icon if it exists
|
||||||
|
const toggleBtn = card.querySelector('.toggle-blur-btn');
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nsfwOverlay.appendChild(warningContent);
|
||||||
|
previewContainer.appendChild(nsfwOverlay);
|
||||||
|
} else {
|
||||||
|
// Update existing overlay
|
||||||
|
const warningText = nsfwOverlay.querySelector('p');
|
||||||
|
if (warningText) {
|
||||||
|
let nsfwText = "Mature Content";
|
||||||
|
if (level >= NSFW_LEVELS.XXX) {
|
||||||
|
nsfwText = "XXX-rated Content";
|
||||||
|
} else if (level >= NSFW_LEVELS.X) {
|
||||||
|
nsfwText = "X-rated Content";
|
||||||
|
} else if (level >= NSFW_LEVELS.R) {
|
||||||
|
nsfwText = "R-rated Content";
|
||||||
|
}
|
||||||
|
warningText.textContent = nsfwText;
|
||||||
|
}
|
||||||
|
nsfwOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create the toggle button in the header
|
||||||
|
const cardHeader = previewContainer.querySelector('.card-header');
|
||||||
|
if (cardHeader) {
|
||||||
|
let toggleBtn = cardHeader.querySelector('.toggle-blur-btn');
|
||||||
|
|
||||||
|
if (!toggleBtn) {
|
||||||
|
toggleBtn = document.createElement('button');
|
||||||
|
toggleBtn.className = 'toggle-blur-btn';
|
||||||
|
toggleBtn.title = 'Toggle blur';
|
||||||
|
toggleBtn.innerHTML = '<i class="fas fa-eye"></i>';
|
||||||
|
|
||||||
|
// Add click event to toggle button
|
||||||
|
toggleBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isBlurred = previewContainer.classList.toggle('blurred');
|
||||||
|
const icon = toggleBtn.querySelector('i');
|
||||||
|
|
||||||
|
// Update icon and overlay visibility
|
||||||
|
if (isBlurred) {
|
||||||
|
icon.className = 'fas fa-eye';
|
||||||
|
nsfwOverlay.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
icon.className = 'fas fa-eye-slash';
|
||||||
|
nsfwOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to the beginning of header
|
||||||
|
cardHeader.insertBefore(toggleBtn, cardHeader.firstChild);
|
||||||
|
|
||||||
|
// Update base model label class
|
||||||
|
const baseModelLabel = cardHeader.querySelector('.base-model-label');
|
||||||
|
if (baseModelLabel && !baseModelLabel.classList.contains('with-toggle')) {
|
||||||
|
baseModelLabel.classList.add('with-toggle');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update existing toggle button
|
||||||
|
toggleBtn.querySelector('i').className = 'fas fa-eye';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove blur
|
||||||
|
previewContainer.classList.remove('blurred');
|
||||||
|
|
||||||
|
// Hide overlay if it exists
|
||||||
|
const overlay = previewContainer.querySelector('.nsfw-overlay');
|
||||||
|
if (overlay) overlay.style.display = 'none';
|
||||||
|
|
||||||
|
// Update or remove toggle button
|
||||||
|
const toggleBtn = card.querySelector('.toggle-blur-btn');
|
||||||
|
if (toggleBtn) {
|
||||||
|
// We'll leave the button but update the icon
|
||||||
|
toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNSFWLevelSelector(x, y, card) {
|
||||||
|
const selector = document.getElementById('nsfwLevelSelector');
|
||||||
|
const currentLevelEl = document.getElementById('currentNSFWLevel');
|
||||||
|
|
||||||
|
// Get current NSFW level
|
||||||
|
let currentLevel = 0;
|
||||||
|
try {
|
||||||
|
const metaData = JSON.parse(card.dataset.meta || '{}');
|
||||||
|
currentLevel = metaData.preview_nsfw_level || 0;
|
||||||
|
|
||||||
|
// Update if we have no recorded level but have a dataset attribute
|
||||||
|
if (!currentLevel && card.dataset.nsfwLevel) {
|
||||||
|
currentLevel = parseInt(card.dataset.nsfwLevel) || 0;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing metadata:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevelEl.textContent = getNSFWLevelName(currentLevel);
|
||||||
|
|
||||||
|
// Position the selector
|
||||||
|
if (x && y) {
|
||||||
|
const viewportWidth = document.documentElement.clientWidth;
|
||||||
|
const viewportHeight = document.documentElement.clientHeight;
|
||||||
|
const selectorRect = selector.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Center the selector if no coordinates provided
|
||||||
|
let finalX = (viewportWidth - selectorRect.width) / 2;
|
||||||
|
let finalY = (viewportHeight - selectorRect.height) / 2;
|
||||||
|
|
||||||
|
selector.style.left = `${finalX}px`;
|
||||||
|
selector.style.top = `${finalY}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight current level button
|
||||||
|
document.querySelectorAll('.nsfw-level-btn').forEach(btn => {
|
||||||
|
if (parseInt(btn.dataset.level) === currentLevel) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store reference to current card
|
||||||
|
selector.dataset.cardPath = card.dataset.filepath;
|
||||||
|
|
||||||
|
// Show selector
|
||||||
|
selector.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
showMenu(x, y, card) {
|
showMenu(x, y, card) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
import { state, saveSettings } from '../state/index.js';
|
import { state, saveSettings } from '../state/index.js';
|
||||||
|
import { resetAndReload } from '../api/loraApi.js';
|
||||||
|
|
||||||
export class SettingsManager {
|
export class SettingsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -89,6 +90,9 @@ export class SettingsManager {
|
|||||||
|
|
||||||
// Apply frontend settings immediately
|
// Apply frontend settings immediately
|
||||||
this.applyFrontendSettings();
|
this.applyFrontendSettings();
|
||||||
|
|
||||||
|
// Reload the loras without updating folders
|
||||||
|
await resetAndReload(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('Failed to save settings: ' + error.message, 'error');
|
showToast('Failed to save settings: ' + error.message, 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const BASE_MODEL_CLASSES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NSFW_LEVELS = {
|
export const NSFW_LEVELS = {
|
||||||
|
UNKNOWN: 0,
|
||||||
PG: 1,
|
PG: 1,
|
||||||
PG13: 2,
|
PG13: 2,
|
||||||
R: 4,
|
R: 4,
|
||||||
|
|||||||
@@ -152,3 +152,14 @@ export function initBackToTop() {
|
|||||||
// Initial check
|
// Initial check
|
||||||
toggleBackToTop();
|
toggleBackToTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNSFWLevelName(level) {
|
||||||
|
if (level === 0) return 'Unknown';
|
||||||
|
if (level >= 32) return 'Blocked';
|
||||||
|
if (level >= 16) return 'XXX';
|
||||||
|
if (level >= 8) return 'X';
|
||||||
|
if (level >= 4) return 'R';
|
||||||
|
if (level >= 2) return 'PG13';
|
||||||
|
if (level >= 1) return 'PG';
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
<div class="context-menu-item" data-action="preview">
|
<div class="context-menu-item" data-action="preview">
|
||||||
<i class="fas fa-image"></i> Replace Preview
|
<i class="fas fa-image"></i> Replace Preview
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="set-nsfw">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Set Content Rating
|
||||||
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item" data-action="move">
|
<div class="context-menu-item" data-action="move">
|
||||||
<i class="fas fa-folder-open"></i> Move to Folder
|
<i class="fas fa-folder-open"></i> Move to Folder
|
||||||
@@ -22,3 +25,20 @@
|
|||||||
<i class="fas fa-trash"></i> Delete Model
|
<i class="fas fa-trash"></i> Delete Model
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="nsfwLevelSelector" class="nsfw-level-selector">
|
||||||
|
<div class="nsfw-level-header">
|
||||||
|
<h3>Set Content Rating</h3>
|
||||||
|
<button class="close-nsfw-selector"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="nsfw-level-content">
|
||||||
|
<div class="current-level">Current: <span id="currentNSFWLevel">Unknown</span></div>
|
||||||
|
<div class="nsfw-level-options">
|
||||||
|
<button class="nsfw-level-btn" data-level="1">PG</button>
|
||||||
|
<button class="nsfw-level-btn" data-level="2">PG13</button>
|
||||||
|
<button class="nsfw-level-btn" data-level="4">R</button>
|
||||||
|
<button class="nsfw-level-btn" data-level="8">X</button>
|
||||||
|
<button class="nsfw-level-btn" data-level="16">XXX</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user