This commit is contained in:
seb
2026-10-06 12:53:32 +02:00
parent b99b39f640
commit 1b5d8efbc3
2 changed files with 119 additions and 160 deletions

View File

@@ -9,7 +9,6 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import CircularProgress from '@mui/material/CircularProgress';
import Tooltip from '@mui/material/Tooltip';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded';
import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded';
@@ -399,7 +398,6 @@ export class VfsAttachmentLink extends PureComponent {
target="_blank"
rel="noopener noreferrer"
onClick={this.handleClick}
title={notFound ? 'Nicht mehr vorhanden (404)' : `VFS Bild im neuen Tab öffnen: ${downloadName}`}
sx={{
display: 'block',
cursor: notFound ? 'not-allowed' : 'pointer',
@@ -437,18 +435,17 @@ export class VfsAttachmentLink extends PureComponent {
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0, flex: 1 }}>
<InsertPhotoRoundedIcon sx={{ fontSize: 16, flexShrink: 0 }} />
<Typography
variant="caption"
sx={{
fontWeight: 600,
fontWeight: 500,
fontSize: '0.72rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{downloadName}
{downloadName} - {effectiveType || 'image'}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.2, flexShrink: 0 }}>
@@ -457,24 +454,22 @@ export class VfsAttachmentLink extends PureComponent {
)}
{!notFound && (
<>
<Tooltip title="In neuem Tab öffnen" arrow>
<IconButton
size="small"
onClick={this.handleOpenInNewTab}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
<Tooltip title="Herunterladen" arrow>
<IconButton
size="small"
onClick={this.handleDownload}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
<IconButton
size="small"
aria-label="In neuem Tab öffnen"
onClick={this.handleOpenInNewTab}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
<IconButton
size="small"
aria-label="Herunterladen"
onClick={this.handleDownload}
sx={{ p: 0.35, color: 'inherit', opacity: 0.85, '&:hover': { opacity: 1 } }}
>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</>
)}
</Box>
@@ -483,142 +478,108 @@ export class VfsAttachmentLink extends PureComponent {
);
}
const titleText = notFound
? `Datei nicht mehr im VFS vorhanden (404): ${downloadName}`
: isOpening
? `Wird im neuen Tab geöffnet…`
: isDownloading
? `Wird heruntergeladen…`
: canView
? `In neuem Tab öffnen (${downloadName})`
: `Herunterladen (${downloadName})`;
return (
<Tooltip title={titleText} arrow>
<Box
component="a"
href={notFound ? undefined : vfsUrl}
target="_blank"
rel="noopener noreferrer"
onClick={this.handleClick}
<Box
component="a"
href={notFound ? undefined : vfsUrl}
target="_blank"
rel="noopener noreferrer"
onClick={this.handleClick}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.85,
my: 0.35,
mx: 0.2,
pl: 1.15,
pr: 0.85,
py: 0.5,
borderRadius: 2,
border: '1px solid',
borderColor: notFound
? (isUser ? 'rgba(255,255,255,0.2)' : 'error.light')
: (isUser ? 'rgba(255, 255, 255, 0.35)' : 'divider'),
bgcolor: notFound
? (isUser ? 'rgba(0,0,0,0.2)' : 'action.disabledBackground')
: (isUser
? 'rgba(255, 255, 255, 0.18)'
: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : 'rgba(0, 0, 0, 0.02)')),
textDecoration: notFound ? 'line-through' : 'none',
color: notFound
? (isUser ? 'rgba(255,255,255,0.6)' : 'text.disabled')
: (isUser ? 'inherit' : 'primary.main'),
opacity: notFound ? 0.7 : ((isDownloading || isOpening) ? 0.75 : 1),
cursor: notFound ? 'not-allowed' : 'pointer',
verticalAlign: 'middle',
maxWidth: '100%',
transition: 'all 150ms ease-in-out',
'&:hover': notFound ? {} : {
bgcolor: isUser ? 'rgba(255, 255, 255, 0.28)' : 'action.hover',
borderColor: isUser ? '#fff' : 'primary.main',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
}}
>
<Typography
component="span"
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.85,
my: 0.35,
mx: 0.2,
pl: 1.15,
pr: 0.85,
py: 0.5,
borderRadius: 2,
border: '1px solid',
borderColor: notFound
? (isUser ? 'rgba(255,255,255,0.2)' : 'error.light')
: (isUser ? 'rgba(255, 255, 255, 0.35)' : 'divider'),
bgcolor: notFound
? (isUser ? 'rgba(0,0,0,0.2)' : 'action.disabledBackground')
: (isUser
? 'rgba(255, 255, 255, 0.18)'
: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : 'rgba(0, 0, 0, 0.02)')),
fontSize: '0.82rem',
fontWeight: 500,
color: 'inherit',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
lineHeight: 1.25,
textDecoration: notFound ? 'line-through' : 'none',
color: notFound
? (isUser ? 'rgba(255,255,255,0.6)' : 'text.disabled')
: (isUser ? 'inherit' : 'primary.main'),
opacity: notFound ? 0.7 : ((isDownloading || isOpening) ? 0.75 : 1),
cursor: notFound ? 'not-allowed' : 'pointer',
verticalAlign: 'middle',
maxWidth: '100%',
transition: 'all 150ms ease-in-out',
'&:hover': notFound ? {} : {
bgcolor: isUser ? 'rgba(255, 255, 255, 0.28)' : 'action.hover',
borderColor: isUser ? '#fff' : 'primary.main',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
}}
>
{isDownloading || isOpening ? (
<CircularProgress size={16} color="inherit" thickness={5} />
) : notFound ? (
<ErrorOutlineRoundedIcon sx={{ fontSize: 20, color: isUser ? 'inherit' : 'error.main' }} />
) : (
getAttachmentIcon(effectiveType, downloadName, isUser)
)}
<Box sx={{ minWidth: 0 }}>
<Typography
component="span"
{downloadName} - {effectiveType || 'VFS'}{notFound ? ' (404)' : ''}
</Typography>
{!notFound && (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.2,
ml: 0.5,
flexShrink: 0,
}}
onClick={(e) => e.stopPropagation()}
>
{(isDownloading || isOpening) && (
<CircularProgress size={13} color="inherit" thickness={4} sx={{ mx: 0.4 }} />
)}
{canView && (
<IconButton
size="small"
aria-label="In neuem Tab öffnen"
onClick={this.handleOpenInNewTab}
sx={{
p: 0.4,
color: 'inherit',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
)}
<IconButton
size="small"
aria-label="Herunterladen"
onClick={this.handleDownload}
sx={{
fontSize: '0.82rem',
fontWeight: 600,
p: 0.4,
color: 'inherit',
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: 300,
lineHeight: 1.25,
textDecoration: notFound ? 'line-through' : 'none',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
{displayName}
</Typography>
<Typography
component="span"
sx={{
fontSize: '0.68rem',
opacity: 0.8,
display: 'block',
lineHeight: 1,
}}
>
{notFound
? 'nicht mehr vorhanden (404)'
: (label && label !== downloadName ? `${downloadName} · ${effectiveType || 'VFS'}` : (effectiveType || 'VFS Datei'))}
</Typography>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Box>
{!notFound && (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.2,
ml: 0.5,
}}
onClick={(e) => e.stopPropagation()}
>
{canView && (
<Tooltip title="In neuem Tab öffnen" arrow>
<IconButton
size="small"
onClick={this.handleOpenInNewTab}
sx={{
p: 0.4,
color: 'inherit',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
)}
<Tooltip title="Herunterladen" arrow>
<IconButton
size="small"
onClick={this.handleDownload}
sx={{
p: 0.4,
color: 'inherit',
opacity: 0.75,
'&:hover': { opacity: 1, bgcolor: 'action.hover' },
}}
>
<DownloadRoundedIcon sx={{ fontSize: 15 }} />
</IconButton>
</Tooltip>
</Box>
)}
</Box>
</Tooltip>
)}
</Box>
);
}
}

