mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
merge: Docs and Zim off Semantic
This commit is contained in:
commit
cd37c9920a
2 changed files with 237 additions and 334 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link, Route, Routes, useLocation, useNavigate, useParams, useSearchParams} from "react-router";
|
||||
import {Grid, Icon as SIcon, Loader, StatisticLabel, StatisticValue} from "semantic-ui-react";
|
||||
import {deleteFileGroups, getDocStatistics, tagFileGroup, untagFileGroup} from "../api";
|
||||
import {Media, ThemeContext} from "../contexts/contexts";
|
||||
import {Media} from "../contexts/contexts";
|
||||
import {
|
||||
APIButton,
|
||||
BackButton,
|
||||
|
|
@ -18,7 +17,7 @@ import {
|
|||
toLocaleString,
|
||||
useTitle
|
||||
} from "./Common";
|
||||
import {Button, darkTheme, Header, Icon, Segment, Statistic, Tab} from "./Theme";
|
||||
import {Button, ButtonGroup, Group, Header, Loading, Panel, Statistic, StatisticGroup, Tabs} from "./ui";
|
||||
import {BulkTagModal} from "./BulkTagModal";
|
||||
import {TaggedDeleteConfirmModal} from "./TaggedDeleteConfirmModal";
|
||||
import {DeepSearchHint, docMimetypeFilterOptions, FilesView, SearchControlBar} from "./Files";
|
||||
|
|
@ -88,15 +87,15 @@ function DocsPage() {
|
|||
<Button color='violet' disabled={_.isEmpty(selectedDocs)}
|
||||
onClick={() => setBulkTagOpen(true)}>Tag</Button>
|
||||
<APIButton
|
||||
color='red'
|
||||
role='danger'
|
||||
disabled={_.isEmpty(selectedDocs)}
|
||||
confirmButton='Delete'
|
||||
confirmContent='Are you sure you want to delete these documents? This cannot be undone.'
|
||||
onClick={() => onDelete(false)}
|
||||
obeyWROLMode={true}
|
||||
>Delete</APIButton>
|
||||
<Button color='grey' onClick={invertSelection} disabled={_.isEmpty(docs)}>Invert</Button>
|
||||
<Button color='yellow' onClick={clearSelection}
|
||||
<Button role='cancel' onClick={invertSelection} disabled={_.isEmpty(docs)}>Invert</Button>
|
||||
<Button role='cancel' onClick={clearSelection}
|
||||
disabled={_.isEmpty(docs) || _.isEmpty(selectedDocs)}>Clear</Button>
|
||||
<BulkTagModal
|
||||
open={bulkTagOpen}
|
||||
|
|
@ -159,7 +158,6 @@ function DocPage() {
|
|||
const {fileGroupId} = useParams();
|
||||
const navigate = useNavigate();
|
||||
const {docFile, doc, fetchDoc} = useDoc(parseInt(fileGroupId));
|
||||
const {theme} = useContext(ThemeContext);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [taggedFileGroups, setTaggedFileGroups] = useState(null);
|
||||
// Deep-link params: `loc` = EPUB spine index, `page` = PDF page, `q` = search term.
|
||||
|
|
@ -173,7 +171,7 @@ function DocPage() {
|
|||
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
||||
|
||||
if (docFile === null) {
|
||||
return <Segment><Loader active/></Segment>;
|
||||
return <Panel><Loading/></Panel>;
|
||||
}
|
||||
if (docFile === undefined) {
|
||||
return <>
|
||||
|
|
@ -282,72 +280,63 @@ function DocPage() {
|
|||
return mt.split('/').pop().toUpperCase();
|
||||
};
|
||||
|
||||
// About pane.
|
||||
const aboutPane = {
|
||||
menuItem: 'About', render: () => <Tab.Pane>
|
||||
{doc && doc.description && <>
|
||||
<Header as='h3'>Description</Header>
|
||||
<p>{doc.description}</p>
|
||||
</>}
|
||||
// About tab.
|
||||
const aboutTab = <>
|
||||
{doc && doc.description && <>
|
||||
<Header as='h3'>Description</Header>
|
||||
<p>{doc.description}</p>
|
||||
</>}
|
||||
|
||||
{doc && doc.publisher && <>
|
||||
<Header as='h3'>Publisher</Header>
|
||||
<p>{doc.publisher}</p>
|
||||
</>}
|
||||
{doc && doc.publisher && <>
|
||||
<Header as='h3'>Publisher</Header>
|
||||
<p>{doc.publisher}</p>
|
||||
</>}
|
||||
|
||||
{doc && doc.language && <>
|
||||
<Header as='h3'>Language</Header>
|
||||
<p>{doc.language}</p>
|
||||
</>}
|
||||
{doc && doc.language && <>
|
||||
<Header as='h3'>Language</Header>
|
||||
<p>{doc.language}</p>
|
||||
</>}
|
||||
|
||||
{doc && doc.subject && <>
|
||||
<Header as='h3'>Subject</Header>
|
||||
<p><Link to={`/docs?subject=${encodeURIComponent(doc.subject)}`}>{doc.subject}</Link></p>
|
||||
</>}
|
||||
{doc && doc.subject && <>
|
||||
<Header as='h3'>Subject</Header>
|
||||
<p><Link to={`/docs?subject=${encodeURIComponent(doc.subject)}`}>{doc.subject}</Link></p>
|
||||
</>}
|
||||
|
||||
{doc && doc.page_count && <>
|
||||
<Header as='h3'>Page Count</Header>
|
||||
<p>{doc.page_count}</p>
|
||||
</>}
|
||||
{doc && doc.page_count && <>
|
||||
<Header as='h3'>Page Count</Header>
|
||||
<p>{doc.page_count}</p>
|
||||
</>}
|
||||
|
||||
<Header as='h3'>Size</Header>
|
||||
<p>{humanFileSize(size)}</p>
|
||||
</Tab.Pane>
|
||||
};
|
||||
<Header as='h3'>Size</Header>
|
||||
<p>{humanFileSize(size)}</p>
|
||||
</>;
|
||||
|
||||
// Files pane.
|
||||
// Files tab.
|
||||
const tableStyle = {width: '100%', borderCollapse: 'separate', borderSpacing: '0 0.7em'};
|
||||
const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'};
|
||||
const filesPane = {
|
||||
menuItem: 'Files', render: () => <Tab.Pane>
|
||||
<table style={tableStyle}>
|
||||
<tbody>
|
||||
{docFile.files && docFile.files.map((file, idx) =>
|
||||
<tr key={idx}>
|
||||
<td style={labelStyle}>
|
||||
<strong>{file.path.split('.').pop().toUpperCase()}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<PreviewPath path={file.path} mimetype={file.mimetype} taggable={false}>
|
||||
{file.path}
|
||||
</PreviewPath>
|
||||
{file.size && <span style={{marginLeft: '1em', color: 'grey'}}>
|
||||
({humanFileSize(file.size)})
|
||||
</span>}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td style={labelStyle}><strong>Directory</strong></td>
|
||||
<td><DirectoryLink path={directory}/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Tab.Pane>
|
||||
};
|
||||
|
||||
const tabPanes = [aboutPane, filesPane];
|
||||
const tabMenu = theme === darkTheme ? {inverted: true, attached: true} : {attached: true};
|
||||
const filesTab = <table style={tableStyle}>
|
||||
<tbody>
|
||||
{docFile.files && docFile.files.map((file, idx) =>
|
||||
<tr key={idx}>
|
||||
<td style={labelStyle}>
|
||||
<strong>{file.path.split('.').pop().toUpperCase()}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<PreviewPath path={file.path} mimetype={file.mimetype} taggable={false}>
|
||||
{file.path}
|
||||
</PreviewPath>
|
||||
{file.size && <span style={{marginLeft: '1em', color: 'var(--muted)'}}>
|
||||
({humanFileSize(file.size)})
|
||||
</span>}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td style={labelStyle}><strong>Directory</strong></td>
|
||||
<td><DirectoryLink path={directory}/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>;
|
||||
|
||||
return <>
|
||||
<BackButton/>
|
||||
|
|
@ -358,56 +347,51 @@ function DocPage() {
|
|||
<iframe
|
||||
src={embedUrl}
|
||||
title={docFile.title || docFile.name}
|
||||
style={{width: '100%', height: '80vh', border: '1px solid #ccc', borderRadius: '4px'}}
|
||||
style={{width: '100%', height: '80vh', border: '1px solid var(--border)'}}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
<Segment>
|
||||
<Panel>
|
||||
<Header as='h2'>{docFile.title || docFile.name}</Header>
|
||||
|
||||
{docFile.author && <Header as='h3'>
|
||||
Author: <Link to={`/docs?author=${encodeURIComponent(docFile.author)}`}>{docFile.author}</Link>
|
||||
</Header>}
|
||||
|
||||
<Grid columns={2} stackable>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<Header as='h4'>Format: {formatLabel()}{doc && doc.page_count && ` (${doc.page_count} pages)`}</Header>
|
||||
</Grid.Column>
|
||||
{publishedDatetimeString && <Grid.Column>
|
||||
<Header as='h4'>Published: {publishedDatetimeString}</Header>
|
||||
</Grid.Column>}
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
<Group gap='xl' wrap='wrap'>
|
||||
<Header as='h4'>Format: {formatLabel()}{doc && doc.page_count && ` (${doc.page_count} pages)`}</Header>
|
||||
{publishedDatetimeString && <Header as='h4'>Published: {publishedDatetimeString}</Header>}
|
||||
</Group>
|
||||
|
||||
{(() => {
|
||||
const formatButtons = viewableFiles.length > 1 ? <Button.Group>
|
||||
const formatButtons = viewableFiles.length > 1 ? <ButtonGroup>
|
||||
{viewableFiles.map((file, idx) => {
|
||||
const mt = file.mimetype || '';
|
||||
let btnColor;
|
||||
if (mt.startsWith('application/epub')) btnColor = 'yellow';
|
||||
else if (mt === 'application/pdf') btnColor = 'red';
|
||||
const isSelected = idx === selectedFileIndex;
|
||||
return <Button
|
||||
key={file.path}
|
||||
active={idx === selectedFileIndex}
|
||||
onClick={() => setSelectedFileIndex(idx)}
|
||||
color={idx === selectedFileIndex ? btnColor : undefined}
|
||||
basic={idx !== selectedFileIndex}
|
||||
color={isSelected ? btnColor : undefined}
|
||||
variant={isSelected ? 'filled' : 'default'}
|
||||
>
|
||||
{formatLabel(file.mimetype)}
|
||||
</Button>;
|
||||
})}
|
||||
</Button.Group> : null;
|
||||
</ButtonGroup> : null;
|
||||
|
||||
const actionButtons = <>
|
||||
{openUrl && <Button as='a' href={openUrl} target='_blank' rel='noreferrer' color='violet'>
|
||||
<SIcon name='expand arrows alternate'/> Open
|
||||
{openUrl && <Button component='a' href={openUrl} target='_blank' rel='noreferrer'
|
||||
color='violet' icon='expand arrows alternate'>
|
||||
Open
|
||||
</Button>}
|
||||
{downloadUrl && <Button as='a' href={downloadUrl}>
|
||||
<SIcon name='download'/> Download
|
||||
{downloadUrl && <Button component='a' href={downloadUrl} icon='download'>
|
||||
Download
|
||||
</Button>}
|
||||
<APIButton
|
||||
color='red'
|
||||
role='danger'
|
||||
confirmContent='Are you sure you want to delete this document? All files will be deleted.'
|
||||
confirmButton='Delete'
|
||||
onClick={() => handleDelete(false)}
|
||||
|
|
@ -431,13 +415,20 @@ function DocPage() {
|
|||
</Media>
|
||||
</>;
|
||||
})()}
|
||||
</Segment>
|
||||
</Panel>
|
||||
|
||||
<Segment>
|
||||
<Panel>
|
||||
<TagsSelector selectedTagNames={docFile.tags} onAdd={localAddTag} onRemove={localRemoveTag}/>
|
||||
</Segment>
|
||||
</Panel>
|
||||
|
||||
<Tab menu={tabMenu} panes={tabPanes}/>
|
||||
<Tabs defaultValue='about'>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value='about'>About</Tabs.Tab>
|
||||
<Tabs.Tab value='files'>Files</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value='about' pt='md'>{aboutTab}</Tabs.Panel>
|
||||
<Tabs.Panel value='files' pt='md'>{filesTab}</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<TaggedDeleteConfirmModal
|
||||
open={taggedFileGroups !== null}
|
||||
|
|
@ -479,22 +470,16 @@ function AuthorsPage() {
|
|||
const searchInputRef = React.useRef();
|
||||
|
||||
const header = <div style={{marginBottom: '1em'}}>
|
||||
<Grid stackable columns={2}>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<SearchInput
|
||||
placeholder='Author filter...'
|
||||
size='large'
|
||||
searchStr={searchStr}
|
||||
disabled={!Array.isArray(authors) || authors.length === 0}
|
||||
onClear={() => setSearchStr('')}
|
||||
onChange={setSearchStr}
|
||||
onSubmit={null}
|
||||
inputRef={searchInputRef}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
<SearchInput
|
||||
placeholder='Author filter...'
|
||||
size='large'
|
||||
searchStr={searchStr}
|
||||
disabled={!Array.isArray(authors) || authors.length === 0}
|
||||
onClear={() => setSearchStr('')}
|
||||
onChange={setSearchStr}
|
||||
onSubmit={null}
|
||||
inputRef={searchInputRef}
|
||||
/>
|
||||
</div>;
|
||||
|
||||
return <>
|
||||
|
|
@ -517,22 +502,16 @@ function SubjectsPage() {
|
|||
const searchInputRef = React.useRef();
|
||||
|
||||
const header = <div style={{marginBottom: '1em'}}>
|
||||
<Grid stackable columns={2}>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<SearchInput
|
||||
placeholder='Subject filter...'
|
||||
size='large'
|
||||
searchStr={searchStr}
|
||||
disabled={!Array.isArray(subjects) || subjects.length === 0}
|
||||
onClear={() => setSearchStr('')}
|
||||
onChange={setSearchStr}
|
||||
onSubmit={null}
|
||||
inputRef={searchInputRef}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
<SearchInput
|
||||
placeholder='Subject filter...'
|
||||
size='large'
|
||||
searchStr={searchStr}
|
||||
disabled={!Array.isArray(subjects) || subjects.length === 0}
|
||||
onClear={() => setSearchStr('')}
|
||||
onChange={setSearchStr}
|
||||
onSubmit={null}
|
||||
inputRef={searchInputRef}
|
||||
/>
|
||||
</div>;
|
||||
|
||||
return <>
|
||||
|
|
@ -568,45 +547,24 @@ function DocsStatisticsPage() {
|
|||
}, []);
|
||||
|
||||
if (statistics === null) {
|
||||
return <Loader active inline='centered'/>;
|
||||
return <Loading/>;
|
||||
}
|
||||
if (statistics === undefined) {
|
||||
return <ErrorMessage>Unable to fetch Doc Statistics</ErrorMessage>;
|
||||
}
|
||||
|
||||
return <Segment>
|
||||
<Header as='h1' textAlign='center'>Documents</Header>
|
||||
<Statistic.Group>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{toLocaleString(statistics.doc_count)}</StatisticValue>
|
||||
<StatisticLabel>Documents</StatisticLabel>
|
||||
</Statistic>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{toLocaleString(statistics.epub_count)}</StatisticValue>
|
||||
<StatisticLabel>eBooks</StatisticLabel>
|
||||
</Statistic>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{toLocaleString(statistics.pdf_count)}</StatisticValue>
|
||||
<StatisticLabel>PDFs</StatisticLabel>
|
||||
</Statistic>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{toLocaleString(statistics.other_count)}</StatisticValue>
|
||||
<StatisticLabel>Other</StatisticLabel>
|
||||
</Statistic>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{humanFileSize(statistics.total_size)}</StatisticValue>
|
||||
<StatisticLabel>Total Size</StatisticLabel>
|
||||
</Statistic>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{toLocaleString(statistics.author_count)}</StatisticValue>
|
||||
<StatisticLabel>Authors</StatisticLabel>
|
||||
</Statistic>
|
||||
<Statistic style={{margin: '2em'}}>
|
||||
<StatisticValue>{toLocaleString(statistics.subject_count)}</StatisticValue>
|
||||
<StatisticLabel>Subjects</StatisticLabel>
|
||||
</Statistic>
|
||||
</Statistic.Group>
|
||||
</Segment>;
|
||||
return <Panel>
|
||||
<Header as='h1' style={{textAlign: 'center'}}>Documents</Header>
|
||||
<StatisticGroup>
|
||||
<Statistic value={toLocaleString(statistics.doc_count)} label='Documents'/>
|
||||
<Statistic value={toLocaleString(statistics.epub_count)} label='eBooks'/>
|
||||
<Statistic value={toLocaleString(statistics.pdf_count)} label='PDFs'/>
|
||||
<Statistic value={toLocaleString(statistics.other_count)} label='Other'/>
|
||||
<Statistic value={humanFileSize(statistics.total_size)} label='Total Size'/>
|
||||
<Statistic value={toLocaleString(statistics.author_count)} label='Authors'/>
|
||||
<Statistic value={toLocaleString(statistics.subject_count)} label='Subjects'/>
|
||||
</StatisticGroup>
|
||||
</Panel>;
|
||||
}
|
||||
|
||||
export function DocsRoute() {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,5 @@
|
|||
import {useOutdatedZims, useSearchZim, useWROLMode} from "../hooks/customHooks";
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
Divider,
|
||||
Header,
|
||||
Icon,
|
||||
Loader,
|
||||
Modal,
|
||||
Placeholder,
|
||||
Segment
|
||||
} from "./Theme";
|
||||
import {HeadlineText} from "./Headline";
|
||||
import {TextPlaceholder} from "./Placeholder";
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionTitle,
|
||||
Button as SButton,
|
||||
Dropdown,
|
||||
Icon as SIcon,
|
||||
Label,
|
||||
Message,
|
||||
PlaceholderHeader,
|
||||
PlaceholderLine,
|
||||
TableCell,
|
||||
TableRow
|
||||
} from "semantic-ui-react";
|
||||
import React, {useContext, useState} from "react";
|
||||
import React, {useState} from "react";
|
||||
import {
|
||||
APIButton,
|
||||
encodeMediaPath,
|
||||
|
|
@ -43,7 +17,23 @@ import {
|
|||
useTitle,
|
||||
WarningMessage
|
||||
} from "./Common";
|
||||
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Header,
|
||||
Label,
|
||||
Loading,
|
||||
Message,
|
||||
Modal,
|
||||
Panel,
|
||||
Placeholder,
|
||||
Select,
|
||||
Table,
|
||||
toast,
|
||||
} from "./ui";
|
||||
import {TextPlaceholder} from "./Placeholder";
|
||||
import {TagsSelector} from "../Tags";
|
||||
import {AddToPlaylistButton} from "./AddToPlaylist";
|
||||
import {
|
||||
|
|
@ -60,19 +50,17 @@ import {
|
|||
} from "../api";
|
||||
import {useSearch} from "./Search";
|
||||
import {TagsQuerySelector} from "./Files";
|
||||
import {ThemeContext} from "../contexts/contexts";
|
||||
import {Link, Route, Routes} from "react-router";
|
||||
import {SortableTable} from "./SortableTable";
|
||||
import {toast} from "./ui";
|
||||
import _ from "lodash";
|
||||
import {ZIM_VIEWER_URI} from "./Vars";
|
||||
import {HeadlineText} from "./Headline";
|
||||
|
||||
export const OutdatedZimsMessage = ({onClick}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const {outdated, current} = useOutdatedZims();
|
||||
|
||||
const onOpen = () => setOpen(true);
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
|
@ -106,9 +94,7 @@ export const OutdatedZimsMessage = ({onClick}) => {
|
|||
}
|
||||
}
|
||||
|
||||
let modalContent = <Placeholder>
|
||||
<PlaceholderLine/>
|
||||
</Placeholder>;
|
||||
let modalContent = <Placeholder lines={1}/>;
|
||||
if (outdated && outdated.length > 0) {
|
||||
modalContent = <>
|
||||
<Header as='h3'>To Delete</Header>
|
||||
|
|
@ -119,38 +105,29 @@ export const OutdatedZimsMessage = ({onClick}) => {
|
|||
</>
|
||||
}
|
||||
|
||||
return <Message icon info>
|
||||
<SIcon name='question'/>
|
||||
<Message.Content>
|
||||
<Message.Header>Outdated Zim Files</Message.Header>
|
||||
New Zim files have been downloaded. Outdated Zim files can be removed.
|
||||
<p></p>
|
||||
return <Message kind='info' icon='question' title='Outdated Zim Files'>
|
||||
New Zim files have been downloaded. Outdated Zim files can be removed.
|
||||
<p></p>
|
||||
|
||||
<SButton primary onClick={() => setOpen(true)}>Delete</SButton>
|
||||
<Modal closeIcon
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<Modal.Header>Delete</Modal.Header>
|
||||
<Modal.Content>
|
||||
{modalContent}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<APIButton
|
||||
color='red'
|
||||
floated='left'
|
||||
onClick={handleDelete}
|
||||
>Delete</APIButton>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
<Button role='danger' onClick={() => setOpen(true)}>Delete</Button>
|
||||
<Modal open={open}
|
||||
onClose={onClose}
|
||||
title='Delete'
|
||||
>
|
||||
{modalContent}
|
||||
<Modal.Actions>
|
||||
<APIButton
|
||||
role='danger'
|
||||
onClick={handleDelete}
|
||||
>Delete</APIButton>
|
||||
<Button role='cancel' onClick={onClose}>Close</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
<Link to='/files?folders=zims'><SButton>Delete Manually</SButton></Link>
|
||||
<APIButton secondary
|
||||
onClick={handleIgnore}
|
||||
>Ignore Forever</APIButton>
|
||||
</Message.Content>
|
||||
<Link to='/files?folders=zims'><Button role='cancel'>Delete Manually</Button></Link>
|
||||
<APIButton role='cancel'
|
||||
onClick={handleIgnore}
|
||||
>Ignore Forever</APIButton>
|
||||
</Message>
|
||||
}
|
||||
|
||||
|
|
@ -188,42 +165,38 @@ const ZimSearchEntry = ({zimId, onTag, onUntag, entry}) => {
|
|||
{tagIcon}
|
||||
</Header>
|
||||
<HeadlineText headline={headline}/>
|
||||
<Modal closeIcon
|
||||
open={open}
|
||||
<Modal open={open}
|
||||
onClose={() => setOpen(false)}>
|
||||
<Modal.Content>
|
||||
<div className='preview-fit'>
|
||||
<ZimViewer src={url} style={{
|
||||
// Override IframeViewer's `fixed` so the iframe stays inside the Modal.
|
||||
position: 'static',
|
||||
height: '100%', width: '100%', border: 'none',
|
||||
// Use white to avoid iframe displaying with dark-theme.
|
||||
backgroundColor: '#ffffff',
|
||||
}}/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<div className='preview-fit'>
|
||||
<ZimViewer src={url} style={{
|
||||
// Override IframeViewer's `fixed` so the iframe stays inside the Modal.
|
||||
position: 'static',
|
||||
height: '100%', width: '100%', border: 'none',
|
||||
// Use white to avoid iframe displaying with dark-theme.
|
||||
backgroundColor: '#ffffff',
|
||||
}}/>
|
||||
</div>
|
||||
<Modal.Actions>
|
||||
<Grid>
|
||||
<Grid.Column mobile={10} tablet={12}>
|
||||
<TagsSelector selectedTagNames={tag_names} onAdd={localAddTag} onRemove={localUntag}/>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={4} textAlign='right'>
|
||||
<Group justify='space-between' align='flex-start' wrap='wrap' style={{width: '100%'}}>
|
||||
<TagsSelector selectedTagNames={tag_names} onAdd={localAddTag} onRemove={localUntag}/>
|
||||
<Group gap='xs'>
|
||||
<AddToPlaylistButton
|
||||
zim={{zimId, entry: path, title: (title || path).replace(/<[^>]*>/g, '')}}
|
||||
content={null} title='Add to Playlist'/>
|
||||
<Button color='blue' as='a' href={url} target='_blank' rel='noopener noreferrer'>Open</Button>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
<Button role='primary' component='a' href={url} target='_blank' rel='noopener noreferrer'>
|
||||
Open
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
</div>
|
||||
}
|
||||
|
||||
const ZimAccordion = ({data, index, activeIndex, onClick, searchStr, activeTags}) => {
|
||||
const ZimAccordionItem = ({value, active, data, searchStr, activeTags}) => {
|
||||
const {id, estimate, metadata} = data;
|
||||
const {title, date} = metadata;
|
||||
const {zim, fetchSearch, pages, loading} = useSearchZim(searchStr, id, index === activeIndex, activeTags);
|
||||
const {t} = useContext(ThemeContext);
|
||||
const {zim, fetchSearch, pages, loading} = useSearchZim(searchStr, id, active, activeTags);
|
||||
|
||||
const localAddTag = async (zimId, path, name) => {
|
||||
await tagZimEntry(zimId, path, name);
|
||||
|
|
@ -239,11 +212,14 @@ const ZimAccordion = ({data, index, activeIndex, onClick, searchStr, activeTags}
|
|||
if (zim && !loading) {
|
||||
const {search} = zim;
|
||||
if (search && search.length > 0) {
|
||||
body = search.map(i => <Segment vertical key={i['path']}>
|
||||
body = search.map(i => <div
|
||||
key={i['path']}
|
||||
style={{borderBottom: '1px solid var(--border)', paddingBottom: '1em', marginBottom: '1em'}}
|
||||
>
|
||||
<ZimSearchEntry zimId={id} onTag={localAddTag} onUntag={localUntag} entry={i}/>
|
||||
</Segment>);
|
||||
</div>);
|
||||
} else {
|
||||
body = <p {...t}>No results</p>;
|
||||
body = <p>No results</p>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,69 +229,52 @@ const ZimAccordion = ({data, index, activeIndex, onClick, searchStr, activeTags}
|
|||
|
||||
const label = <Label color={estimate > 0 ? 'violet' : undefined}>{normalizeEstimate(estimate)}</Label>;
|
||||
|
||||
return <React.Fragment>
|
||||
<AccordionTitle
|
||||
index={index}
|
||||
active={index === activeIndex}
|
||||
onClick={() => onClick(index, activeIndex)}
|
||||
>
|
||||
<Header as='h3'>
|
||||
<Icon name='dropdown'/> {title} {label}
|
||||
</Header>
|
||||
</AccordionTitle>
|
||||
<AccordionContent active={index === activeIndex}>
|
||||
return <Accordion.Item value={value}>
|
||||
<Accordion.Control>
|
||||
<Header as='h3' style={{marginBottom: 0}}>{title} {label}</Header>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Header as='h4'>{date}</Header>
|
||||
{body}
|
||||
{paginator}
|
||||
</AccordionContent>
|
||||
</React.Fragment>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
}
|
||||
|
||||
const ZimsRefreshWarning = () => {
|
||||
return <Message icon warning onClick={refreshFiles}>
|
||||
<SIcon name='hand point right'/>
|
||||
<Message.Content>
|
||||
<Message.Header>No Zims have been indexed.</Message.Header>
|
||||
return <div onClick={refreshFiles} style={{cursor: 'pointer'}}>
|
||||
<Message kind='warning' icon='hand point right' title='No Zims have been indexed.'>
|
||||
<a href='#'>Click here</a> to refresh all your files.
|
||||
</Message.Content>
|
||||
</Message>;
|
||||
</Message>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export const ZimSearchView = ({suggestions, loading}) => {
|
||||
const [activeIndex, setActiveIndex] = React.useState(null);
|
||||
const [activeValue, setActiveValue] = React.useState(null);
|
||||
const {searchStr, activeTags, setTags} = useSearch();
|
||||
const {zimsEstimates} = suggestions;
|
||||
|
||||
const handleClick = (index, activeIndex_) => {
|
||||
setActiveIndex(index === activeIndex_ ? -1 : index);
|
||||
}
|
||||
|
||||
let body;
|
||||
if (!_.isEmpty(zimsEstimates)) {
|
||||
body = zimsEstimates.map((i, index) => <ZimAccordion
|
||||
key={i['path']}
|
||||
index={index}
|
||||
activeIndex={activeIndex}
|
||||
data={i}
|
||||
searchStr={searchStr}
|
||||
activeTags={activeTags}
|
||||
onClick={handleClick}
|
||||
/>);
|
||||
body = <Accordion value={activeValue} onChange={setActiveValue}>
|
||||
{zimsEstimates.map(i => <ZimAccordionItem
|
||||
key={i['path']}
|
||||
value={String(i['id'])}
|
||||
active={activeValue === String(i['id'])}
|
||||
data={i}
|
||||
searchStr={searchStr}
|
||||
activeTags={activeTags}
|
||||
/>)}
|
||||
</Accordion>;
|
||||
} else if (loading) {
|
||||
body = <AccordionContent>
|
||||
<Segment placeholder>
|
||||
<Loader active={true}/>
|
||||
</Segment>
|
||||
</AccordionContent>;
|
||||
body = <Panel><Loading/></Panel>;
|
||||
} else if (_.isEmpty(zimsEstimates)) {
|
||||
body = <ZimsRefreshWarning/>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<TagsQuerySelector onChange={(i, j) => setTags(i)}/>
|
||||
<Accordion>
|
||||
{body}
|
||||
</Accordion>
|
||||
{body}
|
||||
</>
|
||||
}
|
||||
|
||||
|
|
@ -369,39 +328,36 @@ const ZimCatalogItemRow = ({item, subscriptions, iso_639_codes, fetchSubscriptio
|
|||
}
|
||||
}
|
||||
|
||||
const handleLanguageChange = (e, {value}) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
setLanguage(value);
|
||||
}
|
||||
|
||||
const languageOptions = languages.map(i => {
|
||||
return {key: i, value: i, text: iso_639_codes[i]}
|
||||
return {value: i, label: iso_639_codes[i]}
|
||||
});
|
||||
const languageDropdown = <Dropdown fluid search selection
|
||||
placeholder='Language'
|
||||
options={languageOptions}
|
||||
value={language}
|
||||
disabled={wrolModeEnabled}
|
||||
onChange={handleLanguageChange}
|
||||
const languageDropdown = <Select
|
||||
searchable
|
||||
placeholder='Language'
|
||||
data={languageOptions}
|
||||
value={language}
|
||||
disabled={wrolModeEnabled}
|
||||
onChange={(value) => setLanguage(value)}
|
||||
/>;
|
||||
|
||||
const isDestructive = subscription && !languageChange;
|
||||
const subscribeButton = <APIButton
|
||||
color='grey'
|
||||
role={isDestructive ? 'danger' : 'primary'}
|
||||
confirmContent={isDestructive ? 'Are you sure you want to unsubscribe from this Zim?' : undefined}
|
||||
confirmButton={isDestructive ? 'Unsubscribe' : undefined}
|
||||
disabled={pending}
|
||||
onClick={handleButton}
|
||||
obeyWROLMode={true}
|
||||
>
|
||||
{subscription && !languageChange ? 'Unsubscribe' : 'Subscribe'}
|
||||
{isDestructive ? 'Unsubscribe' : 'Subscribe'}
|
||||
</APIButton>
|
||||
|
||||
return <TableRow key={name}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell>{languageDropdown}</TableCell>
|
||||
<TableCell>{subscribeButton}</TableCell>
|
||||
<TableCell>{humanFileSize(size)}</TableCell>
|
||||
</TableRow>
|
||||
return <Table.Row key={name}>
|
||||
<Table.Cell>{name}</Table.Cell>
|
||||
<Table.Cell>{languageDropdown}</Table.Cell>
|
||||
<Table.Cell>{subscribeButton}</Table.Cell>
|
||||
<Table.Cell>{humanFileSize(size)}</Table.Cell>
|
||||
</Table.Row>
|
||||
}
|
||||
|
||||
class ManageZim extends React.Component {
|
||||
|
|
@ -458,11 +414,11 @@ class ManageZim extends React.Component {
|
|||
popupContent='Enable/Disable searching this Zim file in the Global Search.'
|
||||
/>
|
||||
|
||||
return <TableRow key={path}>
|
||||
<TableCell>{path}</TableCell>
|
||||
<TableCell>{humanFileSize(size)}</TableCell>
|
||||
<TableCell>{toggle}</TableCell>
|
||||
</TableRow>
|
||||
return <Table.Row key={path}>
|
||||
<Table.Cell>{path}</Table.Cell>
|
||||
<Table.Cell>{humanFileSize(size)}</Table.Cell>
|
||||
<Table.Cell>{toggle}</Table.Cell>
|
||||
</Table.Row>
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
@ -473,15 +429,9 @@ class ManageZim extends React.Component {
|
|||
{key: 'size', text: 'Size', sortBy: 'size', width: 2},
|
||||
{key: 'search', text: 'Search', sortBy: 'auto_search', width: 2},
|
||||
];
|
||||
let zimFilesBody = <Placeholder>
|
||||
<PlaceholderHeader>
|
||||
<PlaceholderLine/>
|
||||
<PlaceholderLine/>
|
||||
</PlaceholderHeader>
|
||||
</Placeholder>;
|
||||
let zimFilesBody = <Placeholder lines={2}/>;
|
||||
if (zims && zims.length >= 1) {
|
||||
zimFilesBody = <SortableTable
|
||||
tableProps={{striped: true}}
|
||||
data={zims}
|
||||
rowFunc={(i, sortData) => this.zimFileTableRow(i, sortData, this.fetchZims.bind(this))}
|
||||
rowKey='path'
|
||||
|
|
@ -505,12 +455,7 @@ class ManageZim extends React.Component {
|
|||
{key: 'subscription', text: 'Subscription', 'sortBy': null, width: 2},
|
||||
{key: 'size', text: 'Maximum Size', sortBy: 'size', width: 2},
|
||||
];
|
||||
let kiwixCatalog = <Placeholder>
|
||||
<PlaceholderHeader>
|
||||
<PlaceholderLine/>
|
||||
<PlaceholderLine/>
|
||||
</PlaceholderHeader>
|
||||
</Placeholder>;
|
||||
let kiwixCatalog = <Placeholder lines={2}/>;
|
||||
if (catalog && catalog.length > 0) {
|
||||
kiwixCatalog = <SortableTable
|
||||
defaultSortColumn='name'
|
||||
|
|
@ -544,7 +489,7 @@ class ManageZim extends React.Component {
|
|||
}
|
||||
|
||||
function ZimViewer({src = ZIM_VIEWER_URI, style = null}) {
|
||||
const fallback = <Segment>
|
||||
const fallback = <Panel>
|
||||
<Header as='h3'>Failed to fetch Zim service.</Header>
|
||||
<p>You may need to give permission to access the page: <a href={src}>{src}</a></p>
|
||||
|
||||
|
|
@ -553,7 +498,7 @@ function ZimViewer({src = ZIM_VIEWER_URI, style = null}) {
|
|||
|
||||
<p>Check the logs</p>
|
||||
<pre>journalctl -u wrolpi-kiwix</pre>
|
||||
</Segment>;
|
||||
</Panel>;
|
||||
|
||||
return <IframeViewer title='zim' src={src} fallback={fallback} style={style}/>
|
||||
}
|
||||
|
|
@ -573,4 +518,4 @@ export function ZimRoute() {
|
|||
<Route path='manage' exact element={<ManageZim/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue