fix(ui): harden RTL sidebar and desktop window controls

Keep custom Windows/Linux controls available during platform detection, preserve RTL table order while aligning filenames left, and persist sidebar placement overrides.

Refs #17
This commit is contained in:
NimBold
2026-07-19 02:34:32 +03:30
parent 2668f0b722
commit 60f13f756c
15 changed files with 225 additions and 50 deletions
+6
View File
@@ -10,6 +10,10 @@ fn default_language_preference() -> String {
"system".to_string()
}
fn default_sidebar_position() -> String {
"auto".to_string()
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
@@ -273,6 +277,8 @@ pub struct PersistedSettings {
pub speed_limit_preset_values: Vec<f64>,
pub logs_enabled: bool,
pub is_sidebar_visible: bool,
#[serde(default = "default_sidebar_position")]
pub sidebar_position: String,
pub active_settings_tab: SettingsTab,
pub scheduler: SchedulerSettings,
pub scheduler_running: bool,
+16
View File
@@ -197,6 +197,7 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
"language",
&["system", "en", "zh-CN", "he", "fa", "uk", "ru"],
);
sanitize_allowed_string(state, "sidebarPosition", &["auto", "left", "right"]);
sanitize_allowed_string(state, "appFontSize", &["small", "standard", "large"]);
sanitize_allowed_string(state, "listRowDensity", &["compact", "standard", "relaxed"]);
sanitize_allowed_string(state, "activeSettingsTab", &[
@@ -426,6 +427,7 @@ fn default_settings() -> PersistedSettings {
speed_limit_preset_values: vec![1.0, 5.0, 10.0],
logs_enabled: false,
is_sidebar_visible: true,
sidebar_position: "auto".to_string(),
active_settings_tab: SettingsTab::Downloads,
scheduler: SchedulerSettings {
enabled: false,
@@ -506,6 +508,7 @@ mod tests {
"state": {
"maxConcurrentDownloads": 7,
"globalSpeedLimit": "2M",
"sidebarPosition": "right",
"scheduler": {
"enabled": true,
"startTime": "06:30",
@@ -523,6 +526,7 @@ mod tests {
assert_eq!(settings.max_concurrent_downloads, 7);
assert_eq!(settings.global_speed_limit, "2M");
assert_eq!(settings.sidebar_position, "right");
assert_eq!(settings.speed_limit_preset_values, vec![1.0, 5.0, 10.0]);
assert!(!settings.logs_enabled);
assert!(settings.scheduler.enabled);
@@ -660,6 +664,18 @@ mod tests {
assert_eq!(settings.max_concurrent_downloads, 3);
}
#[test]
fn invalid_sidebar_position_uses_automatic_layout() {
let stored = json!({
"state": {"sidebarPosition": "diagonal"},
"version": 5
});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert_eq!(settings.sidebar_position, "auto");
}
#[test]
fn clamps_out_of_range_download_settings() {
let stored = json!({
+19 -6
View File
@@ -14,7 +14,7 @@ import { WindowControls } from "./components/WindowControls";
import { useToast } from "./contexts/ToastContext";
import { setLogStreamActive } from './utils/logger';
import { openUrl } from '@tauri-apps/plugin-opener';
import { getPlatformInfo, usePlatformInfo } from './utils/platform';
import { getPlatformInfo, shouldUseCustomWindowControls, usePlatformInfo } from './utils/platform';
import {
getKeychainAccessReady,
getKeychainConsentVersion,
@@ -24,7 +24,7 @@ import { getVersion } from '@tauri-apps/api/app';
import type { PostQueueAction } from './bindings/PostQueueAction';
import { PanelLeft } from 'lucide-react';
import { isTrustedFirelinkReleaseUrl } from './utils/releaseUrls';
import { changeAppLocale, resolveAppLocale, syncDocumentLocale } from './i18n';
import { changeAppLocale, localeDirection, resolveAppLocale, syncDocumentLocale } from './i18n';
import { useTranslation } from 'react-i18next';
const SettingsView = lazy(() => import('./components/SettingsView'));
@@ -134,6 +134,7 @@ function App() {
const theme = useSettingsStore(state => state.theme);
const languagePreference = useSettingsStore(state => state.language);
const isSidebarVisible = useSettingsStore(state => state.isSidebarVisible);
const sidebarPosition = useSettingsStore(state => state.sidebarPosition);
const toggleSidebar = useSettingsStore(state => state.toggleSidebar);
const activeView = useSettingsStore(state => state.activeView);
const appFontSize = useSettingsStore(state => state.appFontSize);
@@ -178,7 +179,9 @@ function App() {
const activeTransferCount = downloads.filter(download => isTransferActiveStatus(download.status)).length;
const { addToast, removeToast } = useToast();
const isMacUserAgent = navigator.userAgent.includes('Mac');
const usesCustomWindowControls = !isMacUserAgent && platform.os !== 'macos';
const usesCustomWindowControls = shouldUseCustomWindowControls(platform.os, navigator.userAgent);
const isRtl = localeDirection(resolveAppLocale(i18n.language)) === 'rtl';
const isSidebarOnRight = sidebarPosition === 'right' || (sidebarPosition === 'auto' && isRtl);
// Keep dialogs out of the titlebar area while platform detection is still
// resolving. The conservative fallback prevents a startup handoff from
// briefly rendering underneath native or custom window controls.
@@ -285,7 +288,10 @@ function App() {
const startWidth = sidebarWidth;
const handlePointerMove = (moveEvent: PointerEvent) => {
const nextWidth = Math.min(260, Math.max(190, startWidth + moveEvent.clientX - startX));
const delta = isSidebarOnRight
? startX - moveEvent.clientX
: moveEvent.clientX - startX;
const nextWidth = Math.min(260, Math.max(190, startWidth + delta));
setSidebarWidth(nextWidth);
};
@@ -933,16 +939,21 @@ function App() {
return (
<div className={`app-shell flex h-screen w-screen overflow-hidden text-text-primary ${
isSidebarOnRight ? 'app-shell--sidebar-right' : 'app-shell--sidebar-left'
} ${
hasWindowChrome ? 'app-shell--window-chrome' : ''
}`}>
{(platform.os === 'windows' || platform.os === 'linux') && <WindowControls />}
{usesCustomWindowControls && <WindowControls />}
<div
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
isSidebarOnRight ? 'app-sidebar-shell--right' : 'app-sidebar-shell--left'
} ${
isSidebarVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
style={{
width: sidebarWidth,
marginInlineStart: isSidebarVisible ? 0 : -sidebarWidth
marginInlineStart: isSidebarVisible || isSidebarOnRight ? 0 : -sidebarWidth,
marginInlineEnd: isSidebarVisible || !isSidebarOnRight ? 0 : -sidebarWidth
}}
>
<div className="app-sidebar-panel h-full w-full">
@@ -963,6 +974,8 @@ function App() {
<div
className={`app-workspace relative z-0 flex-1 flex flex-col h-full overflow-hidden ${
isSidebarOnRight ? 'app-workspace--sidebar-right' : 'app-workspace--sidebar-left'
} ${
!isSidebarVisible ? 'app-workspace--sidebar-collapsed' : ''
} ${usesCustomWindowControls ? 'app-workspace--custom-window-controls' : ''}`}
>
+1 -1
View File
@@ -8,4 +8,4 @@ import type { SettingsTab } from "./SettingsTab";
import type { SiteLogin } from "./SiteLogin";
import type { Theme } from "./Theme";
export type PersistedSettings = { theme: Theme, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
export type PersistedSettings = { theme: Theme, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
+16
View File
@@ -2,6 +2,7 @@ import { useCallback, useRef, useState, useEffect } from 'react';
import {
type AppFontSize,
type ListRowDensity,
type SidebarPosition,
type SettingsState,
SettingsTab,
runSettingsPersistenceTransaction,
@@ -810,6 +811,21 @@ runEngineChecks(false);
))}
</select>
</div>
<div className="mac-settings-row">
<div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.sidebarPosition)}</span>
<small>{t($ => $.settings.lookAndFeel.sidebarPositionDescription)}</small>
</div>
<select
value={settings.sidebarPosition}
onChange={(event) => settings.setSidebarPosition(event.target.value as SidebarPosition)}
className="app-control w-48"
>
<option value="auto">{t($ => $.settings.lookAndFeel.sidebarPositionAutomatic)}</option>
<option value="left">{t($ => $.settings.lookAndFeel.sidebarPositionLeft)}</option>
<option value="right">{t($ => $.settings.lookAndFeel.sidebarPositionRight)}</option>
</select>
</div>
</div>
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.appTheme)}</h2>
+5
View File
@@ -532,6 +532,11 @@ const common = {
lookAndFeel: {
language: 'Language',
languageDescription: 'Choose the Firelink interface language.',
sidebarPosition: 'Sidebar position',
sidebarPositionDescription: 'Choose where the sidebar appears. Automatic follows the interface direction.',
sidebarPositionAutomatic: 'Automatic (recommended)',
sidebarPositionLeft: 'Left',
sidebarPositionRight: 'Right',
languageSystem: 'System default',
languageEnglish: 'English',
languageChinese: 'Simplified Chinese',
+42 -37
View File
@@ -8,7 +8,7 @@ const fa = {
unfinished: 'ناتمام',
},
categories: {
musics: 'موسیقی‌ها',
musics: 'موسیقی',
movies: 'فیلم‌ها',
compressed: 'فشرده',
documents: 'اسناد',
@@ -19,8 +19,8 @@ const fa = {
folders: 'پوشه‌ها',
queues: 'صف‌ها',
tools: 'ابزارها',
scheduler: 'زمان‌بند',
speedLimiter: 'محدودکننده سرعت',
scheduler: 'زمان‌بندی',
speedLimiter: 'محدودکنندهٔ سرعت',
logs: 'گزارش‌ها',
settings: 'تنظیمات',
},
@@ -63,8 +63,8 @@ const fa = {
window: {
controls: 'کنترل‌های پنجره',
close: 'بستن',
minimize: وچک کردن',
maximize: زرگ کردن',
minimize: مینه کردن',
maximize: یشینه کردن',
},
downloads: {
actions: {
@@ -86,7 +86,7 @@ const fa = {
downloading: 'در حال دانلود',
processing: 'در حال پردازش',
paused: 'متوقف شده',
completed: 'تکمیل شده',
completed: 'تکمیلشده',
failed: 'ناموفق',
retrying: 'در حال تلاش مجدد',
},
@@ -96,9 +96,9 @@ const fa = {
},
},
speedLimiter: {
title: 'محدودکننده سرعت',
title: 'محدودکنندهٔ سرعت',
unlimited: 'نامحدود',
saveLimit: 'ذخیره محدودیت',
saveLimit: 'ذخیره محدودیت سرعت',
globalSpeedLimit: 'محدودیت سرعت کلی',
description: 'در مورد انتقال‌های جدید و فعال aria2 و دانلودهای رسانه جدید yt-dlp اعمال می‌شود. محدودیت‌های خاص هر دانلود برای آن انتقال‌ها به قوت خود باقی می‌ماند.',
quickPresets: 'پیش‌تنظیم‌های سریع',
@@ -215,7 +215,7 @@ const fa = {
speedCap: 'سقف سرعت',
category: 'دسته',
lastTry: 'آخرین تلاش',
dateAdded: 'تاریخ اضافه شدن',
dateAdded: 'تاریخ افزودن',
destination: 'مقصد',
lastError: 'آخرین خطا',
saved: ' (ذخیره شده)',
@@ -260,9 +260,9 @@ const fa = {
title: 'نیاز به دسترسی ذخیره‌سازی اعتبارنامه',
stores: {
portable: 'پوشه داده‌های قابل‌حمل Firelink',
windows: 'Windows Credential Manager',
linux: 'مخزن اعتبارنامه‌های Linux شما',
macos: 'macOS Keychain',
windows: 'مدیر اعتبارنامه Windows',
linux: 'مخزن اعتبارنامه Linux شما',
macos: 'Keychain macOS',
system: "مخزن اعتبارنامه‌های این سیستم",
siteCredentials: "مخزن اعتبارنامه‌های سیستم",
},
@@ -307,7 +307,7 @@ const fa = {
updateAvailable: 'Firelink {{version}} در دسترس است.',
viewRelease: 'مشاهده انتشار',
sleepPreventionFailed: 'جلوگیری از خواب سیستم به‌روزرسانی نشد: {{detail}}',
schedulerNoQueues: 'زمان‌بند هیچ صف معتبری انتخاب نکرده است. تنظیمات زمان‌بند را به‌روزرسانی کنید.',
schedulerNoQueues: 'زمان‌بند هیچ صف معتبری انتخاب نکرده است. تنظیمات زمان‌بندی را به‌روزرسانی کنید.',
schedulerPauseOneFailed: 'زمان‌بند نتوانست ۱ دانلود را متوقف کند.',
schedulerPauseManyFailed: 'زمان‌بند نتوانست {{count}} دانلود را متوقف کند.',
scheduledIncomplete: 'همه دانلودهای زمان‌بندی‌شده تکمیل نشدند. اقدام سیستم پس از صف رد شد.',
@@ -335,7 +335,7 @@ const fa = {
status: 'وضعیت',
speed: 'سرعت',
eta: 'زمان تخمینی',
dateAdded: 'تاریخ اضافه شدن',
dateAdded: 'تاریخ افزودن',
},
addDownload: 'افزودن دانلود',
resumeAll: 'ادامه همه',
@@ -351,8 +351,8 @@ const fa = {
nonResumableOne: '۱ دانلود از ادامه پشتیبانی نمی‌کند. اگر آن را متوقف کنید، بعداً باید دوباره شروع کنید. آیا مطمئن هستید که می‌خواهید متوقف کنید؟',
nonResumableMany: '{{count}} دانلود از ادامه پشتیبانی نمی‌کنند. اگر آنها را متوقف کنید، بعداً باید دوباره شروع کنید. آیا مطمئن هستید که می‌خواهید متوقف کنید؟',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'فایل دانلود شده باز نشد',
revealFileFailed: 'نمایش دانلود در Finder ناموفق بود',
openFileFailed: 'باز کردن فایل دانلودشده ناموفق بود',
revealFileFailed: 'نمایش فایل دانلودشده در پوشه ناموفق بود',
pauseFailed: 'توقف دانلود ناموفق بود',
resumeFailed: 'ادامه {{fileName}} ناموفق بود',
moveManyFailed: 'انتقال دانلودها به صف ناموفق بود',
@@ -373,9 +373,9 @@ const fa = {
resume: 'ادامه',
redownload: 'دانلود مجدد',
copyFilePath: 'کپی مسیر فایل',
properties: 'خصوصیات',
properties: 'ویژگی‌ها',
backendRejectedStart: 'درخواست شروع/ادامه توسط موتور دانلود رد شد.',
transferActive: 'نمی‌توانید خصوصیات را در حالی که انتقال فعال است تغییر دهید. ابتدا آن را متوقف کنید.',
transferActive: 'نمی‌توانید ویژگی‌ها را هنگام فعال بودن انتقال تغییر دهید. ابتدا آن را متوقف کنید.',
redownloadNotFound: 'دانلود مجدد ناموفق: دانلود پیدا نشد.',
redownloadActive: 'نمی‌توانید یک دانلود {{status}} را دوباره دانلود کنید. ابتدا آن را متوقف کنید یا منتظر بمانید تا تمام شود.',
originalUrlMissing: 'دانلود مجدد ناموفق: URL اصلی وجود ندارد.',
@@ -433,14 +433,14 @@ const fa = {
availableMediaStreams: 'جریان‌های رسانه موجود',
metadataUnavailable: 'متادیتا در دسترس نیست. پیش از افزودن این رسانه، متادیتا را تازه‌سازی کنید.',
saveLocation: 'محل ذخیره',
browse: 'مرور',
browse: 'انتخاب',
categoryFolders: 'فایل‌ها در پوشه‌های دسته سازماندهی خواهند شد.',
sharedFolder: 'تمام دانلودهای انتخاب‌شده از این پوشه استفاده خواهند کرد.',
transferSettings: 'تنظیمات انتقال',
connectionsPerFile: 'اتصالات در هر فایل',
connectionsPerFileAria: 'اتصالات در هر فایل',
limitSpeedPerFile: 'محدودیت سرعت در هر فایل',
speedLimitPerFileAria: 'محدودیت سرعت در هر فایل',
speedLimitPerFileAria: 'محدودیت سرعت برای هر فایل',
authorization: 'احراز هویت',
useAuthorization: 'استفاده از احراز هویت',
username: 'نام کاربری',
@@ -448,7 +448,7 @@ const fa = {
advancedTransfer: 'انتقال پیشرفته',
verifyChecksum: 'تأیید چک‌سام',
checksumAlgorithm: 'الگوریتم چک‌سام',
expectedDigest: 'هش موردانتظار',
expectedDigest: 'هش مورد انتظار',
headers: 'هدرها',
requestHeaders: 'هدرهای درخواست',
cookies: 'کوکی‌ها',
@@ -495,11 +495,11 @@ const fa = {
thisOs: 'این سیستم‌عامل',
checking: 'در حال بررسی...',
ready: 'آماده',
errorMissing: 'خطا / ناموجود',
errorMissing: 'خطا / یافت نشد',
hideTechnicalDetails: 'پنهان کردن جزئیات فنی',
showTechnicalDetails: 'نمایش جزئیات فنی',
binary: 'باینری',
expected: 'موردانتظار',
expected: قدار مورد انتظار',
error: 'خطا',
tip: 'نکته',
stderr: 'stderr',
@@ -532,6 +532,11 @@ const fa = {
lookAndFeel: {
language: 'زبان',
languageDescription: 'زبان رابط Firelink را انتخاب کنید.',
sidebarPosition: 'جایگاه نوار کناری',
sidebarPositionDescription: 'مشخص کنید نوار کناری کجا نمایش داده شود. حالت خودکار از جهت رابط کاربری پیروی می‌کند.',
sidebarPositionAutomatic: 'خودکار (پیشنهادشده)',
sidebarPositionLeft: 'چپ',
sidebarPositionRight: 'راست',
languageSystem: 'زبان سیستم',
languageEnglish: 'انگلیسی',
languageChinese: 'چینی ساده‌شده',
@@ -539,32 +544,32 @@ const fa = {
languagePersian: 'فارسی',
languageUkrainian: 'اوکراینی',
languageRussian: 'روسی',
appTheme: 'پوسته برنامه',
theme: 'پوسته',
ariaLabel: 'پوسته برنامه',
appTheme: 'ظاهر برنامه',
theme: 'ظاهر',
ariaLabel: 'ظاهر برنامه',
system: 'سیستم',
light: 'روشن',
dark: 'تاریک',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: 'سیستم از ظاهر روشن یا تاریک فعلی {{platform}} پیروی می‌کند.',
systemAppearance: 'برنامه از حالت روشن یا تاریک فعلی {{platform}} پیروی می‌کند.',
display: 'نمایش',
fontSize: 'اندازه قلم',
fontSizeDescription: 'لیست دانلودها و کنترل‌های فشرده را تغییر مقیاس می‌دهد.',
fontSizeDescription: 'اندازه فهرست دانلود و کنترل‌های فشرده را تغییر می‌دهد.',
small: 'کوچک',
standard: 'استاندارد',
large: 'بزرگ',
listDensity: 'تراکم لیست',
listDensity: 'تراکم فهرست',
listDensityDescription: 'ارتفاع سطر، فاصله‌گذاری و مقیاس نوار پیشرفت را تغییر می‌دهد.',
compact: 'فشرده',
comfortable: 'راحت',
comfortable: 'معمولی',
relaxed: 'بسیار راحت',
osIntegration: 'یکپارچه‌سازی سیستم‌عامل',
dockBadge: 'نمایش نشان روی آیکون Dock',
dockBadgeDescription: 'تعداد دانلودهای فعال را روی آیکون Firelink در Dock نشان می‌دهد.',
menuBarIcon: 'نمایش آیکون نوار منو',
statusIndicatorIcon: 'نمایش آیکون نشانگر وضعیت',
systemTrayIcon: 'نمایش آیکون System Tray',
systemTrayIcon: 'نمایش آیکون سینی سیستم',
macosTrayDescription: 'دسترسی سریع از نوار منوی macOS فراهم می‌کند.',
windowsTrayDescription: 'دسترسی سریع از ناحیه اعلان‌های Windows فراهم می‌کند.',
linuxTrayDescription: 'در صورت وجود، دسترسی سریع از سینی دسکتاپ یا ناحیه وضعیت فراهم می‌کند.',
@@ -602,15 +607,15 @@ const fa = {
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Windows دسکتاپ',
macosDesktop: 'macOS دسکتاپ',
windowsDesktop: 'دسکتاپ Windows',
macosDesktop: 'دسکتاپ macOS',
},
locations: {
baseDownloadFolder: 'پوشه پایه دانلود',
baseDownloadFolder: 'پوشه اصلی دانلود',
baseDownloadFolderDescription: 'پوشه‌های دسته خودکار در داخل این پوشه ایجاد می‌شوند.',
browse: 'مرور',
baseFolderCreateFailed: 'پوشه پایه ذخیره شد، اما ایجاد پوشه‌های دسته ناموفق بود: {{detail}}',
baseFolderUpdated: 'پوشه پایه دانلود به‌روزرسانی شد',
browse: 'انتخاب',
baseFolderCreateFailed: 'پوشه اصلی ذخیره شد، اما ایجاد پوشه‌های دسته ناموفق بود: {{detail}}',
baseFolderUpdated: 'پوشه اصلی دانلود به‌روزرسانی شد',
askWhereToSave: 'هنگام افزودن دانلود بپرس کجا ذخیره شود',
rememberLastUsedDownloadDirectory: 'پوشه دانلود اخیراً استفاده‌شده را به خاطر بسپار',
rememberLastUsedDownloadDirectoryDescription: 'از آخرین پوشه انتخاب‌شده در پنجره افزودن برای دانلود بعدی تا زمان راه‌اندازی مجدد برنامه استفاده کنید.',
+5
View File
@@ -532,6 +532,11 @@ const he = {
lookAndFeel: {
language: 'שפה',
languageDescription: 'בחר את שפת הממשק של Firelink.',
sidebarPosition: 'מיקום סרגל הצד',
sidebarPositionDescription: 'בחר היכן יופיע סרגל הצד. מצב אוטומטי עוקב אחר כיוון הממשק.',
sidebarPositionAutomatic: 'אוטומטי (מומלץ)',
sidebarPositionLeft: 'שמאל',
sidebarPositionRight: 'ימין',
languageSystem: 'שפת המערכת',
languageEnglish: 'אנגלית',
languageChinese: 'סינית מפושטת',
+5
View File
@@ -532,6 +532,11 @@ const ru = {
lookAndFeel: {
language: 'Язык',
languageDescription: 'Выберите язык интерфейса Firelink.',
sidebarPosition: 'Положение боковой панели',
sidebarPositionDescription: 'Выберите, где отображать боковую панель. Автоматический режим учитывает направление интерфейса.',
sidebarPositionAutomatic: 'Автоматически (рекомендуется)',
sidebarPositionLeft: 'Слева',
sidebarPositionRight: 'Справа',
languageSystem: 'Язык системы',
languageEnglish: 'Английский',
languageChinese: 'Упрощённый китайский',
+5
View File
@@ -532,6 +532,11 @@ const uk = {
lookAndFeel: {
language: 'Мова',
languageDescription: 'Виберіть мову інтерфейсу Firelink.',
sidebarPosition: 'Положення бічної панелі',
sidebarPositionDescription: 'Виберіть, де відображати бічну панель. Автоматичний режим враховує напрямок інтерфейсу.',
sidebarPositionAutomatic: 'Автоматично (рекомендовано)',
sidebarPositionLeft: 'Ліворуч',
sidebarPositionRight: 'Праворуч',
languageSystem: 'Мова системи',
languageEnglish: 'Англійська',
languageChinese: 'Спрощена китайська',
+5
View File
@@ -532,6 +532,11 @@ const zhCN = {
lookAndFeel: {
language: '语言',
languageDescription: '选择 Firelink 的界面语言。',
sidebarPosition: '侧边栏位置',
sidebarPositionDescription: '选择侧边栏显示的位置。“自动”会跟随界面文字方向。',
sidebarPositionAutomatic: '自动(推荐)',
sidebarPositionLeft: '左侧',
sidebarPositionRight: '右侧',
languageSystem: '系统默认',
languageEnglish: '英语',
languageChinese: '简体中文',
+65 -5
View File
@@ -1002,6 +1002,7 @@ html[data-list-density="relaxed"] {
}
.app-shell {
direction: ltr;
background: hsl(var(--main-bg));
}
@@ -1011,6 +1012,16 @@ html[data-list-density="relaxed"] {
background: transparent;
}
.app-sidebar-shell--left {
order: 0;
}
.app-sidebar-shell--right {
order: 2;
padding-inline-start: 0;
padding-inline-end: 9px;
}
.sidebar-resize-handle {
position: absolute;
top: 18px;
@@ -1033,6 +1044,16 @@ html[data-list-density="relaxed"] {
transition: background-color 100ms ease;
}
.app-sidebar-shell--right .sidebar-resize-handle {
inset-inline-start: -4px;
inset-inline-end: auto;
}
.app-sidebar-shell--right .sidebar-resize-handle::after {
inset-inline-start: 3px;
inset-inline-end: auto;
}
.sidebar-resize-handle:hover::after,
body.is-resizing .sidebar-resize-handle::after {
background: hsl(var(--accent-color) / 0.55);
@@ -1059,19 +1080,36 @@ html[data-list-density="relaxed"] {
.app-workspace {
order: 1;
direction: ltr;
background: hsl(var(--workspace-bg));
}
html[dir="rtl"] .app-sidebar-panel,
html[dir="rtl"] .app-workspace {
direction: rtl;
}
.app-sidebar-reveal-button {
position: absolute;
top: 12px;
inset-inline-start: 88px;
left: 88px;
right: auto;
z-index: 80;
-webkit-app-region: no-drag;
}
.app-workspace--custom-window-controls .app-sidebar-reveal-button {
inset-inline-start: 124px;
left: 124px;
}
.app-workspace--sidebar-right .app-sidebar-reveal-button {
left: auto;
right: 88px;
}
.app-workspace--sidebar-right.app-workspace--custom-window-controls .app-sidebar-reveal-button {
right: 124px;
}
.app-statusbar {
@@ -1584,6 +1622,7 @@ html[data-list-density="relaxed"] {
position: fixed;
top: 15px;
left: 22px;
right: auto;
z-index: 60;
display: inline-flex;
align-items: center;
@@ -1592,6 +1631,11 @@ html[data-list-density="relaxed"] {
pointer-events: auto;
}
.app-shell--sidebar-right .window-controls {
left: auto;
right: 22px;
}
.window-control {
width: 14px;
height: 14px;
@@ -1643,11 +1687,21 @@ html[data-list-density="relaxed"] {
}
.app-workspace--sidebar-collapsed .main-titlebar {
padding-inline-start: 124px;
padding-left: 124px;
padding-right: 18px;
}
.app-workspace--sidebar-collapsed.app-workspace--custom-window-controls .main-titlebar {
padding-inline-start: 160px;
padding-left: 160px;
}
.app-workspace--sidebar-right.app-workspace--sidebar-collapsed .main-titlebar {
padding-left: 18px;
padding-right: 124px;
}
.app-workspace--sidebar-right.app-workspace--sidebar-collapsed.app-workspace--custom-window-controls .main-titlebar {
padding-right: 160px;
}
.main-titlebar-title {
@@ -1824,6 +1878,8 @@ html[data-list-density="relaxed"] {
.download-table-header > div:first-child {
padding-inline-start: 0;
direction: ltr;
text-align: left;
}
.download-table-header > div:last-child {
@@ -1949,6 +2005,8 @@ html[data-list-density="relaxed"] .download-ghost-row {
}
.download-file-cell {
direction: ltr;
text-align: left;
display: flex;
align-items: center;
gap: var(--download-cell-gap);
@@ -1957,6 +2015,8 @@ html[data-list-density="relaxed"] .download-ghost-row {
}
.download-file-name {
direction: ltr;
text-align: left;
display: block;
flex: 1 1 auto;
min-width: 0;
@@ -2344,7 +2404,7 @@ html[dir="rtl"] .sidebar-inner .mr-3 {
margin-left: 0.75rem;
}
html[dir="rtl"] .app-sidebar-panel {
.app-sidebar-shell--right .app-sidebar-panel {
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.045),
-2px 0 10px hsl(0 0% 0% / 0.20);
+12 -1
View File
@@ -70,6 +70,7 @@ const notifySettingsPersistenceError = () => {
const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const;
const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const;
const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const;
const SIDEBAR_POSITION_VALUES = ['auto', 'left', 'right'] as const;
const PROXY_MODE_VALUES = ['none', 'system', 'custom'] as const;
const MEDIA_COOKIE_SOURCE_VALUES = [
'none', 'safari', 'chrome', 'chromium', 'firefox', 'edge', 'brave', 'opera', 'vivaldi', 'whale'
@@ -164,6 +165,8 @@ export type {
Theme
};
export type SidebarPosition = 'auto' | 'left' | 'right';
export interface SettingsState {
theme: Theme;
language: AppLocalePreference;
@@ -180,6 +183,7 @@ export interface SettingsState {
speedLimitPresetValues: number[];
logsEnabled: boolean;
isSidebarVisible: boolean;
sidebarPosition: SidebarPosition;
activeView: ActiveView;
activeSettingsTab: SettingsTab;
scheduler: SchedulerSettings;
@@ -225,6 +229,7 @@ export interface SettingsState {
setGlobalSpeedLimit: (limit: string) => Promise<void>;
setSpeedLimitPresetValues: (values: number[]) => void;
setLogsEnabled: (enabled: boolean) => void;
setSidebarPosition: (position: SidebarPosition) => void;
setActiveView: (view: ActiveView) => void;
setActiveSettingsTab: (tab: SettingsTab) => void;
setScheduler: (settings: SchedulerSettings) => void;
@@ -288,6 +293,7 @@ export const useSettingsStore = create<SettingsState>()(
activeView: 'downloads',
activeSettingsTab: 'downloads',
isSidebarVisible: true,
sidebarPosition: 'auto',
scheduler: {
enabled: false,
startTime: '00:00',
@@ -362,6 +368,7 @@ export const useSettingsStore = create<SettingsState>()(
},
setSpeedLimitPresetValues: (speedLimitPresetValues) => set({ speedLimitPresetValues }),
setLogsEnabled: (logsEnabled) => set({ logsEnabled }),
setSidebarPosition: (sidebarPosition) => set({ sidebarPosition }),
setActiveView: (view) => set({ activeView: view }),
setActiveSettingsTab: (activeSettingsTab) => set({ activeSettingsTab }),
setScheduler: (scheduler) => set({ scheduler }),
@@ -500,7 +507,7 @@ export const useSettingsStore = create<SettingsState>()(
{
name: 'firelink-settings',
storage: createJSONStorage(() => tauriStorage),
version: 4,
version: 5,
migrate: (persistedState) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState as SettingsState;
@@ -548,6 +555,7 @@ export const useSettingsStore = create<SettingsState>()(
speedLimitPresetValues: state.speedLimitPresetValues,
logsEnabled: state.logsEnabled,
isSidebarVisible: state.isSidebarVisible,
sidebarPosition: state.sidebarPosition,
activeSettingsTab: state.activeSettingsTab,
scheduler: state.scheduler,
schedulerRunning: state.schedulerRunning,
@@ -604,6 +612,9 @@ export const useSettingsStore = create<SettingsState>()(
listRowDensity: isAllowedSetting(LIST_ROW_DENSITY_VALUES, persisted.listRowDensity)
? persisted.listRowDensity
: currentState.listRowDensity,
sidebarPosition: isAllowedSetting(SIDEBAR_POSITION_VALUES, persisted.sidebarPosition)
? persisted.sidebarPosition
: currentState.sidebarPosition,
proxyMode: isAllowedSetting(PROXY_MODE_VALUES, persisted.proxyMode)
? persisted.proxyMode
: currentState.proxyMode,
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { shouldUseCustomWindowControls } from './platform';
describe('shouldUseCustomWindowControls', () => {
it('keeps custom controls present while Windows/Linux detection is unresolved', () => {
expect(shouldUseCustomWindowControls('unknown', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe(true);
expect(shouldUseCustomWindowControls('unknown', 'Mozilla/5.0 (X11; Linux x86_64)')).toBe(true);
});
it('does not render custom controls for macOS', () => {
expect(shouldUseCustomWindowControls('unknown', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0)')).toBe(false);
expect(shouldUseCustomWindowControls('macos', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0)')).toBe(false);
});
it('only opts into the supported desktop platforms', () => {
expect(shouldUseCustomWindowControls('windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe(true);
expect(shouldUseCustomWindowControls('linux', 'Mozilla/5.0 (X11; Linux x86_64)')).toBe(true);
expect(shouldUseCustomWindowControls('android', 'Mozilla/5.0 (Linux; Android 14)')).toBe(false);
});
});
+3
View File
@@ -9,6 +9,9 @@ const fallback: PlatformInfo = {
portable: false
};
export const shouldUseCustomWindowControls = (os: string, userAgent: string): boolean =>
!userAgent.includes('Mac') && (os === 'windows' || os === 'linux' || os === 'unknown');
let cached: PlatformInfo | null = null;
let pending: Promise<PlatformInfo> | null = null;