mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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' : ''}`}
|
||||
>
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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: 'از آخرین پوشه انتخابشده در پنجره افزودن برای دانلود بعدی تا زمان راهاندازی مجدد برنامه استفاده کنید.',
|
||||
|
||||
@@ -532,6 +532,11 @@ const he = {
|
||||
lookAndFeel: {
|
||||
language: 'שפה',
|
||||
languageDescription: 'בחר את שפת הממשק של Firelink.',
|
||||
sidebarPosition: 'מיקום סרגל הצד',
|
||||
sidebarPositionDescription: 'בחר היכן יופיע סרגל הצד. מצב אוטומטי עוקב אחר כיוון הממשק.',
|
||||
sidebarPositionAutomatic: 'אוטומטי (מומלץ)',
|
||||
sidebarPositionLeft: 'שמאל',
|
||||
sidebarPositionRight: 'ימין',
|
||||
languageSystem: 'שפת המערכת',
|
||||
languageEnglish: 'אנגלית',
|
||||
languageChinese: 'סינית מפושטת',
|
||||
|
||||
@@ -532,6 +532,11 @@ const ru = {
|
||||
lookAndFeel: {
|
||||
language: 'Язык',
|
||||
languageDescription: 'Выберите язык интерфейса Firelink.',
|
||||
sidebarPosition: 'Положение боковой панели',
|
||||
sidebarPositionDescription: 'Выберите, где отображать боковую панель. Автоматический режим учитывает направление интерфейса.',
|
||||
sidebarPositionAutomatic: 'Автоматически (рекомендуется)',
|
||||
sidebarPositionLeft: 'Слева',
|
||||
sidebarPositionRight: 'Справа',
|
||||
languageSystem: 'Язык системы',
|
||||
languageEnglish: 'Английский',
|
||||
languageChinese: 'Упрощённый китайский',
|
||||
|
||||
@@ -532,6 +532,11 @@ const uk = {
|
||||
lookAndFeel: {
|
||||
language: 'Мова',
|
||||
languageDescription: 'Виберіть мову інтерфейсу Firelink.',
|
||||
sidebarPosition: 'Положення бічної панелі',
|
||||
sidebarPositionDescription: 'Виберіть, де відображати бічну панель. Автоматичний режим враховує напрямок інтерфейсу.',
|
||||
sidebarPositionAutomatic: 'Автоматично (рекомендовано)',
|
||||
sidebarPositionLeft: 'Ліворуч',
|
||||
sidebarPositionRight: 'Праворуч',
|
||||
languageSystem: 'Мова системи',
|
||||
languageEnglish: 'Англійська',
|
||||
languageChinese: 'Спрощена китайська',
|
||||
|
||||
@@ -532,6 +532,11 @@ const zhCN = {
|
||||
lookAndFeel: {
|
||||
language: '语言',
|
||||
languageDescription: '选择 Firelink 的界面语言。',
|
||||
sidebarPosition: '侧边栏位置',
|
||||
sidebarPositionDescription: '选择侧边栏显示的位置。“自动”会跟随界面文字方向。',
|
||||
sidebarPositionAutomatic: '自动(推荐)',
|
||||
sidebarPositionLeft: '左侧',
|
||||
sidebarPositionRight: '右侧',
|
||||
languageSystem: '系统默认',
|
||||
languageEnglish: '英语',
|
||||
languageChinese: '简体中文',
|
||||
|
||||
+65
-5
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user