wrolpi/app/src/components/Archive.js

360 lines
11 KiB
JavaScript

import React, {useState} from "react";
import {Card, Confirm, Container, Icon, Image, Placeholder, Tab} from "semantic-ui-react";
import Paginator, {ClearButton, ExternalLink, SearchInput, textEllipsis, uploadDate} from "./Common";
import {deleteArchive, postDownload, refreshArchives} from "../api";
import Button from "semantic-ui-react/dist/commonjs/elements/Button";
import {Link, NavLink} from "react-router-dom";
import {ArchivePlaceholder} from "./Placeholder";
import Table from "semantic-ui-react/dist/commonjs/collections/Table";
import {useArchives} from "../hooks/useArchives";
import {useDomains} from "../hooks/useDomains";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
function FailedArchiveCard({archive, syncArchive, deleteArchive}) {
let syncIcon = (
<Button onClick={syncArchive}>
<Icon name='sync' size='big'/>
</Button>
);
let trashIcon = (
<Button onClick={deleteArchive}>
<Icon name='trash' size='big'/>
</Button>
);
let externalIcon = (
<a href={archive.url} target='_blank' rel='noopener noreferrer'>
<Icon name='sign-out' size='big'/>
</a>
);
return (
<Card>
<Card.Content>
<Card.Header>
{archive.url}
</Card.Header>
<Card.Description>
<p>Failed!</p>
{syncIcon}
{trashIcon}
{externalIcon}
</Card.Description>
</Card.Content>
</Card>
);
}
function ArchiveCard({archive, syncArchive, deleteArchive}) {
const [syncOpen, setSyncOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const localSyncArchive = async () => {
setSyncOpen(false);
await syncArchive(archive.url);
}
const localDeleteArchive = async () => {
setDeleteOpen(false);
await deleteArchive(archive.id);
}
let imageSrc = archive.screenshot_path ? `/media/${archive.screenshot_path}` : null;
let singlefileUrl = archive.singlefile_path ? `/media/${archive.singlefile_path}` : null;
if (archive.status === 'failed') {
return <FailedArchiveCard archive={archive} syncArchive={localSyncArchive} deleteArchive={localDeleteArchive}/>;
}
let readabilityUrl = archive.readability_path ? `/media/${archive.readability_path}` : null;
let readabilityIcon = <Button icon disabled><Icon name='book' size='large'/></Button>;
if (readabilityUrl) {
readabilityIcon = (
<Button icon href={readabilityUrl} target='_blank' rel='noopener noreferrer'>
<Icon name='book' size='large'/>
</Button>);
}
let syncIcon = null;
if (syncArchive) {
syncIcon = (<>
<Button icon onClick={() => setSyncOpen(true)}>
<Icon name='sync' size='large'/>
</Button>
<Confirm
open={syncOpen}
content='Download the latest version of this URL?'
confirmButton='Confirm'
onCancel={() => setSyncOpen(false)}
onConfirm={localSyncArchive}
/>
</>);
}
let deleteIcon = null;
if (deleteArchive) {
deleteIcon = (<>
<Button icon onClick={() => setDeleteOpen(true)}>
<Icon name='trash' size='large'/>
</Button>
<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}
/>
</>
);
}
let externalIcon = (
<Button icon href={archive.url} target='_blank' rel='noopener noreferrer'>
<Icon name='sign-out' size='large'/>
</Button>
);
return (
<Card>
<ExternalLink to={singlefileUrl} className='no-link-underscore card-link'>
<Image src={imageSrc} wrapped style={{position: 'relative', width: '100%'}}/>
</ExternalLink>
<Card.Content>
<Card.Header>
<Container textAlign='left'>
<ExternalLink to={singlefileUrl} className='no-link-underscore card-link'>
<p>{textEllipsis(archive.title || archive.url, 100)}</p>
</ExternalLink>
</Container>
</Card.Header>
<Card.Meta>
{uploadDate(archive.archive_datetime)}
</Card.Meta>
<Card.Description>
<Container textAlign='left'>
{readabilityIcon}
{syncIcon}
{deleteIcon}
{externalIcon}
</Container>
</Card.Description>
</Card.Content>
</Card>
)
}
function ArchiveCards({archives, syncArchive, deleteArchive}) {
return (
<Card.Group>
{archives.map((i) => {
return <ArchiveCard key={i['id']} archive={i} syncArchive={syncArchive} deleteArchive={deleteArchive}/>
})}
</Card.Group>
)
}
export function ArchivesList({archives, searchStr, syncArchive, localDeleteArchive}) {
if (archives === null || archives === undefined) {
// Fetching archives.
return <ArchivePlaceholder/>;
} else if (archives.length === 0 && searchStr) {
// Search with no results.
return <p>No archives found! Is your search too restrictive?</p>;
} else if (archives.length === 0) {
// No archives fetched.
return <Message>
<Message.Header>No archives</Message.Header>
<Message.Content>Have you archived any webpages?</Message.Content>
</Message>;
} else {
// Archives fetched successfully!
return <ArchiveCards archives={archives} syncArchive={syncArchive} deleteArchive={localDeleteArchive}/>;
}
}
export function Archives() {
const {archivesData, setPage, totalPages, searchStr, activePage, setSearchStr, domain, setDomain, search} =
useArchives();
const {archives} = archivesData;
const syncArchive = async (url) => {
await postDownload(url, 'archive');
await search();
}
const localDeleteArchive = async (archive_id) => {
await deleteArchive(archive_id);
await search();
}
let domainClearButton = null;
if (domain) {
domainClearButton = <ClearButton
onClick={() => setDomain(null)}
style={{marginBottom: '1em'}}
label={`Domain: ${domain}`}
/>;
}
return (
<>
<SearchInput initValue={searchStr} onSubmit={setSearchStr}/>
{domainClearButton}
<ArchivesList
archives={archives}
searchStr={searchStr}
syncArchive={syncArchive}
localDeleteArchive={localDeleteArchive}
/>
<div style={{marginTop: '3em', textAlign: 'center'}}>
<Paginator
activePage={activePage}
changePage={setPage}
totalPages={totalPages}
/>
</div>
</>
)
}
class ManageArchives extends React.Component {
async refresh() {
await refreshArchives();
}
render() {
return (<>
<Button secondary
onClick={this.refresh}>
Refresh Archive Files
</Button>
</>)
}
}
function Domains() {
const [domains] = useDomains();
const row = (domain) => {
return <Table.Row key={domain['domain']}>
<Table.Cell>
<Link to={`/archive?domain=${domain['domain']}`}>
{domain['domain']}
</Link>
</Table.Cell>
<Table.Cell>{domain['url_count']}</Table.Cell>
</Table.Row>
}
if (domains && domains.length > 0) {
return (
<>
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Domain</Table.HeaderCell>
<Table.HeaderCell>Archives</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{domains.map(row)}
</Table.Body>
</Table>
</>
)
}
if (domains === null) {
return (<>
<Placeholder>
<Placeholder.Header>
<Placeholder.Line/>
<Placeholder.Line/>
</Placeholder.Header>
</Placeholder>
</>)
}
return (<>
<Message>
<Message.Header>No domains yet.</Message.Header>
<Message.Content>Archive some webpages!</Message.Content>
</Message>
</>)
}
export class ArchiveRoute extends React.Component {
constructor(props) {
super(props);
this.panesTos = ['/archive', '/archive/domains', '/archive/manage'];
this.state = {
activeIndex: this.matchPaneTo(),
}
this.archivePanes = [
{
menuItem: {
as: NavLink,
content: 'Archives',
id: 'archive',
to: '/archive',
exact: true,
key: 'home',
},
render: () => <Tab.Pane><Archives/></Tab.Pane>
},
{
menuItem: {
as: NavLink,
content: 'Domains',
id: 'domains',
to: '/archive/domains',
exact: true,
key: 'domains',
},
render: () => <Tab.Pane><Domains/></Tab.Pane>
},
{
menuItem: {
as: NavLink,
content: 'Manage',
id: 'manage',
to: '/archive/manage',
exact: true,
key: 'manage',
},
render: () => <Tab.Pane><ManageArchives/></Tab.Pane>
},
];
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.location.pathname !== this.props.location.pathname) {
this.setState({activeIndex: this.matchPaneTo()});
}
}
matchPaneTo = () => {
return this.panesTos.indexOf(this.props.location.pathname);
}
render() {
return (
<Container fluid>
<Tab
panes={this.archivePanes}
activeIndex={this.state.activeIndex}
renderActiveOnly={true}
/>
</Container>
)
}
}