From 5adf2dc92089d77ced2d7ae83fe2d13cb5fc71d8 Mon Sep 17 00:00:00 2001 From: euzu Date: Sat, 4 Jan 2025 12:28:02 +0100 Subject: [PATCH] Added Schedule editor --- frontend/package.json | 28 +++---- frontend/src/app/app.tsx | 4 +- .../authentication/authentication.tsx | 4 +- frontend/src/component/checkbox/checkbox.tsx | 2 +- .../clipboard-viewer/clipboard-viewer.tsx | 2 +- .../src/component/form-view/from-view.tsx | 6 +- frontend/src/component/login/login.tsx | 6 +- .../main-config-view/main-config-view.tsx | 9 +- .../src/component/map-editor/map-editor.tsx | 4 +- .../playlist-filter/playlist-filter.tsx | 2 +- .../playlist-video/playlist-video.tsx | 2 +- .../src/component/popup-menu/popup-menu.tsx | 2 +- .../schedule-editor/schedule-editor.scss | 37 +++++++++ .../schedule-editor/schedule-editor.tsx | 83 +++++++++++++++++++ frontend/src/component/sidebar/sidebar.tsx | 2 +- .../source-selector/source-selector.tsx | 3 +- frontend/src/icons/icons.tsx | 2 + frontend/src/model/server-config.ts | 7 +- src/model/config.rs | 2 +- 19 files changed, 169 insertions(+), 38 deletions(-) create mode 100644 frontend/src/component/schedule-editor/schedule-editor.scss create mode 100644 frontend/src/component/schedule-editor/schedule-editor.tsx diff --git a/frontend/package.json b/frontend/package.json index e90cf70e5..045bca42c 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -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": { diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx index fe03a0d24..a5954aacd 100755 --- a/frontend/src/app/app.tsx +++ b/frontend/src/app/app.tsx @@ -30,9 +30,9 @@ export default function App(props: AppProps) { const [progress, setProgress] = useState(false); const [playlist, setPlaylist] = useState([]); const [serverConfig, setServerConfig] = useState(undefined); - const [preferencesVisible, setPreferencesVisible] = useState(false); + const [preferencesVisible, setPreferencesVisible] = useState(true); const clipboardChannel = useMemo>(() => new Subject(), []); - const viewerRef = useRef(); + const viewerRef = useRef(null); const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); const services = useServices(); const videoChannel = useMemo(() => new Subject(), []); diff --git a/frontend/src/component/authentication/authentication.tsx b/frontend/src/component/authentication/authentication.tsx index e0051c26c..c879d7c9e 100644 --- a/frontend/src/component/authentication/authentication.tsx +++ b/frontend/src/component/authentication/authentication.tsx @@ -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 ? : } + return authenticated ? : } \ No newline at end of file diff --git a/frontend/src/component/checkbox/checkbox.tsx b/frontend/src/component/checkbox/checkbox.tsx index c48d24cb1..baefa76fd 100644 --- a/frontend/src/component/checkbox/checkbox.tsx +++ b/frontend/src/component/checkbox/checkbox.tsx @@ -11,7 +11,7 @@ interface CheckboxProps { export default function Checkbox(props: CheckboxProps) { const {label, value, onSelect, checked} = props; - const inputRef = useRef(); + const inputRef = useRef(null); const handleSelect = useCallback((evt: any) => { evt.preventDefault(); diff --git a/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx b/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx index b054a8f08..e5728e98a 100644 --- a/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx +++ b/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx @@ -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"; diff --git a/frontend/src/component/form-view/from-view.tsx b/frontend/src/component/form-view/from-view.tsx index 6c5d3ce9e..e9d350629 100644 --- a/frontend/src/component/form-view/from-view.tsx +++ b/frontend/src/component/form-view/from-view.tsx @@ -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
case FormFieldType.TAGS: return + case FormFieldType.SCHEDULE: + return case FormFieldType.NUMBER: case FormFieldType.TEXT: default: diff --git a/frontend/src/component/login/login.tsx b/frontend/src/component/login/login.tsx index e7a42cce4..02a454470 100644 --- a/frontend/src/component/login/login.tsx +++ b/frontend/src/component/login/login.tsx @@ -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(); - const passwordRef = useRef(); + const usernameRef = useRef(null); + const passwordRef = useRef(null); const services = useServices(); const [authorized, setAuthorized] = useState(true); diff --git a/frontend/src/component/main-config-view/main-config-view.tsx b/frontend/src/component/main-config-view/main-config-view.tsx index d9584ea26..5dc480277 100644 --- a/frontend/src/component/main-config-view/main-config-view.tsx +++ b/frontend/src/component/main-config-view/main-config-view.tsx @@ -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'}) diff --git a/frontend/src/component/map-editor/map-editor.tsx b/frontend/src/component/map-editor/map-editor.tsx index e793a7f60..8c32f8042 100644 --- a/frontend/src/component/map-editor/map-editor.tsx +++ b/frontend/src/component/map-editor/map-editor.tsx @@ -22,8 +22,8 @@ interface MapEditorProps { export default function MapEditor(props: MapEditorProps) { const {name, values, onChange} = props; const uuid = useMemo(() => genUuid(), []); - const keyRef = useRef(); - const valRef = useRef(); + const keyRef = useRef(null); + const valRef = useRef(null); const [data, setData] = useState([]); useEffect(() => { diff --git a/frontend/src/component/playlist-filter/playlist-filter.tsx b/frontend/src/component/playlist-filter/playlist-filter.tsx index a602c11af..d53123670 100755 --- a/frontend/src/component/playlist-filter/playlist-filter.tsx +++ b/frontend/src/component/playlist-filter/playlist-filter.tsx @@ -9,7 +9,7 @@ interface PlaylistFilterProps { export default function PlaylistFilter(props: PlaylistFilterProps) { const {onFilter} = props; - const textField = useRef(); + const textField = useRef(null); const [useRegexp, setUseRegexp] = useState(false); const handleSearch = useCallback(() => { diff --git a/frontend/src/component/playlist-video/playlist-video.tsx b/frontend/src/component/playlist-video/playlist-video.tsx index e448eddf1..1630ab547 100644 --- a/frontend/src/component/playlist-video/playlist-video.tsx +++ b/frontend/src/component/playlist-video/playlist-video.tsx @@ -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"; diff --git a/frontend/src/component/popup-menu/popup-menu.tsx b/frontend/src/component/popup-menu/popup-menu.tsx index 599bcb29b..e9adb3718 100644 --- a/frontend/src/component/popup-menu/popup-menu.tsx +++ b/frontend/src/component/popup-menu/popup-menu.tsx @@ -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({top: 0, left: -2000}) useEffect(() => { diff --git a/frontend/src/component/schedule-editor/schedule-editor.scss b/frontend/src/component/schedule-editor/schedule-editor.scss new file mode 100644 index 000000000..c2e698c2c --- /dev/null +++ b/frontend/src/component/schedule-editor/schedule-editor.scss @@ -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(); + } + +} diff --git a/frontend/src/component/schedule-editor/schedule-editor.tsx b/frontend/src/component/schedule-editor/schedule-editor.tsx new file mode 100644 index 000000000..b4e71bbe5 --- /dev/null +++ b/frontend/src/component/schedule-editor/schedule-editor.tsx @@ -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
+ + +
+ + {index === lastIndex && + } +
+
+ }, [targets, handleScheduleChange, handleTargetChange, handleScheduleRemove, handleScheduleAdd, lastIndex]); + + return ( +
+ {schedules?.map(renderSchedule)} + {lastIndex < 0 && + } +
+ ); +} diff --git a/frontend/src/component/sidebar/sidebar.tsx b/frontend/src/component/sidebar/sidebar.tsx index 61eb9561a..fc0bb85ba 100644 --- a/frontend/src/component/sidebar/sidebar.tsx +++ b/frontend/src/component/sidebar/sidebar.tsx @@ -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 */ diff --git a/frontend/src/component/source-selector/source-selector.tsx b/frontend/src/component/source-selector/source-selector.tsx index 246fb5f3f..a31e1d5a8 100755 --- a/frontend/src/component/source-selector/source-selector.tsx +++ b/frontend/src/component/source-selector/source-selector.tsx @@ -13,7 +13,7 @@ interface SourceSelectorProps { } export default function SourceSelector(props: SourceSelectorProps) { - const textField = useRef(); + const textField = useRef(null); const [popupVisible, setPopupVisible] = useState<{ x: number, y: number }>(undefined); const [sources, setSources] = useState([]); const [selected, setSelected] = useState(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 { diff --git a/frontend/src/icons/icons.tsx b/frontend/src/icons/icons.tsx index 99e8d976e..228c4f859 100644 --- a/frontend/src/icons/icons.tsx +++ b/frontend/src/icons/icons.tsx @@ -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', } diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts index 06899428c..960971376 100644 --- a/frontend/src/model/server-config.ts +++ b/frontend/src/model/server-config.ts @@ -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; } diff --git a/src/model/config.rs b/src/model/config.rs index 24fde96c1..ec1683ded 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -754,7 +754,7 @@ pub struct ConfigDto { #[serde(default)] pub video: Option, #[serde(default)] - pub schedule: Option, + pub schedules: Option>, #[serde(default)] pub messaging: Option, }