mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Replacing icon property when icon has been requested.
Creating <APIButton/> which handles success/failure/pending during API calls.
This commit is contained in:
parent
237b429cbf
commit
59c4ab2502
24 changed files with 483 additions and 334 deletions
|
|
@ -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/>}/>
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ function CoinRow({header, qrCodeValue, address, buttonColor}) {
|
|||
</>
|
||||
}
|
||||
|
||||
export function Donate() {
|
||||
export function DonatePage() {
|
||||
useTitle('Donate');
|
||||
|
||||
return <PageContainer>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} <Icon name='lock'/></> : name;
|
||||
const topNavText = wrolModeEnabled ? <>{name} <Icon name='lock'/></> : name;
|
||||
const {i} = React.useContext(ThemeContext);
|
||||
|
||||
const homeLink = <NavLink className='item' to='/' style={{paddingTop: 0, paddingBottom: 0}}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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/>}/>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import asyncio
|
||||
from http import HTTPStatus
|
||||
|
||||
from sanic import response, Request
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue