mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-02 05:52:26 +02:00
Added Schedule editor
This commit is contained in:
+14
-14
@@ -20,8 +20,8 @@
|
||||
"dependencies": {
|
||||
"file-saver": "2.0.5",
|
||||
"notistack": "3.0.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-tag-input-component": "2.0.2",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
@@ -29,29 +29,29 @@
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/video.js": "7.3.58",
|
||||
"@typescript-eslint/eslint-plugin": "8.15.0",
|
||||
"@typescript-eslint/parser": "8.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.19.0",
|
||||
"@typescript-eslint/parser": "8.19.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"concurrently": "9.1.0",
|
||||
"concurrently": "9.1.2",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "9.15.0",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "28.9.0",
|
||||
"eslint-plugin-jest": "28.10.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-react": "7.37.2",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"eslint-plugin-react": "7.37.3",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"prettier": "3.4.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "1.81.0",
|
||||
"sass": "1.83.1",
|
||||
"source-map-explorer": "2.5.3",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.7.2",
|
||||
"wait-on": "8.0.1"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -30,9 +30,9 @@ export default function App(props: AppProps) {
|
||||
const [progress, setProgress] = useState<boolean>(false);
|
||||
const [playlist, setPlaylist] = useState<PlaylistGroup[]>([]);
|
||||
const [serverConfig, setServerConfig] = useState<ServerConfig>(undefined);
|
||||
const [preferencesVisible, setPreferencesVisible] = useState<boolean>(false);
|
||||
const [preferencesVisible, setPreferencesVisible] = useState<boolean>(true);
|
||||
const clipboardChannel = useMemo<Subject<string>>(() => new Subject<string>(), []);
|
||||
const viewerRef = useRef<IPlaylistViewer>();
|
||||
const viewerRef = useRef<IPlaylistViewer>(null);
|
||||
const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar();
|
||||
const services = useServices();
|
||||
const videoChannel = useMemo(() => new Subject<PlaylistItem>(), []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {JSX, useEffect, useState} from 'react';
|
||||
import App from "../../app/app";
|
||||
import Login from "../login/login";
|
||||
import {useServices} from "../../provider/service-provider";
|
||||
@@ -31,6 +31,6 @@ export default function Authentication(): JSX.Element {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <>{authenticated ? <App/> : <Login/>}</>
|
||||
return authenticated ? <App/> : <Login/>
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@ interface CheckboxProps {
|
||||
export default function Checkbox(props: CheckboxProps) {
|
||||
|
||||
const {label, value, onSelect, checked} = props;
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSelect = useCallback((evt: any) => {
|
||||
evt.preventDefault();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {JSX, useCallback, useEffect, useState} from 'react';
|
||||
import './clipboard-viewer.scss';
|
||||
import {noop, Observable} from "rxjs";
|
||||
import copyToClipboard from "../../utils/clipboard";
|
||||
|
||||
@@ -4,6 +4,7 @@ import Checkbox from "../checkbox/checkbox";
|
||||
import TagSelect from "../tag-select/tags-select";
|
||||
import MapEditor from "../map-editor/map-editor";
|
||||
import TagInput from "../tag-input/tag-input";
|
||||
import ScheduleEditor from "../schedule-editor/schedule-editor";
|
||||
|
||||
// export const isNumber = (value: string): boolean => {
|
||||
// return !isNaN(value as any);
|
||||
@@ -17,7 +18,8 @@ export enum FormFieldType {
|
||||
SINGLE_SELECT = 'single_select',
|
||||
CHECK = 'checkbox',
|
||||
TAGS = 'tags',
|
||||
MAP = 'map'
|
||||
MAP = 'map',
|
||||
SCHEDULE = 'schedule'
|
||||
}
|
||||
|
||||
export type FormField = {
|
||||
@@ -71,6 +73,8 @@ export default function FormView(props: FormViewProps) {
|
||||
return <div className="form-view__map-editor"><MapEditor onChange={handleChange} name={field.name} values={data?.[field.name]}></MapEditor></div>
|
||||
case FormFieldType.TAGS:
|
||||
return <TagInput placeHolder={''} onChange={handleChange} name={field.name} values={data?.[field.name] || []}></TagInput>
|
||||
case FormFieldType.SCHEDULE:
|
||||
return <ScheduleEditor onChange={handleChange} name={field.name} values={data?.[field.name] || []} sources={data?.sources || []}></ScheduleEditor>
|
||||
case FormFieldType.NUMBER:
|
||||
case FormFieldType.TEXT:
|
||||
default:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import React, {JSX, useCallback, useRef, useState} from 'react';
|
||||
import './login.scss';
|
||||
import {useServices} from "../../provider/service-provider";
|
||||
import {first} from "rxjs/operators";
|
||||
@@ -7,8 +7,8 @@ const checkUserPwd = (username: string, password: string) => username.trim().len
|
||||
|
||||
export default function Login(): JSX.Element {
|
||||
|
||||
const usernameRef = useRef<HTMLInputElement>();
|
||||
const passwordRef = useRef<HTMLInputElement>();
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const services = useServices();
|
||||
const [authorized, setAuthorized] = useState(true);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const CONFIG_FIELDS = [
|
||||
{name: 'threads', label: 'Threads', fieldType: FormFieldType.NUMBER, validator: isNumber},
|
||||
{name: 'working_dir', label: 'Working dir', fieldType: FormFieldType.TEXT, validator: undefined},
|
||||
{name: 'backup_dir', label: 'Backup dir', fieldType: FormFieldType.TEXT, validator: undefined},
|
||||
{name: 'schedule', label: 'Schedule', fieldType: FormFieldType.TEXT, validator: undefined},
|
||||
{name: 'schedules', label: 'Schedules', fieldType: FormFieldType.SCHEDULE, validator: undefined},
|
||||
];
|
||||
|
||||
const CONFIG_MESSAGING_FIELDS = [
|
||||
@@ -87,9 +87,10 @@ export default function MainConfigView(props: MainConfigViewProps) {
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (mainConfig && apiConfig) {
|
||||
let telegramConfigAvailable = telegramConfig?.bot_token?.trim().length && telegramConfig?.chat_ids?.length;
|
||||
const cfgMessaging: MessagingConfig = {
|
||||
notify_on: messagingConfig.notify_on,
|
||||
telegram: telegramConfig
|
||||
telegram: telegramConfigAvailable ? telegramConfig : null,
|
||||
};
|
||||
|
||||
const cfgVideo = {
|
||||
@@ -101,14 +102,12 @@ export default function MainConfigView(props: MainConfigViewProps) {
|
||||
api: apiConfig,
|
||||
working_dir: mainConfig.working_dir,
|
||||
backup_dir: mainConfig.backup_dir,
|
||||
schedule: mainConfig.schedule,
|
||||
schedules: mainConfig.schedules?.filter(s => s.schedule?.trim().length),
|
||||
threads: mainConfig.threads,
|
||||
messaging: cfgMessaging,
|
||||
video: cfgVideo,
|
||||
};
|
||||
|
||||
//enqueueSnackbar("not implemented!", {variant: 'error'})
|
||||
|
||||
services.config().saveMainConfig(cfgMain).subscribe({
|
||||
next: () => enqueueSnackbar("Main config saved!", {variant: 'success'}),
|
||||
error: (err: any) => enqueueSnackbar("Failed to save main config!", {variant: 'error'})
|
||||
|
||||
@@ -22,8 +22,8 @@ interface MapEditorProps {
|
||||
export default function MapEditor(props: MapEditorProps) {
|
||||
const {name, values, onChange} = props;
|
||||
const uuid = useMemo(() => genUuid(), []);
|
||||
const keyRef = useRef<HTMLInputElement>();
|
||||
const valRef = useRef<HTMLInputElement>();
|
||||
const keyRef = useRef<HTMLInputElement>(null);
|
||||
const valRef = useRef<HTMLInputElement>(null);
|
||||
const [data, setData] = useState<KeyValue[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface PlaylistFilterProps {
|
||||
|
||||
export default function PlaylistFilter(props: PlaylistFilterProps) {
|
||||
const {onFilter} = props;
|
||||
const textField = useRef<HTMLInputElement>();
|
||||
const textField = useRef<HTMLInputElement>(null);
|
||||
const [useRegexp, setUseRegexp] = useState<boolean>(false);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useRef} from "react";
|
||||
import React, {JSX, useRef} from "react";
|
||||
import VideoPlayer from "../video-player/video-player";
|
||||
import {PlaylistItem} from "../../model/playlist";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@@ -11,7 +11,7 @@ interface PopupMenuProps {
|
||||
|
||||
export default function PopupMenu(props: PopupMenuProps) {
|
||||
const {position, onHide} = props;
|
||||
const popupRef = useRef();
|
||||
const popupRef = useRef(null);
|
||||
const [popupPosition, setPopupPosition] = useState<any>({top: 0, left: -2000})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
@use '../../scss/common';
|
||||
@use '../../scss/preferences';
|
||||
|
||||
$input-width: 150px;
|
||||
|
||||
.schedule-editor {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
gap: 2px;
|
||||
|
||||
&__schedule {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
height: 1.5rem;
|
||||
|
||||
input {
|
||||
width: $input-width;
|
||||
min-width: $input-width;
|
||||
max-width: $input-width;
|
||||
}
|
||||
@include preferences.prefsInput();
|
||||
|
||||
&-toolbar {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
button {
|
||||
@include common.iconButton();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, {JSX, useCallback, useEffect, useMemo, useState} from "react";
|
||||
import "./schedule-editor.scss";
|
||||
import {Schedule, SourceConfig} from "../../model/server-config";
|
||||
import TagSelect from "../tag-select/tags-select";
|
||||
import {getIconByName} from "../../icons/icons";
|
||||
|
||||
interface ScheduleEditorProps {
|
||||
name: string;
|
||||
values: Schedule[],
|
||||
sources: SourceConfig[];
|
||||
onChange: (name: string, values: Schedule[]) => void;
|
||||
}
|
||||
|
||||
export default function ScheduleEditor(props: ScheduleEditorProps) {
|
||||
const { name, values, sources, onChange} = props;
|
||||
const targets = useMemo(() => sources.flatMap(s => s.targets)
|
||||
.filter(Boolean).flatMap(t => ({value: t.name, label: t.name})), [sources]);
|
||||
const [schedules, setSchedules] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const schedulesCopy = values?.map(s => ({...s})) || [];
|
||||
setSchedules(schedulesCopy);
|
||||
}, [values])
|
||||
|
||||
const handleTargetChange = useCallback((field: string, selected: any):void => {
|
||||
setSchedules(data => {
|
||||
data[field as any].targets = selected;
|
||||
onChange(name, data);
|
||||
return data;
|
||||
});
|
||||
}, [name, onChange]);
|
||||
|
||||
const handleScheduleChange = useCallback((evt: any) => {
|
||||
const field = evt.target.dataset.field;
|
||||
setSchedules(data => {
|
||||
data[field as any].schedule = evt.target.value;
|
||||
onChange(name, data);
|
||||
return data;
|
||||
});
|
||||
|
||||
}, [name, onChange]);
|
||||
|
||||
const handleScheduleRemove = useCallback((evt: any) => {
|
||||
const index = evt.target.dataset.index;
|
||||
setSchedules(data => {
|
||||
data.splice(index, 1);
|
||||
let newData = [...data];
|
||||
onChange(name, newData);
|
||||
return newData;
|
||||
});
|
||||
}, [name, onChange]);
|
||||
|
||||
const handleScheduleAdd = useCallback((evt: any) => {
|
||||
setSchedules(data => {
|
||||
let newData = [...data, {schedule: '', targets: null}];
|
||||
onChange(name, newData);
|
||||
return newData;
|
||||
});
|
||||
}, [name, onChange]);
|
||||
|
||||
let lastIndex = (schedules?.length ?? 0) -1;
|
||||
|
||||
const renderSchedule = useCallback((schedule: Schedule, index: number): JSX.Element => {
|
||||
return <div key={schedule.schedule + index} className="schedule-editor__schedule">
|
||||
<input defaultValue={schedule.schedule} data-field={index} onChange={handleScheduleChange}></input>
|
||||
<TagSelect options={targets} name={index + ''} defaultValues={schedule.targets || []} multi={true}
|
||||
onSelect={handleTargetChange}></TagSelect>
|
||||
<div className={"schedule-editor__schedule-toolbar"}>
|
||||
<button title={'Delete'} data-index={index} onClick={handleScheduleRemove}>{getIconByName('ScheduleRemove')}</button>
|
||||
{index === lastIndex &&
|
||||
<button title={'Add'} onClick={handleScheduleAdd}>{getIconByName('ScheduleAdd')}</button>}
|
||||
</div>
|
||||
</div>
|
||||
}, [targets, handleScheduleChange, handleTargetChange, handleScheduleRemove, handleScheduleAdd, lastIndex]);
|
||||
|
||||
return (
|
||||
<div className="schedule-editor">
|
||||
{schedules?.map(renderSchedule)}
|
||||
{lastIndex < 0 &&
|
||||
<button title={'Add'} onClick={handleScheduleAdd}>{getIconByName('ScheduleAdd')}</button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useState} from "react";
|
||||
import React, {JSX, useCallback, useState} from "react";
|
||||
import './sidebar.scss';
|
||||
import {getIconByName} from "../../icons/icons";
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
|
||||
@@ -13,7 +13,7 @@ interface SourceSelectorProps {
|
||||
}
|
||||
|
||||
export default function SourceSelector(props: SourceSelectorProps) {
|
||||
const textField = useRef<HTMLInputElement>();
|
||||
const textField = useRef<HTMLInputElement>(null);
|
||||
const [popupVisible, setPopupVisible] = useState<{ x: number, y: number }>(undefined);
|
||||
const [sources, setSources] = useState<InputConfig[]>([]);
|
||||
const [selected, setSelected] = useState<InputConfig>(undefined);
|
||||
@@ -23,6 +23,7 @@ export default function SourceSelector(props: SourceSelectorProps) {
|
||||
const handleDownload = useCallback(() => {
|
||||
const value = textField.current.value;
|
||||
if (value && value.trim().length > 0) {
|
||||
// eslint-disable-next-line
|
||||
if (value.trim() == selected?.name) {
|
||||
onDownload({input_id: selected.id});
|
||||
} else {
|
||||
|
||||
@@ -42,6 +42,8 @@ const DEFAULT_ICONS = {
|
||||
Warn: 'M 11.999765,2 2,22 h 20 z m 0,4.1999619 6.845654,13.6948891 H 5.1545815 Z m -0.909027,4.2211761 v 5.263416 h 1.818524 v -5.263416 z m 0,6.31599 v 2.105149 h 1.818524 v -2.105149 z',
|
||||
Gallery: 'M 11.999742,2 C 6.5002947,2 1.9999999,6.5002944 2,11.999742 2,17.499188 6.5002947,22 11.999742,22 17.499187,22 22,17.499188 22,11.999742 22,6.5002944 17.499187,1.9999999 11.999742,2 Z M 7.4796784,5.5010206 H 9.2940081 C 10.454244,5.5013381 11.3944,6.4424302 11.393587,7.6026664 v 1.8143297 c -3.18e-4,1.1594289 -0.94015,2.0992499 -2.0995789,2.0995789 H 7.4796784 C 6.3194422,11.517395 5.3783618,10.577232 5.3780327,9.4169961 V 7.6026664 C 5.3772125,6.4416227 6.3186347,5.5002073 7.4796784,5.5010206 Z m 7.2263136,0 h 1.816396 c 1.160237,3.175e-4 2.100392,0.9414098 2.099579,2.1016458 v 1.8143297 c -3.17e-4,1.1594289 -0.94015,2.0992499 -2.099579,2.0995789 h -1.816396 c -1.159429,-3.18e-4 -2.09925,-0.94015 -2.099579,-2.0995789 V 7.6026664 c -8.2e-4,-1.1602362 0.939342,-2.1013166 2.099579,-2.1016458 z M 7.4796784,12.483425 h 1.8143297 c 1.1594289,3.18e-4 2.0992499,0.94015 2.0995789,2.099579 v 1.816397 c -3.18e-4,1.159428 -0.94015,2.099249 -2.0995789,2.099578 H 7.4796784 C 6.3194422,18.4998 5.3783618,17.559637 5.3780327,16.399401 v -1.816397 c 3.175e-4,-1.160236 0.9414095,-2.100391 2.1016457,-2.099579 z m 7.2263136,0 h 1.816396 c 1.159429,3.18e-4 2.09925,0.94015 2.099579,2.099579 v 1.816397 c -3.17e-4,1.159428 -0.94015,2.099249 -2.099579,2.099578 h -1.816396 c -1.159429,-3.17e-4 -2.09925,-0.94015 -2.099579,-2.099578 v -1.816397 c 3.17e-4,-1.159429 0.94015,-2.09925 2.099579,-2.099579 z',
|
||||
Editor: 'M8 5v14l11-7z',
|
||||
ScheduleAdd: 'm 15.804098,2 v 3.8053896 h -3.804356 v 2.3894789 h 3.804356 v 3.8048735 h 2.391546 V 8.1948685 H 22 V 5.8053896 H 18.195644 V 2 Z M 10.476345,5.1284395 C 5.8079923,5.1284397 1.9999999,8.9173585 2,13.563961 2,18.21056 5.8079923,22 10.476345,22 c 4.668353,0 8.474796,-3.78944 8.474796,-8.436039 0,-0.07367 -0.0022,-0.146917 -0.0041,-0.220138 h -2.087693 c 0.0025,0.07313 0.0052,0.146378 0.0052,0.220138 0,3.52422 -2.847415,6.359714 -6.388135,6.359714 -3.5407204,0 -6.3891688,-2.835494 -6.3891688,-6.359714 0,-3.524221 2.8484484,-6.3581631 6.3891688,-6.3581631 0.17353,0 0.344785,0.00932 0.514689,0.022738 a 1.8901578,1.8901578 0 0 1 -0.0088,-0.1777637 V 5.3651146 a 1.8901578,1.8901578 0 0 1 0.01447,-0.2191045 C 10.824461,5.1354508 10.651328,5.1284395 10.476398,5.1284395 Z M 9.122962,8.7819031 v 6.4460119 h 6.400537 V 13.520037 H 10.838591 V 8.7819031 Z',
|
||||
ScheduleRemove: 'M 11.999742,4.7152991 V 7.1229687 H 22 V 4.7152991 Z M 10.476345,3.5000004 C 5.8079923,3.5000006 1.9999999,7.3177637 2,11.999739 2,16.681712 5.8079923,20.5 10.476345,20.5 c 4.668353,0 8.474796,-3.818288 8.474796,-8.500261 0,-0.07423 -0.0022,-0.148035 -0.0041,-0.221814 h -2.087693 c 0.0025,0.07369 0.0052,0.147493 0.0052,0.221814 0,3.551049 -2.847415,6.408129 -6.388135,6.408129 -3.5407204,0 -6.3891688,-2.85708 -6.3891688,-6.408129 0,-3.5510493 2.8484484,-6.4065657 6.3891688,-6.4065657 0.17353,0 0.344785,0.00939 0.514689,0.022911 -0.0057,-0.059533 -0.0087,-0.1193051 -0.0088,-0.1791169 V 3.7384773 c 5.73e-4,-0.073811 0.0054,-0.1475264 0.01447,-0.2207725 C 10.824461,3.5070654 10.651328,3.5 10.476398,3.5 Z M 9.122962,7.1812771 V 13.67636 h 6.400537 V 11.955481 H 10.838591 V 7.1812771 Z',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -151,12 +151,17 @@ export interface ServerApiConfig {
|
||||
web_root: string
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
schedule: string;
|
||||
targets: string[];
|
||||
}
|
||||
|
||||
export interface ServerMainConfig {
|
||||
api: ServerApiConfig;
|
||||
threads: number;
|
||||
working_dir: string;
|
||||
backup_dir: string;
|
||||
schedule: string;
|
||||
schedules: Schedule[];
|
||||
messaging?: MessagingConfig;
|
||||
video?: VideoConfig;
|
||||
}
|
||||
|
||||
+1
-1
@@ -754,7 +754,7 @@ pub struct ConfigDto {
|
||||
#[serde(default)]
|
||||
pub video: Option<VideoConfig>,
|
||||
#[serde(default)]
|
||||
pub schedule: Option<String>,
|
||||
pub schedules: Option<Vec<ScheduleConfig>>,
|
||||
#[serde(default)]
|
||||
pub messaging: Option<MessagingConfig>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user