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.
123 lines
4.5 KiB
JavaScript
123 lines
4.5 KiB
JavaScript
import React from 'react';
|
|
import {Link, Route, Routes} from "react-router";
|
|
import {
|
|
ErrorMessage,
|
|
humanFileSize,
|
|
humanNumber,
|
|
mimetypeColor,
|
|
PageContainer,
|
|
toLocaleString,
|
|
useTitle
|
|
} from "./Common";
|
|
import {Header, Label, Loading, Panel, Statistic, StatisticGroup, TextInput} from "./ui";
|
|
import {useStatistics} from "../hooks/customHooks";
|
|
import {CalculatorsPage} from "./Calculators";
|
|
|
|
function StatisticsPage() {
|
|
useTitle('Statistics');
|
|
|
|
const {statistics} = useStatistics();
|
|
|
|
if (statistics === undefined) {
|
|
return <>
|
|
<Header as='h1'>Statistics</Header>
|
|
<ErrorMessage>Failed to fetch statistics</ErrorMessage>
|
|
</>
|
|
}
|
|
|
|
if (statistics['global_statistics']) {
|
|
let {global_statistics, file_statistics} = statistics;
|
|
const {
|
|
archive_count,
|
|
audio_count,
|
|
ebook_count,
|
|
image_count,
|
|
pdf_count,
|
|
total_count,
|
|
video_count,
|
|
zip_count,
|
|
total_size,
|
|
tagged_files,
|
|
tagged_zims,
|
|
tags_count,
|
|
} = file_statistics;
|
|
const {db_size} = global_statistics;
|
|
return <>
|
|
<Header as='h1'>Statistics</Header>
|
|
<Header as='h2'>Files</Header>
|
|
<Panel>
|
|
<StatisticGroup>
|
|
<Statistic value={toLocaleString(total_count)} label='All Files'/>
|
|
<Statistic value={humanFileSize(total_size)} label='Total Size'/>
|
|
</StatisticGroup>
|
|
</Panel>
|
|
<Panel>
|
|
<StatisticGroup>
|
|
<Link to={'/videos/statistics'}>
|
|
<Statistic color={mimetypeColor('video/')} value={toLocaleString(video_count)}
|
|
label='Videos'/>
|
|
</Link>
|
|
<Statistic color={mimetypeColor('application/pdf')} value={toLocaleString(pdf_count)}
|
|
label='PDFs'/>
|
|
<Statistic color={mimetypeColor('application/epub')} value={toLocaleString(ebook_count)}
|
|
label='eBooks'/>
|
|
<Statistic color={mimetypeColor('text/html')} value={toLocaleString(archive_count)}
|
|
label='Archives'/>
|
|
<Statistic color={mimetypeColor('image/')} value={toLocaleString(image_count)}
|
|
label='Images'/>
|
|
</StatisticGroup>
|
|
</Panel>
|
|
<Panel>
|
|
<StatisticGroup>
|
|
<Statistic value={toLocaleString(zip_count)} label='ZIP'/>
|
|
<Statistic color={mimetypeColor('audio/')} value={toLocaleString(audio_count)} label='Audio'/>
|
|
</StatisticGroup>
|
|
</Panel>
|
|
|
|
<Header as='h2'>Tags</Header>
|
|
<Panel>
|
|
<StatisticGroup>
|
|
<Statistic value={humanNumber(tags_count)} label='Tags'/>
|
|
<Statistic value={humanNumber(tagged_files)} label='Tagged Files'/>
|
|
<Statistic value={humanNumber(tagged_zims)} label='Tagged Zims'/>
|
|
</StatisticGroup>
|
|
</Panel>
|
|
|
|
<Header as='h2'>Database</Header>
|
|
<Panel>
|
|
<StatisticGroup>
|
|
<Statistic value={humanFileSize(db_size)} label='Size'/>
|
|
</StatisticGroup>
|
|
</Panel>
|
|
</>;
|
|
}
|
|
|
|
return <>
|
|
<Header as='h1'>Statistics</Header>
|
|
<Panel><Loading/></Panel>
|
|
</>;
|
|
|
|
}
|
|
|
|
export function MoreRoute(props) {
|
|
return <PageContainer>
|
|
<Routes>
|
|
<Route path='calculators' element={<CalculatorsPage/>}/>
|
|
<Route path='statistics' exact element={<StatisticsPage/>}/>
|
|
</Routes>
|
|
</PageContainer>
|
|
}
|
|
|
|
/**
|
|
* A text input with a colored tag attached to its leading (or trailing) edge.
|
|
* Replaces Semantic's `<Input label={<Label color={...}/>} labelPosition=.../>`.
|
|
*/
|
|
export function ColoredInput({name, value, label, color, labelPosition = 'left', fluid, style, ...props}) {
|
|
const labelNode = label ? <Label color={color || 'grey'}>{label}</Label> : null;
|
|
|
|
return <div style={{display: 'flex', alignItems: 'stretch', gap: 6, width: fluid ? '100%' : undefined, ...style}}>
|
|
{labelNode && labelPosition === 'left' && labelNode}
|
|
<TextInput name={name} value={value} style={{flex: fluid ? 1 : undefined}} {...props}/>
|
|
{labelNode && labelPosition !== 'left' && labelNode}
|
|
</div>
|
|
}
|