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