Lets users drop an uploaded file from the UI list before or after conversion, without cancelling anything server-side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
146 lines
4.8 KiB
React
146 lines
4.8 KiB
React
import { X } from '@phosphor-icons/react';
|
|
import { formatBytes } from '../utils/formatBytes.js';
|
|
import { fileTypeMeta } from '../utils/fileFamily.js';
|
|
|
|
const QUALITY_FORMATS = ['jpg', 'jpeg', 'webp', 'avif', 'tiff'];
|
|
const ICON_SIZES = [16, 32, 48, 256, 512];
|
|
const DEFAULT_ICON_SIZE = 256;
|
|
|
|
function extensionOf(fileName) {
|
|
return fileName.split('.').pop();
|
|
}
|
|
|
|
function rangeProgress(value, min, max) {
|
|
return `${((value - min) / (max - min)) * 100}%`;
|
|
}
|
|
|
|
function RangeField({ label, value, min, max, onChange }) {
|
|
return (
|
|
<div className="range-field">
|
|
<div className="range-field-header">
|
|
<span>{label}</span>
|
|
<span className="value-pill">{value}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min={min}
|
|
max={max}
|
|
value={value}
|
|
style={{ '--range-progress': rangeProgress(value, min, max) }}
|
|
onChange={(event) => onChange(Number(event.target.value))}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function FileConfigCard({ item, index, t, onTargetFormatChange, onQualityChange, onIconSizeChange, onRemove }) {
|
|
const { Icon, colorVar } = fileTypeMeta(extensionOf(item.file.name));
|
|
|
|
return (
|
|
<li className="file-config-card">
|
|
<div className="file-tile-header">
|
|
<span className="file-tile-icon" style={{ '--tile-color': `var(${colorVar})` }}>
|
|
<Icon size={20} weight="regular" />
|
|
</span>
|
|
<span className="file-tile-name">{item.file.name}</span>
|
|
<span className="file-tile-size">{formatBytes(item.file.size)}</span>
|
|
<button
|
|
type="button"
|
|
className="file-remove-button"
|
|
onClick={() => onRemove(index)}
|
|
aria-label={t('common.remove')}
|
|
>
|
|
<X size={16} weight="bold" />
|
|
</button>
|
|
</div>
|
|
|
|
{item.targets.length > 0 ? (
|
|
<div className="file-config-controls">
|
|
<div className="chip-field">
|
|
<span>{t('quality.targetFormat')}</span>
|
|
<div className="chip-group" role="group" aria-label={t('quality.targetFormat')}>
|
|
{item.targets.map((target) => (
|
|
<button
|
|
key={target}
|
|
type="button"
|
|
className={`chip${item.targetFormat === target ? ' chip-active' : ''}`}
|
|
aria-pressed={item.targetFormat === target}
|
|
onClick={() => onTargetFormatChange(index, target)}
|
|
>
|
|
{target.toUpperCase()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{QUALITY_FORMATS.includes(item.targetFormat) && (
|
|
<RangeField
|
|
label={t('quality.label')}
|
|
value={item.quality}
|
|
min={1}
|
|
max={100}
|
|
onChange={(value) => onQualityChange(index, value)}
|
|
/>
|
|
)}
|
|
|
|
{item.targetFormat === 'png' && (
|
|
<RangeField
|
|
label={t('quality.compression')}
|
|
value={item.quality}
|
|
min={0}
|
|
max={9}
|
|
onChange={(value) => onQualityChange(index, value)}
|
|
/>
|
|
)}
|
|
|
|
{item.targetFormat === 'ico' && (
|
|
<div className="chip-field">
|
|
<span>{t('quality.iconSize')}</span>
|
|
<div className="chip-group" role="group" aria-label={t('quality.iconSize')}>
|
|
{ICON_SIZES.map((size) => (
|
|
<button
|
|
key={size}
|
|
type="button"
|
|
className={`chip${(item.iconSize ?? DEFAULT_ICON_SIZE) === size ? ' chip-active' : ''}`}
|
|
aria-pressed={(item.iconSize ?? DEFAULT_ICON_SIZE) === size}
|
|
onClick={() => onIconSizeChange(index, size)}
|
|
>
|
|
{size}px
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{item.targetFormat === 'pdf' && (
|
|
<div className="pdf-field">
|
|
<label className="switch-row">
|
|
<span className="switch">
|
|
<input
|
|
type="checkbox"
|
|
checked={item.quality !== null}
|
|
onChange={(event) => onQualityChange(index, event.target.checked ? 90 : null)}
|
|
/>
|
|
<span className="switch-track" aria-hidden="true" />
|
|
</span>
|
|
<span>{t('quality.compressPdf')}</span>
|
|
</label>
|
|
{item.quality !== null && (
|
|
<RangeField
|
|
label={t('quality.label')}
|
|
value={item.quality}
|
|
min={1}
|
|
max={100}
|
|
onChange={(value) => onQualityChange(index, value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span className="error">{t('hero.unsupportedFormat')}</span>
|
|
)}
|
|
</li>
|
|
);
|
|
}
|