Replacing icon property when icon has been requested.

Creating <APIButton/> which handles success/failure/pending during API calls.
This commit is contained in:
Roland 2023-07-21 15:14:09 -06:00
parent 237b429cbf
commit 59c4ab2502
No known key found for this signature in database
GPG key ID: 9D6DBD3ACF11AB93
24 changed files with 483 additions and 334 deletions

View file

@ -3,7 +3,7 @@ import './App.css';
import {NavBar} from "./components/Nav";
import {createBrowserRouter, createRoutesFromElements, Link, Outlet, Route, RouterProvider} from "react-router-dom";
import {VideosRoute, VideoWrapper} from "./components/Videos";
import Admin from "./components/admin/Admin";
import AdminRoute from "./components/admin/AdminRoute";
import {Container} from "semantic-ui-react";
import 'semantic-ui-offline/semantic.min.css';
import {MoreRoute} from "./components/Apps";
@ -14,8 +14,8 @@ import {StatusProvider} from "./hooks/customHooks";
import {MapRoute} from "./components/Map";
import {MediaContextProvider, mediaStyles, StatusContext, ThemeContext} from "./contexts/contexts";
import {Header, ThemeProvider} from "./components/Theme";
import {Dashboard} from "./Dashboard";
import {Donate} from "./components/Donate";
import {DashboardPage} from "./DashboardPage";
import {DonatePage} from "./components/DonatePage";
import {useEventsInterval} from "./Events";
import {SemanticToastContainer} from "react-semantic-toasts-2";
import {FilePreviewProvider} from "./components/FilePreview";
@ -63,13 +63,13 @@ const router = createBrowserRouter(createRoutesFromElements(<Route
element={<Root/>}
errorElement={<PageNotFound/>}
>
<Route index element={<Dashboard/>}/>
<Route path='search/*' element={<Dashboard/>}/>
<Route path='donate' element={<Donate/>}/>
<Route index element={<DashboardPage/>}/>
<Route path='search/*' element={<DashboardPage/>}/>
<Route path='donate' element={<DonatePage/>}/>
<Route path='videos/video/:videoId' exact element={<VideoWrapper/>}/>
<Route path='videos/channel/:channelId/video/:videoId' exact element={<VideoWrapper/>}/>
<Route path="videos/*" element={<VideosRoute/>}/>
<Route path="admin/*" element={<Admin/>}/>
<Route path="admin/*" element={<AdminRoute/>}/>
<Route path="more/*" element={<MoreRoute/>}/>
<Route path="inventory/*" element={<InventoryRoute/>}/>
<Route path='archive/*' element={<ArchiveRoute/>}/>

View file

@ -24,7 +24,7 @@ import {TagsDashboard} from "./Tags";
import {Upload} from "./components/Upload";
import {SearchView, useSearch} from "./components/Search";
import {KiwixRestartMessage, OutdatedZimsMessage} from "./components/Zim";
import {useSettings} from "./hooks/customHooks";
import {useSettings, useWROLMode} from "./hooks/customHooks";
function FlagsMessages({flags}) {
const {settings, fetchSettings} = useSettings();
@ -82,13 +82,14 @@ function FlagsMessages({flags}) {
</>
}
export function Dashboard() {
export function DashboardPage() {
useTitle('Dashboard');
const navigate = useNavigate();
const {searchStr, setSearchStr, activeTags} = useSearch();
const {status} = useContext(StatusContext);
const navigate = useNavigate();
const wrol_mode = status ? status['wrol_mode'] : null;
const wrolModeEnabled = useWROLMode();
// Getters are Downloads or Uploads.
const [selectedGetter, setSelectedGetter] = useState(null);
@ -150,7 +151,7 @@ export function Dashboard() {
body = <SearchView/>;
} else {
body = <>
{!wrol_mode && getter}
{!wrolModeEnabled && getter}
{getterModal}
<TagsDashboard/>
<DashboardStatus/>

View file

@ -1,10 +1,8 @@
import React, {useEffect, useState} from "react";
import React, {useEffect} from "react";
import {deleteTag, getTags, saveTag} from "./api";
import {
Confirm,
Dimmer,
Divider,
Dropdown,
Form,
Grid,
Label,
@ -15,7 +13,7 @@ import {
TableHeaderCell,
TableRow,
} from "semantic-ui-react";
import {contrastingColor, scrollToTopOfElement} from "./components/Common";
import {APIButton, contrastingColor, scrollToTopOfElement} from "./components/Common";
import {
Button,
FormInput,
@ -150,18 +148,15 @@ export const useTagsInterval = () => {
function EditTagRow({tag, onDelete, onEdit}) {
const {NameToTagLabel} = React.useContext(TagsContext);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const {name, color, id, count} = tag;
const deleteConfirm = <>
<Button color='red' onClick={() => setConfirmDeleteOpen(true)} icon='trash'/>
<Confirm
id={`confirm${name}`}
open={confirmDeleteOpen}
content={`Are you sure you want to delete: ${name}?`}
<APIButton
icon='trash'
color='red'
confirmContent={`Are you sure you want to delete: ${name}?`}
confirmButton='Delete'
onCancel={() => setConfirmDeleteOpen(false)}
onConfirm={async () => onDelete(id, name)}
onClick={async () => onDelete(id, name)}
/>
</>;
const editButton = <Button primary onClick={() => onEdit(name, color, id)} icon='edit'/>;
@ -259,14 +254,13 @@ function EditTagsModal() {
<HexColorPicker color={tagColor} onChange={setTagColor} style={{marginTop: '1em'}}/>
<Button color='violet'
size='big'
onClick={localSaveTag}
style={{marginTop: '2em'}}
disabled={!!!tagName}
>
Save
</Button>
<APIButton
color='violet'
size='big'
onClick={localSaveTag}
style={{marginTop: '2em'}}
disabled={!!!tagName}
>Save</APIButton>
<Divider/>
<Table striped basic='very' unstackable>

View file

@ -82,7 +82,13 @@ export async function createChannel(channel) {
}
export async function deleteChannel(channelId) {
return apiDelete(`${VIDEOS_API}/channels/${channelId}`);
const response = await apiDelete(`${VIDEOS_API}/channels/${channelId}`);
if (response.status !== 204) {
const content = await response.json();
console.error(content);
throw Error(content);
}
return response;
}
export async function getChannels() {
@ -155,16 +161,13 @@ export async function deleteVideos(videoIds) {
time: 5000,
});
}
console.debug(`Deleting Videos: ${videoIds}`);
console.info(`Deleting Videos: ${videoIds}`);
const i = videoIds.join(',');
let response = await apiDelete(`${VIDEOS_API}/video/${i}`);
if (response.status !== 204) {
toast({
type: 'error',
title: 'Unexpected server response',
description: 'Failed to delete videos. See server logs.',
time: 5000,
});
const content = response.json();
console.error(content);
throw Error(content);
}
}
@ -249,7 +252,7 @@ export async function downloadChannel(channelId) {
export async function refreshChannel(channelId) {
let url = `${VIDEOS_API}/channels/refresh/${channelId}`;
await fetch(url, {method: 'POST'});
return await fetch(url, {method: 'POST'});
}
export async function encryptOTP(otp, plaintext) {
@ -361,15 +364,13 @@ export async function deleteArchives(archiveIds) {
time: 5000,
});
}
console.debug(`Deleting Archives: ${archiveIds}`);
console.log(`Deleting Archives: ${archiveIds}`);
let i = archiveIds.join(',');
try {
return await apiDelete(`${API_URI}/archive/${i}`);
} catch (e) {
console.error(e);
toast({
type: 'error', title: 'Unexpected server response', description: 'Unable to delete archives', time: 5000,
});
const response = await apiDelete(`${API_URI}/archive/${i}`);
if (response.status !== 204) {
const content = await response.json();
console.debug(content);
throw Error(content['error']);
}
}
@ -462,7 +463,11 @@ export async function postDownload(urls, downloader, frequency, sub_downloader,
body['tag_names'] = tagNames;
}
let response = await apiPost(`${API_URI}/download`, body);
return response;
if (response.status !== 204) {
const content = await response.json();
console.error(content);
throw Error(content['error']);
}
}
export async function killDownload(download_id) {

View file

@ -2,7 +2,7 @@ import React, {useContext} from 'react';
import {Divider, Input, SegmentGroup, StatisticLabel, StatisticValue, TableCell, TableRow} from "semantic-ui-react";
import {Link, Route, Routes} from "react-router-dom";
import {decryptOTP, encryptOTP} from "../api";
import {humanFileSize, mimetypeColor, PageContainer, toLocaleString, useTitle} from "./Common";
import {APIButton, humanFileSize, mimetypeColor, PageContainer, toLocaleString, useTitle} from "./Common";
import {ThemeContext} from "../contexts/contexts";
import {Button, Header, Loader, Segment, Statistic, StatisticGroup, Table, TextArea} from "./Theme";
import {useStatistics, useVINDecoder} from "../hooks/customHooks";
@ -15,14 +15,9 @@ class Encrypt extends React.Component {
}
}
handleSubmit = async (e) => {
e.preventDefault();
try {
let {otp, plaintext, ciphertext} = await encryptOTP(this.state.otp, this.state.plaintext);
this.setState({otp, plaintext, ciphertext});
} catch (e) {
console.error(e);
}
handleSubmit = async () => {
let {otp, plaintext, ciphertext} = await encryptOTP(this.state.otp, this.state.plaintext);
this.setState({otp, plaintext, ciphertext});
}
handleInputChange = async (event, {name, value}) => {
@ -30,6 +25,8 @@ class Encrypt extends React.Component {
}
render() {
const disabled = !(this.state.otp && this.state.plaintext);
return <>
<Header as='h2'>Encrypt</Header>
<SegmentGroup>
@ -59,7 +56,10 @@ class Encrypt extends React.Component {
</Segment>
</SegmentGroup>
<br/>
<Button color='violet' onClick={this.handleSubmit}>Encrypt</Button>
<APIButton
disabled={disabled}
onClick={this.handleSubmit}
>Encrypt</APIButton>
</>
}
}
@ -68,18 +68,13 @@ class Decrypt extends React.Component {
constructor(props) {
super(props);
this.state = {
otp: '', plaintext: '', ciphertext: '',
otp: '', plaintext: '', ciphertext: ''
}
}
handleSubmit = async (e) => {
e.preventDefault();
try {
let {otp, plaintext, ciphertext} = await decryptOTP(this.state.otp, this.state.ciphertext);
this.setState({otp, plaintext, ciphertext});
} catch (e) {
console.error(e);
}
handleSubmit = async () => {
let {otp, plaintext, ciphertext} = await decryptOTP(this.state.otp, this.state.ciphertext);
this.setState({otp, plaintext, ciphertext});
}
handleInputChange = async (event, {name, value}) => {
@ -87,6 +82,8 @@ class Decrypt extends React.Component {
}
render() {
const disabled = !(this.state.otp && this.state.ciphertext);
return <>
<Header as='h2'>Decrypt</Header>
<SegmentGroup>
@ -115,8 +112,13 @@ class Decrypt extends React.Component {
<pre>{this.state.plaintext || 'Enter the encrypted message above'}</pre>
</Segment>
</SegmentGroup>
<br/>
<Button color='violet' onClick={this.handleSubmit}>Decrypt</Button>
<APIButton
disabled={disabled}
onClick={this.handleSubmit}
>Decrypt</APIButton>
</>
}
}

View file

@ -15,6 +15,7 @@ import {
TableRow
} from "semantic-ui-react";
import {
APIButton,
BackButton,
CardLink,
cardTitleWrapper,
@ -45,10 +46,9 @@ import {Media, ThemeContext} from "../contexts/contexts";
import {Button, Card, CardIcon, Header, Loader, Placeholder, Segment} from "./Theme";
import {SortableTable} from "./SortableTable";
import {taggedImageLabel, TagsSelector} from "../Tags";
import {toast} from "react-semantic-toasts-2";
function ArchivePage() {
const [deleteOpen, setDeleteOpen] = useState(false);
const [updateOpen, setUpdateOpen] = useState(false);
const navigate = useNavigate();
const {archiveId} = useParams();
const {archiveFile, history, fetchArchive} = useArchive(archiveId);
@ -88,35 +88,41 @@ function ArchivePage() {
null;
const localDeleteArchive = async () => {
setDeleteOpen(false);
await deleteArchives([data.id]);
toast({
type: 'success',
title: 'Archive Deleted',
description: 'Archive was successfully deleted.',
time: 2000,
})
navigate(-1);
}
const localUpdateArchive = async () => {
setUpdateOpen(false);
await postDownload(data.url, 'archive');
toast({
type: 'success',
title: 'Archive Downloading',
description: 'Archive update has been scheduled.',
time: 2000,
})
}
const updateButton = (<>
<Button color='green' onClick={() => setUpdateOpen(true)} content='Update'/>
<Confirm
open={updateOpen}
content='Download the latest version of this URL?'
confirmButton='Update'
onCancel={() => setUpdateOpen(false)}
onConfirm={localUpdateArchive}
/>
</>);
const deleteButton = (<>
<Button color='red' onClick={() => setDeleteOpen(true)} content='Delete' aria-label='Delete'/>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this archive? All files will be deleted'
confirmButton='Delete'
onCancel={() => setDeleteOpen(false)}
onConfirm={localDeleteArchive}
/>
</>);
const updateButton = <APIButton
text='Update'
color='green'
confirmContent='Download the latest version of this URL?'
confirmButton='Update'
onClick={localUpdateArchive}
obeyWROLMode={true}
>Update</APIButton>;
const deleteButton = <APIButton
text='Delete'
color='red'
confirmContent='Are you sure you want to delete this archive? All files will be deleted'
confirmButton='Delete'
onClick={localDeleteArchive}
obeyWROLMode={true}
>Delete</APIButton>;
let historyList = <Loader active/>;
if (history && history.length === 0) {
@ -323,7 +329,7 @@ export function SearchDomain() {
return <></>
}
function Archives() {
function ArchivesPage() {
const [selectedArchives, setSelectedArchives] = useState([]);
const [deleteOpen, setDeleteOpen] = useState(false);
@ -497,7 +503,7 @@ export function ArchiveRoute() {
return <PageContainer>
<TabLinks links={links}/>
<Routes>
<Route path='/' element={<Archives/>}/>
<Route path='/' element={<ArchivesPage/>}/>
<Route path='domains' element={<DomainsPage/>}/>
<Route path=':archiveId' element={<ArchivePage/>}/>
</Routes>

View file

@ -14,6 +14,7 @@ import {
} from "semantic-ui-react";
import {createChannel, deleteChannel, downloadChannel, refreshChannel, updateChannel, validateRegex} from "../api";
import {
APIButton,
BackButton,
DirectoryInput,
frequencyOptions,
@ -26,7 +27,6 @@ import {
} from "./Common";
import {Link, useNavigate, useParams} from "react-router-dom";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
import Confirm from "semantic-ui-react/dist/commonjs/addons/Confirm";
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import {useChannel, useChannels} from "../hooks/customHooks";
import _ from "lodash";
@ -75,7 +75,6 @@ function ChannelStatistics({statistics}) {
function ChannelPage({create, header}) {
const [disabled, setDisabled] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [validRegex, setValidRegex] = useState(true);
const [activeIndex, setActiveIndex] = useState(-1);
const [errors, setErrors] = useState({});
@ -133,7 +132,9 @@ function ChannelPage({create, header}) {
}
const handleSubmit = async (e) => {
e.preventDefault();
if (e) {
e.preventDefault();
}
const body = {
name: channel.name,
directory: channel.directory,
@ -180,7 +181,11 @@ function ChannelPage({create, header}) {
);
} else {
await fetchChannel();
setSuccessMessage('Channel updated', 'Your channel was updated');
toast({
type: 'success',
title: 'Channel updated',
description: 'The channel was updated',
})
}
} else if (response) {
@ -218,7 +223,9 @@ function ChannelPage({create, header}) {
}
const handleDownloadChannel = async (e) => {
e.preventDefault();
if (e) {
e.preventDefault();
}
try {
const response = await downloadChannel(channelId);
if (response.status === 204) {
@ -235,23 +242,29 @@ function ChannelPage({create, header}) {
}
const handleRefreshChannel = async (e) => {
e.preventDefault();
return await refreshChannel(channelId);
}
const handleDeleteConfirm = async () => {
setDeleteOpen(false);
let response = await deleteChannel(channelId);
if (response.status === 204) {
navigate('/videos/channel');
} else {
setSuccessMessage('Failed to delete', 'Failed to delete this channel, check logs.');
if (e) {
e.preventDefault();
}
const response = await refreshChannel(channelId);
if (response.status !== 204) {
toast({
type: 'error',
title: 'Failed to refresh',
description: "Failed to refresh this channel's directory",
time: 5000,
})
}
}
const handleDeleteButton = (e) => {
e.preventDefault();
setDeleteOpen(true);
const handleDelete = async () => {
try {
let response = await deleteChannel(channelId);
if (response.status === 204) {
navigate('/videos/channel');
}
} catch (e) {
setErrorMessage('Failed to delete', 'Failed to delete this channel, check logs.');
}
}
return <>
@ -266,7 +279,6 @@ function ChannelPage({create, header}) {
<WROLModeMessage content='Channel page is disabled while WROL Mode is enabled.'/>
<Form
id="editChannel"
onSubmit={handleSubmit}
error={error}
success={success}
autoComplete="off"
@ -391,39 +403,39 @@ function ChannelPage({create, header}) {
<Grid.Column>
{!create &&
<>
<Button color='red' onClick={handleDeleteButton} size='small'>
Delete
</Button>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this channel? No video files will be deleted.'
<APIButton
color='red'
size='small'
confirmContent='Are you sure you want to delete this channel? No video files will be deleted.'
confirmButton='Delete'
onCancel={() => setDeleteOpen(false)}
onConfirm={handleDeleteConfirm}
/>
<Button
onClick={handleDelete}
obeyWROLMode={true}
>Delete</APIButton>
<APIButton
color='green'
size='small'
onClick={handleDownloadChannel}
disabled={!channel.url || !channel.download_frequency}
>
Download
</Button>
<Button color='blue' size='small' onClick={handleRefreshChannel}>
Refresh
</Button>
obeyWROLMode={true}
>Download</APIButton>
<APIButton
color='blue'
size='small'
onClick={handleRefreshChannel}
obeyWROLMode={true}
>Refresh</APIButton>
</>
}
</Grid.Column>
<Grid.Column>
<Button
<APIButton
color='violet'
size='big'
type="submit"
floated='right'
>
{disabled ? <Loader active inline/> : 'Save'}
</Button>
onClick={handleSubmit}
disabled={disabled}
obeyWROLMode={true}
>Save</APIButton>
</Grid.Column>
</Grid.Row>
</Grid>

View file

@ -1,5 +1,17 @@
import React, {useContext, useEffect, useState} from "react";
import {ButtonGroup, Card, Container, IconGroup, Input, Label, Pagination, Search} from 'semantic-ui-react';
import {
Button as SButton,
ButtonGroup,
Card,
Confirm,
Container,
IconGroup,
Input,
Label,
Pagination,
Search,
Transition
} from 'semantic-ui-react';
import {Link, NavLink, useNavigate, useSearchParams} from "react-router-dom";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
import {
@ -8,7 +20,8 @@ import {
useSearchDirectories,
useSearchOrder,
useSettings,
useThrottle
useThrottle,
useWROLMode
} from "../hooks/customHooks";
import {Media, StatusContext, ThemeContext} from "../contexts/contexts";
import {
@ -419,10 +432,9 @@ export function SearchInput({
}
export function WROLModeMessage({content}) {
const {status} = useContext(StatusContext);
const wrol_mode = status ? status.wrol_mode : null;
const wrolModeEnabled = useWROLMode();
if (wrol_mode) {
if (wrolModeEnabled) {
return <Message icon='lock' header='WROL Mode Enabled' content={content}/>
}
return null;
@ -674,6 +686,7 @@ export function DisableDownloadsToggle() {
const {status, fetchStatus} = React.useContext(StatusContext);
const {downloads} = status ? status : {downloads: null};
const wrolModeEnabled = useWROLMode();
const setDownloads = async (enable) => {
setPending(true);
@ -690,7 +703,7 @@ export function DisableDownloadsToggle() {
return <Form>
<Toggle
label={on === true ? 'Downloading Enabled' : 'Downloading Disabled'}
disabled={pending || downloads === null}
disabled={wrolModeEnabled || pending || downloads === null}
checked={on === true}
onChange={setDownloads}
/>
@ -1190,4 +1203,166 @@ export function TagIcon() {
export function normalizeEstimate(estimate) {
return estimate > 999 ? '>999' : estimate;
}
export function useAPIButton(
color = 'violet',
size = 'medium',
floated,
onClick,
disabled,
confirmContent,
confirmButton,
themed = true,
obeyWROLMode = false,
icon = null,
props
) {
props = props || {};
const ref = React.useRef();
const [confirmOpen, setConfirmOpen] = React.useState(false);
const [pending, setPending] = React.useState(false);
const [animation, setAnimation] = React.useState('jiggle');
const [animationVisible, setAnimationVisible] = React.useState(true);
const [showSuccess, setShowSuccess] = React.useState(false);
const [showFailure, setShowFailure] = React.useState(false);
const wrolModeEnabled = useWROLMode();
// Disable when API call is pending, or button is disabled.
disabled = pending || disabled;
// Disable when WROL Mode is enabled, otherwise normal disabled.
disabled = obeyWROLMode ? wrolModeEnabled || disabled : disabled;
const reset = () => {
setShowSuccess(false);
setShowFailure(false);
};
const setSuccess = () => {
setShowSuccess(true);
setAnimation('pulse');
setAnimationVisible(!animationVisible);
setTimeout(reset, 2000);
}
const setFailure = () => {
setShowFailure(true);
setAnimation('shake');
setAnimationVisible(!animationVisible);
setTimeout(reset, 2000);
}
const handleAPICall = async () => {
// Handle when user clicks button, or clicks confirm.
setPending(true);
try {
await onClick();
setSuccess();
} catch (e) {
console.log(e);
setFailure();
}
setPending(false);
}
const localOnClick = async (e) => {
if (e) {
e.preventDefault();
}
if (confirmContent) {
// Clicking button opens confirm.
setConfirmOpen(true);
} else if (onClick) {
// No <Confirm/> send the API request.
await handleAPICall();
} else {
throw Error('No onClick defined!');
}
}
const localOnConfirm = async () => {
// User clicked the "OK" button in the <Confirm/>.
setConfirmOpen(false);
if (onClick) {
await handleAPICall();
}
}
// Create button with or without theme. Pass all props to the <Button/> (except props.children).
const buttonArgs = {color, onClick: localOnClick, disabled, loading: pending, size, floated, ...props};
let buttonContent = <>{props.children}</>;
if (icon) {
// Send Icon as Button properties.
buttonContent = null;
buttonArgs['icon'] = showSuccess ? 'check' : showFailure ? 'close' : icon;
} else if (showSuccess || showFailure) {
// Show ✔ or ✖ overtop the contents after API call has completed.
buttonContent = <>
<Icon style={{position: 'absolute'}} name={showSuccess ? 'check' : 'close'}/>
{/* Keep contents to avoid resizing button */}
<div style={{opacity: 0}}>{buttonContent}</div>
</>
}
let button = themed ?
<Button ref={ref} {...buttonArgs}>{buttonContent}</Button>
: <SButton ref={ref} {...buttonArgs}>{buttonContent}</SButton>;
// Wrap button in <Transition/> to show success or failure animations.
button = <Transition animation={animation} duration={500} visible={animationVisible}>
{button}
</Transition>;
if (confirmContent) {
// Wrap button with <Confirm/>
button = <>
{button}
<Confirm open={confirmOpen}
content={confirmContent}
onClose={() => setConfirmOpen(false)}
onCancel={() => setConfirmOpen(false)}
onConfirm={localOnConfirm}
/>
</>
}
button = <>
{button}
</>
return {button, ref}
}
export function APIButton({
color,
size,
floated,
onClick,
disabled,
confirmContent,
confirmButton,
themed,
obeyWROLMode,
icon,
...props
}) {
const {button} = useAPIButton(
color,
size,
floated,
onClick,
disabled,
confirmContent,
confirmButton,
themed,
obeyWROLMode,
icon,
props
);
return button;
}

View file

@ -54,7 +54,7 @@ function CoinRow({header, qrCodeValue, address, buttonColor}) {
</>
}
export function Donate() {
export function DonatePage() {
useTitle('Donate');
return <PageContainer>

View file

@ -1,6 +1,6 @@
import React, {useContext, useState} from "react";
import {getDownloaders, postDownload} from "../api";
import {DirectorySearch, frequencyOptions, HelpPopup, rssFrequencyOptions, WROLModeMessage} from "./Common";
import {APIButton, DirectorySearch, frequencyOptions, HelpPopup, rssFrequencyOptions, WROLModeMessage} from "./Common";
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
import {ThemeContext} from "../contexts/contexts";
@ -146,7 +146,11 @@ class Downloader extends React.Component {
{withSearchDirectory ? destinationRequired ? directorySearch : advancedAccordion : null}
<Button content='Cancel' onClick={this.props.clearSelected}/>
<Button primary style={{marginTop: '1em'}} disabled={disabled}>Download</Button>
<APIButton
disabled={disabled}
onClick={this.submitDownload}
style={{marginTop: '0.5em'}}
>Download</APIButton>
{submitted && viewDownloads}
</Form>)}
</ThemeContext.Consumer>
@ -230,12 +234,10 @@ class ChannelDownload extends React.Component {
{frequency === 0 && onceMessage}
<Button content='Cancel' onClick={this.props.clearSelected}/>
<Button
color='blue'
content='Download'
<APIButton
onClick={this.handleSubmit}
disabled={buttonDisabled}
/>
>Download</APIButton>
{pending && <Loader active={pending}/>}
{success && <Icon name='check'/>}
</Form>

View file

@ -1,7 +1,6 @@
import {Button, Header, Icon, Modal, ModalActions, ModalContent, ModalHeader, Placeholder, Table} from "./Theme";
import {
Checkbox,
Confirm,
IconGroup,
Input,
PlaceholderLine,
@ -12,14 +11,15 @@ import {
TableHeaderCell,
TableRow
} from "semantic-ui-react";
import {DirectorySearch, FileIcon, humanFileSize} from "./Common";
import {APIButton, DirectorySearch, FileIcon, humanFileSize} from "./Common";
import React, {useEffect, useState} from "react";
import {deleteFile, makeDirectory, movePaths, renamePath} from "../api";
import _ from 'lodash';
import {SortableTable} from "./SortableTable";
import {useBrowseFiles, useMediaDirectory} from "../hooks/customHooks";
import {useBrowseFiles, useMediaDirectory, useWROLMode} from "../hooks/customHooks";
import {DirectoryRefreshButton, FileRowTagIcon, FilesRefreshButton} from "./Files";
import {FilePreviewContext} from "./FilePreview";
import {StatusContext} from "../contexts/contexts";
function depthIndentation(path) {
// Repeated spaces for every folder a path is in.
@ -139,6 +139,7 @@ export function FileBrowser() {
const selectedPathsCount = selectedPaths ? selectedPaths.length : 0;
const {browseFiles, openFolders, setOpenFolders, fetchFiles} = useBrowseFiles();
const wrolModeEnabled = useWROLMode();
const headers = [
{key: 'select', text: ''},
@ -195,21 +196,19 @@ export function FileBrowser() {
<DirectoryRefreshButton paths={selectedPaths}/>
: <FilesRefreshButton/>
}
<Button icon='trash'
color='red'
onClick={() => setDeleteOpen(true)}
disabled={selectedPathsCount === 0}
/>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete these files?'
onConfirm={onDelete}
onCancel={() => setDeleteOpen(false)}
<APIButton
icon='trash'
color='red'
confirmContent='Are you sure you want to delete these files?'
confirmButton='Delete'
onClick={onDelete}
disabled={selectedPathsCount === 0}
obeyWROLMode={true}
/>
<Button icon='text cursor'
color='yellow'
onClick={() => setRenameOpen(true)}
disabled={selectedPathsCount !== 1}
disabled={wrolModeEnabled || selectedPathsCount !== 1}
/>
{selectedPathsCount === 1 ?
<RenameModal
@ -221,7 +220,7 @@ export function FileBrowser() {
/> : null}
<Button icon='move'
color='violet'
disabled={selectedPathsCount === 0}
disabled={wrolModeEnabled || selectedPathsCount === 0}
onClick={() => setMoveOpen(true)}
/>
{selectedPathsCount > 0 ?
@ -233,7 +232,7 @@ export function FileBrowser() {
<Button
color='blue'
onClick={() => setMakeDirectoryOpen(true)}
disabled={selectedPaths && selectedPaths.length > 1}
disabled={wrolModeEnabled || selectedPaths && selectedPaths.length > 1}
style={{paddingLeft: '1em', paddingRight: '0.8em'}}
>
<IconGroup>
@ -246,6 +245,7 @@ export function FileBrowser() {
onClose={() => setMakeDirectoryOpen(false)}
parent={selectedPaths.length ? selectedPaths[0] : null}
onSubmit={handleMakeDirectory}
disabled={wrolModeEnabled}
/>
</TableHeaderCell>
</TableRow>

View file

@ -37,7 +37,8 @@ import {
useQuery,
useSearchFiles,
useSearchFilter,
useSearchView
useSearchView,
useWROLMode
} from "../hooks/customHooks";
import {Route, Routes} from "react-router-dom";
import {CardPlaceholder} from "./Placeholder";
@ -572,6 +573,7 @@ export function FilesRoute() {
export function FilesRefreshButton() {
const {status} = useContext(StatusContext);
const wrolModeEnabled = useWROLMode();
const refreshing = status && status['flags'] && status['flags'].indexOf('refreshing') >= 0;
const refreshingDirectory = status && status['flags'] && status['flags'].indexOf('refreshing_directory') >= 0;
@ -589,7 +591,7 @@ export function FilesRefreshButton() {
labelPosition='left'
loading={loading || refreshing || refreshingDirectory}
onClick={handleClick}
disabled={loading || refreshing}>
disabled={wrolModeEnabled || loading || refreshing}>
<Icon name='refresh'/>
Refresh All
</Button>;
@ -599,6 +601,7 @@ export function DirectoryRefreshButton({paths}) {
const {status} = useContext(StatusContext);
const refreshing = status && status['flags'] && status['flags'].indexOf('refreshing') >= 0;
const refreshingDirectory = status && status['flags'] && status['flags'].indexOf('refreshing_directory') >= 0;
const wrolModeEnabled = useWROLMode();
const [loading, setLoading] = React.useState(false);
@ -615,7 +618,7 @@ export function DirectoryRefreshButton({paths}) {
labelPosition='left'
loading={loading || refreshing || refreshingDirectory}
onClick={handleClick}
disabled={loading || refreshing || refreshingDirectory}
disabled={wrolModeEnabled || loading || refreshing || refreshingDirectory}
>
<Icon name='refresh'/>
Refresh

View file

@ -267,7 +267,7 @@ class ManageMap extends React.Component {
}
}
function MapApp() {
function MapPage() {
return <iframe
title='map'
src={`http://${window.location.hostname}:8084/`}
@ -290,7 +290,7 @@ export function MapRoute() {
return <div style={{marginTop: '2em'}}>
<TabLinks links={links}/>
<Routes>
<Route path='/' exact element={<MapApp/>}/>
<Route path='/' exact element={<MapPage/>}/>
<Route path='manage' exact element={<ManageMap/>}/>
</Routes>
</div>

View file

@ -5,6 +5,7 @@ import {Media, StatusContext, ThemeContext} from "../contexts/contexts";
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import {DarkModeToggle, HotspotStatusIcon, NAME} from "./Common";
import {ShareButton} from "./Share";
import {useWROLMode} from "../hooks/customHooks";
const links = [
{text: 'Videos', to: '/videos', key: 'videos'},
@ -56,11 +57,10 @@ function NavIcon(props) {
}
export function NavBar() {
const {status} = React.useContext(StatusContext);
const wrol_mode = status ? status['wrol_mode'] : null;
const wrolModeEnabled = useWROLMode();
const wrolpiIcon = <img src='/icon.svg' height='32px' width='32px' alt='WROLPi Home Icon'/>;
const name = NAME || wrolpiIcon;
const topNavText = wrol_mode ? <>{name}&nbsp; <Icon name='lock'/></> : name;
const topNavText = wrolModeEnabled ? <>{name}&nbsp; <Icon name='lock'/></> : name;
const {i} = React.useContext(ThemeContext);
const homeLink = <NavLink className='item' to='/' style={{paddingTop: 0, paddingBottom: 0}}>

View file

@ -173,11 +173,11 @@ export function Accordion(props) {
return <SAccordion {...props}/>
}
export function Button(props) {
export const Button = React.forwardRef((props, ref) => {
const {i, inverted} = useContext(ThemeContext);
props = defaultGrey({...i, ...props}, inverted);
return <SButton {...props}/>
}
return <SButton ref={ref} {...props}/>
});
export function Divider({...props}) {
const {i} = useContext(ThemeContext);

View file

@ -1,8 +1,9 @@
import React, {useContext, useState} from 'react';
import {tagFileGroup, deleteVideos, untagFileGroup} from "../api";
import React, {useContext} from 'react';
import {deleteVideos, tagFileGroup, untagFileGroup} from "../api";
import {Link, useNavigate, useParams} from "react-router-dom";
import _ from "lodash";
import {
APIButton,
BackButton,
encodeMediaPath,
humanFileSize,
@ -12,7 +13,6 @@ import {
PreviewPath,
useTitle
} from "./Common";
import {Confirm} from "semantic-ui-react";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import Container from "semantic-ui-react/dist/commonjs/elements/Container";
import {VideoPlaceholder} from "./Placeholder";
@ -44,7 +44,6 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
const navigate = useNavigate();
const videoRef = React.useRef();
let [deleteOpen, setDeleteOpen] = useState(false);
useTitle(videoFile === null || videoFile === undefined ? null : videoFile.video.title);
let video;
@ -69,13 +68,7 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
}
const handleDeleteVideo = async (video_id) => {
try {
await deleteVideos([video_id])
setDeleteOpen(false);
} catch (e) {
setDeleteOpen(false);
throw e;
}
await deleteVideos([video_id])
navigate(-1);
}
@ -198,24 +191,17 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
</h3>
<p>
<a href={downloadUrl} target='_blank' rel='noopener noreferrer'>
<Button style={{margin: '0.5em'}}>
<Icon name='download'/>
Download
</Button>
</a>
<Button
<Button as='a' href={downloadUrl}>
<Icon name='download'/>
Download
</Button>
<APIButton
color='red'
onClick={() => setDeleteOpen(true)}
style={{margin: '0.5em'}}
>Delete</Button>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this video? All files related to this video will be deleted. It will not be downloaded again!'
confirmContent='Are you sure you want to delete this video? All files related to this video will be deleted. It will not be downloaded again!'
confirmButton='Delete'
onCancel={() => setDeleteOpen(false)}
onConfirm={() => handleDeleteVideo(video.id)}
/>
onClick={async () => await handleDeleteVideo(video.id)}
obeyWROLMode={true}
>Delete</APIButton>
</p>
<br/>
</Segment>

View file

@ -1,4 +1,4 @@
import {useOutdatedZims, useSearchZim} from "../hooks/customHooks";
import {useOutdatedZims, useSearchZim, useWROLMode} from "../hooks/customHooks";
import {
Accordion,
Button,
@ -31,6 +31,7 @@ import {
} from "semantic-ui-react";
import React, {useContext, useState} from "react";
import {
APIButton,
encodeMediaPath,
humanFileSize,
normalizeEstimate,
@ -55,7 +56,7 @@ import {
} from "../api";
import {useSearch} from "./Search";
import {TagsQuerySelector} from "./Files";
import {ThemeContext} from "../contexts/contexts";
import {StatusContext, ThemeContext} from "../contexts/contexts";
import {Link, Route, Routes} from "react-router-dom";
import {SortableTable} from "./SortableTable";
import {toast} from "react-semantic-toasts-2";
@ -70,10 +71,7 @@ export const OutdatedZimsMessage = ({onClick}) => {
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
const handleDelete = async (e) => {
if (e) {
e.preventDefault();
}
const handleDelete = async () => {
try {
const success = await deleteOutdatedZims();
if (success) {
@ -96,10 +94,7 @@ export const OutdatedZimsMessage = ({onClick}) => {
}
}
const handleIgnore = async (e) => {
if (e) {
e.preventDefault();
}
const handleIgnore = async () => {
const config = {ignore_outdated_zims: true};
await saveSettings(config);
if (onClick) {
@ -138,13 +133,19 @@ export const OutdatedZimsMessage = ({onClick}) => {
{modalContent}
</ModalContent>
<ModalActions>
<Button color='red' floated='left' onClick={handleDelete}>Delete</Button>
<APIButton
color='red'
floated='left'
onClick={handleDelete}
>Delete</APIButton>
<Button onClick={onClose}>Close</Button>
</ModalActions>
</Modal>
<Link to='/files?folders=zims'><SButton>Delete Manually</SButton></Link>
<SButton secondary onClick={handleIgnore}>Ignore Forever</SButton>
<APIButton secondary
onClick={handleIgnore}
>Ignore Forever</APIButton>
</Message.Content>
</Message>
}
@ -333,10 +334,9 @@ const ZimCatalogItemRow = ({item, subscriptions, iso_639_codes, fetchSubscriptio
const [pending, setPending] = useState(false);
const languageChange = subscription ? langauge !== subscription['language'] : false;
const handleButton = async (e) => {
if (e) {
e.preventDefault();
}
const wrolModeEnabled = useWROLMode();
const handleButton = async () => {
let success = false;
try {
setPending(true);
@ -375,14 +375,18 @@ const ZimCatalogItemRow = ({item, subscriptions, iso_639_codes, fetchSubscriptio
placeholder='Language'
options={languageOptions}
value={langauge}
disabled={wrolModeEnabled}
onChange={handleLanguageChange}
/>;
const subscribeButton = <Button
const subscribeButton = <APIButton
color='grey'
disabled={pending}
onClick={handleButton}>
onClick={handleButton}
obeyWROLMode={true}
>
{subscription && !languageChange ? 'Unsubscribe' : 'Subscribe'}
</Button>
</APIButton>
return <TableRow key={name}>
<TableCell>{name}</TableCell>

View file

@ -5,7 +5,7 @@ import {PageContainer, TabLinks, Toggle} from "../Common";
import {Route, Routes} from "react-router-dom";
import {ThemeContext} from "../../contexts/contexts";
import {Header, Loader, Segment} from "../Theme";
import {Downloads} from "./Downloads";
import {DownloadsPage} from "./Downloads";
import {Settings} from "./Settings";
import {Status} from "./Status";
@ -65,7 +65,7 @@ class WROLMode extends React.Component {
}
}
export default function Admin() {
export default function AdminRoute() {
const links = [
{text: 'Downloads', to: '/admin', key: 'admin', end: true},
@ -77,7 +77,7 @@ export default function Admin() {
return <PageContainer>
<TabLinks links={links}/>
<Routes>
<Route path='/' exact element={<Downloads/>}/>
<Route path='/' exact element={<DownloadsPage/>}/>
<Route path='settings' exact element={<Settings/>}/>
<Route path='status' exact element={<Status/>}/>
<Route path='wrol' exact element={<WROLMode/>}/>

View file

@ -2,6 +2,7 @@ import React from "react";
import {clearCompletedDownloads, clearFailedDownloads, deleteDownload, killDownload, postDownload} from "../../api";
import {Link} from "react-router-dom";
import {
APIButton,
DisableDownloadsToggle,
isoDatetimeToElapsedPopup,
secondsToFrequency,
@ -40,45 +41,35 @@ function ClearCompleteDownloads({callback}) {
}
return <>
<Button
<APIButton
onClick={localClearDownloads}
disabled={disabled}
color='yellow'
>Clear Completed</Button>
obeyWROLMode={true}
>Clear Completed</APIButton>
</>
}
function ClearFailedDownloads({callback}) {
const [open, setOpen] = React.useState(false);
const [disabled, setDisabled] = React.useState(false);
async function localDeleteFailed() {
setDisabled(true);
try {
await clearFailedDownloads();
} finally {
setDisabled(false);
setOpen(false);
if (callback) {
callback()
}
}
}
return <>
<Button
onClick={() => setOpen(true)}
disabled={disabled}
color='red'
>Clear Failed</Button>
<Confirm
open={open}
content='Are you sure you want to delete failed downloads? They will not be retried.'
confirmButton='Delete'
onCancel={() => setOpen(false)}
onConfirm={localDeleteFailed}
/>
</>
return <APIButton
color='red'
onClick={localDeleteFailed}
confirmContent='Are you sure you want to delete failed downloads? They will not be retried.'
confirmButton='Delete'
obeyWROLMode={true}
>
Clear Failed
</APIButton>
}
class DownloadRow extends React.Component {
@ -104,10 +95,7 @@ class DownloadRow extends React.Component {
this.setState({deleteOpen: false});
}
handleDelete = async (e) => {
if (e) {
e.preventDefault();
}
handleDelete = async () => {
const {id} = this.props;
try {
await deleteDownload(id);
@ -144,13 +132,13 @@ class DownloadRow extends React.Component {
</Modal>;
const deleteButton = <>
<Button icon='trash' onClick={this.openDelete} color='red'/>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this download?'
<APIButton
color='red'
icon='trash'
confirmContent='Are you sure you want to delete this download?'
confirmButton='Delete'
onCancel={this.closeDelete}
onConfirm={this.handleDelete}
onClick={this.handleDelete}
obeyWROLMode={true}
/>
</>;
@ -184,62 +172,29 @@ class StoppableRow extends React.Component {
constructor(props) {
super(props);
this.state = {
stopOpen: false,
startOpen: false,
errorModalOpen: false,
deleteOpen: false,
};
}
openDelete = () => {
this.setState({deleteOpen: true});
}
closeDelete = () => {
this.setState({deleteOpen: false});
}
openStop = () => {
this.setState({stopOpen: true});
}
closeStop = () => {
this.setState({stopOpen: false});
}
openStart = () => {
this.setState({startOpen: true});
}
closeStart = () => {
this.setState({startOpen: false});
}
handleDelete = async (e) => {
e.preventDefault();
handleDelete = async () => {
await deleteDownload(this.props.id);
this.closeDelete();
await this.props.fetchDownloads();
};
handleStop = async (e) => {
e.preventDefault();
handleStop = async () => {
await killDownload(this.props.id);
this.closeStop();
await this.props.fetchDownloads();
};
handleStart = async (e) => {
e.preventDefault();
handleStart = async () => {
let downloader = this.props.downloader;
await postDownload(`${this.props.url}`, downloader);
this.closeStart();
await this.props.fetchDownloads();
};
render() {
let {url, last_successful_download, status, location, error} = this.props;
let {stopOpen, startOpen, errorModalOpen, deleteOpen} = this.state;
let {errorModalOpen} = this.state;
// Open downloads (/download), or external links in an anchor.
const link = location && !location.startsWith('/download') ?
@ -251,38 +206,33 @@ class StoppableRow extends React.Component {
if (status === 'pending' || status === 'new') {
buttonCell = (
<TableCell>
<Button
onClick={this.openStop}
<APIButton
color='red'
>Stop</Button>
<Confirm
open={stopOpen}
content='Are you sure you want to stop this download? It will not be retried.'
onClick={this.handleStop}
confirmContent='Are you sure you want to stop this download? It will not be retried.'
confirmButton='Stop'
onCancel={this.closeStop}
onConfirm={this.handleStop}
/>
obeyWROLMode={true}
>Stop</APIButton>
</TableCell>
);
} else if (status === 'failed' || status === 'deferred') {
buttonCell = (
<TableCell>
<Button icon='trash' onClick={this.openDelete} color='red'/>
<Confirm
open={deleteOpen}
content='Are you sure you want to delete this download?'
<APIButton
color='red'
icon='trash'
onClick={this.handleDelete}
confirmContent='Are you sure you want to delete this download?'
confirmButton='Delete'
onCancel={this.closeDelete}
onConfirm={this.handleDelete}
obeyWROLMode={true}
/>
<Button onClick={this.openStart} color='green'>Retry</Button>
<Confirm
open={startOpen}
content='Are you sure you want to restart this download?'
<APIButton
color='green'
confirmContent='Are you sure you want to restart this download?'
confirmButton='Start'
onCancel={this.closeStart}
onConfirm={this.handleStart}
/>
onClick={this.handleStart}
obeyWROLMode={true}
>Retry</APIButton>
</TableCell>
);
} else if (status === 'complete' && location) {
@ -296,18 +246,12 @@ class StoppableRow extends React.Component {
completedAtCell = (
<Modal
closeIcon
onClose={() => this.setState({errorModalOpen: false})}
onOpen={() => this.setState({errorModalOpen: true})}
open={errorModalOpen}
trigger={<Button icon='exclamation circle' color='red'/>}
>
<ModalHeader>Download Error</ModalHeader>
<ModalContent>
<pre style={{overflowX: 'scroll'}}>{error}</pre>
</ModalContent>
<ModalActions>
<SButton onClick={() => this.setState({errorModalOpen: false})}>Close</SButton>
</ModalActions>
</Modal>
)
}
@ -379,7 +323,7 @@ export function RecurringDownloadsTable({downloads, fetchDownloads}) {
}
}
export function Downloads() {
export function DownloadsPage() {
useTitle('Downloads');
const {onceDownloads, recurringDownloads, fetchDownloads} = useDownloads();

View file

@ -3,7 +3,7 @@ import {getSettings, saveSettings} from "../../api";
import {Button, Form, FormGroup, FormInput, Header, Loader, Modal, ModalContent, ModalHeader, Segment} from "../Theme";
import {Container, Dimmer, Icon} from "semantic-ui-react";
import {ThemeContext} from "../../contexts/contexts";
import {HelpPopup, HotspotToggle, ThrottleToggle, Toggle, WROLModeMessage} from "../Common";
import {APIButton, HelpPopup, HotspotToggle, ThrottleToggle, Toggle, WROLModeMessage} from "../Common";
import QRCode from "react-qr-code";
export class Settings extends React.Component {
@ -57,7 +57,9 @@ export class Settings extends React.Component {
}
async handleSubmit(e) {
e.preventDefault();
if (e) {
e.preventDefault();
}
this.setState({disabled: true, pending: true});
let settings = {
download_on_startup: this.state.download_on_startup,
@ -228,9 +230,13 @@ export class Settings extends React.Component {
<br/>
<Button color='violet' type="submit" disabled={disabled} size='large'>
Save
</Button>
<APIButton
color='violet'
size='big'
onClick={this.handleSubmit}
obeyWROLMode={true}
disabled={disabled}
>Save</APIButton>
<Dimmer active={pending}>
<Loader active={pending} size='large'/>

View file

@ -1064,3 +1064,9 @@ export const useVINDecoder = (defaultVINNumber = '') => {
return {value, setValue, vin}
}
export const useWROLMode = () => {
// Returns the current boolean WROL Mode, during fetch this returns null.
const {status} = useContext(StatusContext);
return status ? status.wrol_mode : null;
}

View file

@ -1,3 +1,4 @@
import asyncio
from http import HTTPStatus
from sanic import response, Request

View file

@ -17,8 +17,8 @@ from wrolpi.captions import FFMPEG_BIN
from wrolpi.cmd import FFPROBE_BIN
from wrolpi.common import logger, get_media_directory
from wrolpi.db import get_db_session, get_db_curs
from .errors import ChannelNameConflict, ChannelURLConflict, ChannelDirectoryConflict, ChannelSourceIdConflict
from wrolpi.vars import DEFAULT_FILE_PERMISSIONS
from .errors import ChannelNameConflict, ChannelURLConflict, ChannelDirectoryConflict, ChannelSourceIdConflict
from .models import Channel
logger = logger.getChild(__name__)
@ -80,9 +80,10 @@ def check_for_channel_conflicts(session: Session, id_=None, url=None, name=None,
raise ChannelSourceIdConflict()
# Assure that this Channel's directory is not in another Channel's directory.
directory_str = str(directory)
directory = f'{directory}/'
for channel in session.query(Channel).filter(Channel.id != id_):
if directory_str.startswith(str(channel.directory)) or str(channel.directory).startswith(directory_str):
channel_directory = f'{channel.directory}/'
if directory.startswith(channel_directory) or channel_directory.startswith(directory):
raise ChannelDirectoryConflict()

View file

@ -1,3 +1,4 @@
import asyncio
from http import HTTPStatus
from sanic import response, Blueprint
@ -60,7 +61,7 @@ async def search(_: Request, body: schema.VideoSearchRequest):
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
@wrol_mode_check
@run_after(lib.save_channels_config)
def video_delete(_: Request, video_ids: str):
async def video_delete(_: Request, video_ids: str):
try:
video_ids = [int(i) for i in str(video_ids).split(',')]
except Exception:
@ -69,4 +70,4 @@ def video_delete(_: Request, video_ids: str):
lib.delete_videos(*video_ids)
Events.send_deleted(f'Deleted {len(video_ids)} videos')
return response.raw('', HTTPStatus.NO_CONTENT)
return response.empty()