Renaming examples to avoid grouping of pdf with ebooks. Reworking URLs for media files to avoid escaping url safe characters.

This commit is contained in:
Roland 2023-03-29 13:46:39 -06:00
parent b481334e1a
commit 44ebc876fa
No known key found for this signature in database
GPG key ID: 9D6DBD3ACF11AB93
21 changed files with 277 additions and 186 deletions

View file

@ -81,18 +81,18 @@ export default function App() {
useEventsInterval();
return <ThemeWrapper>
<FilePreviewWrapper>
{/* Context and style to handle switching between mobile/computer. */}
<style>{mediaStyles}</style>
{/* Toasts can be on any page. */}
<SemanticToastContainer position='top-right'/>
<MediaContextProvider>
<TagsContext.Provider value={tagsValue}>
<TagsContext.Provider value={tagsValue}>
<FilePreviewWrapper>
{/* Context and style to handle switching between mobile/computer. */}
<style>{mediaStyles}</style>
{/* Toasts can be on any page. */}
<SemanticToastContainer position='top-right'/>
<MediaContextProvider>
<StatusContext.Provider value={statusValue}>
<RouterProvider router={router}/>
</StatusContext.Provider>
</TagsContext.Provider>
</MediaContextProvider>
</FilePreviewWrapper>
</MediaContextProvider>
</FilePreviewWrapper>
</TagsContext.Provider>
</ThemeWrapper>
}

View file

