mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Phase 2, part 3. Baseline 63 -> 50.
Common.js is the one that mattered: 51 files import it, so its Semantic usage
was propping up the rest of the migration. Everything in it now comes from the
library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the
toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer,
CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons.
Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist,
TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory
components.
New in the library, all in the gallery and tested:
SearchBox Semantic's `<Search category>` with its grouped results shape
kept, because four call sites already build it. A real
combobox rather than a div that listens for clicks.
Pagination Semantic's prop names; onPageChange gets the page number.
TabBar the strip only -- routing stays with the caller.
IconStack Semantic's IconGroup, named once for assistive technology.
Table.HeaderCell sorted/onSort -- a real button carrying aria-sort.
Button/IconButton translate Semantic's size names.
That last one is defensive: unmigrated callers still pass size='tiny', and
Mantine drops a size it does not recognise without a word, so the button
quietly renders at the wrong size. Mapping here keeps the mixed state honest.
Three real bugs found and fixed on the way, none of them cosmetic:
* `fileSuffixIconName` returned fourteen icon names the map did not have, so
opening a folder with a PDF, a Word document or an archive in it logged an
error and drew a fallback glyph. All fourteen mapped, with a test that
walks the mapper's own return values, and another asserting every mapped
name resolves to a Tabler component that exists.
* `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on
the dashboard the load averages ran together into "1 MIN. LOAD5 MIN.
LOAD15 MIN. LOAD". It carries its own spacing now.
* Semantic's inverted stylesheet colors every descendant of an inverted
segment, so Toggle labels inherited #dddddd -- white pixels in night mode,
on the Settings page, which is exactly where someone adjusts their WROLPi
in the dark. Restated from tokens.
The baseline ratchet gained a third assertion: a migrated file may still lean
on Semantic's CSS classes without importing the library, which the import check
cannot see and which would collapse silently when Phase 4 deletes the
stylesheet. There is one such bridge today and it is now declared.
120 lines
3.9 KiB
JavaScript
120 lines
3.9 KiB
JavaScript
import React from "react";
|
|
import {downloadArchiveMember, getArchiveContents} from "../api";
|
|
import {fileSuffixIconName, humanFileSize} from "./Common";
|
|
import {Icon, Message, Modal, Panel, Placeholder, Table} from "./ui";
|
|
|
|
function ArchiveEntry({entry, depth, archivePath}) {
|
|
const [expanded, setExpanded] = React.useState(depth === 0);
|
|
|
|
const indent = '\xa0\xa0\xa0\xa0'.repeat(depth);
|
|
|
|
if (entry.is_dir) {
|
|
const folderRows = expanded && entry.children ? entry.children.map((child, i) =>
|
|
<ArchiveEntry key={child.path || i} entry={child} depth={depth + 1} archivePath={archivePath}/>
|
|
) : null;
|
|
|
|
return <React.Fragment>
|
|
<Table.Row onClick={() => setExpanded(!expanded)} style={{cursor: 'pointer'}}>
|
|
<Table.Cell className='file-path'>
|
|
{indent}
|
|
<Icon name={expanded ? 'folder open' : 'folder'}/>
|
|
{' '}{entry.name}/
|
|
</Table.Cell>
|
|
<Table.Cell numeric>
|
|
—
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
{folderRows}
|
|
</React.Fragment>
|
|
}
|
|
|
|
const handleDownload = () => {
|
|
downloadArchiveMember(archivePath, entry.path);
|
|
};
|
|
|
|
return <Table.Row>
|
|
<Table.Cell className='file-path' onClick={handleDownload} style={{cursor: 'pointer'}}>
|
|
{indent}
|
|
<Icon name={fileSuffixIconName(entry.name)}/>
|
|
{' '}{entry.name}
|
|
{' '}
|
|
<Icon name='download' size='small'/>
|
|
</Table.Cell>
|
|
<Table.Cell numeric>
|
|
{humanFileSize(entry.size)}
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
}
|
|
|
|
function SkeletonRows() {
|
|
return <React.Fragment>
|
|
{[1, 2, 3].map(i =>
|
|
<Table.Row key={i}>
|
|
<Table.Cell colSpan={2}>
|
|
<Placeholder lines={1}/>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
)}
|
|
</React.Fragment>
|
|
}
|
|
|
|
export function ArchivePreviewContent({previewFile}) {
|
|
const [contents, setContents] = React.useState(null);
|
|
const [loading, setLoading] = React.useState(true);
|
|
const [error, setError] = React.useState(null);
|
|
|
|
const path = previewFile['primary_path'] || previewFile['path'];
|
|
|
|
React.useEffect(() => {
|
|
const fetchContents = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const result = await getArchiveContents(path);
|
|
if (result) {
|
|
setContents(result);
|
|
} else {
|
|
setError('Could not read archive contents.');
|
|
}
|
|
} catch (e) {
|
|
setError(e.message || 'Could not read archive contents.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchContents();
|
|
}, [path]);
|
|
|
|
if (error) {
|
|
return <Modal.Content>
|
|
<Message kind='error' title='Cannot read archive'>
|
|
<p>{error}</p>
|
|
</Message>
|
|
</Modal.Content>
|
|
}
|
|
|
|
return <Modal.Content scrolling>
|
|
{!loading && contents && <Panel>
|
|
<Icon name='archive'/>
|
|
{' '}
|
|
<strong>{contents.total_files}</strong> files,{' '}
|
|
<strong>{humanFileSize(contents.total_size)}</strong> total
|
|
</Panel>}
|
|
<Table>
|
|
<Table.Header>
|
|
<Table.Row>
|
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
<Table.HeaderCell>Size</Table.HeaderCell>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body>
|
|
{loading
|
|
? <SkeletonRows/>
|
|
: contents && contents.entries.map((entry, i) =>
|
|
<ArchiveEntry key={entry.path || i} entry={entry} depth={0} archivePath={path}/>
|
|
)
|
|
}
|
|
</Table.Body>
|
|
</Table>
|
|
</Modal.Content>
|
|
}
|