mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 17:54:14 +00:00
Add topbar dropdown menu (#937)
* Add basic menu * Add workflows/edit to menu bar * Add command store * Fix z-index * Fix beta menu setting switch * nit * Drop to center * Fix command invocation
This commit is contained in:
@@ -138,6 +138,7 @@ test.describe('Color Palette', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', {})
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
})
|
||||
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
|
||||
@@ -85,7 +85,21 @@ test.describe('Menu', () => {
|
||||
const count = await comfyPage.getGraphNodesCount()
|
||||
// Drag the node onto the canvas
|
||||
const canvasSelector = '#graph-canvas'
|
||||
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector)
|
||||
|
||||
// Get the bounding box of the canvas element
|
||||
const canvasBoundingBox = (await comfyPage.page
|
||||
.locator(canvasSelector)
|
||||
.boundingBox())!
|
||||
|
||||
// Calculate the center position of the canvas
|
||||
const targetPosition = {
|
||||
x: canvasBoundingBox.x + canvasBoundingBox.width / 2,
|
||||
y: canvasBoundingBox.y + canvasBoundingBox.height / 2
|
||||
}
|
||||
|
||||
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
|
||||
targetPosition
|
||||
})
|
||||
|
||||
// Verify the node is added to the canvas
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
</head>
|
||||
<body class="litegraph">
|
||||
<body class="litegraph grid">
|
||||
<div id="vue-app"></div>
|
||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
||||
<main class="comfy-user-selection-inner">
|
||||
|
||||
@@ -82,7 +82,8 @@ body {
|
||||
grid-column: 1/-1;
|
||||
/* Position at the first row */
|
||||
grid-row: 1;
|
||||
z-index: 10;
|
||||
/* Top menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
:disabled="!executingPrompt"
|
||||
@click="actions.interrupt"
|
||||
@click="commandStore.getCommand('Comfy.Interrupt')"
|
||||
></Button>
|
||||
|
||||
<ButtonGroup>
|
||||
@@ -73,25 +73,19 @@
|
||||
v-tooltip.bottom="$t('menu.refresh')"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
@click="actions.refresh"
|
||||
@click="commandStore.getCommand('Comfy.RefreshNodeDefinitions')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.clipspace')"
|
||||
icon="pi pi-clipboard"
|
||||
severity="secondary"
|
||||
@click="actions.openClipspace"
|
||||
@click="commandStore.getCommand('Comfy.OpenClipspace')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.resetView')"
|
||||
icon="pi pi-expand"
|
||||
severity="secondary"
|
||||
@click="actions.resetView"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.clear')"
|
||||
icon="pi pi-ban"
|
||||
severity="secondary"
|
||||
@click="actions.clearWorkflow"
|
||||
@click="commandStore.getCommand('Comfy.ResetView')"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
@@ -115,12 +109,12 @@ import {
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { batchCount, mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
@@ -147,31 +141,6 @@ const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
const queuePrompt = (e: MouseEvent) => {
|
||||
app.queuePrompt(e.shiftKey ? -1 : 0, batchCount.value)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
interrupt: async () => {
|
||||
await api.interrupt()
|
||||
useToastStore().add({
|
||||
severity: 'info',
|
||||
summary: 'Interrupted',
|
||||
detail: 'Execution has been interrupted',
|
||||
life: 1000
|
||||
})
|
||||
},
|
||||
clearWorkflow: () => {
|
||||
if (
|
||||
!(settingsStore.get('Comfy.ComfirmClear') ?? true) ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
api.dispatchEvent(new CustomEvent('graphCleared'))
|
||||
}
|
||||
},
|
||||
resetView: () => app.resetView(),
|
||||
openClipspace: () => app['openClipspace'](),
|
||||
refresh: () => app.refreshComboInNodes()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,27 +6,27 @@
|
||||
icon="pi pi-th-large"
|
||||
v-tooltip="$t('sideToolbar.browseTemplates')"
|
||||
text
|
||||
@click="browseTemplates"
|
||||
@click="() => commandStore.getCommand('Comfy.BrowseTemplates')()"
|
||||
/>
|
||||
<Button
|
||||
class="browse-workflows-button"
|
||||
icon="pi pi-folder-open"
|
||||
v-tooltip="'Browse for an image or exported workflow'"
|
||||
text
|
||||
@click="browse"
|
||||
@click="() => commandStore.getCommand('Comfy.OpenWorkflow')()"
|
||||
/>
|
||||
<Button
|
||||
class="new-default-workflow-button"
|
||||
icon="pi pi-code"
|
||||
v-tooltip="'Load default workflow'"
|
||||
text
|
||||
@click="loadDefault"
|
||||
@click="() => commandStore.getCommand('Comfy.LoadDefaultWorkflow')()"
|
||||
/>
|
||||
<Button
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
v-tooltip="'Create a new blank workflow'"
|
||||
@click="createBlank"
|
||||
@click="() => commandStore.getCommand('Comfy.NewBlankWorkflow')()"
|
||||
text
|
||||
/>
|
||||
</template>
|
||||
@@ -114,12 +114,12 @@ import TextDivider from '@/components/common/TextDivider.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||
import { showTemplateWorkflowsDialog } from '@/services/dialogService'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const isSearching = computed(() => searchQuery.value.length > 0)
|
||||
@@ -144,25 +144,7 @@ const handleSearch = (query: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const loadDefault = () => {
|
||||
app.loadGraphData()
|
||||
app.resetView()
|
||||
}
|
||||
|
||||
const browse = () => {
|
||||
app.ui.loadFile()
|
||||
}
|
||||
|
||||
const browseTemplates = () => {
|
||||
showTemplateWorkflowsDialog()
|
||||
}
|
||||
|
||||
const createBlank = () => {
|
||||
app.workflowManager.setWorkflow(null)
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.workflowManager.activeWorkflow.track()
|
||||
}
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
58
src/components/topbar/TopMenubar.vue
Normal file
58
src/components/topbar/TopMenubar.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<teleport to=".comfyui-body-top">
|
||||
<div class="top-menubar comfyui-menu" v-if="betaMenuEnabled">
|
||||
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
|
||||
<Menubar
|
||||
:model="items"
|
||||
class="border-none p-0 bg-transparent"
|
||||
:pt="{
|
||||
rootList: 'gap-0'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Menubar from 'primevue/menubar'
|
||||
import { useCoreMenuItemStore } from '@/stores/coreMenuItemStore'
|
||||
import { computed } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
const coreMenuItemsStore = useCoreMenuItemStore()
|
||||
const items = coreMenuItemsStore.menuItems
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfyui-menu {
|
||||
width: 100vw;
|
||||
background: var(--comfy-menu-bg);
|
||||
color: var(--fg-color);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
z-index: 1000;
|
||||
order: 0;
|
||||
grid-column: 1/-1;
|
||||
overflow: auto;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.comfyui-logo {
|
||||
font-size: 1.2em;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.top-menubar .p-menubar-item-link svg {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
16
src/components/topbar/WorkflowTabs.vue
Normal file
16
src/components/topbar/WorkflowTabs.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="workflow-tabs">
|
||||
<SelectButton
|
||||
v-model="workflowStore.activeWorkflow"
|
||||
:options="workflowStore.openWorkflows"
|
||||
aria-labelledby="basic"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
</script>
|
||||
@@ -7,8 +7,8 @@ import { ComfyWorkflow } from './workflows'
|
||||
export class ChangeTracker {
|
||||
static MAX_HISTORY = 50
|
||||
#app: ComfyApp
|
||||
undo = []
|
||||
redo = []
|
||||
undoQueue = []
|
||||
redoQueue = []
|
||||
activeState = null
|
||||
isOurLoad = false
|
||||
workflow: ComfyWorkflow | null
|
||||
@@ -54,12 +54,12 @@ export class ChangeTracker {
|
||||
return
|
||||
}
|
||||
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
||||
this.undo.push(this.activeState)
|
||||
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
|
||||
this.undo.shift()
|
||||
this.undoQueue.push(this.activeState)
|
||||
if (this.undoQueue.length > ChangeTracker.MAX_HISTORY) {
|
||||
this.undoQueue.shift()
|
||||
}
|
||||
this.activeState = clone(currentState)
|
||||
this.redo.length = 0
|
||||
this.redoQueue.length = 0
|
||||
this.workflow.unsaved = true
|
||||
api.dispatchEvent(
|
||||
new CustomEvent('graphChanged', { detail: this.activeState })
|
||||
@@ -80,13 +80,21 @@ export class ChangeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
async undo() {
|
||||
await this.updateState(this.undoQueue, this.redoQueue)
|
||||
}
|
||||
|
||||
async redo() {
|
||||
await this.updateState(this.redoQueue, this.undoQueue)
|
||||
}
|
||||
|
||||
async undoRedo(e) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'y') {
|
||||
this.updateState(this.redo, this.undo)
|
||||
await this.redo()
|
||||
return true
|
||||
} else if (e.key === 'z') {
|
||||
this.updateState(this.undo, this.redo)
|
||||
await this.undo()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -276,4 +284,4 @@ export class ChangeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
const globalTracker = new ChangeTracker({} as ComfyWorkflow)
|
||||
export const globalTracker = new ChangeTracker({} as ComfyWorkflow)
|
||||
|
||||
@@ -122,24 +122,6 @@
|
||||
}
|
||||
|
||||
/* Menu */
|
||||
.comfyui-menu {
|
||||
width: 100vw;
|
||||
background: var(--comfy-menu-bg);
|
||||
color: var(--fg-color);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
z-index: 1000;
|
||||
order: 0;
|
||||
grid-column: 1/-1;
|
||||
overflow: auto;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.comfyui-menu>* {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -183,13 +165,6 @@
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.comfyui-logo {
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/** Send to workflow widget selection dialog */
|
||||
.comfy-widget-selection-dialog {
|
||||
border: none;
|
||||
|
||||
87
src/stores/commandStore.ts
Normal file
87
src/stores/commandStore.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { globalTracker } from '@/scripts/changeTracker'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { showTemplateWorkflowsDialog } from '@/services/dialogService'
|
||||
|
||||
type Command = () => void | Promise<void>
|
||||
|
||||
const getTracker = () =>
|
||||
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
|
||||
|
||||
export const useCommandStore = defineStore('command', () => {
|
||||
const settingStore = useSettingStore()
|
||||
const commands = ref<Record<string, Command>>({
|
||||
'Comfy.NewBlankWorkflow': () => {
|
||||
app.workflowManager.setWorkflow(null)
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.workflowManager.activeWorkflow.track()
|
||||
},
|
||||
'Comfy.OpenWorkflow': () => {
|
||||
app.ui.loadFile()
|
||||
},
|
||||
'Comfy.LoadDefaultWorkflow': async () => {
|
||||
await app.loadGraphData()
|
||||
},
|
||||
'Comfy.SaveWorkflow': () => {
|
||||
app.workflowManager.activeWorkflow.save()
|
||||
},
|
||||
'Comfy.SaveWorkflowAs': () => {
|
||||
app.workflowManager.activeWorkflow.save(true)
|
||||
},
|
||||
'Comfy.ExportWorkflow': () => {
|
||||
app.menu.exportWorkflow('workflow', 'workflow')
|
||||
},
|
||||
'Comfy.ExportWorkflowAPI': () => {
|
||||
app.menu.exportWorkflow('workflow_api', 'output')
|
||||
},
|
||||
'Comfy.Undo': async () => {
|
||||
await getTracker().undo()
|
||||
},
|
||||
'Comfy.Redo': async () => {
|
||||
await getTracker().redo()
|
||||
},
|
||||
'Comfy.ClearWorkflow': () => {
|
||||
if (
|
||||
!settingStore.get('Comfy.ComfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
api.dispatchEvent(new CustomEvent('graphCleared'))
|
||||
}
|
||||
},
|
||||
'Comfy.ResetView': () => {
|
||||
app.resetView()
|
||||
},
|
||||
'Comfy.OpenClipspace': () => {
|
||||
app['openClipspace']?.()
|
||||
},
|
||||
'Comfy.RefreshNodeDefinitions': async () => {
|
||||
await app.refreshComboInNodes()
|
||||
},
|
||||
'Comfy.Interrupt': async () => {
|
||||
await api.interrupt()
|
||||
useToastStore().add({
|
||||
severity: 'info',
|
||||
summary: 'Interrupted',
|
||||
detail: 'Execution has been interrupted',
|
||||
life: 1000
|
||||
})
|
||||
},
|
||||
'Comfy.BrowseTemplates': showTemplateWorkflowsDialog
|
||||
})
|
||||
|
||||
const getCommand = (command: string) => {
|
||||
return commands.value[command] ?? (() => {})
|
||||
}
|
||||
|
||||
return {
|
||||
commands,
|
||||
getCommand
|
||||
}
|
||||
})
|
||||
85
src/stores/coreMenuItemStore.ts
Normal file
85
src/stores/coreMenuItemStore.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { computed } from 'vue'
|
||||
import { useCommandStore } from './commandStore'
|
||||
|
||||
export const useCoreMenuItemStore = defineStore('coreMenuItem', () => {
|
||||
const commandStore = useCommandStore()
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Workflow',
|
||||
items: [
|
||||
{
|
||||
label: 'New',
|
||||
icon: 'pi pi-plus',
|
||||
command: commandStore.commands['Comfy.NewBlankWorkflow']
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Open',
|
||||
icon: 'pi pi-folder-open',
|
||||
command: commandStore.commands['Comfy.OpenWorkflow']
|
||||
},
|
||||
{
|
||||
label: 'Browse Templates',
|
||||
icon: 'pi pi-th-large',
|
||||
command: commandStore.commands['Comfy.BrowseTemplates']
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
icon: 'pi pi-save',
|
||||
command: commandStore.commands['Comfy.SaveWorkflow']
|
||||
},
|
||||
{
|
||||
label: 'Save As',
|
||||
icon: 'pi pi-save',
|
||||
command: commandStore.commands['Comfy.SaveWorkflowAs']
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
icon: 'pi pi-download',
|
||||
command: commandStore.commands['Comfy.ExportWorkflow']
|
||||
},
|
||||
{
|
||||
label: 'Export (API Format)',
|
||||
icon: 'pi pi-download',
|
||||
command: commandStore.commands['Comfy.ExportWorkflowAPI']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
items: [
|
||||
{
|
||||
label: 'Undo',
|
||||
icon: 'pi pi-undo',
|
||||
command: commandStore.commands['Comfy.Undo']
|
||||
},
|
||||
{
|
||||
label: 'Redo',
|
||||
icon: 'pi pi-refresh',
|
||||
command: commandStore.commands['Comfy.Redo']
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Clear Workflow',
|
||||
icon: 'pi pi-trash',
|
||||
command: commandStore.commands['Comfy.ClearWorkflow']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
menuItems
|
||||
}
|
||||
})
|
||||
@@ -4,6 +4,7 @@
|
||||
<UnloadWindowConfirmDialog />
|
||||
<BrowserTabTitle />
|
||||
<AppMenu />
|
||||
<TopMenubar />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -40,6 +41,7 @@ import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDi
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
import AppMenu from '@/components/appMenu/AppMenu.vue'
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import TopMenubar from '@/components/topbar/TopMenubar.vue'
|
||||
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
||||
|
||||
setupAutoQueueHandler()
|
||||
@@ -91,16 +93,10 @@ watchEffect(() => {
|
||||
watchEffect(() => {
|
||||
const useNewMenu = settingStore.get('Comfy.UseNewMenu')
|
||||
if (useNewMenu === 'Disabled') {
|
||||
app.ui.menuContainer.style.removeProperty('display')
|
||||
app.ui.restoreMenuPosition()
|
||||
document.body.style.removeProperty('display')
|
||||
if (app.ui.menuContainer) {
|
||||
app.ui.menuContainer.style.removeProperty('display')
|
||||
}
|
||||
} else {
|
||||
document.body.style.setProperty('display', 'grid')
|
||||
if (app.ui.menuContainer) {
|
||||
app.ui.menuContainer.style.setProperty('display', 'none')
|
||||
}
|
||||
app.ui.menuContainer.style.setProperty('display', 'none')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user