@ -22,13 +22,21 @@ import {
TableRow,
} from "semantic-ui-react";
import {contrastingColor, HelpPopup} from "./components/Common";
import {Button, Segment} from "./components/Theme";
import {Segment} from "./components/Theme";
import _ from "lodash";
import {Link} from "react-router-dom";
import {HexColorPicker} from "react-colorful";
export const TagsContext = React.createContext({
NameToTagLabel: null,
TagsGroup: null,
TagsLinkGroup: null,
fetchTags: null,
findTagByName: null,
tagNames: [],
tags: [],
});
const DEFAULT_TAG_COLOR = '#000000';
export const TagsContext = React.createContext({tags: [], fetchTags: null, findTagByName: null});
export function useTags() {
const [tags, setTags] = React.useState(null);
@ -94,9 +102,9 @@ export function useTags() {
}
return <Label.Group tag>
{tagNames.map(i =>
<Link key={i} to={`/?tag=${i}`} style={{marginLeft: '0.3em', marginRight: '0.3em'}}>
<a key={i} href={`/?tag=${i}`} style={{marginLeft: '0.3em', marginRight: '0.3em'}}>
<NameToTagLabel name={i} {...props}/>
</Link>
</a>
)}
</Label.Group>
}
@ -116,7 +124,7 @@ function EditTagLabel({tag, onDelete, onEdit}) {
return <TableRow>
<TableCell>
<SButton color='red' onClick={() => setConfirmDeleteOpen(true)} icon='close'/>
<SButton color='red' onClick={() => setConfirmDeleteOpen(true)} icon='trash'/>
<Confirm
id={`confirm${name}`}
open={confirmDeleteOpen}
@ -232,8 +240,7 @@ function EditTagsModal() {
export function TagsModal({fileGroup, onClick}) {
const {tags: usedTags} = fileGroup;
const {TagsGroup} = useTags();
const {tags: availableTags} = React.useContext(TagsContext);
const {tags: availableTags, TagsGroup} = React.useContext(TagsContext);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
@ -241,7 +248,9 @@ export function TagsModal({fileGroup, onClick}) {
setLoading(true);
try {
await addTag(fileGroup, name);
await onClick();
if (onClick) {
await onClick();
}
console.debug('Added tag');
} finally {
setLoading(false);
@ -252,7 +261,9 @@ export function TagsModal({fileGroup, onClick}) {
setLoading(true);
try {
await removeTag(fileGroup, name);
await onClick();
if (onClick) {
await onClick();
}
console.debug('Removed tag');
} finally {
setLoading(false);
@ -294,7 +305,7 @@ export function TagsModal({fileGroup, onClick}) {
</Modal>
return <>
<Button icon='tag' onClick={() => setOpen(true)}/>
<SButton icon='tag' onClick={() => setOpen(true)}/>
{modal}
</>
}
@ -302,7 +313,11 @@ export function TagsModal({fileGroup, onClick}) {
export const taggedImageLabel = {corner: 'left', icon: 'tag', color: 'green'};
export const TagsDisplay = ({fileGroup, onClick}) => {
const {TagsLinkGroup} = useTags();
const {TagsLinkGroup} = React.useContext(TagsContext);
if (!fileGroup || !TagsLinkGroup) {
return
}
return <Grid>
<Grid.Row>
@ -320,7 +335,7 @@ export const TagsDisplay = ({fileGroup, onClick}) => {
}
export const TagsDashboard = () => {
const {tagNames, TagsLinkGroup} = useTags();
const {tagNames, TagsLinkGroup} = React.useContext(TagsContext);
let availableTagsGroup = <SLoader active inline/>;
if (tagNames && tagNames.length) {

View file

@ -18,8 +18,10 @@ import {
CardLink,
cardTitleWrapper,
defaultSearchOrder,
encodeMediaPath,
ExternalCardLink,
FileIcon,
findPosterPath,
HelpHeader,
isoDatetimeToString,
mimetypeColor,
@ -68,8 +70,8 @@ function ArchivePage() {
const {data} = archiveFile;
const singlefileUrl = data.singlefile_path ? `/media/${encodeURIComponent(data.singlefile_path)}` : null;
const screenshotUrl = data.screenshot_path ? `/media/${encodeURIComponent(data.screenshot_path)}` : null;
const singlefileUrl = data.singlefile_path ? `/media/${encodeMediaPath(data.singlefile_path)}` : null;
const screenshotUrl = data.screenshot_path ? `/media/${encodeMediaPath(data.screenshot_path)}` : null;
const readabilityUrl = data.readability_path;
const singlefileButton = <ExternalCardLink to={singlefileUrl}>
@ -178,8 +180,8 @@ export function ArchiveCard({file}) {
const {s} = useContext(ThemeContext);
const {data} = file;
const imageSrc = data.screenshot_path ? `/media/${encodeURIComponent(data.screenshot_path)}` : null;
const singlefileUrl = data.singlefile_path ? `/media/${encodeURIComponent(data.singlefile_path)}` : null;
const imageSrc = data.screenshot_path ? `/media/${encodeMediaPath(data.screenshot_path)}` : null;
const singlefileUrl = data.singlefile_path ? `/media/${encodeMediaPath(data.singlefile_path)}` : null;
let screenshot = <CardIcon><FileIcon file={file}/></CardIcon>;
const imageLabel = file.tags && file.tags.length ? taggedImageLabel : null;
@ -436,7 +438,8 @@ export function ArchiveRowCells({file}) {
const {data} = file;
const archiveUrl = `/archive/${data.id}`;
const posterUrl = data.screenshot_path ? `/media/${encodeURIComponent(data.screenshot_path)}` : null;
const posterPath = findPosterPath(file);
const posterUrl = posterPath ? `/media/${encodeMediaPath(posterPath)}` : null;
let poster;
if (posterUrl) {

View file

@ -1,11 +1,12 @@
import React, {useContext, useEffect, useState} from "react";
import {Card, Container, IconGroup, Image, Input, Modal, Pagination} from 'semantic-ui-react';
import {Card, Container, IconGroup, Input, Modal, Pagination} from 'semantic-ui-react';
import {Link, NavLink, useNavigate} from "react-router-dom";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
import {useDirectories, useDownloaders, useHotspot, useSettings, useThrottle} from "../hooks/customHooks";
import {Media, StatusContext, ThemeContext} from "../contexts/contexts";
import {Button, darkTheme, Form, Header, Icon, lightTheme, Menu, Popup, Statistic} from "./Theme";
import {Button, CardIcon, darkTheme, Form, Header, Icon, lightTheme, Menu, Popup, Statistic} from "./Theme";
import {FilePreviewContext} from "./FilePreview";
import _ from "lodash";
export const API_URI = process.env && process.env.REACT_APP_API_URI ? process.env.REACT_APP_API_URI : `${window.location.protocol}//${window.location.host}/api`;
export const VIDEOS_API = `${API_URI}/videos`;
@ -441,23 +442,77 @@ export function CardGroupCentered(props) {
</div>
}
export function CardPosterLink({to, poster_url, imageLabel, external = false}) {
export function findPosterPath(file) {
if (!file) {
return;
}
const {files, poster_path, cover_path, screenshot_path, video} = file;
if (poster_path) {
return poster_path;
}
if (cover_path) {
return cover_path;
}
if (screenshot_path) {
return screenshot_path;
}
if (video && video['poster_path']) {
// file is a video model.
return video['poster_path'];
}
if (!_.isEmpty(files)) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file['mimetype'] && file['mimetype'].startsWith('image/')) {
// Found an image file, use that as the poster.
return file['path'];
}
}
}
}
export function CardPoster({to, file}) {
const {s} = useContext(ThemeContext);
// Used to center posters in CardIcon.
const style = {display: 'flex', justifyContent: 'center', ...s['style']};
const image = <Image src={poster_url} label={imageLabel}
style={{maxHeight: '163px', width: 'auto'}}
/>;
if (external === true && to) {
return <a href={to} target='_blank' rel='noreferrer' style={style}>
{image}
</a>
} else if (to) {
const cardTagIcon = <div className="ui green left corner label">
<i aria-hidden="true" className="tag icon"></i>
</div>;
let imageLabel = !_.isEmpty(file.tags) ? cardTagIcon : null;
let posterPath = findPosterPath(file);
if (!posterPath) {
// No poster, use a FileIcon.
return <PreviewLink file={file}>
<CardIcon>
{imageLabel}
<FileIcon file={file}/>
</CardIcon>
</PreviewLink>
}
posterPath = `/media/${encodeMediaPath(posterPath)}`;
const image = <>
{/* Replicate <Image label/> but with maxHeight applied to image */}
{imageLabel}
<img alt='poster' src={posterPath} style={{maxHeight: '163px', width: 'auto'}}/>
</>;
if (to) {
// Link using React Router.
return <Link to={to} style={style}>
{image}
</Link>
} else {
return <div style={style}>{image}</div>;
// Preview the file.
return <div style={style}>
<PreviewLink file={file}>
{image}
</PreviewLink>
</div>
}
}
@ -840,7 +895,7 @@ export const filterToMimetypes = (filter) => {
} else if (filter === 'image') {
return ['image'];
} else if (filter === 'zip') {
return ['application/zip'];
return ['application/zip', 'application/zlib', 'application/x-bzip2', 'application/x-xz', 'application/x-bzip', 'application/x-bzip2', 'application/gzip', 'application/vnd.rar', 'application/x-tar', 'application/x-7z-compressed'];
}
}
@ -874,3 +929,12 @@ function hexToRGBArray(color) {
export function contrastingColor(color) {
return (luma(color) >= 120) ? '#000000' : '#dddddd';
}
export const encodeMediaPath = (path) => {
// Replace % first to avoid replacing the other replacements.
path = path.replaceAll('%', '%25');
path = path.replaceAll('#', '%23');
path = path.replaceAll(' ', '%20');
return path
}

View file

@ -39,7 +39,6 @@ class Downloader extends React.Component {
}
handleInputChange = async (event, {name, value}) => {
console.log(name, value);
this.setState({[name]: value}, this.validateUrls);
}

View file

@ -2,6 +2,7 @@ import React from "react";
import _ from "lodash";
import {
Button as SButton,
Grid,
Header as SHeader,
Image,
Modal,
@ -9,33 +10,36 @@ import {
ModalContent,
ModalHeader
} from "semantic-ui-react";
import {TagsDisplay} from "../Tags";
import {Media} from "../contexts/contexts";
import {encodeMediaPath} from "./Common";
function getMediaPathURL(file) {
if (file['primary_path']) {
return `/media/${encodeURIComponent(file['primary_path'])}`;
function getMediaPathURL(previewFile) {
if (previewFile['primary_path']) {
return `/media/${encodeMediaPath(previewFile['primary_path'])}`;
} else {
return `/media/${encodeURIComponent(file['path'])}`;
return `/media/${encodeMediaPath(previewFile['path'])}`;
}
}
function getDownloadPathURL(file) {
if (file['primary_path']) {
return `/download/${encodeURIComponent(file['primary_path'])}`;
function getDownloadPathURL(previewFile) {
if (previewFile['primary_path']) {
return `/download/${encodeMediaPath(previewFile['primary_path'])}`;
} else {
return `/download/${encodeURIComponent(file['path'])}`;
return `/download/${encodeMediaPath(previewFile['path'])}`;
}
}
function getEpubViewerURL(file) {
if (file['primary_path']) {
return `/epub.html?url=download/${encodeURIComponent(file['primary_path'])}`;
function getEpubViewerURL(previewFile) {
if (previewFile['primary_path']) {
return `/epub.html?url=download/${encodeMediaPath(previewFile['primary_path'])}`;
} else {
return `/epub.html?url=download/${encodeURIComponent(file['path'])}`;
return `/epub.html?url=download/${encodeMediaPath(previewFile['path'])}`;
}
}
function getIframeModal(path) {
const url = getMediaPathURL(path);
function getIframeModal(previewFile) {
const url = getMediaPathURL(previewFile);
return <ModalContent>
<div className='full-height'>
<iframe title='textModal' src={url}
@ -46,9 +50,11 @@ function getIframeModal(path) {
</ModalContent>
}
function getImageModal(path) {
const url = getMediaPathURL(path);
const name = path['path'].replace(/^.*[\\\/]/, '');
function getImageModal(previewFile) {
const path = previewFile.primary_path ?? previewFile.path;
const url = getMediaPathURL(previewFile);
// Get the file name from the absolute path.
const name = path.replace(/^.*[\\\/]/, '');
return <React.Fragment>
<ModalHeader>
{name}
@ -61,8 +67,8 @@ function getImageModal(path) {
</React.Fragment>
}
function getEpubModal(path) {
const downloadURL = getDownloadPathURL(path);
function getEpubModal(previewFile) {
const downloadURL = getDownloadPathURL(previewFile);
const viewerUrl = `/epub.html?url=${downloadURL}`;
return <ModalContent>
<div className='full-height'>
@ -74,10 +80,11 @@ function getEpubModal(path) {
</ModalContent>
}
function getVideoModal(path) {
const url = getMediaPathURL(path);
const name = path['path'].replace(/^.*[\\\/]/, '');
const type = path['mimetype'] ? path['mimetype'] : 'video/mp4';
function getVideoModal(previewFile) {
const url = getMediaPathURL(previewFile);
const path = previewFile.primary_path ?? previewFile.path;
const name = path.replace(/^.*[\\\/]/, '');
const type = previewFile.mimetype ? previewFile.mimetype : 'video/mp4';
return <React.Fragment>
<ModalHeader>
{name}
@ -96,11 +103,12 @@ function getVideoModal(path) {
</React.Fragment>
}
function getAudioModal(path) {
const url = getMediaPathURL(path);
const name = path['path'].replace(/^.*[\\\/]/, '');
function getAudioModal(previewFile) {
const url = getMediaPathURL(previewFile);
const path = previewFile.primary_path ?? previewFile.path;
const name = path.replace(/^.*[\\\/]/, '');
// Some audio files may not have the correct mimetype. Trust the suffix identifies an audio file.
const type = path['mimetype'] !== 'application/octet-stream' ? path['mimetype'] : 'audio/mpeg';
const type = previewFile['mimetype'] !== 'application/octet-stream' ? previewFile['mimetype'] : 'audio/mpeg';
return <React.Fragment>
<ModalHeader>
{name}
@ -132,6 +140,16 @@ export function FilePreviewWrapper({children}) {
};
function setModalContent(content, url, downloadURL) {
const openButton = <SButton color='blue' as='a' href={url}>Open</SButton>;
const closeButton = <SButton onClick={() => setPreviewFile(null)}>Close</SButton>;
const tagsDisplay = <TagsDisplay fileGroup={previewFile}/>;
let downloadButton;
if (downloadURL) {
downloadButton = <SButton color='yellow' as='a' href={downloadURL} floated='left'>
Download
</SButton>;
}
setPreviewModal(<Modal closeIcon
size='fullscreen'
open={true}
@ -139,10 +157,28 @@ export function FilePreviewWrapper({children}) {
>
{content}
<ModalActions>
{downloadURL &&
<SButton color='yellow' onClick={() => window.open(downloadURL)} floated='left'>Download</SButton>}
<SButton color='blue' onClick={() => window.open(url)}>Open</SButton>
<SButton onClick={() => setPreviewFile(null)}>Close</SButton>
<Media at='mobile'>
<Grid>
<Grid.Row>
<Grid.Column width={6}>{downloadButton}</Grid.Column>
<Grid.Column width={5}>{openButton}</Grid.Column>
<Grid.Column width={5}>{closeButton}</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={16}>{tagsDisplay}</Grid.Column>
</Grid.Row>
</Grid>
</Media>
<Media greaterThanOrEqual='tablet'>
<Grid>
<Grid.Row>
<Grid.Column width={2}>{downloadButton}</Grid.Column>
<Grid.Column width={10}>{tagsDisplay}</Grid.Column>
<Grid.Column width={2}>{openButton}</Grid.Column>
<Grid.Column width={2}>{closeButton}</Grid.Column>
</Grid.Row>
</Grid>
</Media>
</ModalActions>
</Modal>);
}
@ -173,6 +209,8 @@ export function FilePreviewWrapper({children}) {
} else if (mimetype.startsWith('application/epub')) {
const viewerURL = getEpubViewerURL(previewFile);
setModalContent(getEpubModal(previewFile), viewerURL, downloadURL);
} else if (mimetype.startsWith('application/pdf')) {
setModalContent(getIframeModal(previewFile), url, downloadURL);
} else if (mimetype.startsWith('image/')) {
setModalContent(getImageModal(previewFile), url);
} else if (mimetype.startsWith('application/octet-stream') && lowerPath.endsWith('.mp3')) {

View file

@ -14,10 +14,12 @@ import {
} from "semantic-ui-react";
import {
CardGroupCentered,
CardPosterLink,
CardPoster,
cardTitleWrapper,
encodeMediaPath,
ExternalCardLink,
FileIcon,
findPosterPath,
humanFileSize,
isoDatetimeToString,
mimetypeColor,
@ -40,33 +42,24 @@ import _ from 'lodash';
import {FileBrowser} from "./FileBrowser";
import {refreshDirectoryFiles, refreshFiles} from "../api";
import {useSubscribeEventName} from "../Events";
import {FilePreviewContext} from "./FilePreview";
import {taggedImageLabel, TagsContext} from "../Tags";
import {TagsContext} from "../Tags";
function EbookCard({file}) {
const {s} = useContext(ThemeContext);
let {data} = file;
const downloadUrl = `/download/${encodeURIComponent(file.primary_path)}`;
const downloadUrl = `/download/${encodeMediaPath(file.primary_path)}`;
const isEpub = file['mimetype'].startsWith('application/epub');
const viewerUrl = isEpub ? `/epub.html?url=${downloadUrl}` : null;
let cover = <FileIcon file={file}/>;
if (data && data.cover_path) {
const coverSrc = `/media/${encodeURIComponent(data.cover_path)}`;
cover = <CardPosterLink poster_url={coverSrc}/>;
}
const color = mimetypeColor(file.mimetype);
return <Card color={color}>
<PreviewLink file={file}>
{cover}
</PreviewLink>
<CardPoster file={file} preview={true}/>
<CardContent {...s}>
<CardHeader>
<Container textAlign='left'>
<ExternalCardLink to={viewerUrl || downloadUrl}>
{data ? data.title : file.title}
{cardTitleWrapper(data ? data.title : file.title)}
</ExternalCardLink>
</Container>
</CardHeader>
@ -81,22 +74,11 @@ function EbookCard({file}) {
function ImageCard({file}) {
const {s} = useContext(ThemeContext);
const {setPreviewFile} = React.useContext(FilePreviewContext);
const url = `/media/${encodeURIComponent(file.primary_path)}`;
let poster = <FileIcon file={file}/>;
if (file.size && file.size < 50000000) {
// Image is less than 5mb, use it.
poster = <Image wrapped
src={url}
style={{position: 'relative', width: '100%'}}
onClick={() => setPreviewFile(file)}
/>
}
const url = `/media/${encodeMediaPath(file.primary_path)}`;
return <Card color={mimetypeColor(file.mimetype)}>
<PreviewLink file={file}>
{poster}
<CardPoster file={file}/>
</PreviewLink>
<CardContent {...s}>
<CardHeader>
@ -114,18 +96,6 @@ function ImageCard({file}) {
</Card>
}
function findPosterPath(files) {
if (_.isEmpty(files)) {
return;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file['mimetype'] && file['mimetype'].startsWith('image/')) {
return file['path'];
}
}
}
function FileCard({file}) {
const {s} = useContext(ThemeContext);
@ -143,27 +113,17 @@ function FileCard({file}) {
return <EbookCard key={file['primary_path']} file={file}/>;
}
const {data, tags} = file;
const {data} = file;
let author;
if (data) {
author = data['author'];
}
const downloadUrl = `/download/${encodeURIComponent(file.primary_path)}`;
const downloadUrl = `/download/${encodeMediaPath(file.primary_path)}`;
const color = mimetypeColor(file.mimetype);
const size = file.size !== null && file.size !== undefined ? humanFileSize(file.size) : null;
const posterFile = findPosterPath(file['files']);
let poster = <CardIcon><FileIcon file={file}/></CardIcon>;
let imageLabel = !_.isEmpty(tags) ? taggedImageLabel : null;
if (posterFile) {
const posterUrl = `/media/${encodeURIComponent(posterFile)}`;
poster = <CardPosterLink to={downloadUrl} poster_url={posterUrl} imageLabel={imageLabel}/>;
}
return <Card color={color}>
<PreviewLink file={file}>
{poster}
</PreviewLink>
<CardPoster to={downloadUrl} file={file}/>
<CardContent {...s}>
<CardHeader>
<ExternalCardLink to={downloadUrl}>
@ -190,7 +150,7 @@ export function FileCards({files}) {
}
function ImageRowCells({file}) {
const url = `/media/${encodeURIComponent(file.primary_path)}`;
const url = `/media/${encodeMediaPath(file.primary_path)}`;
let poster = <FileIcon file={file} size='large'/>;
if (file.size && file.size < 50000000) {
@ -214,11 +174,10 @@ function ImageRowCells({file}) {
}
export function EbookRowCells({file}) {
const {data} = file;
let cover = <CardIcon><FileIcon file={file}/></CardIcon>;
if (data && data.cover_path) {
const coverSrc = `/media/${encodeURIComponent(data.cover_path)}`;
const posterPath = findPosterPath(file);
if (posterPath) {
const coverSrc = `/media/${encodeMediaPath(posterPath)}`;
cover = <Image wrapped src={coverSrc} width='50px'/>;
}
@ -229,6 +188,7 @@ export function EbookRowCells({file}) {
</TableCell>
<TableCell>
<PreviewLink file={file}>
<FileRowTagIcon file={file}/>
{textEllipsis(file.title || file.stem)}
</PreviewLink>
</TableCell>
@ -256,7 +216,8 @@ function FileRow({file}) {
<center><FileIcon file={file} size='large'/></center>
</TableCell>
<TableCell>
<PreviewLink file={file}>{textEllipsis(file.title)}</PreviewLink>
<FileRowTagIcon file={file}/>
<PreviewLink file={file}>{textEllipsis(file.title || file.name || file.stem)}</PreviewLink>
</TableCell>
</React.Fragment>
}
@ -397,7 +358,7 @@ export function FilesView({
}
export function TagsFilesGroupDropdown({onChange}) {
// Creates a dropdown that the User can use to manipulate the tag query.
// Creates dropdown that the User can use to manipulate the tag query.
const {searchParams, updateQuery} = useQuery();
const activeTag = searchParams.get('tag');
const {tags} = React.useContext(TagsContext);

View file

@ -11,7 +11,8 @@ const links = [
{text: 'Map', to: '/map', key: 'map'},
{text: 'Files', to: '/files', key: 'files'},
{text: 'Inventory', to: '/inventory', key: 'inventory'},
{text: 'More', key: 'more',
{
text: 'More', key: 'more',
links: [
{to: '/more/otp', text: 'One Time Pad', end: true},
{to: '/more/statistics', text: 'Statistics', end: true},

View file

@ -4,6 +4,7 @@ import {Link, useNavigate, useParams} from "react-router-dom";
import _ from "lodash";
import {
BackButton,
encodeMediaPath,
humanFileSize,
humanNumber,
isoDatetimeToString,
@ -28,8 +29,8 @@ function videoFileLink(path, directory = false) {
if (path) {
const href = directory ?
// Add / to end of directory.
`${MEDIA_PATH}/${encodeURIComponent(path)}/` :
`${MEDIA_PATH}/${encodeURIComponent(path)}`;
`${MEDIA_PATH}/${encodeMediaPath(path)}/` :
`${MEDIA_PATH}/${encodeMediaPath(path)}`;
return <a href={href} target='_blank' rel='noopener noreferrer'>
<pre>{path}</pre>
</a>
@ -78,11 +79,11 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
navigate(-1);
}
let videoUrl = `${MEDIA_PATH}/${encodeURIComponent(video.video_path)}`;
let downloadUrl = `/download/${encodeURIComponent(video.video_path)}`;
let videoUrl = `${MEDIA_PATH}/${encodeMediaPath(video.video_path)}`;
let downloadUrl = `/download/${encodeMediaPath(video.video_path)}`;
let posterUrl = video.poster_path ? `${MEDIA_PATH}/${encodeURIComponent(video.poster_path)}` : null;
let captionsUrl = video.caption_path ? `${MEDIA_PATH}/${encodeURIComponent(video.caption_path)}` : null;
let posterUrl = video.poster_path ? `${MEDIA_PATH}/${encodeMediaPath(video.poster_path)}` : null;
let captionsUrl = video.caption_path ? `${MEDIA_PATH}/${encodeMediaPath(video.caption_path)}` : null;
let vttCaptions = video.caption_path && video.caption_path.endsWith('.vtt');
let description = 'No description available.';

View file

@ -2,12 +2,14 @@ import React, {Fragment, useContext, useEffect, useState} from 'react';
import {Link, Route, Routes, useParams} from "react-router-dom";
import {
CardLink,
CardPosterLink,
CardPoster,
cardTitleWrapper,
defaultSearchOrder,
defaultVideoOrder,
Duration,
encodeMediaPath,
FileIcon,
findPosterPath,
isoDatetimeToString,
mimetypeColor,
PageContainer,
@ -37,11 +39,10 @@ import {useChannel, useQuery, useSearchVideos, useVideo, useVideoStatistics} fro
import {FileRowTagIcon, FilesView} from "./Files";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import {Button, Card, CardIcon, Header, Loader, Placeholder, Segment, Statistic, StatisticGroup} from "./Theme";
import {Button, Card, Header, Loader, Placeholder, Segment, Statistic, StatisticGroup} from "./Theme";
import {deleteVideos} from "../api";
import {ThemeContext} from "../contexts/contexts";
import _ from "lodash";
import {taggedImageLabel} from "../Tags";
export function VideoWrapper() {
const {videoId} = useParams();
@ -285,7 +286,7 @@ export function VideosRoute(props) {
}
export function VideoCard({file}) {
const {video, tags} = file;
const {video} = file;
const {s} = useContext(ThemeContext);
let video_url = `/videos/video/${video.id}`;
@ -297,13 +298,8 @@ export function VideoCard({file}) {
channel_url = `/videos/channel/${channel.id}/video`;
video_url = `/videos/channel/${channel.id}/video/${video.id}`;
}
const poster_url = video.poster_path ? `/media/${encodeURIComponent(video.poster_path)}` : null;
let imageLabel = !_.isEmpty(tags) ? taggedImageLabel : null;
let poster = <Link to={video_url}><CardIcon><FileIcon file={file}/></CardIcon></Link>;
if (poster_url) {
poster = <CardPosterLink to={video_url} poster_url={poster_url} imageLabel={imageLabel}/>;
}
let poster = <CardPoster to={video_url} file={file}/>;
const color = mimetypeColor(file.mimetype);
return <Card color={color}>
@ -333,7 +329,8 @@ export function VideoRowCells({file}) {
const {video} = file;
let video_url = `/videos/video/${video.id}`;
const poster_url = video.poster_path ? `/media/${encodeURIComponent(video.poster_path)}` : null;
const poster_path = findPosterPath(file);
const poster_url = poster_path ? `/media/${encodeMediaPath(poster_path)}` : null;
let poster;
if (poster_url) {

View file

@ -1,13 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -1 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

@ -1,13 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View file

@ -4,11 +4,14 @@
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
window.matchMedia = window.matchMedia || function() {
window.matchMedia = window.matchMedia || function () {
return {
matches: false,
addListener: function() {},
addEventListener: function() {},
removeListener: function() {}
addListener: function () {
},
addEventListener: function () {
},
removeListener: function () {
}
};
};

View file

@ -213,7 +213,7 @@ CHANNELS_CONFIG: ChannelsConfig = ChannelsConfig(global_=True)
TEST_CHANNELS_CONFIG: ChannelsConfig = None
def get_channels_config():
def get_channels_config() -> ChannelsConfig:
global TEST_CHANNELS_CONFIG
if isinstance(TEST_CHANNELS_CONFIG, ConfigFile):
return TEST_CHANNELS_CONFIG
@ -425,6 +425,8 @@ def import_channels_config():
save_config = True
if not channel.source_id:
channel_import_logger.warning(f'Unable to fetch source_id for {channel.url}')
else:
session.commit()
channel_import_logger.debug(f'Updated {repr(channel.name)}'
f' url={channel.url}'
@ -435,7 +437,7 @@ def import_channels_config():
if save_config:
# Information about the channel was fetched, store it.
config.save()
save_channels_config()
with get_db_session(commit=True) as session:
channels_by_link = {sanitize_link(i.name): i for i in session.query(Channel)}

View file

@ -258,22 +258,22 @@ def bad_vtt_file(test_directory) -> pathlib.Path:
@pytest.fixture
def example_pdf(test_directory) -> pathlib.Path:
destination = test_directory / 'example.pdf'
shutil.copy(PROJECT_DIR / 'test/example.pdf', destination)
destination = test_directory / 'pdf example.pdf'
shutil.copy(PROJECT_DIR / 'test/pdf example.pdf', destination)
yield destination
@pytest.fixture
def example_epub(test_directory) -> pathlib.Path:
destination = test_directory / 'example.epub'
shutil.copy(PROJECT_DIR / 'test/example.epub', destination)
destination = test_directory / 'ebook example.epub'
shutil.copy(PROJECT_DIR / 'test/ebook example.epub', destination)
yield destination
@pytest.fixture
def example_mobi(test_directory) -> pathlib.Path:
destination = test_directory / 'example.mobi'
shutil.copy(PROJECT_DIR / 'test/example.mobi', destination)
destination = test_directory / 'ebook example.mobi'
shutil.copy(PROJECT_DIR / 'test/ebook example.mobi', destination)
yield destination

View file

@ -24,8 +24,8 @@ async def test_index(test_session, test_directory, example_epub, example_mobi):
assert ebook.creator == 'roland'
assert ebook.size == 292579
assert ebook.file_group.data == {'creator': 'roland', 'title': 'WROLPi Test Book',
'cover_path': test_directory / 'example.jpeg',
'ebook_path': test_directory / 'example.epub',
'cover_path': test_directory / 'ebook example.jpeg',
'ebook_path': test_directory / 'ebook example.epub',
}
assert ebook.file_group.a_text, 'Book title was not indexed'
@ -95,10 +95,11 @@ def test_search(test_session, test_client, example_epub):
assert response.json
file_group = response.json['file_groups'][0]
epub_file = file_group['files'][0]
assert epub_file['path'] == 'example.epub' and epub_file['mimetype'] == 'application/epub+zip'
assert epub_file['path'] == 'ebook example.epub' and epub_file['mimetype'] == 'application/epub+zip'
assert_dict_contains(
file_group['data'],
{'cover_path': 'example.jpeg', 'ebook_path': 'example.epub', 'title': 'WROLPi Test Book', 'creator': 'roland'},
{'cover_path': 'ebook example.jpeg', 'ebook_path': 'ebook example.epub', 'title': 'WROLPi Test Book',
'creator': 'roland'},
)
# No Mobi ebook.
@ -122,11 +123,11 @@ async def test_discover_calibre_cover(test_session, test_directory, example_epub
ebook: EBook = test_session.query(EBook).one()
assert ebook.file_group.title == 'WROLPi Test Book'
# The cover in the ebook was extracted.
assert ebook.cover_file['path'] == test_directory / 'example.jpeg'
assert (test_directory / 'example.jpeg').is_file()
assert ebook.cover_file['path'] == test_directory / 'ebook example.jpeg'
assert (test_directory / 'ebook example.jpeg').is_file()
# Delete the extracted cover, use the cover image from Calibre.
(test_directory / 'example.jpeg').unlink()
(test_directory / 'ebook example.jpeg').unlink()
cover_image = test_directory / 'cover.jpg'
shutil.move(image_file, cover_image)

View file

@ -21,7 +21,7 @@ async def test_pdf_title(test_session, example_pdf):
assert pdf.indexed is True
assert pdf.title == pdf.a_text == 'WROLPi Test PDF'
assert pdf.b_text == 'roland'
assert pdf.c_text == 'example pdf'
assert pdf.c_text == 'pdf example pdf'
assert pdf.d_text and pdf.d_text.startswith('Page one\n') and len(pdf.d_text) == 467
assert pdf.data and pdf.data.get('author') == 'roland'
@ -40,7 +40,7 @@ async def test_pdf_modeler(test_session, example_pdf):
# The author.
assert b_text == 'roland'
# The parsed file name.
assert c_text == 'example pdf'
assert c_text == 'pdf example pdf'
# All pages are extracted. Text is formatted to fit on vertical screen.
assert d_text == 'Page one\n' \
'Page two\n' \