Added Schedule editor

This commit is contained in:
euzu
2025-01-04 12:28:02 +01:00
parent d08a1c398a
commit 5adf2dc920
19 changed files with 169 additions and 38 deletions
+14 -14
View File
@@ -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": {
+2 -2
View File
@@ -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/>
}
+1 -1
View File
@@ -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:
+3 -3
View File
@@ -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 -1
View File
@@ -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 {
+2
View File
@@ -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',
}
+6 -1
View File
@@ -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
View File
@@ -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>,
}