mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-11 03:59:09 +00:00
feat(settings): add opt-in calendar localization
This commit is contained in:
@@ -4,6 +4,8 @@ import { Play, Pause, MoreVertical, Clock, ArrowUp, ArrowDown, GripVertical } fr
|
||||
import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem';
|
||||
import { canPauseDownload, canStartDownload } from '../utils/downloadActions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { formatDateTime } from '../utils/dateTime';
|
||||
import {
|
||||
downloadProgressColorClass,
|
||||
formatDownloadTotal,
|
||||
@@ -59,7 +61,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
onQueueDragStart,
|
||||
onClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const calendarPreference = useSettingsStore(state => state.calendarPreference);
|
||||
const liveProgress = useDownloadProgressStore(state => state.progressMap[download.id]);
|
||||
const rowRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isRowHovered, setIsRowHovered] = React.useState(false);
|
||||
@@ -71,6 +74,12 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
const normalized = download.mediaQuality.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return normalized.length > 0 && normalized.length <= 48 ? normalized : undefined;
|
||||
})();
|
||||
const dateAddedLabel = download.dateAdded
|
||||
? formatDateTime(download.dateAdded, {
|
||||
locale: i18n.language,
|
||||
calendar: calendarPreference
|
||||
})
|
||||
: '-';
|
||||
|
||||
const updateActionPosition = React.useCallback(() => {
|
||||
const row = rowRef.current;
|
||||
@@ -327,9 +336,9 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
<div className="download-column-cell download-cell-right download-column-date-added" style={columnStyle('Date Added')}>
|
||||
<span
|
||||
className="download-cell-content download-date-value tabular-nums"
|
||||
title={download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
|
||||
title={dateAddedLabel}
|
||||
>
|
||||
{download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
|
||||
{dateAddedLabel}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -18,19 +18,25 @@ import {
|
||||
} from '../utils/downloadProgress';
|
||||
import { resolveDownloadConnections } from '../utils/downloads';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDateTime, type CalendarPreference } from '../utils/dateTime';
|
||||
|
||||
type LoginMode = 'matching' | 'custom' | 'none';
|
||||
|
||||
const formatLastTry = (value?: string): string => {
|
||||
const formatLastTry = (
|
||||
value: string | undefined,
|
||||
locale: string,
|
||||
calendar: CalendarPreference
|
||||
): string => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime())
|
||||
? '-'
|
||||
: date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
|
||||
return formatDateTime(value, {
|
||||
locale,
|
||||
calendar,
|
||||
options: { dateStyle: 'medium', timeStyle: 'short' }
|
||||
});
|
||||
};
|
||||
|
||||
export const PropertiesModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const categoryLabel = (category: string) => {
|
||||
switch (category) {
|
||||
case 'Musics': return t($ => $.navigation.categories.musics);
|
||||
@@ -55,7 +61,7 @@ export const PropertiesModal = () => {
|
||||
: undefined
|
||||
));
|
||||
|
||||
const { baseDownloadFolder, perServerConnections } = useSettingsStore();
|
||||
const { baseDownloadFolder, perServerConnections, calendarPreference } = useSettingsStore();
|
||||
|
||||
// Form states
|
||||
const [url, setUrl] = useState('');
|
||||
@@ -373,9 +379,9 @@ export const PropertiesModal = () => {
|
||||
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium shrink-0 whitespace-nowrap">{t($ => $.properties.connections)}</span><span className="text-text-secondary truncate whitespace-nowrap" title={item.connections !== undefined ? t($ => $.properties.savedTooltip) : t($ => $.properties.defaultTooltip)}><bdi>{connectionStatus}</bdi></span></div>
|
||||
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[60px] shrink-0">{t($ => $.properties.speedCap)}</span><span className="text-text-secondary truncate">{item.speedLimit || '-'}</span></div>
|
||||
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[55px] shrink-0">{t($ => $.properties.category)}</span><span className="text-text-secondary truncate">{categoryLabel(item.category)}</span></div>
|
||||
<div className="flex gap-1.5"><span className="text-text-muted font-medium w-[50px]">{t($ => $.properties.lastTry)}</span><span className="text-text-secondary truncate">{formatLastTry(item.lastTry)}</span></div>
|
||||
<div className="flex gap-1.5"><span className="text-text-muted font-medium w-[50px]">{t($ => $.properties.lastTry)}</span><span className="text-text-secondary truncate">{formatLastTry(item.lastTry, i18n.language, calendarPreference)}</span></div>
|
||||
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[90px]">{t($ => $.properties.dateAdded)}</span><span className="text-text-secondary truncate">{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}</span></div>
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[90px]">{t($ => $.properties.dateAdded)}</span><span className="text-text-secondary truncate">{formatDateTime(item.dateAdded, { locale: i18n.language, calendar: calendarPreference, options: { dateStyle: 'medium', timeStyle: 'short' } })}</span></div>
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[70px]">{t($ => $.properties.destination)}</span><span className="text-text-secondary truncate" title={saveLocation}>{saveLocation || baseDownloadFolder}</span></div>
|
||||
{item.lastError && (item.status === 'failed' || item.status === 'retrying') && (
|
||||
<div className="flex gap-1.5 col-span-4 min-w-0">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { WindowDragRegion } from './WindowDragRegion';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { usePlatformInfo } from '../utils/platform';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDateTime } from '../utils/dateTime';
|
||||
|
||||
const days = [
|
||||
{ value: 0, key: 'su' },
|
||||
@@ -54,9 +55,10 @@ function nextScheduledRun(settings: SchedulerSettings): Date | 'disabled' | 'non
|
||||
}
|
||||
|
||||
export default function SchedulerView() {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const savedSettings = useSettingsStore(state => state.scheduler);
|
||||
const schedulerRunning = useSettingsStore(state => state.schedulerRunning);
|
||||
const calendarPreference = useSettingsStore(state => state.calendarPreference);
|
||||
const setScheduler = useSettingsStore(state => state.setScheduler);
|
||||
const queues = useDownloadStore(state => state.queues);
|
||||
const [draft, setDraft] = useState<SchedulerSettings>(savedSettings);
|
||||
@@ -75,14 +77,18 @@ export default function SchedulerView() {
|
||||
const scheduledRun = nextScheduledRun(draft);
|
||||
if (scheduledRun === 'disabled') return t($ => $.scheduler.disabled);
|
||||
if (scheduledRun === 'none') return t($ => $.scheduler.noScheduledDay);
|
||||
return scheduledRun.toLocaleString(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
return formatDateTime(scheduledRun, {
|
||||
locale: i18n.language,
|
||||
calendar: calendarPreference,
|
||||
options: {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}
|
||||
});
|
||||
}, [draft, t]);
|
||||
}, [calendarPreference, draft, i18n.language, t]);
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() => JSON.stringify(draft) !== JSON.stringify(savedSettings),
|
||||
[draft, savedSettings]
|
||||
|
||||
@@ -693,6 +693,11 @@ runEngineChecks(false);
|
||||
{ value: 'uk', label: t($ => $.settings.lookAndFeel.languageUkrainian) },
|
||||
{ value: 'ru', label: t($ => $.settings.lookAndFeel.languageRussian) },
|
||||
] as const;
|
||||
const calendarOptions = [
|
||||
{ value: 'gregorian', label: t($ => $.settings.lookAndFeel.calendarGregorian) },
|
||||
{ value: 'persian', label: t($ => $.settings.lookAndFeel.calendarPersian) },
|
||||
{ value: 'hebrew', label: t($ => $.settings.lookAndFeel.calendarHebrew) },
|
||||
] as const;
|
||||
|
||||
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
|
||||
const active = activeTab === type;
|
||||
@@ -870,6 +875,21 @@ runEngineChecks(false);
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mac-settings-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.lookAndFeel.calendar)}</span>
|
||||
<small>{t($ => $.settings.lookAndFeel.calendarDescription)}</small>
|
||||
</div>
|
||||
<select
|
||||
value={settings.calendarPreference}
|
||||
onChange={(event) => settings.setCalendarPreference(event.target.value as typeof settings.calendarPreference)}
|
||||
className="app-control w-48"
|
||||
>
|
||||
{calendarOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mac-settings-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.lookAndFeel.sidebarPosition)}</span>
|
||||
|
||||
Reference in New Issue
Block a user