mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
cleanup
This commit is contained in:
@@ -7,8 +7,6 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"test": "vitest",
|
|
||||||
"test:run": "vitest run",
|
|
||||||
"lint": "biome check src/",
|
"lint": "biome check src/",
|
||||||
"lint:fix": "biome check src/ --write",
|
"lint:fix": "biome check src/ --write",
|
||||||
"format": "biome format src/ --write",
|
"format": "biome format src/ --write",
|
||||||
@@ -76,9 +74,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@testing-library/jest-dom": "^6.4.0",
|
|
||||||
"@testing-library/react": "^15.0.7",
|
|
||||||
"@testing-library/user-event": "^14.5.2",
|
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/pg": "^8.15.4",
|
"@types/pg": "^8.15.4",
|
||||||
@@ -86,11 +81,9 @@
|
|||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"jsdom": "^26.0.0",
|
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.7.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3"
|
||||||
"vitest": "^2.1.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react'
|
|
||||||
import { Timeline } from '../index'
|
|
||||||
|
|
||||||
// Mock the stores and hooks
|
|
||||||
vi.mock('@/stores/timeline-store', () => ({
|
|
||||||
useTimelineStore: () => ({
|
|
||||||
tracks: [],
|
|
||||||
addTrack: vi.fn(),
|
|
||||||
addElementToTrack: vi.fn(),
|
|
||||||
removeElementFromTrack: vi.fn(),
|
|
||||||
removeElementFromTrackWithRipple: vi.fn(),
|
|
||||||
selectedElements: [],
|
|
||||||
clearSelectedElements: vi.fn(),
|
|
||||||
splitElement: vi.fn(),
|
|
||||||
splitAndKeepLeft: vi.fn(),
|
|
||||||
splitAndKeepRight: vi.fn(),
|
|
||||||
separateAudio: vi.fn(),
|
|
||||||
snappingEnabled: false,
|
|
||||||
toggleSnapping: vi.fn(),
|
|
||||||
deleteElements: vi.fn(),
|
|
||||||
duplicateElement: vi.fn(),
|
|
||||||
selectAll: vi.fn(),
|
|
||||||
zoomLevel: 1,
|
|
||||||
setZoomLevel: vi.fn(),
|
|
||||||
dragState: { isDragging: false, draggedElement: null, dragOffset: { x: 0, y: 0 } },
|
|
||||||
getTotalDuration: () => 100,
|
|
||||||
setSelectedElements: vi.fn(),
|
|
||||||
toggleTrackMute: vi.fn(),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/stores/media-store', () => ({
|
|
||||||
useMediaStore: () => ({
|
|
||||||
mediaItems: [],
|
|
||||||
addMediaItem: vi.fn(),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/stores/project-store', () => ({
|
|
||||||
useProjectStore: () => ({
|
|
||||||
activeProject: null,
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockSeek = vi.fn()
|
|
||||||
|
|
||||||
vi.mock('@/stores/playback-store', () => ({
|
|
||||||
usePlaybackStore: () => ({
|
|
||||||
currentTime: 0,
|
|
||||||
duration: 100,
|
|
||||||
seek: mockSeek,
|
|
||||||
setDuration: vi.fn(),
|
|
||||||
isPlaying: false,
|
|
||||||
toggle: vi.fn(),
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock the timeline playhead hook
|
|
||||||
vi.mock('@/hooks/use-timeline-playhead', () => ({
|
|
||||||
useTimelinePlayhead: () => ({
|
|
||||||
playheadRef: { current: null },
|
|
||||||
updatePlayheadPosition: vi.fn(),
|
|
||||||
onPlayheadDrag: vi.fn(),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-editor-actions', () => ({
|
|
||||||
useEditorActions: () => ({})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-timeline-context-menu', () => ({
|
|
||||||
useTimelineContextMenu: () => ({
|
|
||||||
contextMenu: null,
|
|
||||||
handleContextMenu: vi.fn(),
|
|
||||||
closeContextMenu: vi.fn(),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-timeline-selection', () => ({
|
|
||||||
useTimelineSelection: () => ({
|
|
||||||
isSelecting: false,
|
|
||||||
selectionBox: null,
|
|
||||||
handleMouseDown: vi.fn(),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-timeline-drag-drop', () => ({
|
|
||||||
useTimelineDragDrop: () => ({
|
|
||||||
dragProps: {},
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock UI components to avoid dependency issues
|
|
||||||
vi.mock('../../ui/tooltip', () => ({
|
|
||||||
Tooltip: ({ children }: any) => children,
|
|
||||||
TooltipTrigger: ({ children }: any) => children,
|
|
||||||
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
|
||||||
TooltipProvider: ({ children }: any) => children,
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('../../ui/button', () => ({
|
|
||||||
Button: ({ children, onClick, ...props }: any) => (
|
|
||||||
<button onClick={onClick} {...props} aria-label={props['aria-label']}>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('../../ui/scroll-area', () => ({
|
|
||||||
ScrollArea: ({ children }: any) => <div>{children}</div>,
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('lucide-react', () => ({
|
|
||||||
SkipBack: () => <svg data-testid="skip-back-icon" />,
|
|
||||||
Play: () => <svg data-testid="play-icon" />,
|
|
||||||
Pause: () => <svg data-testid="pause-icon" />,
|
|
||||||
Scissors: () => <svg />,
|
|
||||||
ArrowLeftToLine: () => <svg />,
|
|
||||||
ArrowRightToLine: () => <svg />,
|
|
||||||
Trash2: () => <svg />,
|
|
||||||
Snowflake: () => <svg />,
|
|
||||||
Copy: () => <svg />,
|
|
||||||
SplitSquareHorizontal: () => <svg />,
|
|
||||||
Video: () => <svg />,
|
|
||||||
Music: () => <svg />,
|
|
||||||
TypeIcon: () => <svg />,
|
|
||||||
Magnet: () => <svg />,
|
|
||||||
Link: () => <svg />,
|
|
||||||
ZoomIn: () => <svg />,
|
|
||||||
ZoomOut: () => <svg />,
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/constants/timeline', () => ({
|
|
||||||
TIMELINE_CONSTANTS: {
|
|
||||||
DEFAULT_TEXT_DURATION: 5,
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/lib/utils', () => ({
|
|
||||||
cn: (...args: any[]) => args.join(' '),
|
|
||||||
snapTimeToFrame: (time: number) => time,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock timeline utilities
|
|
||||||
vi.mock('@/lib/timeline-utils', () => ({
|
|
||||||
getTotalDuration: () => 100,
|
|
||||||
getCurrentSnapPoint: () => null,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock context menu components
|
|
||||||
vi.mock('../../ui/context-menu', () => ({
|
|
||||||
ContextMenu: ({ children }: any) => children,
|
|
||||||
ContextMenuTrigger: ({ children }: any) => children,
|
|
||||||
ContextMenuContent: ({ children }: any) => <div>{children}</div>,
|
|
||||||
ContextMenuItem: ({ children }: any) => <div>{children}</div>,
|
|
||||||
ContextMenuSeparator: () => <div />,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock dialog components
|
|
||||||
vi.mock('../../ui/dialog', () => ({
|
|
||||||
Dialog: ({ children }: any) => children,
|
|
||||||
DialogContent: ({ children }: any) => <div>{children}</div>,
|
|
||||||
DialogHeader: ({ children }: any) => <div>{children}</div>,
|
|
||||||
DialogTitle: ({ children }: any) => <div>{children}</div>,
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('Timeline - Return to Start Button', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockSeek.mockClear()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render Return to Start button', () => {
|
|
||||||
render(<Timeline />)
|
|
||||||
|
|
||||||
// Look for the SkipBack icon which indicates our button
|
|
||||||
const skipBackIcon = screen.getByTestId('skip-back-icon')
|
|
||||||
expect(skipBackIcon).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call seek(0) when Return to Start button is clicked', () => {
|
|
||||||
render(<Timeline />)
|
|
||||||
|
|
||||||
// Find the button that contains the SkipBack icon
|
|
||||||
const skipBackIcon = screen.getByTestId('skip-back-icon')
|
|
||||||
const button = skipBackIcon.closest('button')
|
|
||||||
|
|
||||||
expect(button).toBeInTheDocument()
|
|
||||||
fireEvent.click(button!)
|
|
||||||
|
|
||||||
expect(mockSeek).toHaveBeenCalledWith(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have tooltip structure for Return to Start button', () => {
|
|
||||||
render(<Timeline />)
|
|
||||||
|
|
||||||
// Find the skip back button and verify it's wrapped in tooltip components
|
|
||||||
const skipBackIcon = screen.getByTestId('skip-back-icon')
|
|
||||||
const button = skipBackIcon.closest('button')
|
|
||||||
|
|
||||||
expect(button).toBeInTheDocument()
|
|
||||||
// The button should exist and be clickable, which means the tooltip structure is in place
|
|
||||||
expect(button).toHaveAttribute('data-state', 'closed') // Tooltip trigger state
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have SkipBack icon in Return to Start button', () => {
|
|
||||||
render(<Timeline />)
|
|
||||||
|
|
||||||
const skipBackIcon = screen.getByTestId('skip-back-icon')
|
|
||||||
expect(skipBackIcon).toBeInTheDocument()
|
|
||||||
|
|
||||||
// Verify it's inside a button
|
|
||||||
const button = skipBackIcon.closest('button')
|
|
||||||
expect(button).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Button } from "../../ui/button";
|
import { Button } from "../../ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -609,7 +608,11 @@ export function Timeline() {
|
|||||||
onMouseEnter={() => setIsInTimeline(true)}
|
onMouseEnter={() => setIsInTimeline(true)}
|
||||||
onMouseLeave={() => setIsInTimeline(false)}
|
onMouseLeave={() => setIsInTimeline(false)}
|
||||||
>
|
>
|
||||||
<TimelineToolbar zoomLevel={zoomLevel} setZoomLevel={setZoomLevel} seek={seek} />
|
<TimelineToolbar
|
||||||
|
zoomLevel={zoomLevel}
|
||||||
|
setZoomLevel={setZoomLevel}
|
||||||
|
seek={seek}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Timeline Container */}
|
{/* Timeline Container */}
|
||||||
<div
|
<div
|
||||||
@@ -959,7 +962,6 @@ function TimelineToolbar({
|
|||||||
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
|
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
|
||||||
const { toggleBookmark, isBookmarked } = useProjectStore();
|
const { toggleBookmark, isBookmarked } = useProjectStore();
|
||||||
|
|
||||||
// Action handlers
|
|
||||||
const handleSplitSelected = () => {
|
const handleSplitSelected = () => {
|
||||||
if (selectedElements.length === 0) return;
|
if (selectedElements.length === 0) return;
|
||||||
let splitCount = 0;
|
let splitCount = 0;
|
||||||
@@ -1075,7 +1077,6 @@ function TimelineToolbar({
|
|||||||
clearSelectedElements();
|
clearSelectedElements();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Zoom handlers
|
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
setZoomLevel(Math.min(4, zoomLevel + 0.25));
|
setZoomLevel(Math.min(4, zoomLevel + 0.25));
|
||||||
};
|
};
|
||||||
@@ -1092,13 +1093,11 @@ function TimelineToolbar({
|
|||||||
await toggleBookmark(currentTime);
|
await toggleBookmark(currentTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the current time is bookmarked
|
|
||||||
const currentBookmarked = isBookmarked(currentTime);
|
const currentBookmarked = isBookmarked(currentTime);
|
||||||
return (
|
return (
|
||||||
<div className="border-b flex items-center justify-between px-2 py-1">
|
<div className="border-b flex items-center justify-between px-2 py-1">
|
||||||
<div className="flex items-center gap-1 w-full">
|
<div className="flex items-center gap-1 w-full">
|
||||||
<TooltipProvider delayDuration={500}>
|
<TooltipProvider delayDuration={500}>
|
||||||
{/* Play/Pause Button */}
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -1118,31 +1117,26 @@ function TimelineToolbar({
|
|||||||
{isPlaying ? "Pause (Space)" : "Play (Space)"}
|
{isPlaying ? "Pause (Space)" : "Play (Space)"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* Return to Start Button - Seeks timeline to 00:00:00 */}
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => seek(0)} // Seek to timeline start position
|
onClick={() => seek(0)}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
>
|
>
|
||||||
<SkipBack className="h-4 w-4" />
|
<SkipBack className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>Return to Start (Home / Enter)</TooltipContent>
|
||||||
Return to Start (Home / Enter)
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
{/* Time Display */}
|
|
||||||
<div
|
<div
|
||||||
className="text-xs text-muted-foreground font-mono px-2"
|
className="text-xs text-muted-foreground font-mono px-2"
|
||||||
style={{ minWidth: "18ch", textAlign: "center" }}
|
style={{ minWidth: "18ch", textAlign: "center" }}
|
||||||
>
|
>
|
||||||
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
||||||
</div>
|
</div>
|
||||||
{/* Test Clip Button - for debugging */}
|
|
||||||
{tracks.length === 0 && (
|
{tracks.length === 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useRef, useState, useEffect } from "react";
|
import { useRef, useState, useEffect } from "react";
|
||||||
import { TimelineTrack } from "@/types/timeline";
|
import { TimelineTrack } from "@/types/timeline";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead";
|
import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead";
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
|
||||||
import { renderHook } from '@testing-library/react'
|
|
||||||
import { useKeyboardShortcutsHelp } from '../use-keyboard-shortcuts-help'
|
|
||||||
import { useKeybindingsStore } from '../../stores/keybindings-store'
|
|
||||||
|
|
||||||
describe('useKeyboardShortcutsHelp', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset store to default state before each test
|
|
||||||
useKeybindingsStore.getState().resetToDefaults()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should format Enter key correctly in shortcuts', () => {
|
|
||||||
const { result } = renderHook(() => useKeyboardShortcutsHelp())
|
|
||||||
|
|
||||||
// Find the goto-start action in shortcuts
|
|
||||||
const gotoStartShortcut = result.current.shortcuts.find(
|
|
||||||
shortcut => shortcut.action === 'goto-start'
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(gotoStartShortcut).toBeDefined()
|
|
||||||
expect(gotoStartShortcut?.keys).toContain('Enter')
|
|
||||||
expect(gotoStartShortcut?.keys).toContain('Home')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include goto-start action with correct description', () => {
|
|
||||||
const { result } = renderHook(() => useKeyboardShortcutsHelp())
|
|
||||||
|
|
||||||
const gotoStartShortcut = result.current.shortcuts.find(
|
|
||||||
shortcut => shortcut.action === 'goto-start'
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(gotoStartShortcut).toBeDefined()
|
|
||||||
expect(gotoStartShortcut?.description).toBe('Go to timeline start')
|
|
||||||
expect(gotoStartShortcut?.category).toBe('Navigation')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have multiple keys for goto-start action', () => {
|
|
||||||
const { result } = renderHook(() => useKeyboardShortcutsHelp())
|
|
||||||
|
|
||||||
const gotoStartShortcut = result.current.shortcuts.find(
|
|
||||||
shortcut => shortcut.action === 'goto-start'
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(gotoStartShortcut?.keys.length).toBeGreaterThanOrEqual(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -77,7 +77,7 @@ const formatKey = (key: string): string => {
|
|||||||
.replace("down", "↓")
|
.replace("down", "↓")
|
||||||
.replace("space", "Space")
|
.replace("space", "Space")
|
||||||
.replace("home", "Home")
|
.replace("home", "Home")
|
||||||
.replace("enter", "Enter") // Format Enter key for display in help dialog
|
.replace("enter", "Enter")
|
||||||
.replace("end", "End")
|
.replace("end", "End")
|
||||||
.replace("delete", "Delete")
|
.replace("delete", "Delete")
|
||||||
.replace("backspace", "Backspace")
|
.replace("backspace", "Backspace")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const defaultKeybindings: KeybindingConfig = {
|
|||||||
"shift+left": "jump-backward",
|
"shift+left": "jump-backward",
|
||||||
"shift+right": "jump-forward",
|
"shift+right": "jump-forward",
|
||||||
home: "goto-start",
|
home: "goto-start",
|
||||||
enter: "goto-start", // More intuitive alternative to Home key for timeline navigation
|
enter: "goto-start",
|
||||||
end: "goto-end",
|
end: "goto-end",
|
||||||
s: "split-element",
|
s: "split-element",
|
||||||
n: "toggle-snapping",
|
n: "toggle-snapping",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import '@testing-library/jest-dom'
|
|
||||||
|
|
||||||
// Mock ResizeObserver
|
|
||||||
global.ResizeObserver = class ResizeObserver {
|
|
||||||
observe() {}
|
|
||||||
unobserve() {}
|
|
||||||
disconnect() {}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config'
|
|
||||||
import { resolve } from 'path'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
|
||||||
globals: true,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, './src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user