mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-19 07:36:17 +00:00
fix(ui): harden add downloads modal layout
This commit is contained in:
@@ -2032,14 +2032,19 @@ export const AddDownloadsModal = () => {
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
className={`add-download-control add-download-links-input w-full h-32 p-3 text-[13px] resize-none ${
|
||||
id="add-download-links-input"
|
||||
aria-describedby="add-download-links-hint"
|
||||
className={`add-download-control add-download-links-input w-full h-24 p-3 text-[13px] resize-none ${
|
||||
isRtl ? 'add-download-links-input--rtl' : ''
|
||||
}`}
|
||||
placeholder={t($ => $.addDownloads.pastePlaceholder)}
|
||||
value={urls}
|
||||
onChange={(e) => setUrls(e.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<div className="add-download-url-meta px-1">
|
||||
<p id="add-download-links-hint" className="add-download-input-hint min-w-0 flex-1 text-[10px] leading-4 text-text-muted">
|
||||
{t($ => $.addDownloads.pasteHint)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void addTorrentFiles()}
|
||||
@@ -2062,15 +2067,14 @@ export const AddDownloadsModal = () => {
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
<div className="add-download-selection-toolbar px-1">
|
||||
<div
|
||||
className="add-download-selection-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
aria-label={localizedSelectedSummary}
|
||||
>
|
||||
<span className="sr-only">{localizedSelectedSummary}</span>
|
||||
<div
|
||||
className="add-download-selection-summary px-1"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<span className="sr-only">{localizedSelectedSummary}</span>
|
||||
<div className="add-download-selection-status" aria-hidden="true">
|
||||
<span className="add-download-status-chip" data-tone="ready">
|
||||
<strong>{readyMetadataCount}</strong>
|
||||
<span>{t($ => $.addDownloads.selectedSummaryReady)}</span>
|
||||
@@ -2088,7 +2092,23 @@ export const AddDownloadsModal = () => {
|
||||
<span>{t($ => $.addDownloads.selectedSummaryBlocked)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="add-download-selection-actions">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<SummaryBox title={t($ => $.addDownloads.files)} value={selectedItems.length === parsedItems.length ? parsedItems.length : `${selectedItems.length}/${parsedItems.length}`} icon={FileText} color="text-blue-500" />
|
||||
<SummaryBox title={t($ => $.addDownloads.required)} value={requiredStr === 'Unknown' ? t($ => $.addDownloads.unknown) : requiredStr} icon={Database} color="text-orange-500" />
|
||||
<SummaryBox title={t($ => $.addDownloads.free)} value={freeSpace === 'Unknown' ? t($ => $.addDownloads.unknown) : freeSpace} icon={HardDrive} color="text-green-500" />
|
||||
<SummaryBox title={t($ => $.addDownloads.unknown)} value={selectedItems.filter(i => !i.sizeBytes).length} icon={FileText} color="text-purple-500" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1 min-h-0 min-w-0 overflow-hidden">
|
||||
<div className="add-download-preview-heading flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="add-download-section-title flex items-center gap-2">
|
||||
<ArrowRight size={16} className="text-blue-500" />
|
||||
{t($ => $.addDownloads.preview)}
|
||||
</div>
|
||||
<div className="add-download-preview-actions flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setParsedItems(refreshFailedMetadataRows)}
|
||||
@@ -2101,26 +2121,12 @@ export const AddDownloadsModal = () => {
|
||||
type="button"
|
||||
onClick={toggleAllRows}
|
||||
disabled={parsedItems.length === 0}
|
||||
className="add-download-link-button ms-3 text-[11px] font-medium"
|
||||
className="add-download-link-button text-[11px] font-medium"
|
||||
>
|
||||
{allRowsSelected ? t($ => $.addDownloads.clearSelection) : t($ => $.addDownloads.selectAll)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<SummaryBox title={t($ => $.addDownloads.files)} value={selectedItems.length === parsedItems.length ? parsedItems.length : `${selectedItems.length}/${parsedItems.length}`} icon={FileText} color="text-blue-500" />
|
||||
<SummaryBox title={t($ => $.addDownloads.required)} value={requiredStr === 'Unknown' ? t($ => $.addDownloads.unknown) : requiredStr} icon={Database} color="text-orange-500" />
|
||||
<SummaryBox title={t($ => $.addDownloads.free)} value={freeSpace === 'Unknown' ? t($ => $.addDownloads.unknown) : freeSpace} icon={HardDrive} color="text-green-500" />
|
||||
<SummaryBox title={t($ => $.addDownloads.unknown)} value={selectedItems.filter(i => !i.sizeBytes).length} icon={FileText} color="text-purple-500" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1 min-h-0 min-w-0 overflow-hidden">
|
||||
<div className="add-download-section-title flex items-center gap-2">
|
||||
<ArrowRight size={16} className="text-blue-500" />
|
||||
{t($ => $.addDownloads.preview)}
|
||||
</div>
|
||||
<div className="add-download-preview flex-1 min-h-0 min-w-0 overflow-hidden flex flex-col">
|
||||
<div className="add-download-preview-header px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
|
||||
<div className="flex-[2]">{t($ => $.addDownloads.file)}</div>
|
||||
|
||||
@@ -653,7 +653,8 @@ const common = {
|
||||
pauseBeforeReplace: 'Pause {{file}} before replacing it.',
|
||||
cannotReplace: 'Cannot replace {{file}}: file is not owned by a Firelink download.',
|
||||
downloadLinks: 'Download Links',
|
||||
pastePlaceholder: 'Paste HTTP, HTTPS, FTP, SFTP, or magnet URLs here... You can also choose .torrent files below.\n\nFor media downloads, paste links from YouTube, X, TikTok, Instagram, Reddit, etc.',
|
||||
pastePlaceholder: 'Paste HTTP(S), FTP/SFTP, magnet, or media URLs…',
|
||||
pasteHint: 'Media links from YouTube, X, TikTok, Instagram, and Reddit are supported.',
|
||||
playlistSummary: 'Playlist “{{title}}”: {{loaded}}{{total}} entries loaded{{truncated}}{{skipped}}',
|
||||
safeEntryLimit: ' (safe entry limit reached)',
|
||||
selectedSummary: '{{ready}} selected ready, {{fallback}} fallback, {{mediaRetry}} media retry, {{blocked}} blocked',
|
||||
|
||||
@@ -653,7 +653,8 @@ const fa = {
|
||||
pauseBeforeReplace: 'قبل از جایگزینی {{file}}، آن را متوقف کنید.',
|
||||
cannotReplace: 'نمیتوان {{file}} را جایگزین کرد: فایل متعلق به یک دانلود Firelink نیست.',
|
||||
downloadLinks: 'پیوندهای دانلود',
|
||||
pastePlaceholder: 'URLهای HTTP، HTTPS، FTP، SFTP یا magnet را اینجا جایگذاری کنید… همچنین میتوانید فایلهای .torrent را از پایین انتخاب کنید.\n\nبرای دانلود رسانه، پیوندهایی از \u2066YouTube\u2069، \u2066X\u2069، \u2066TikTok\u2069، \u2066Instagram\u2069، \u2066Reddit\u2069 و غیره جایگذاری کنید.',
|
||||
pastePlaceholder: 'URLهای \u2066HTTP(S)\u2069، \u2066FTP/SFTP\u2069، \u2066magnet\u2069 یا رسانه را اینجا جایگذاری کنید…',
|
||||
pasteHint: 'پیوندهای \u2066YouTube\u2069، \u2066X\u2069، \u2066TikTok\u2069، \u2066Instagram\u2069 و \u2066Reddit\u2069 پشتیبانی میشوند.',
|
||||
playlistSummary: 'لیست پخش "{{title}}": {{loaded}} از {{total}} ورودی بارگیری شد{{truncated}}{{skipped}}',
|
||||
safeEntryLimit: ' (به حد مجاز ایمن ورودیها رسیدیم)',
|
||||
selectedSummary: '{{ready}} انتخابشده آماده، {{fallback}} اطلاعات جایگزین، {{mediaRetry}} تلاش مجدد رسانه، {{blocked}} مسدودشده',
|
||||
|
||||
@@ -653,7 +653,8 @@ const he = {
|
||||
pauseBeforeReplace: 'השהה את {{file}} לפני החלפתו.',
|
||||
cannotReplace: 'לא ניתן להחליף את {{file}}: הקובץ אינו שייך להורדת Firelink.',
|
||||
downloadLinks: 'קישורי הורדה',
|
||||
pastePlaceholder: 'הדבק כאן כתובות \u2066HTTP\u2069, \u2066HTTPS\u2069, \u2066FTP\u2069, \u2066SFTP\u2069 או magnet… אפשר גם לבחור קובצי .torrent למטה.\n\nעבור הורדות מדיה, הדבק קישורים מ-\u2066YouTube\u2069, \u2066X\u2069, \u2066TikTok\u2069, \u2066Instagram\u2069, \u2066Reddit\u2069 וכו\'.',
|
||||
pastePlaceholder: 'הדביקו כאן כתובות \u2066HTTP(S)\u2069, \u2066FTP/SFTP\u2069, \u2066magnet\u2069 או מדיה…',
|
||||
pasteHint: 'קישורי \u2066YouTube\u2069, \u2066X\u2069, \u2066TikTok\u2069, \u2066Instagram\u2069 ו-\u2066Reddit\u2069 נתמכים.',
|
||||
playlistSummary: 'רשימת השמעה "{{title}}": {{loaded}} מתוך {{total}} פריטים נטענו{{truncated}}{{skipped}}',
|
||||
safeEntryLimit: ' (הושגה מגבלת הפריטים הבטוחה)',
|
||||
selectedSummary: '{{ready}} נבחרו ומוכנים, {{fallback}} לגיבוי, {{mediaRetry}} מדיה לניסיון חוזר, {{blocked}} חסומים',
|
||||
|
||||
@@ -653,7 +653,8 @@ const ru = {
|
||||
pauseBeforeReplace: 'Приостановите {{file}} перед заменой.',
|
||||
cannotReplace: 'Невозможно заменить {{file}}: файл не принадлежит загрузке Firelink.',
|
||||
downloadLinks: 'Ссылки для скачивания',
|
||||
pastePlaceholder: 'Вставьте сюда URL-адреса HTTP, HTTPS, FTP, SFTP или magnet… Также можно выбрать файлы .torrent ниже.\n\nДля загрузки медиа вставляйте ссылки с YouTube, X, TikTok, Instagram, Reddit и т. д.',
|
||||
pastePlaceholder: 'Вставьте URL HTTP(S), FTP/SFTP, magnet или медиа…',
|
||||
pasteHint: 'Поддерживаются ссылки YouTube, X, TikTok, Instagram и Reddit.',
|
||||
playlistSummary: 'Плейлист «{{title}}»: загружено {{loaded}} из {{total}} элементов{{truncated}}{{skipped}}',
|
||||
safeEntryLimit: ' (достигнут безопасный лимит элементов)',
|
||||
selectedSummary: 'Выбрано: {{ready}} готовых, {{fallback}} с резервными данными, {{mediaRetry}} повторов медиа, {{blocked}} заблокировано',
|
||||
|
||||
@@ -653,7 +653,8 @@ const uk = {
|
||||
pauseBeforeReplace: 'Призупиніть {{file}} перед заміною.',
|
||||
cannotReplace: 'Неможливо замінити {{file}}: файл не належить до завантажень Firelink.',
|
||||
downloadLinks: 'Посилання для завантаження',
|
||||
pastePlaceholder: 'Вставте сюди URL-адреси HTTP, HTTPS, FTP, SFTP або magnet… Також можна вибрати файли .torrent нижче.\n\nДля медіазавантажень вставте посилання з YouTube, X, TikTok, Instagram, Reddit тощо.',
|
||||
pastePlaceholder: 'Вставте URL HTTP(S), FTP/SFTP, magnet або медіа…',
|
||||
pasteHint: 'Підтримуються посилання YouTube, X, TikTok, Instagram і Reddit.',
|
||||
playlistSummary: 'Плейлист “{{title}}”: {{loaded}} з {{total}} елементів завантажено{{truncated}}{{skipped}}',
|
||||
safeEntryLimit: ' (досягнуто безпечного ліміту елементів)',
|
||||
selectedSummary: '{{ready}} вибрано готових, {{fallback}} резервних, {{mediaRetry}} повторних медіа, {{blocked}} заблоковано',
|
||||
|
||||
@@ -653,7 +653,8 @@ const zhCN = {
|
||||
pauseBeforeReplace: '请在替换 {{file}} 前暂停它。',
|
||||
cannotReplace: '无法替换 {{file}}:文件不属于 Firelink 下载。',
|
||||
downloadLinks: '下载链接',
|
||||
pastePlaceholder: '在此粘贴 HTTP、HTTPS、FTP、SFTP 或 magnet URL…也可以在下方选择 .torrent 文件。\n\n对于媒体下载,请粘贴来自 YouTube、X、TikTok、Instagram、Reddit 等的链接。',
|
||||
pastePlaceholder: '在此粘贴 HTTP(S)、FTP/SFTP、magnet 或媒体 URL…',
|
||||
pasteHint: '支持 YouTube、X、TikTok、Instagram 和 Reddit 链接。',
|
||||
playlistSummary: '播放列表“{{title}}”:已加载 {{loaded}} / {{total}} 个条目{{truncated}}{{skipped}}',
|
||||
safeEntryLimit: ' (达到安全条目限制)',
|
||||
selectedSummary: '准备就绪 {{ready}} 个,后备项 {{fallback}} 个,媒体重试 {{mediaRetry}} 个,已屏蔽 {{blocked}} 个',
|
||||
|
||||
@@ -76,6 +76,16 @@ describe('translation catalogs', () => {
|
||||
expect(allMismatches).toEqual([]);
|
||||
});
|
||||
|
||||
it('isolates mixed-direction Add Downloads guidance in RTL locales', () => {
|
||||
for (const locale of ['fa', 'he'] as const) {
|
||||
for (const key of ['addDownloads.pastePlaceholder', 'addDownloads.pasteHint']) {
|
||||
const value = catalogFor(locale).get(key) ?? '';
|
||||
expect(value).toContain('\u2066');
|
||||
expect(value).toContain('\u2069');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('reports exact English duplicates for translation review', () => {
|
||||
const english = catalogFor('en');
|
||||
const duplicates = APP_LOCALES.filter((locale) => locale !== 'en').flatMap((locale) => {
|
||||
|
||||
+41
-16
@@ -1602,6 +1602,18 @@ html[data-list-density="relaxed"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.add-download-url-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.add-download-input-hint {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.add-download-link-button {
|
||||
border-radius: 6px;
|
||||
color: hsl(var(--accent-color));
|
||||
@@ -1613,19 +1625,16 @@ html[data-list-density="relaxed"] {
|
||||
background: hsl(var(--accent-color) / 0.1);
|
||||
}
|
||||
|
||||
.add-download-selection-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.add-download-selection-summary {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-download-selection-status {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 10px;
|
||||
line-height: 1.25;
|
||||
@@ -1635,12 +1644,20 @@ html[data-list-density="relaxed"] {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 22px;
|
||||
padding: 3px 7px;
|
||||
border: 1px solid hsl(var(--border-modal));
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--surface-raised) / 0.46);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.add-download-status-chip > span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-download-status-chip strong {
|
||||
@@ -1666,11 +1683,17 @@ html[data-list-density="relaxed"] {
|
||||
color: hsl(0 78% 68%);
|
||||
}
|
||||
|
||||
.add-download-selection-actions {
|
||||
.add-download-preview-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-download-preview-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.add-download-torrent-advanced-toggle {
|
||||
@@ -1699,13 +1722,15 @@ html[data-list-density="relaxed"] {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.add-download-selection-toolbar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: stretch;
|
||||
.add-download-preview-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.add-download-selection-actions {
|
||||
justify-content: flex-end;
|
||||
@media (min-width: 900px) {
|
||||
.add-download-selection-status {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user