mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 01:44:01 +00:00
@@ -25,6 +25,7 @@ import { getPlatformInfo } from '../utils/platform';
|
||||
import { isTransferLocked } from '../utils/downloadActions';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { localePluralVariant } from '../i18n/locales';
|
||||
import {
|
||||
canSubmitMetadataRows,
|
||||
appendRequestUrlsAfterVersion,
|
||||
@@ -114,7 +115,7 @@ const extensionHeaders = (context: PendingAddRequestContext | undefined) => [
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
export const AddDownloadsModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { addToast } = useToast();
|
||||
const {
|
||||
isAddModalOpen,
|
||||
@@ -1132,19 +1133,60 @@ export const AddDownloadsModal = () => {
|
||||
);
|
||||
const playlistSummaries = Object.entries(playlistExpansions)
|
||||
.filter(([sourceUrl]) => activePlaylistUrls.has(sourceUrl));
|
||||
const pluralMessage = (
|
||||
count: number,
|
||||
one: () => string,
|
||||
few: () => string,
|
||||
many: () => string
|
||||
): string => {
|
||||
switch (localePluralVariant(i18n.language, count)) {
|
||||
case 'one': return one();
|
||||
case 'few': return few();
|
||||
case 'many': return many();
|
||||
}
|
||||
};
|
||||
const metadataSummary = (() => {
|
||||
const summary = metadataSummaryState(parsedItems);
|
||||
const plural = (count: number) => count === 1 ? '' : 's';
|
||||
switch (summary.type) {
|
||||
case 'empty': return t($ => $.addDownloads.pasteOneOrMore);
|
||||
case 'none-selected': return t($ => $.addDownloads.selectAtLeastOne);
|
||||
case 'invalid': return t($ => $.addDownloads.correctInvalid, { count: summary.count, plural: plural(summary.count) });
|
||||
case 'loading': return t($ => $.addDownloads.waitingForMetadata, { count: summary.count, plural: plural(summary.count) });
|
||||
case 'unsafe': return t($ => $.addDownloads.removeUnsafe, { count: summary.count, plural: plural(summary.count) });
|
||||
case 'media-error': return t($ => $.addDownloads.mediaMetadataUnavailableSummary, { count: summary.count, plural: plural(summary.count) });
|
||||
case 'invalid': return pluralMessage(
|
||||
summary.count,
|
||||
() => t($ => $.addDownloads.correctInvalidOne, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.correctInvalidFew, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.correctInvalidMany, { count: summary.count })
|
||||
);
|
||||
case 'loading': return pluralMessage(
|
||||
summary.count,
|
||||
() => t($ => $.addDownloads.waitingForMetadataOne, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.waitingForMetadataFew, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.waitingForMetadataMany, { count: summary.count })
|
||||
);
|
||||
case 'unsafe': return pluralMessage(
|
||||
summary.count,
|
||||
() => t($ => $.addDownloads.removeUnsafeOne, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.removeUnsafeFew, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.removeUnsafeMany, { count: summary.count })
|
||||
);
|
||||
case 'media-error': return pluralMessage(
|
||||
summary.count,
|
||||
() => t($ => $.addDownloads.mediaMetadataUnavailableSummaryOne, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.mediaMetadataUnavailableSummaryFew, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.mediaMetadataUnavailableSummaryMany, { count: summary.count })
|
||||
);
|
||||
case 'all-error': return t($ => $.addDownloads.metadataUnavailableFallback);
|
||||
case 'fallback': return t($ => $.addDownloads.fallbackReady, { ready: summary.ready, readyPlural: plural(summary.ready), failed: summary.failed });
|
||||
case 'ready': return t($ => $.addDownloads.readyToAdd, { count: summary.count, plural: plural(summary.count) });
|
||||
case 'fallback': return pluralMessage(
|
||||
summary.ready,
|
||||
() => t($ => $.addDownloads.fallbackReadyOne, { ready: summary.ready, failed: summary.failed }),
|
||||
() => t($ => $.addDownloads.fallbackReadyFew, { ready: summary.ready, failed: summary.failed }),
|
||||
() => t($ => $.addDownloads.fallbackReadyMany, { ready: summary.ready, failed: summary.failed })
|
||||
);
|
||||
case 'ready': return pluralMessage(
|
||||
summary.count,
|
||||
() => t($ => $.addDownloads.readyToAddOne, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.readyToAddFew, { count: summary.count }),
|
||||
() => t($ => $.addDownloads.readyToAddMany, { count: summary.count })
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1194,7 +1236,7 @@ export const AddDownloadsModal = () => {
|
||||
<div className="flex flex-1 min-h-0 overflow-hidden">
|
||||
|
||||
{/* Left Column: URLs and Preview */}
|
||||
<div className="add-download-left w-[55%] border-r border-border-modal flex flex-col">
|
||||
<div className="add-download-left w-[55%] flex flex-col">
|
||||
<div className="add-download-pane p-5 flex-1 min-h-0 min-w-0 flex flex-col gap-5">
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -1245,7 +1287,7 @@ export const AddDownloadsModal = () => {
|
||||
type="button"
|
||||
onClick={toggleAllRows}
|
||||
disabled={parsedItems.length === 0}
|
||||
className="add-download-link-button ml-3 text-[11px] font-medium"
|
||||
className="add-download-link-button ms-3 text-[11px] font-medium"
|
||||
>
|
||||
{allRowsSelected ? t($ => $.addDownloads.clearSelection) : t($ => $.addDownloads.selectAll)}
|
||||
</button>
|
||||
@@ -1302,9 +1344,9 @@ export const AddDownloadsModal = () => {
|
||||
onChange={() => toggleRowSelection(i)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
aria-label={t($ => $.addDownloads.selectItem, { file: item.file })}
|
||||
className="mr-2 shrink-0 accent-purple-500"
|
||||
className="me-2 shrink-0 accent-purple-500"
|
||||
/>
|
||||
<div className="flex-[2] text-text-primary font-medium truncate pr-2" title={item.file}>{item.file}</div>
|
||||
<div className="flex-[2] text-text-primary font-medium truncate pe-2" title={item.file}>{item.file}</div>
|
||||
<div className={`flex-1 font-mono ${item.status === 'loading' ? 'text-text-muted/50' : 'text-text-muted'}`}>{item.size || t($ => $.addDownloads.unknown)}</div>
|
||||
<div className={`flex-[1.5] font-medium ${item.status === 'metadata-error' || item.status === 'invalid' ? 'text-red-500' : item.status === 'loading' ? 'text-orange-400' : 'text-blue-500'}`}>
|
||||
{item.status === 'loading' ? (
|
||||
@@ -1337,7 +1379,7 @@ export const AddDownloadsModal = () => {
|
||||
{/* Media Format (Dynamic) */}
|
||||
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
|
||||
<section className="add-download-section add-download-media-section relative overflow-hidden p-4">
|
||||
<div className="absolute top-0 right-0 p-2 opacity-10">
|
||||
<div className="absolute top-0 end-0 p-2 opacity-10">
|
||||
<Video size={48} />
|
||||
</div>
|
||||
<div className="add-download-section-title flex items-center gap-2 mb-3 relative z-10">
|
||||
@@ -1358,7 +1400,7 @@ export const AddDownloadsModal = () => {
|
||||
<div className="space-y-3 relative z-10">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] uppercase font-bold tracking-wider text-text-muted">{t($ => $.addDownloads.availableStreams)}</label>
|
||||
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1" role="radiogroup" aria-label={t($ => $.addDownloads.availableMediaStreams)}>
|
||||
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pe-1" role="radiogroup" aria-label={t($ => $.addDownloads.availableMediaStreams)}>
|
||||
{parsedItems[selectedItemIndex].formats!.map((f, idx) => {
|
||||
const isSelected = parsedItems[selectedItemIndex].selectedFormat === idx;
|
||||
const Icon = f.type === 'Audio' ? Music : Film;
|
||||
@@ -1474,7 +1516,7 @@ export const AddDownloadsModal = () => {
|
||||
</label>
|
||||
|
||||
{useAuth && (
|
||||
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
|
||||
<div className="add-download-nested-fields space-y-2.5">
|
||||
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder={t($ => $.addDownloads.username)} className="add-download-control w-full px-3 py-1.5 text-xs" />
|
||||
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder={t($ => $.addDownloads.password)} className="add-download-control w-full px-3 py-1.5 text-xs" />
|
||||
</div>
|
||||
@@ -1493,7 +1535,7 @@ export const AddDownloadsModal = () => {
|
||||
</button>
|
||||
|
||||
{advancedExpanded && (
|
||||
<div className="mt-4 space-y-4 pl-6">
|
||||
<div className="add-download-advanced-fields mt-4 space-y-4">
|
||||
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
|
||||
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="add-download-checkbox" />
|
||||
{t($ => $.addDownloads.verifyChecksum)}
|
||||
@@ -1573,12 +1615,12 @@ export const AddDownloadsModal = () => {
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isQueueMenuOpen}
|
||||
>
|
||||
{t($ => $.addDownloads.addToQueue)} <ChevronDown size={14} className="ml-1" />
|
||||
{t($ => $.addDownloads.addToQueue)} <ChevronDown size={14} className="ms-1" />
|
||||
</button>
|
||||
{isQueueMenuOpen && (
|
||||
<div
|
||||
role="menu"
|
||||
className="app-modal absolute bottom-full right-0 z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs"
|
||||
className="add-download-queue-menu app-modal absolute bottom-full z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs"
|
||||
>
|
||||
{queues.map(queue => (
|
||||
<button
|
||||
@@ -1589,7 +1631,7 @@ export const AddDownloadsModal = () => {
|
||||
setIsQueueMenuOpen(false);
|
||||
void handleAction({ type: 'add-to-queue', queueId: queue.id });
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-item-hover"
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-start hover:bg-item-hover"
|
||||
>
|
||||
<span className="truncate">{queue.name}</span>
|
||||
</button>
|
||||
|
||||
@@ -692,9 +692,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
<div className="group relative">
|
||||
<button className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors flex justify-between items-center">
|
||||
{t($ => $.downloadTable.addToQueue)}
|
||||
<ChevronRight size={14} />
|
||||
<ChevronRight size={14} className="download-context-menu-chevron" />
|
||||
</button>
|
||||
<div className="absolute left-full top-0 hidden group-hover:block ml-1 min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
|
||||
<div className="download-context-submenu absolute top-0 hidden group-hover:block min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
|
||||
{queues.map(q => (
|
||||
<button key={q.id} onClick={() => {
|
||||
setContextMenu(null);
|
||||
@@ -811,9 +811,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
<div className="group relative">
|
||||
<button className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors flex justify-between items-center">
|
||||
{t($ => $.downloadTable.addToQueue)}
|
||||
<ChevronRight size={14} />
|
||||
<ChevronRight size={14} className="download-context-menu-chevron" />
|
||||
</button>
|
||||
<div className="absolute left-full top-0 hidden group-hover:block ml-1 min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
|
||||
<div className="download-context-submenu absolute top-0 hidden group-hover:block min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
|
||||
{queues.map(q => (
|
||||
<button key={q.id} onClick={() => {
|
||||
setContextMenu(null);
|
||||
|
||||
@@ -328,7 +328,7 @@ export default function LogsView() {
|
||||
<div className="bg-black/10 border-y border-border-modal px-4 py-2 shrink-0 flex items-center gap-2 text-text-muted text-[10px] select-none">
|
||||
<Info size={12} className="text-text-muted opacity-80 shrink-0" />
|
||||
<span className="opacity-90 leading-tight">
|
||||
<strong className="font-medium text-text-primary mr-1">{t($ => $.logs.localDiagnostics)}</strong>
|
||||
<strong className="font-medium text-text-primary me-1">{t($ => $.logs.localDiagnostics)}</strong>
|
||||
{t($ => $.logs.diagnosticsDescription)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -362,10 +362,10 @@ export default function LogsView() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="w-full text-left px-3 py-2 flex items-center hover:bg-item-hover transition-colors"
|
||||
className="w-full text-start px-3 py-2 flex items-center hover:bg-item-hover transition-colors"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy size={13} className="mr-2 text-text-secondary" />
|
||||
<Copy size={13} className="me-2 text-text-secondary" />
|
||||
{t($ => $.logs.copy)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -644,6 +644,15 @@ runEngineChecks(false);
|
||||
default: return t($ => $.navigation.categories.other);
|
||||
}
|
||||
};
|
||||
const languageOptions = [
|
||||
{ value: 'system', label: t($ => $.settings.lookAndFeel.languageSystem) },
|
||||
{ value: 'en', label: t($ => $.settings.lookAndFeel.languageEnglish) },
|
||||
{ value: 'zh-CN', label: t($ => $.settings.lookAndFeel.languageChinese) },
|
||||
{ value: 'he', label: t($ => $.settings.lookAndFeel.languageHebrew) },
|
||||
{ value: 'fa', label: t($ => $.settings.lookAndFeel.languagePersian) },
|
||||
{ value: 'uk', label: t($ => $.settings.lookAndFeel.languageUkrainian) },
|
||||
{ value: 'ru', label: t($ => $.settings.lookAndFeel.languageRussian) },
|
||||
] as const;
|
||||
|
||||
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
|
||||
const active = activeTab === type;
|
||||
@@ -784,6 +793,25 @@ runEngineChecks(false);
|
||||
{/* Look & Feel Pane */}
|
||||
{activeTab === 'lookandfeel' && (
|
||||
<div className="settings-pane max-w-[720px]">
|
||||
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.language)}</h2>
|
||||
<div className="mac-settings-group">
|
||||
<div className="mac-settings-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.lookAndFeel.language)}</span>
|
||||
<small>{t($ => $.settings.lookAndFeel.languageDescription)}</small>
|
||||
</div>
|
||||
<select
|
||||
value={settings.language}
|
||||
onChange={(event) => settings.setLanguage(event.target.value as typeof settings.language)}
|
||||
className="app-control w-48"
|
||||
>
|
||||
{languageOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.appTheme)}</h2>
|
||||
<div className="mac-settings-group">
|
||||
<div className="mac-settings-row settings-choice-row">
|
||||
|
||||
+13
-13
@@ -122,13 +122,13 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
data-active={isSelected}
|
||||
className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
|
||||
className="sidebar-nav-item group flex w-full items-center text-[13px] text-start cursor-default font-medium"
|
||||
onClick={() => onSelectFilter(filter)}
|
||||
>
|
||||
<Icon className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
|
||||
<Icon className="w-[18px] h-[18px] me-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
|
||||
<span className="truncate">{label}</span>
|
||||
{getCount(filter) > 0 && (
|
||||
<span className="sidebar-count ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold">
|
||||
<span className="sidebar-count ms-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold">
|
||||
{getCount(filter)}
|
||||
</span>
|
||||
)}
|
||||
@@ -182,7 +182,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<div className="flex items-center px-2.5 py-1 rounded-lg mb-0.5 bg-item-hover">
|
||||
<List className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} />
|
||||
<List className="w-4 h-4 me-2 text-text-secondary" strokeWidth={2} />
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
type="text"
|
||||
@@ -205,12 +205,12 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
data-active={isSelected}
|
||||
onContextMenu={e => handleQueueContextMenu(e, queue.id)}
|
||||
onClick={() => onSelectFilter(filterId)}
|
||||
className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
|
||||
className="sidebar-nav-item group flex w-full items-center text-[13px] text-start cursor-default font-medium"
|
||||
>
|
||||
<List className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
|
||||
<List className="w-[18px] h-[18px] me-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
|
||||
<span className="truncate">{queue.name}</span>
|
||||
{getCount(filterId) > 0 && (
|
||||
<span className="sidebar-count ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold shrink-0">
|
||||
<span className="sidebar-count ms-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold shrink-0">
|
||||
{getCount(filterId)}
|
||||
</span>
|
||||
)}
|
||||
@@ -225,9 +225,9 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
type="button"
|
||||
data-active={isSelected}
|
||||
onClick={() => setActiveView(view)}
|
||||
className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
|
||||
className="sidebar-nav-item group flex w-full items-center text-[13px] text-start cursor-default font-medium"
|
||||
>
|
||||
<Icon className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
|
||||
<Icon className="w-[18px] h-[18px] me-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
@@ -298,7 +298,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
))}
|
||||
{isAddingQueue ? (
|
||||
<div className="flex items-center px-3.5 py-1.5 rounded-lg bg-item-hover mb-1">
|
||||
<Plus className="w-4 h-4 mr-2 text-text-secondary shrink-0" strokeWidth={2} />
|
||||
<Plus className="w-4 h-4 me-2 text-text-secondary shrink-0" strokeWidth={2} />
|
||||
<input
|
||||
ref={addInputRef}
|
||||
type="text"
|
||||
@@ -319,7 +319,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
onClick={() => { addQueueSubmitRef.current = false; setIsAddingQueue(true); setNewQueueName(''); }}
|
||||
className="flex w-full items-center px-3.5 py-1.5 rounded-lg text-[13px] text-text-muted hover:bg-item-hover hover:text-text-secondary cursor-default transition-colors mb-1"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2 shrink-0" strokeWidth={2} />
|
||||
<Plus className="w-4 h-4 me-2 shrink-0" strokeWidth={2} />
|
||||
<span className="truncate">{t($ => $.actions.addNewQueue)}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -338,9 +338,9 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
type="button"
|
||||
data-active={activeView === 'settings'}
|
||||
onClick={() => setActiveView('settings')}
|
||||
className="sidebar-nav-item sidebar-settings-button group flex w-full items-center text-[13px] text-left cursor-default font-medium transition-colors"
|
||||
className="sidebar-nav-item sidebar-settings-button group flex w-full items-center text-[13px] text-start cursor-default font-medium transition-colors"
|
||||
>
|
||||
<Settings className={`w-[18px] h-[18px] mr-3 shrink-0 ${activeView === 'settings' ? 'text-white' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.5 : 2} />
|
||||
<Settings className={`w-[18px] h-[18px] me-3 shrink-0 ${activeView === 'settings' ? 'text-white' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.5 : 2} />
|
||||
<span>{t($ => $.navigation.settings)}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -218,7 +218,7 @@ export default function SpeedLimiterView() {
|
||||
}`}>
|
||||
{enabled ? `${currentDisplayValue} ${unit}` : t($ => $.speedLimiter.unlimited)}
|
||||
</span>
|
||||
<button onClick={() => void save()} disabled={isSaving} className="app-button app-button-primary ml-auto px-3 text-[11px] disabled:opacity-50">
|
||||
<button onClick={() => void save()} disabled={isSaving} className="app-button app-button-primary ms-auto px-3 text-[11px] disabled:opacity-50">
|
||||
<Save size={14} /> {t($ => $.speedLimiter.saveLimit)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -275,7 +275,7 @@ export default function SpeedLimiterView() {
|
||||
type="button"
|
||||
disabled={!enabled || isSaving}
|
||||
onClick={() => preset(presetValue)}
|
||||
className="h-full flex-1 px-3 text-left disabled:opacity-50"
|
||||
className="h-full flex-1 px-3 text-start disabled:opacity-50"
|
||||
>
|
||||
{formatPresetValue(displayValue)} {unit}
|
||||
</button>
|
||||
@@ -283,7 +283,7 @@ export default function SpeedLimiterView() {
|
||||
type="button"
|
||||
disabled={!enabled || isSaving}
|
||||
onClick={() => removePreset(presetValue)}
|
||||
className="flex h-full w-7 items-center justify-center border-l border-border-modal text-text-muted transition-colors hover:bg-red-500/10 hover:text-red-400 focus-visible:bg-red-500/10 focus-visible:text-red-400 disabled:opacity-50"
|
||||
className="flex h-full w-7 items-center justify-center border-s border-border-modal text-text-muted transition-colors hover:bg-red-500/10 hover:text-red-400 focus-visible:bg-red-500/10 focus-visible:text-red-400 disabled:opacity-50"
|
||||
title={t($ => $.speedLimiter.removePreset, { value: formatPresetValue(displayValue), unit })}
|
||||
aria-label={t($ => $.speedLimiter.removePreset, { value: formatPresetValue(displayValue), unit })}
|
||||
>
|
||||
@@ -292,7 +292,7 @@ export default function SpeedLimiterView() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="ml-1 flex h-8 items-center gap-1.5 rounded-md border border-border-modal bg-bg-input px-2">
|
||||
<div className="ms-1 flex h-8 items-center gap-1.5 rounded-md border border-border-modal bg-bg-input px-2">
|
||||
<input
|
||||
type="number"
|
||||
min={speedValueFromKiB(1, unit)}
|
||||
|
||||
Reference in New Issue
Block a user