Files
donetick/src/views/Modals/Inputs/AttachmentViewerModal.jsx
everysingletear bdac10ac57 i18n: extract shared strings into the existing common namespace
Part of #145.

Fourteen files whose user-facing strings are generic enough to belong in
`common` — loading and empty states, the confirmation modal, the shared
input modals (text/date/user/attachment viewer), the mobile nav bar, the
autocomplete input, the error screen and the file-upload error paths.

Extends the namespace that already exists, so `src/i18n/config.js` is
untouched and this cannot collide with any other extraction PR over the
`ns:` array. 28 keys added to `public/locales/en/common.json`.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (36 call sites).

One value is matched loosely and worth naming: `errorScreen.hideDetails`.
The base renders `{showDetails ? 'Hide' : 'Show'} error details`, so
neither full phrase exists contiguously in the source — only one branch of
the ternary can. Both keys hold exactly what each branch renders. The
sentence is kept whole rather than split around the ternary, because a
split sentence cannot be reordered by a translator.
2026-08-13 10:16:14 +08:00

107 lines
2.9 KiB
JavaScript

import { Browser } from '@capacitor/browser'
import { useTranslation } from 'react-i18next'
import { Capacitor } from '@capacitor/core'
import { Download } from '@mui/icons-material'
import { Box, CircularProgress, Typography } from '@mui/joy'
import { useState } from 'react'
import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
const openUrl = async url => {
if (Capacitor.isNativePlatform()) {
await Browser.open({ url })
} else {
window.open(url, '_blank', 'noopener,noreferrer')
}
}
const downloadUrl = (url, fileName) => {
if (Capacitor.isNativePlatform()) {
Browser.open({ url })
} else {
const a = document.createElement('a')
a.href = url
a.download = fileName || 'attachment'
a.rel = 'noopener'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
}
function AttachmentViewerModal({ config }) {
const { t } = useTranslation('common')
const { ResponsiveModal } = useResponsiveModal()
const [imgLoaded, setImgLoaded] = useState(false)
const [imgError, setImgError] = useState(false)
const { isOpen, url, fileName, onClose } = config || {}
const handleClose = () => {
setImgLoaded(false)
setImgError(false)
onClose?.()
}
return (
<ResponsiveModal
open={!!isOpen}
onClose={handleClose}
title={fileName || 'Attachment'}
maxHeight='92vh'
footer={
<ModalActions
secondary={{ label: t('close'), onClick: handleClose }}
primary={{
label: 'Download',
startDecorator: <Download />,
onClick: () => downloadUrl(url, fileName),
disabled: !url,
}}
/>
}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 200,
position: 'relative',
}}
>
{!imgLoaded && !imgError && (
<CircularProgress sx={{ position: 'absolute' }} size='md' />
)}
{imgError ? (
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{t('imageLoadFailed')}
</Typography>
) : (
<Box
component='img'
src={url}
alt={fileName}
onClick={() => url && openUrl(url)}
onLoad={() => setImgLoaded(true)}
onError={() => {
setImgLoaded(true)
setImgError(true)
}}
sx={{
cursor: url ? 'zoom-in' : 'default',
maxWidth: '100%',
maxHeight: '65vh',
borderRadius: 'md',
objectFit: 'contain',
display: imgLoaded && !imgError ? 'block' : 'none',
}}
/>
)}
</Box>
</ResponsiveModal>
)
}
export default AttachmentViewerModal