merge: Docs and Zim off Semantic

This commit is contained in:
Roland 2026-07-30 13:10:49 -06:00
commit cd37c9920a
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 237 additions and 334 deletions

View file

@ -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() {

View file

@ -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>
}
}