View File

@@ -9,7 +9,7 @@ import {
TABLE_HEADER_PAIR,
byMode,
} from '../../colors';
import { VfsAttachmentLink, parseAttachmentTag, classifyLinkTarget } from './ChatAttachmentItems';
import { VfsAttachmentLink, parseAttachmentTag, classifyLinkTarget, resolveVfsUrl } from './ChatAttachmentItems';
/**
* Healing markdown renderer.
@@ -43,16 +43,14 @@ function resolveImageUrl(rawUrl) {
const url = String(rawUrl ?? '').trim();
if (!url) return null;
if (/^data:image\//i.test(url)) return url;
if (/^https?:\/\//i.test(url) || url.startsWith('/')) return url;
// Everything else is a VFS name: strip a leading `./`, encode each segment, keep subdirs.
const segments = url
.replace(/^\.\//, '')
.split('/')
.filter((s) => s && s !== '.')
.map((s) => encodeURIComponent(s));
if (segments.length === 0 || segments.includes('..')) return null;
return `/api/vfs/${segments.join('/')}`;
const classified = classifyLinkTarget(url);
if (classified.kind === 'web') return classified.url;
if (classified.kind === 'vfs') {
if (classified.path.includes('..')) return null;
return resolveVfsUrl(classified.path);
}
if (classified.kind === 'nav') return classified.url;
return null;
}
/**