}
return null;
}
// Thanks https://www.npmjs.com/package/text-ellipsis
export function textEllipsis(str, maxLength = 100, {side = "end", ellipsis = "..."} = {}) {
if (str && str.length > maxLength) {
switch (side) {
case "start":
return ellipsis + str.slice(-(maxLength - ellipsis.length));
case "end":
default:
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
}
}
return str;
}
export function TabLinks({links}) {
return
}
export function PageContainer(props) {
return <>
{props.children}
{props.children}
>;
}
export function CardGroupCentered(props) {
return
{props.children}
{props.children}
}
export function CardPosterLink({to, poster_url, imageLabel, external = false}) {
const {s} = useContext(ThemeContext);
const style = {display: 'flex', justifyContent: 'center', ...s['style']};
const image =
;
if (external === true && to) {
return
{image}
} else if (to) {
// Link using React Router.
return
{image}
} else {
return {image}
;
}
}
export function HelpPopup({icon, size, content, position}) {
return }
/>
}
export function HelpHeader({icon, headerSize, iconSize, headerContent, popupContent}) {
return
}
export function HotspotToggle() {
let {on, setHotspot} = useHotspot();
const disabled = on === null;
return
setHotspot(checked)}
/>
{disabled && }
;
}
export function ThrottleToggle() {
let {on, setThrottle} = useThrottle();
const disabled = on === null;
return
setThrottle(checked)}
/>
{disabled && }
;
}
export function Toggle({label, checked, disabled, onChange, icon}) {
// Custom toggle because Semantic UI does not handle inverted labels correctly.
const {t, inverted} = useContext(ThemeContext);
let style = {marginLeft: '1em'};
if (t && t.style) {
style = {...t.style, ...style};
}
disabled = disabled === true ? 'disabled' : '';
let inputClassName = `${disabled} ${inverted}`;
let sliderClassName = `${disabled} ${inverted} slider`;
if (inverted) {
style['color'] = '#dddddd';
}
if (disabled && inverted) {
style['color'] = '#888888';
}
let onMouseUp = () => {
};
if (onChange) {
onMouseUp = (e) => {
if (disabled) {
return;
}
e.preventDefault();
if (onChange) {
onChange(!checked);
}
}
}
if (icon) {
icon =
}
return <>
{icon}
{label}
>
}
export function DisableDownloadsToggle() {
let {on, setDownloads} = useDownloaders();
return ;
}
export function emptyToNull(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (obj[key] === '') {
obj[key] = null;
}
}
return obj;
}
export function mimetypeColor(mimetype) {
if (!mimetype) {
return 'grey';
}
try {
if (mimetype === 'application/pdf') {
return 'red'
} else if (mimetype.startsWith('video/')) {
return 'blue'
} else if (mimetype.startsWith('image/')) {
return 'pink'
} else if (mimetype.startsWith('text/html')) {
return 'green'
} else if (mimetype.startsWith('application/zip') || mimetype.startsWith('application/zlib') || mimetype.startsWith('application/x-7z-compressed') || mimetype.startsWith('application/x-bzip2') || mimetype.startsWith('application/x-xz')) {
return 'purple'
} else if (mimetype.startsWith('application/epub') || mimetype.startsWith('application/x-mobipocket-ebook')) {
return 'yellow'
} else if (mimetype.startsWith('audio/')) {
return 'violet'
}
} catch (e) {
console.error(e);
console.error('Unable to choose mimetype color');
}
return 'grey'
}
export function isZipMimetype(mimetype) {
return mimetype.startsWith('application/zip') || mimetype.startsWith('application/zlib') || mimetype.startsWith('application/x-7z-compressed') || mimetype.startsWith('application/x-bzip2') || mimetype.startsWith('application/x-xz') || mimetype.startsWith('application/gzip') || mimetype.startsWith('application/x-rar');
}
export function FileIcon({file, disabled = true, size = 'huge', ...props}) {
// Default to a grey file icon.
const {mimetype, path} = file;
const lowerPath = path.toLowerCase();
props['name'] = 'file';
props['color'] = mimetypeColor(mimetype);
if (mimetype) {
if (mimetype.startsWith('text/html') || mimetype.startsWith('application/json') || mimetype.startsWith('text/yaml') || mimetype.startsWith('text/xml')) {
props['name'] = 'file code';
} else if (mimetype.startsWith('application/pdf')) {
props['name'] = 'file pdf';
} else if (mimetype.startsWith('text/plain')) {
props['name'] = 'file text';
} else if (mimetype.startsWith('image/')) {
props['name'] = 'image';
} else if (mimetype.startsWith('video/')) {
props['name'] = 'film';
} else if (mimetype.startsWith('message/rfc822')) {
props['name'] = 'mail';
} else if (isZipMimetype(mimetype)) {
props['name'] = 'file archive';
} else if (mimetype.startsWith('application/x-iso9660-image')) {
props['name'] = 'dot circle';
} else if (mimetype.startsWith('application/epub') || mimetype.startsWith('application/x-mobipocket-ebook') || mimetype.startsWith('application/vnd.amazon.mobi8-ebook')) {
props['name'] = 'book';
} else if (mimetype.startsWith('text/vtt') || mimetype.startsWith('text/srt')) {
props['name'] = 'closed captioning';
} else if (mimetype.startsWith('application/x-openscad') || mimetype.startsWith('model/stl')) {
props['name'] = 'cube';
} else if (mimetype.startsWith('application/x-dosexec') || mimetype.startsWith('application/x-msi')) {
props['name'] = 'microsoft';
} else if (mimetype.startsWith('audio/')) {
props['name'] = 'file audio';
} else if (mimetype.startsWith('application/octet-stream')) {
if (lowerPath.endsWith('.mp3')) {
props['name'] = 'file audio';
} else if (lowerPath.endsWith('.stl')) {
props['name'] = 'cube';
} else if (lowerPath.endsWith('.blend')) {
props['name'] = 'cube';
} else if (lowerPath.endsWith('.dmg')) {
props['name'] = 'apple';
} else if (lowerPath.endsWith('.azw3')) {
props['name'] = 'book';
}
}
}
return
}
export function LoadStatistic({label, value, cores, ...props}) {
const quarter = cores / 4;
if (cores && value >= (quarter * 3)) {
props['color'] = 'red';
} else if (cores && value >= (quarter * 2)) {
props['color'] = 'orange';
}
return ;
}
export function DarkModeToggle() {
const {savedTheme, cycleSavedTheme} = useContext(ThemeContext);
let iconName = 'lightbulb outline';
if (savedTheme === darkTheme) {
iconName = 'moon';
} else if (savedTheme === lightTheme) {
iconName = 'sun';
}
return
}
export function UnsupportedModal(header, message, icon) {
const [open, setOpen] = useState(false);
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
const modal =
{header || 'Unsupported'}
{message}
;
return {modal, doClose: onClose, doOpen: onOpen};
}
export function HotspotStatusIcon() {
const {on, setHotspot, dockerized} = useHotspot();
const {modal, doOpen} = UnsupportedModal('Unsupported on Docker', 'You cannot toggle the hotspot on Docker.');
const toggleHotspot = (e) => {
e.preventDefault();
if (dockerized) {
doOpen();
}
if (on != null) {
setHotspot(!on);
}
}
return <>
{on === false && }
{on === null && }
{modal}
>
}
export function useTitle(title) {
const documentDefined = typeof document !== 'undefined';
const originalTitle = React.useRef(documentDefined ? document.title : null);
const name = NAME ? `${NAME} WROLPi` : `WROLPi`;
useEffect(() => {
if (!documentDefined) {
return;
}
const newTitle = `${title} - ${name}`
if (title && document.title !== newTitle) {
document.title = newTitle;
}
return () => {
document.title = originalTitle.current;
}
}, [title]);
}
export function DirectoryInput({disabled, error, placeholder, setInput, value, required, isDirectory}) {
const {directory, directories, setDirectory, isDir} = useDirectories(value);
const {settings} = useSettings();
if (!directories || !settings) {
return <>>;
}
const localSetInput = (e, {value}) => {
setInput(value);
setDirectory(value);
}
const {media_directory} = settings;
error = error || isDirectory && !isDir;
return
;
}
export const BackButton = () => {
const navigate = useNavigate();
return