mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +00:00
@@ -0,0 +1,45 @@
|
|||||||
|
@use '../../../theme/v2Styles' as *;
|
||||||
|
@use '../../../theme/ontimeColours' as *;
|
||||||
|
|
||||||
|
.drawerContent {
|
||||||
|
background-color: $gray-1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawerHeader {
|
||||||
|
@extend .drawerContent;
|
||||||
|
color: $section-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawerFooter {
|
||||||
|
@extend .drawerContent;
|
||||||
|
display: flex;
|
||||||
|
gap: $element-spacing;
|
||||||
|
|
||||||
|
button[type='submit'] {
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnSection {
|
||||||
|
display: flex;
|
||||||
|
padding: $element-spacing;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $element-inner-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
|
display: block;
|
||||||
|
color: $modal-note-color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { FormEvent, useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Drawer,
|
||||||
|
DrawerBody,
|
||||||
|
DrawerCloseButton,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import EditFormInput from './EditFormInput';
|
||||||
|
import { ParamField } from './types';
|
||||||
|
|
||||||
|
import style from './EditFormDrawer.module.scss';
|
||||||
|
|
||||||
|
interface EditFormDrawerProps {
|
||||||
|
paramFields: ParamField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditFormDrawer({ paramFields }: EditFormDrawerProps) {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isEditing = searchParams.get('edit');
|
||||||
|
|
||||||
|
if (isEditing === 'true') {
|
||||||
|
return onOpen();
|
||||||
|
}
|
||||||
|
}, [searchParams, onOpen]);
|
||||||
|
|
||||||
|
const onEditDrawerClose = () => {
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
searchParams.delete('edit');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||||
|
formEvent.preventDefault();
|
||||||
|
|
||||||
|
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||||
|
const newSearchParams = Object.entries(newParamsObject).reduce((newSearchParams, [id, value]) => {
|
||||||
|
if (typeof value === 'string' && value.length) {
|
||||||
|
newSearchParams.set(id, value);
|
||||||
|
|
||||||
|
return newSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSearchParams;
|
||||||
|
}, new URLSearchParams());
|
||||||
|
|
||||||
|
onEditDrawerClose();
|
||||||
|
setSearchParams(newSearchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer isOpen={isOpen} placement='right' onClose={onEditDrawerClose} size='lg'>
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerHeader className={style.drawerHeader}>
|
||||||
|
<DrawerCloseButton _hover={{ bg: '#ebedf0', color: '#333' }} size='lg' />
|
||||||
|
Customise
|
||||||
|
</DrawerHeader>
|
||||||
|
|
||||||
|
<DrawerBody className={style.drawerContent}>
|
||||||
|
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
|
||||||
|
{paramFields.map((field) => (
|
||||||
|
<div key={field.title} className={style.columnSection}>
|
||||||
|
<label className={style.label}>
|
||||||
|
<span className={style.title}>{field.title}</span>
|
||||||
|
<span className={style.description}>{field.description}</span>
|
||||||
|
<EditFormInput key={field.title} paramField={field} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
</DrawerBody>
|
||||||
|
|
||||||
|
<DrawerFooter className={style.drawerFooter}>
|
||||||
|
<Button variant='ontime-subtle' onClick={onEditDrawerClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Input, Select, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||||
|
|
||||||
|
import { ParamField } from './types';
|
||||||
|
|
||||||
|
interface EditFormInputProps {
|
||||||
|
paramField: ParamField;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditFormInput({ paramField }: EditFormInputProps) {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { id, type } = paramField;
|
||||||
|
|
||||||
|
if (type === 'option') {
|
||||||
|
const optionFromParams = searchParams.get(id);
|
||||||
|
const defaultOptionValue = paramField.values.find((value) => value === optionFromParams);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select placeholder='Select an option' variant='ontime' name={id} defaultValue={defaultOptionValue}>
|
||||||
|
{paramField.values.map((value) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'boolean') {
|
||||||
|
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) ?? false;
|
||||||
|
|
||||||
|
// checked value should be 'true', so it can be captured by the form event
|
||||||
|
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
const defaultNumberValue = searchParams.get(id) ?? '';
|
||||||
|
|
||||||
|
return <Input type='number' variant='ontime-filled' name={id} defaultValue={defaultNumberValue} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultStringValue = searchParams.get(id) ?? '';
|
||||||
|
|
||||||
|
return <Input variant='ontime-filled' name={id} defaultValue={defaultStringValue} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { ParamField } from './types';
|
||||||
|
|
||||||
|
export const TIME_FORMAT_OPTION: ParamField = {
|
||||||
|
id: 'format',
|
||||||
|
title: '12 / 24 hour timer',
|
||||||
|
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
||||||
|
type: 'option',
|
||||||
|
values: ['12', '24'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CLOCK_OPTIONS: ParamField[] = [
|
||||||
|
{
|
||||||
|
id: 'key',
|
||||||
|
title: 'Key',
|
||||||
|
description: 'Background colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'text',
|
||||||
|
title: 'Text Colour',
|
||||||
|
description: 'Text colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'textbg',
|
||||||
|
title: 'Text Background',
|
||||||
|
description: 'Colour of text background in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'font',
|
||||||
|
title: 'Font',
|
||||||
|
description: 'Font family, will use the fonts available in the system',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'size',
|
||||||
|
title: 'Text Size',
|
||||||
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alignx',
|
||||||
|
title: 'Align Horizontal',
|
||||||
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
|
type: 'option',
|
||||||
|
values: ['start', 'center', 'end'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'offsetx',
|
||||||
|
title: 'Offset Horizontal',
|
||||||
|
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aligny',
|
||||||
|
title: 'Align Vertical',
|
||||||
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
|
type: 'option',
|
||||||
|
values: ['start', 'center', 'end'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'offsety',
|
||||||
|
title: 'Offset Vertical',
|
||||||
|
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hidenav',
|
||||||
|
title: 'Hide Nav',
|
||||||
|
description: 'Whether to hide the nav logo in the right corner',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
TIME_FORMAT_OPTION,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TIMER_OPTIONS: ParamField[] = [
|
||||||
|
{
|
||||||
|
id: 'progress',
|
||||||
|
title: 'Progress Bar',
|
||||||
|
description: 'Whether bar counts up or down',
|
||||||
|
type: 'option',
|
||||||
|
values: ['up', 'down'],
|
||||||
|
},
|
||||||
|
TIME_FORMAT_OPTION,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||||
|
{
|
||||||
|
id: 'key',
|
||||||
|
title: 'Key',
|
||||||
|
description: 'Background colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'text',
|
||||||
|
title: 'Text Colour',
|
||||||
|
description: 'Text colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'textbg',
|
||||||
|
title: 'Text Background',
|
||||||
|
description: 'Colour of text background in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'font',
|
||||||
|
title: 'Font',
|
||||||
|
description: 'Font family, will use the fonts available in the system',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'size',
|
||||||
|
title: 'Text Size',
|
||||||
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alignx',
|
||||||
|
title: 'Align Horizontal',
|
||||||
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
|
type: 'option',
|
||||||
|
values: ['start', 'center', 'end'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'offsetx',
|
||||||
|
title: 'Offset Horizontal',
|
||||||
|
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aligny',
|
||||||
|
title: 'Align Vertical',
|
||||||
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
|
type: 'option',
|
||||||
|
values: ['start', 'center', 'end'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'offsety',
|
||||||
|
title: 'Offset Vertical',
|
||||||
|
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hidenav',
|
||||||
|
title: 'Hide Nav',
|
||||||
|
description: 'Whether to hide the nav logo in the right corner',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideovertime',
|
||||||
|
title: 'Hide Overtime',
|
||||||
|
description: 'Whether to supress overtime styles (red borders and red text)',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hidemessages',
|
||||||
|
title: 'Hide Message Overlay',
|
||||||
|
description: 'Whether to hide the overlay from showing timer screen messages',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
||||||
|
{
|
||||||
|
id: 'preset',
|
||||||
|
title: 'Preset',
|
||||||
|
description: 'Selects a style preset',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'size',
|
||||||
|
title: 'Size',
|
||||||
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transition',
|
||||||
|
title: 'Transition',
|
||||||
|
description: 'Transition in time in seconds (default 5)',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'text',
|
||||||
|
title: 'Text',
|
||||||
|
description: 'Text colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bg',
|
||||||
|
title: 'BG',
|
||||||
|
description: 'Text background colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'key',
|
||||||
|
title: 'Key',
|
||||||
|
description: 'Screen background colour in hexadecimal',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fadeout',
|
||||||
|
title: 'Fadeout',
|
||||||
|
description: 'Time (in seconds) the lower third displays before fading out',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
TIME_FORMAT_OPTION,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
||||||
|
{
|
||||||
|
id: 'seconds',
|
||||||
|
title: 'Seconds',
|
||||||
|
description: 'Shows seconds in clock',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
TIME_FORMAT_OPTION,
|
||||||
|
];
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
type BaseField = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OptionsField = { type: 'option'; values: string[] };
|
||||||
|
type StringField = { type: 'string' };
|
||||||
|
type BooleanField = { type: 'boolean' };
|
||||||
|
type NumberField = { type: 'number' };
|
||||||
|
|
||||||
|
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@use "../../../theme/v2Styles" as *;
|
@use '../../../theme/v2Styles' as *;
|
||||||
@use "../../../theme/mixins" as *;
|
@use '../../../theme/mixins' as *;
|
||||||
@use "../../../theme/ontimeColours" as *;
|
@use '../../../theme/ontimeColours' as *;
|
||||||
|
|
||||||
$menu-bg: $gray-1200;
|
$menu-bg: $gray-1200;
|
||||||
$menu-hover-bg: $gray-1350;
|
$menu-hover-bg: $gray-1350;
|
||||||
@@ -14,14 +14,26 @@ $button-size: 48px;
|
|||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navButton {
|
.buttonContainer {
|
||||||
z-index: 2;
|
display: flex;
|
||||||
position: absolute;
|
flex-direction: column;
|
||||||
left: 0.5em;
|
row-gap: 1rem;
|
||||||
top: 0.5em;
|
padding: 0.5em;
|
||||||
|
|
||||||
transition-property: opacity;
|
transition-property: opacity;
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: $icon-color;
|
color: $icon-color;
|
||||||
background-color: $button-bg;
|
background-color: $button-bg;
|
||||||
@@ -30,10 +42,11 @@ $button-size: 48px;
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
&.hidden {
|
.navButton {
|
||||||
opacity: 0;
|
@extend .button;
|
||||||
}
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuContainer {
|
.menuContainer {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||||
|
import { IoPencilSharp } from '@react-icons/all-files/io5/IoPencilSharp';
|
||||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
@@ -21,12 +22,13 @@ export default function NavigationMenu() {
|
|||||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||||
const [showButton, setShowButton] = useState(false);
|
const [showButton, setShowButton] = useState(false);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
useClickOutside(menuRef, () => setShowMenu(false));
|
useClickOutside(menuRef, () => setShowMenu(false));
|
||||||
|
|
||||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||||
useKeyDown(toggleMenu, ' ');
|
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let fadeOut: NodeJS.Timeout | null = null;
|
let fadeOut: NodeJS.Timeout | null = null;
|
||||||
@@ -49,63 +51,68 @@ export default function NavigationMenu() {
|
|||||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
||||||
const handleFullscreen = () => toggleFullScreen();
|
const handleFullscreen = () => toggleFullScreen();
|
||||||
const handleMirror = () => toggleMirror();
|
const handleMirror = () => toggleMirror();
|
||||||
|
const showEditFormDrawer = () => {
|
||||||
|
searchParams.append('edit', 'true');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
||||||
<button
|
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
||||||
onClick={toggleMenu}
|
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
||||||
aria-label='toggle menu'
|
<IoApps />
|
||||||
className={`${style.navButton} ${!showButton && !showMenu ? style.hidden : ''}`}
|
</button>
|
||||||
>
|
<button className={style.button} onClick={showEditFormDrawer}>
|
||||||
<IoApps />
|
<IoPencilSharp />
|
||||||
</button>
|
</button>
|
||||||
|
{showMenu && (
|
||||||
{showMenu && (
|
<div className={style.menuContainer} data-testid='navigation-menu'>
|
||||||
<div className={style.menuContainer} data-testid='navigation-menu'>
|
<div className={style.buttonsContainer}>
|
||||||
<div className={style.buttonsContainer}>
|
<div
|
||||||
<div
|
className={style.link}
|
||||||
className={style.link}
|
tabIndex={0}
|
||||||
tabIndex={0}
|
role='button'
|
||||||
role='button'
|
onClick={handleFullscreen}
|
||||||
onClick={handleFullscreen}
|
onKeyDown={(event) => {
|
||||||
onKeyDown={(event) => {
|
isKeyEnter(event) && handleFullscreen();
|
||||||
isKeyEnter(event) && handleFullscreen();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Toggle Fullscreen
|
||||||
Toggle Fullscreen
|
{isFullScreen ? <IoContract /> : <IoExpand />}
|
||||||
{isFullScreen ? <IoContract /> : <IoExpand />}
|
</div>
|
||||||
|
<div
|
||||||
|
className={style.link}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={handleMirror}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && handleMirror();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Flip Screen
|
||||||
|
<IoSwapVertical />
|
||||||
|
</div>
|
||||||
|
{/*<div className={style.link} tabIndex={0}>*/}
|
||||||
|
{/* Rename Client*/}
|
||||||
|
{/*</div>*/}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<hr className={style.separator} />
|
||||||
className={style.link}
|
{navigatorConstants.map((route) => (
|
||||||
tabIndex={0}
|
<Link
|
||||||
role='button'
|
key={route.url}
|
||||||
onClick={handleMirror}
|
to={route.url}
|
||||||
onKeyDown={(event) => {
|
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||||
isKeyEnter(event) && handleMirror();
|
tabIndex={0}
|
||||||
}}
|
>
|
||||||
>
|
{route.label}
|
||||||
Flip Screen
|
<IoArrowUp className={style.linkIcon} />
|
||||||
<IoSwapVertical />
|
</Link>
|
||||||
</div>
|
))}
|
||||||
{/*<div className={style.link} tabIndex={0}>*/}
|
|
||||||
{/* Rename Client*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</div>
|
</div>
|
||||||
<hr className={style.separator} />
|
)}
|
||||||
{navigatorConstants.map((route) => (
|
</div>
|
||||||
<Link
|
|
||||||
key={route.url}
|
|
||||||
to={route.url}
|
|
||||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{route.label}
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export const useKeyDown = (callback: () => void, targetKey: string) => {
|
type UseKeyDown = (callback: () => void, targetKey: string, options?: { isDisabled?: boolean }) => void;
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
|
||||||
const targetKeyPressed = event.key === targetKey && !event.repeat;
|
export const useKeyDown: UseKeyDown = (callback, targetKey, options = {}) => {
|
||||||
if (targetKeyPressed) {
|
const { isDisabled = false } = options;
|
||||||
event.preventDefault();
|
|
||||||
callback();
|
const onKeyDown = useCallback(
|
||||||
}
|
(event: KeyboardEvent) => {
|
||||||
};
|
const targetKeyPressed = event.key === targetKey && !event.repeat;
|
||||||
|
if (targetKeyPressed && isDisabled === false) {
|
||||||
|
event.preventDefault();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[callback, isDisabled, targetKey],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [onKeyDown]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||||||
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { TIME_FORMAT_OPTION } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
@@ -77,7 +79,7 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={[TIME_FORMAT_OPTION]} />
|
||||||
<div className='event-header'>
|
<div className='event-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { CLOCK_OPTIONS } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
@@ -132,6 +134,7 @@ export default function Clock(props: ClockProps) {
|
|||||||
data-testid='clock-view'
|
data-testid='clock-view'
|
||||||
>
|
>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={CLOCK_OPTIONS} />
|
||||||
<div
|
<div
|
||||||
className='clock'
|
className='clock'
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { TIME_FORMAT_OPTION } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
@@ -110,6 +112,7 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={[TIME_FORMAT_OPTION]} />
|
||||||
{follow === null ? (
|
{follow === null ? (
|
||||||
<CountdownSelect events={backstageEvents} />
|
<CountdownSelect events={backstageEvents} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ export default function LowerClean(props) {
|
|||||||
if (!options.fadeOut) return;
|
if (!options.fadeOut) return;
|
||||||
|
|
||||||
// Calculate time
|
// Calculate time
|
||||||
const fadeOutTime =
|
const fadeOutTime = (parseInt(options.fadeOut, 10) + (options.transitionIn || defaults.transitionIn)) * 1000;
|
||||||
(parseInt(options.fadeOut, 10) +
|
|
||||||
(options.transitionIn || defaults.transitionIn)) *
|
|
||||||
1000;
|
|
||||||
if (isNaN(fadeOutTime)) return;
|
if (isNaN(fadeOutTime)) return;
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
@@ -86,8 +83,7 @@ export default function LowerClean(props) {
|
|||||||
fontSize: `${sizeMultiplier}vh`,
|
fontSize: `${sizeMultiplier}vh`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<NavigationMenu isEditBtnHidden />
|
||||||
<NavigationMenu />
|
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showLower && (
|
{showLower && (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
import { LOWER_THIRDS_OPTIONS } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
|
|
||||||
import './LowerLines.scss';
|
import './LowerLines.scss';
|
||||||
@@ -20,10 +22,7 @@ export default function LowerLines(props) {
|
|||||||
if (!options.fadeOut) return;
|
if (!options.fadeOut) return;
|
||||||
|
|
||||||
// Calculate time
|
// Calculate time
|
||||||
const fadeOutTime =
|
const fadeOutTime = (parseInt(options.fadeOut, 10) + (options.transitionIn || defaults.transitionIn)) * 1000;
|
||||||
(parseInt(options.fadeOut, 10) +
|
|
||||||
(options.transitionIn || defaults.transitionIn)) *
|
|
||||||
1000;
|
|
||||||
if (isNaN(fadeOutTime)) return;
|
if (isNaN(fadeOutTime)) return;
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
@@ -129,8 +128,8 @@ export default function LowerLines(props) {
|
|||||||
fontSize: `${sizeMultiplier}vh`,
|
fontSize: `${sizeMultiplier}vh`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={LOWER_THIRDS_OPTIONS} />
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showLower && (
|
{showLower && (
|
||||||
@@ -142,24 +141,15 @@ export default function LowerLines(props) {
|
|||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div className='title-container' variants={titleContainerVariants}>
|
||||||
className='title-container'
|
|
||||||
variants={titleContainerVariants}
|
|
||||||
>
|
|
||||||
<motion.div className='title' variants={titleVariants}>
|
<motion.div className='title' variants={titleVariants}>
|
||||||
{title.titleNow}
|
{title.titleNow}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<div className='title-decor' />
|
<div className='title-decor' />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div className='subtitle-container' variants={subtitleContainerVariants}>
|
||||||
className='subtitle-container'
|
|
||||||
variants={subtitleContainerVariants}
|
|
||||||
>
|
|
||||||
<div className='sub-decor' />
|
<div className='sub-decor' />
|
||||||
<motion.div
|
<motion.div className='subtitle' variants={subtitleVariants}>
|
||||||
className='subtitle'
|
|
||||||
variants={subtitleVariants}
|
|
||||||
>
|
|
||||||
{title.presenterNow}
|
{title.presenterNow}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { EventData, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
import { EventData, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { MINIMAL_TIMER_OPTIONS } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
@@ -172,6 +174,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
data-testid='minimal-timer'
|
data-testid='minimal-timer'
|
||||||
>
|
>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={MINIMAL_TIMER_OPTIONS} />
|
||||||
{!hideMessagesOverlay && (
|
{!hideMessagesOverlay && (
|
||||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||||
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||||||
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { TIME_FORMAT_OPTION } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
@@ -55,7 +57,7 @@ export default function Public(props: BackstageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={[TIME_FORMAT_OPTION]} />
|
||||||
<div className='event-header'>
|
<div className='event-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { millisToString } from 'ontime-utils';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { STUDIO_CLOCK_OPTIONS } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import useFitText from '../../../common/hooks/useFitText';
|
import useFitText from '../../../common/hooks/useFitText';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -73,6 +75,7 @@ export default function StudioClock(props) {
|
|||||||
return (
|
return (
|
||||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={STUDIO_CLOCK_OPTIONS} />
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||||||
import { EventData, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
import { EventData, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
|
import { TIMER_OPTIONS } from '../../../common/components/edit-form-drawer/constants';
|
||||||
|
import EditFormDrawer from '../../../common/components/edit-form-drawer/EditFormDrawer';
|
||||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
@@ -98,6 +100,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
<EditFormDrawer paramFields={TIMER_OPTIONS} />
|
||||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||||
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user