Unifying Files tab appearance for model pages.
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Controller Tests / controller-tests (push) Has been cancelled
Jest Tests / jest-tests (push) Has been cancelled

This commit is contained in:
Roland 2026-03-28 14:19:09 -06:00
parent 2943b2a9c8
commit bf5b787fb3
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 153 additions and 88 deletions

View file

@ -23,6 +23,7 @@ import {
APIButton,
BackButton,
CardLink,
DirectoryLink,
encodeMediaPath,
ErrorMessage,
ExternalCardLink,
@ -89,19 +90,6 @@ import {CollectionEditForm} from "./collections/CollectionEditForm";
import {RecurringDownloadsTable} from "./admin/Downloads";
import {DestinationForm} from "./Download";
function archiveFileLink(path, directory = false) {
if (path) {
const href = directory ?
`/media/${encodeMediaPath(path)}/`
: `/media/${encodeMediaPath(path)}`;
return <a href={href} target='_blank' rel='noopener noreferrer'>
<pre>{path}</pre>
</a>
} else {
return <p>Unknown</p>
}
}
function ArchivePage() {
const navigate = useNavigate();
const {fileGroupId} = useParams();
@ -258,37 +246,63 @@ function ArchivePage() {
</Tab.Pane>
};
// Helper to find file size from the files array by resolved path
const findFileSize = (resolvedPath) => {
if (!resolvedPath || !archiveFile.files) return null;
const file = archiveFile.files.find(i => String(i.path) === String(resolvedPath));
return file && file.size
? <span style={{marginLeft: '1em', color: 'grey'}}>({humanFileSize(file.size)})</span>
: null;
};
// Helper to resolve and preview a data path
const localPreviewPath = (dataPath, mimetype) => {
const resolvedPath = resolveDataPath(dataPath, directory);
if (resolvedPath) {
return <PreviewPath path={resolvedPath} mimetype={mimetype} taggable={false}>{resolvedPath}</PreviewPath>
return <><PreviewPath path={resolvedPath} mimetype={mimetype}
taggable={false}>{resolvedPath}</PreviewPath>{findFileSize(resolvedPath)}</>
} else {
return 'Unknown'
}
}
const tableStyle = {width: '100%', borderCollapse: 'separate', borderSpacing: '0 0.7em'};
const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'};
const filesPane = {
menuItem: 'Files', render: () => <Tab.Pane>
<Header as={'h3'}>Singlefile File</Header>
{localPreviewPath(data.singlefile_path, 'text/html')}
<Header as={'h3'}>Readability File</Header>
{localPreviewPath(data.readability_path, 'text/html')}
<Header as={'h3'}>Readability Text File</Header>
{localPreviewPath(data.readability_txt_path, 'text/plain')}
<Header as={'h3'}>Readability JSON File</Header>
{localPreviewPath(data.readability_json_path, 'application/json')}
<Header as={'h3'}>Screenshot File</Header>
{screenshotPath ?
archiveFileLink(screenshotPath)
: 'Unknown'}
<Header as={'h3'}>Directory</Header>
{archiveFileLink(archiveFile.directory, true)}
<table style={tableStyle}>
<tbody>
<tr>
<td style={labelStyle}><strong>Singlefile File</strong></td>
<td>{localPreviewPath(data.singlefile_path, 'text/html')}</td>
</tr>
<tr>
<td style={labelStyle}><strong>Readability File</strong></td>
<td>{localPreviewPath(data.readability_path, 'text/html')}</td>
</tr>
<tr>
<td style={labelStyle}><strong>Readability Text File</strong></td>
<td>{localPreviewPath(data.readability_txt_path, 'text/plain')}</td>
</tr>
<tr>
<td style={labelStyle}><strong>Readability JSON File</strong></td>
<td>{localPreviewPath(data.readability_json_path, 'application/json')}</td>
</tr>
<tr>
<td style={labelStyle}><strong>Screenshot File</strong></td>
<td>
{screenshotPath
? <><PreviewPath path={screenshotPath} mimetype='image/*'
taggable={false}>{screenshotPath}</PreviewPath>{findFileSize(screenshotPath)}</>
: 'Unknown'}
</td>
</tr>
<tr>
<td style={labelStyle}><strong>Directory</strong></td>
<td><DirectoryLink path={archiveFile.directory}/></td>
</tr>
</tbody>
</table>
</Tab.Pane>
};

View file

@ -246,6 +246,15 @@ export function PreviewPath({path, mimetype, taggable = true, ...props}) {
return <PreviewLink file={{path, mimetype, taggable}} {...props}/>
}
export function DirectoryLink({path}) {
if (!path) return <span>Unknown</span>;
const href = `/media/${encodeMediaPath(path)}/`;
return <a href={href} target='_blank' rel='noopener noreferrer'
style={{color: 'inherit', textDecoration: 'none'}}>
{path}
</a>
}
export function RequiredAsterisk() {
return <span style={{color: '#db2828'}}> *</span>
}

View file

@ -6,6 +6,7 @@ import {Media, ThemeContext} from "../contexts/contexts";
import {
APIButton,
BackButton,
DirectoryLink,
encodeMediaPath,
ErrorMessage,
humanFileSize,
@ -288,21 +289,33 @@ function DocPage() {
};
// Files pane.
const tableStyle = {width: '100%', borderCollapse: 'separate', borderSpacing: '0 0.7em'};
const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'};
const filesPane = {
menuItem: 'Files', render: () => <Tab.Pane>
{docFile.files && docFile.files.map((file, idx) =>
<div key={idx} style={{marginBottom: '0.5em'}}>
<PreviewPath path={file.path} mimetype={file.mimetype} taggable={false}>
{file.path}
</PreviewPath>
{file.size && <span style={{marginLeft: '1em', color: 'grey'}}>
({humanFileSize(file.size)})
</span>}
</div>
)}
<Header as='h3'>Directory</Header>
<p>{directory || 'Unknown'}</p>
<table style={tableStyle}>
<tbody>
{docFile.files && docFile.files.map((file, idx) =>
<tr key={idx}>
<td style={labelStyle}>
<strong>{file.path.split('.').pop().toUpperCase()}</strong>
</td>
<td>
<PreviewPath path={file.path} mimetype={file.mimetype} taggable={false}>
{file.path}
</PreviewPath>
{file.size && <span style={{marginLeft: '1em', color: 'grey'}}>
({humanFileSize(file.size)})
</span>}
</td>
</tr>
)}
<tr>
<td style={labelStyle}><strong>Directory</strong></td>
<td><DirectoryLink path={directory}/></td>
</tr>
</tbody>
</table>
</Tab.Pane>
};

View file

@ -6,6 +6,7 @@ import _ from "lodash";
import {
APIButton,
BackButton,
DirectoryLink,
encodeMediaPath,
humanFileSize,
humanNumber,
@ -29,19 +30,6 @@ import {Comment, CommentGroup, Input, Label} from "semantic-ui-react";
const MEDIA_PATH = '/media';
function videoFileLink(path, directory = false) {
if (path) {
const href = directory ?
// Add / to end of directory.
`${MEDIA_PATH}/${encodeMediaPath(path)}/` :
`${MEDIA_PATH}/${encodeMediaPath(path)}`;
return <a href={href} target='_blank' rel='noopener noreferrer'>
<pre>{path}</pre>
</a>
} else {
return 'Unknown'
}
}
export function CaptionTrack({src, ...props}) {
if (src.endsWith('.de.vtt')) {
@ -311,44 +299,84 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
};
const getFile = (suffix) => videoFile.files.find(i => i.path.toLowerCase().endsWith(suffix));
const fileSize = (file) => file && file.size
? <span style={{marginLeft: '1em', color: 'grey'}}>({humanFileSize(file.size)})</span>
: null;
const {poster_file, info_json_file} = video;
const videoPathFile = videoFile.files.find(i => i.path === video['video_path']);
const ffprobeJsonFile = getFile('.ffprobe.json');
const tableStyle = {width: '100%', borderCollapse: 'separate', borderSpacing: '0 0.7em'};
const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'};
const filesPane = {
menuItem: 'Files', render: () => <Tab.Pane>
<h3>Video File</h3>
{videoFileLink(video['video_path'])}
<table style={tableStyle}>
<tbody>
<tr>
<td style={labelStyle}><strong>Video File</strong></td>
<td>
{video['video_path']
? <><PreviewPath path={video['video_path']} mimetype='video/*' taggable={false}>
{video['video_path']}
</PreviewPath>{fileSize(videoPathFile)}</>
: 'Unknown'}
</td>
</tr>
<h4>Caption Files</h4>
{caption_files ?
caption_files.map(i => <p key={i['path']}><PreviewPath {...i} taggable={false}>
{i['path']}
</PreviewPath></p>) : 'No caption files'
}
<tr>
<td style={labelStyle}><strong>Caption Files</strong></td>
<td>
{caption_files
? caption_files.map(i => <div key={i['path']} style={{marginBottom: '0.3em'}}>
<PreviewPath {...i} taggable={false}>{i['path']}</PreviewPath>
{fileSize(i)}
</div>)
: 'No caption files'}
</td>
</tr>
<h4>Poster File</h4>
{poster_file ?
<PreviewPath path={poster_file['path']} mimetype={poster_file['mimetype']} taggable={false}>
{poster_file['path']}
</PreviewPath> : 'No poster file'
}
<tr>
<td style={labelStyle}><strong>Poster File</strong></td>
<td>
{poster_file
? <><PreviewPath path={poster_file['path']} mimetype={poster_file['mimetype']}
taggable={false}>
{poster_file['path']}
</PreviewPath>{fileSize(poster_file)}</>
: 'No poster file'}
</td>
</tr>
<h4>Info JSON File</h4>
{info_json_file ?
<PreviewPath path={info_json_file['path']} mimetype={info_json_file['mimetype']} taggable={false}>
{info_json_file['path']}
</PreviewPath> : 'No yt-dlp info json file'
}
<tr>
<td style={labelStyle}><strong>Info JSON File</strong></td>
<td>
{info_json_file
? <><PreviewPath path={info_json_file['path']} mimetype={info_json_file['mimetype']}
taggable={false}>
{info_json_file['path']}
</PreviewPath>{fileSize(info_json_file)}</>
: 'No yt-dlp info json file'}
</td>
</tr>
<h4>ffprobe JSON File</h4>
{ffprobeJsonFile ?
<PreviewPath path={ffprobeJsonFile['path']} mimetype={ffprobeJsonFile['mimetype']} taggable={false}>
{ffprobeJsonFile['path']}
</PreviewPath> : 'No ffprobe json file'
}
<tr>
<td style={labelStyle}><strong>ffprobe JSON File</strong></td>
<td>
{ffprobeJsonFile
? <><PreviewPath path={ffprobeJsonFile['path']} mimetype={ffprobeJsonFile['mimetype']}
taggable={false}>
{ffprobeJsonFile['path']}
</PreviewPath>{fileSize(ffprobeJsonFile)}</>
: 'No ffprobe json file'}
</td>
</tr>
<h4>Directory</h4>
{videoFileLink(videoFile['directory'], true)}
<tr>
<td style={labelStyle}><strong>Directory</strong></td>
<td><DirectoryLink path={videoFile['directory']}/></td>
</tr>
</tbody>
</table>
</Tab.Pane>
}

View file

@ -272,7 +272,8 @@ class FileGroup(ModelHelper, Base):
new_files = list(self.files) if self.files else list()
for path in paths:
# Store just the filename, not the full path
new_files.append(dict(path=path.name, mimetype=get_mimetype(path)))
size = path.stat().st_size if path.exists() else None
new_files.append(dict(path=path.name, mimetype=get_mimetype(path), size=size))
self.files = unique_by_predicate(new_files, lambda i: i['path'])
# Update modification_datetime to include the new files' mtimes
if paths: