mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
Add trigger words editting
This commit is contained in:
@@ -384,7 +384,7 @@ class ApiRoutes:
|
||||
# 准备要处理的 loras
|
||||
to_process = [
|
||||
lora for lora in cache.raw_data
|
||||
if lora.get('sha256') and not lora.get('civitai') and lora.get('from_civitai')
|
||||
if lora.get('sha256') and (not lora.get('civitai') or 'id' not in lora.get('civitai')) and lora.get('from_civitai') # TODO: for lora not from CivitAI but added traineWords
|
||||
]
|
||||
total_to_process = len(to_process)
|
||||
|
||||
@@ -617,8 +617,15 @@ class ApiRoutes:
|
||||
else:
|
||||
metadata = {}
|
||||
|
||||
# Update metadata with new values
|
||||
metadata.update(metadata_updates)
|
||||
# Handle nested updates (for civitai.trainedWords)
|
||||
for key, value in metadata_updates.items():
|
||||
if isinstance(value, dict) and key in metadata and isinstance(metadata[key], dict):
|
||||
# Deep update for nested dictionaries
|
||||
for nested_key, nested_value in value.items():
|
||||
metadata[key][nested_key] = nested_value
|
||||
else:
|
||||
# Regular update for top-level keys
|
||||
metadata[key] = value
|
||||
|
||||
# Save updated metadata
|
||||
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||
|
||||
@@ -121,6 +121,15 @@ async def load_metadata(file_path: str) -> Optional[LoraMetadata]:
|
||||
elif preview_url != normalize_path(preview_url):
|
||||
data['preview_url'] = normalize_path(preview_url)
|
||||
needs_update = True
|
||||
|
||||
# Ensure all fields are present, due to updates adding new fields
|
||||
if 'tags' not in data:
|
||||
data['tags'] = []
|
||||
needs_update = True
|
||||
|
||||
if 'modelDescription' not in data:
|
||||
data['modelDescription'] = ""
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||
|
||||
@@ -163,12 +163,57 @@
|
||||
border: 1px solid var(--lora-border);
|
||||
}
|
||||
|
||||
/* New header style for trigger words */
|
||||
.trigger-words-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.edit-trigger-words-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
padding: 2px 5px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-trigger-words-btn:hover {
|
||||
opacity: 0.8;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .edit-trigger-words-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Edit mode active state */
|
||||
.trigger-words.edit-mode .edit-trigger-words-btn {
|
||||
opacity: 0.8;
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.trigger-words-content {
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.trigger-words-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
/* No trigger words message */
|
||||
.no-trigger-words {
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Update Trigger Words styles */
|
||||
@@ -182,6 +227,7 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Update trigger word content color to use theme accent */
|
||||
@@ -207,6 +253,123 @@
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
/* Delete button for trigger word */
|
||||
.delete-trigger-word-btn {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--lora-error);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.delete-trigger-word-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Edit controls */
|
||||
.trigger-words-edit-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.trigger-words-edit-controls button {
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.trigger-words-edit-controls button:hover {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.trigger-words-edit-controls button i {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.save-trigger-words-btn {
|
||||
background: var(--lora-accent) !important;
|
||||
color: white !important;
|
||||
border-color: var(--lora-accent) !important;
|
||||
}
|
||||
|
||||
.save-trigger-words-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Add trigger word form */
|
||||
.add-trigger-word-form {
|
||||
margin-top: var(--space-2);
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.new-trigger-word-input {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.new-trigger-word-input:focus {
|
||||
border-color: var(--lora-accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.confirm-add-trigger-word-btn,
|
||||
.cancel-add-trigger-word-btn {
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.confirm-add-trigger-word-btn {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.confirm-add-trigger-word-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cancel-add-trigger-word-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .cancel-add-trigger-word-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Editable Fields */
|
||||
.editable-field {
|
||||
position: relative;
|
||||
@@ -724,7 +887,7 @@
|
||||
}
|
||||
|
||||
.model-description-content blockquote {
|
||||
border-left: 3px solid var(--lora-accent);
|
||||
border-left: 3px solid var (--lora-accent);
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
|
||||
@@ -67,7 +67,7 @@ export function showLoraModal(lora) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${renderTriggerWords(escapedWords)}
|
||||
${renderTriggerWords(escapedWords, lora.file_path)}
|
||||
<div class="info-item notes">
|
||||
<label>Additional Notes</label>
|
||||
<div class="editable-field">
|
||||
@@ -120,6 +120,7 @@ export function showLoraModal(lora) {
|
||||
setupShowcaseScroll();
|
||||
setupTabSwitching();
|
||||
setupTagTooltip();
|
||||
setupTriggerWordsEditMode();
|
||||
|
||||
// If we have a model ID but no description, fetch it
|
||||
if (lora.civitai?.modelId && !lora.modelDescription) {
|
||||
@@ -488,35 +489,75 @@ async function saveModelMetadata(filePath, data) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderTriggerWords(words) {
|
||||
function renderTriggerWords(words, filePath) {
|
||||
if (!words.length) return `
|
||||
<div class="info-item full-width trigger-words">
|
||||
<label>Trigger Words</label>
|
||||
<span>No trigger word needed</span>
|
||||
<div class="trigger-words-header">
|
||||
<label>Trigger Words</label>
|
||||
<button class="edit-trigger-words-btn" data-file-path="${filePath}" title="Edit trigger words">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="trigger-words-content">
|
||||
<span class="no-trigger-words">No trigger word needed</span>
|
||||
<div class="trigger-words-tags" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="trigger-words-edit-controls" style="display:none;">
|
||||
<button class="add-trigger-word-btn" title="Add a trigger word">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</button>
|
||||
<button class="save-trigger-words-btn" title="Save changes">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="add-trigger-word-form" style="display:none;">
|
||||
<input type="text" class="new-trigger-word-input" placeholder="Enter trigger word">
|
||||
<button class="confirm-add-trigger-word-btn">Add</button>
|
||||
<button class="cancel-add-trigger-word-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="info-item full-width trigger-words">
|
||||
<label>Trigger Words</label>
|
||||
<div class="trigger-words-tags">
|
||||
${words.map(word => `
|
||||
<div class="trigger-word-tag" onclick="copyTriggerWord('${word}')">
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
<div class="trigger-words-header">
|
||||
<label>Trigger Words</label>
|
||||
<button class="edit-trigger-words-btn" data-file-path="${filePath}" title="Edit trigger words">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="trigger-words-content">
|
||||
<div class="trigger-words-tags">
|
||||
${words.map(word => `
|
||||
<div class="trigger-word-tag" data-word="${word}" onclick="copyTriggerWord('${word}')">
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
<button class="delete-trigger-word-btn" style="display:none;" onclick="event.stopPropagation();">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="trigger-words-edit-controls" style="display:none;">
|
||||
<button class="add-trigger-word-btn" title="Add a trigger word">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</button>
|
||||
<button class="save-trigger-words-btn" title="Save changes">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="add-trigger-word-form" style="display:none;">
|
||||
<input type="text" class="new-trigger-word-input" placeholder="Enter trigger word">
|
||||
<button class="confirm-add-trigger-word-btn">Add</button>
|
||||
<button class="cancel-add-trigger-word-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderShowcaseImages(images) {
|
||||
return renderShowcaseContent(images);
|
||||
}
|
||||
|
||||
export function toggleShowcase(element) {
|
||||
const carousel = element.nextElementSibling;
|
||||
const isCollapsed = carousel.classList.contains('collapsed');
|
||||
@@ -738,4 +779,243 @@ function setupTagTooltip() {
|
||||
tooltip.classList.remove('visible');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up trigger words edit mode
|
||||
function setupTriggerWordsEditMode() {
|
||||
const editBtn = document.querySelector('.edit-trigger-words-btn');
|
||||
if (!editBtn) return;
|
||||
|
||||
editBtn.addEventListener('click', function() {
|
||||
const triggerWordsSection = this.closest('.trigger-words');
|
||||
const isEditMode = triggerWordsSection.classList.toggle('edit-mode');
|
||||
|
||||
// Toggle edit mode UI elements
|
||||
const triggerWordTags = triggerWordsSection.querySelectorAll('.trigger-word-tag');
|
||||
const editControls = triggerWordsSection.querySelector('.trigger-words-edit-controls');
|
||||
const noTriggerWords = triggerWordsSection.querySelector('.no-trigger-words');
|
||||
const tagsContainer = triggerWordsSection.querySelector('.trigger-words-tags');
|
||||
|
||||
if (isEditMode) {
|
||||
this.innerHTML = '<i class="fas fa-times"></i>'; // Change to cancel icon
|
||||
this.title = "Cancel editing";
|
||||
editControls.style.display = 'flex';
|
||||
|
||||
// If we have no trigger words yet, hide the "No trigger word needed" text
|
||||
// and show the empty tags container
|
||||
if (noTriggerWords) {
|
||||
noTriggerWords.style.display = 'none';
|
||||
if (tagsContainer) tagsContainer.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Disable click-to-copy and show delete buttons
|
||||
triggerWordTags.forEach(tag => {
|
||||
tag.onclick = null;
|
||||
tag.querySelector('.trigger-word-copy').style.display = 'none';
|
||||
tag.querySelector('.delete-trigger-word-btn').style.display = 'block';
|
||||
});
|
||||
} else {
|
||||
this.innerHTML = '<i class="fas fa-pencil-alt"></i>'; // Change back to edit icon
|
||||
this.title = "Edit trigger words";
|
||||
editControls.style.display = 'none';
|
||||
|
||||
// If we have no trigger words, show the "No trigger word needed" text
|
||||
// and hide the empty tags container
|
||||
const currentTags = triggerWordsSection.querySelectorAll('.trigger-word-tag');
|
||||
if (noTriggerWords && currentTags.length === 0) {
|
||||
noTriggerWords.style.display = '';
|
||||
if (tagsContainer) tagsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Restore original state
|
||||
triggerWordTags.forEach(tag => {
|
||||
const word = tag.dataset.word;
|
||||
tag.onclick = () => copyTriggerWord(word);
|
||||
tag.querySelector('.trigger-word-copy').style.display = 'flex';
|
||||
tag.querySelector('.delete-trigger-word-btn').style.display = 'none';
|
||||
});
|
||||
|
||||
// Hide add form if open
|
||||
triggerWordsSection.querySelector('.add-trigger-word-form').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Set up add trigger word button
|
||||
const addBtn = document.querySelector('.add-trigger-word-btn');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', function() {
|
||||
const triggerWordsSection = this.closest('.trigger-words');
|
||||
const addForm = triggerWordsSection.querySelector('.add-trigger-word-form');
|
||||
addForm.style.display = 'flex';
|
||||
addForm.querySelector('input').focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Set up confirm and cancel add buttons
|
||||
const confirmAddBtn = document.querySelector('.confirm-add-trigger-word-btn');
|
||||
const cancelAddBtn = document.querySelector('.cancel-add-trigger-word-btn');
|
||||
const triggerWordInput = document.querySelector('.new-trigger-word-input');
|
||||
|
||||
if (confirmAddBtn && triggerWordInput) {
|
||||
confirmAddBtn.addEventListener('click', function() {
|
||||
addNewTriggerWord(triggerWordInput.value);
|
||||
});
|
||||
|
||||
// Add keydown event to input
|
||||
triggerWordInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewTriggerWord(this.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelAddBtn) {
|
||||
cancelAddBtn.addEventListener('click', function() {
|
||||
const addForm = this.closest('.add-trigger-word-form');
|
||||
addForm.style.display = 'none';
|
||||
addForm.querySelector('input').value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Set up save button
|
||||
const saveBtn = document.querySelector('.save-trigger-words-btn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', saveTriggerWords);
|
||||
}
|
||||
|
||||
// Set up delete buttons
|
||||
document.querySelectorAll('.delete-trigger-word-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const tag = this.closest('.trigger-word-tag');
|
||||
tag.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to add a new trigger word
|
||||
function addNewTriggerWord(word) {
|
||||
word = word.trim();
|
||||
if (!word) return;
|
||||
|
||||
const triggerWordsSection = document.querySelector('.trigger-words');
|
||||
let tagsContainer = document.querySelector('.trigger-words-tags');
|
||||
|
||||
// Ensure tags container exists and is visible
|
||||
if (tagsContainer) {
|
||||
tagsContainer.style.display = 'flex';
|
||||
} else {
|
||||
// Create tags container if it doesn't exist
|
||||
const contentDiv = triggerWordsSection.querySelector('.trigger-words-content');
|
||||
if (contentDiv) {
|
||||
tagsContainer = document.createElement('div');
|
||||
tagsContainer.className = 'trigger-words-tags';
|
||||
contentDiv.appendChild(tagsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tagsContainer) return;
|
||||
|
||||
// Hide "no trigger words" message if it exists
|
||||
const noTriggerWordsMsg = triggerWordsSection.querySelector('.no-trigger-words');
|
||||
if (noTriggerWordsMsg) {
|
||||
noTriggerWordsMsg.style.display = 'none';
|
||||
}
|
||||
|
||||
// Validation: Check length
|
||||
if (word.split(/\s+/).length > 30) {
|
||||
showToast('Trigger word should not exceed 30 words', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: Check total number
|
||||
const currentTags = tagsContainer.querySelectorAll('.trigger-word-tag');
|
||||
if (currentTags.length >= 10) {
|
||||
showToast('Maximum 10 trigger words allowed', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: Check for duplicates
|
||||
const existingWords = Array.from(currentTags).map(tag => tag.dataset.word);
|
||||
if (existingWords.includes(word)) {
|
||||
showToast('This trigger word already exists', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new tag
|
||||
const newTag = document.createElement('div');
|
||||
newTag.className = 'trigger-word-tag';
|
||||
newTag.dataset.word = word;
|
||||
newTag.innerHTML = `
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-copy" style="display:none;">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
<button class="delete-trigger-word-btn" onclick="event.stopPropagation();">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add event listener to delete button
|
||||
const deleteBtn = newTag.querySelector('.delete-trigger-word-btn');
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
newTag.remove();
|
||||
});
|
||||
|
||||
tagsContainer.appendChild(newTag);
|
||||
|
||||
// Clear and hide the input form
|
||||
const triggerWordInput = document.querySelector('.new-trigger-word-input');
|
||||
triggerWordInput.value = '';
|
||||
document.querySelector('.add-trigger-word-form').style.display = 'none';
|
||||
}
|
||||
|
||||
// Function to save updated trigger words
|
||||
async function saveTriggerWords() {
|
||||
const filePath = document.querySelector('.edit-trigger-words-btn').dataset.filePath;
|
||||
const triggerWordTags = document.querySelectorAll('.trigger-word-tag');
|
||||
const words = Array.from(triggerWordTags).map(tag => tag.dataset.word);
|
||||
|
||||
try {
|
||||
// Special format for updating nested civitai.trainedWords
|
||||
await saveModelMetadata(filePath, {
|
||||
civitai: { trainedWords: words }
|
||||
});
|
||||
|
||||
// Update UI
|
||||
const editBtn = document.querySelector('.edit-trigger-words-btn');
|
||||
editBtn.click(); // Exit edit mode
|
||||
|
||||
// Update the LoRA card's dataset
|
||||
const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
if (loraCard && loraCard.dataset.civitai) {
|
||||
const civitaiData = JSON.parse(loraCard.dataset.civitai);
|
||||
civitaiData.trainedWords = words;
|
||||
loraCard.dataset.civitai = JSON.stringify(civitaiData);
|
||||
}
|
||||
|
||||
// If we saved an empty array and there's a no-trigger-words element, show it
|
||||
const noTriggerWords = document.querySelector('.no-trigger-words');
|
||||
const tagsContainer = document.querySelector('.trigger-words-tags');
|
||||
if (words.length === 0 && noTriggerWords) {
|
||||
noTriggerWords.style.display = '';
|
||||
if (tagsContainer) tagsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
showToast('Trigger words updated successfully', 'success');
|
||||
} catch (error) {
|
||||
showToast('Failed to update trigger words', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Add copy trigger word function
|
||||
window.copyTriggerWord = async function(word) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(word);
|
||||
showToast('Trigger word copied', 'success');
|
||||
} catch (err) {
|
||||
console.error('Copy failed:', err);
|
||||
showToast('Copy failed', 'error');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user