mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
New: Option to specify timezone for formatting times in the UI
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
@@ -15,6 +14,7 @@ import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { convertToTimezone } from 'Utilities/Date/convertToTimezone';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -58,9 +58,8 @@ function AgendaEvent(props: AgendaEventProps) {
|
||||
const series = useSeries(seriesId)!;
|
||||
const episodeFile = useEpisodeFile(episodeFileId);
|
||||
const queueItem = useQueueItemForEpisode(id);
|
||||
const { timeFormat, longDateFormat, enableColorImpairedMode } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
const { timeFormat, longDateFormat, enableColorImpairedMode, timeZone } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const {
|
||||
showEpisodeInformation,
|
||||
@@ -71,8 +70,11 @@ function AgendaEvent(props: AgendaEventProps) {
|
||||
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
|
||||
const startTime = moment(airDateUtc);
|
||||
const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
|
||||
const startTime = convertToTimezone(airDateUtc, timeZone);
|
||||
const endTime = convertToTimezone(airDateUtc, timeZone).add(
|
||||
series.runtime,
|
||||
'minutes'
|
||||
);
|
||||
const downloading = !!(queueItem || grabbed);
|
||||
const isMonitored = series.monitored && monitored;
|
||||
const statusStyle = getStatusStyle(
|
||||
@@ -110,9 +112,10 @@ function AgendaEvent(props: AgendaEventProps) {
|
||||
)}
|
||||
>
|
||||
<div className={styles.time}>
|
||||
{formatTime(airDateUtc, timeFormat)} -{' '}
|
||||
{formatTime(airDateUtc, timeFormat, { timeZone })} -{' '}
|
||||
{formatTime(endTime.toISOString(), timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
timeZone,
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
@@ -14,6 +13,7 @@ import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { convertToTimezone } from 'Utilities/Date/convertToTimezone';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -60,7 +60,7 @@ function CalendarEvent(props: CalendarEventProps) {
|
||||
const episodeFile = useEpisodeFile(episodeFileId);
|
||||
const queueItem = useQueueItemForEpisode(id);
|
||||
|
||||
const { timeFormat, enableColorImpairedMode } = useSelector(
|
||||
const { timeFormat, enableColorImpairedMode, timeZone } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
@@ -88,8 +88,11 @@ function CalendarEvent(props: CalendarEventProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startTime = moment(airDateUtc);
|
||||
const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
|
||||
const startTime = convertToTimezone(airDateUtc, timeZone);
|
||||
const endTime = convertToTimezone(airDateUtc, timeZone).add(
|
||||
series.runtime,
|
||||
'minutes'
|
||||
);
|
||||
const isDownloading = !!(queueItem || grabbed);
|
||||
const isMonitored = series.monitored && monitored;
|
||||
const statusStyle = getStatusStyle(
|
||||
@@ -217,9 +220,10 @@ function CalendarEvent(props: CalendarEventProps) {
|
||||
) : null}
|
||||
|
||||
<div className={styles.airTime}>
|
||||
{formatTime(airDateUtc, timeFormat)} -{' '}
|
||||
{formatTime(airDateUtc, timeFormat, { timeZone })} -{' '}
|
||||
{formatTime(endTime.toISOString(), timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
timeZone,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIsDownloadingEpisodes } from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
@@ -12,6 +11,7 @@ import { icons, kinds } from 'Helpers/Props';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { CalendarItem } from 'typings/Calendar';
|
||||
import { convertToTimezone } from 'Utilities/Date/convertToTimezone';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -34,7 +34,7 @@ function CalendarEventGroup({
|
||||
const isDownloading = useIsDownloadingEpisodes(episodeIds);
|
||||
const series = useSeries(seriesId)!;
|
||||
|
||||
const { timeFormat, enableColorImpairedMode } = useSelector(
|
||||
const { timeFormat, enableColorImpairedMode, timeZone } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
@@ -46,8 +46,11 @@ function CalendarEventGroup({
|
||||
const firstEpisode = events[0];
|
||||
const lastEpisode = events[events.length - 1];
|
||||
const airDateUtc = firstEpisode.airDateUtc;
|
||||
const startTime = moment(airDateUtc);
|
||||
const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes');
|
||||
const startTime = convertToTimezone(airDateUtc, timeZone);
|
||||
const endTime = convertToTimezone(lastEpisode.airDateUtc, timeZone).add(
|
||||
series.runtime,
|
||||
'minutes'
|
||||
);
|
||||
const seasonNumber = firstEpisode.seasonNumber;
|
||||
|
||||
const { allDownloaded, anyGrabbed, anyMonitored, allAbsoluteEpisodeNumbers } =
|
||||
@@ -194,9 +197,10 @@ function CalendarEventGroup({
|
||||
|
||||
<div className={styles.airingInfo}>
|
||||
<div className={styles.airTime}>
|
||||
{formatTime(airDateUtc, timeFormat)} -{' '}
|
||||
{formatTime(airDateUtc, timeFormat, { timeZone })} -{' '}
|
||||
{formatTime(endTime.toISOString(), timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
timeZone,
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const { longDateFormat, timeFormat } = useSelector(
|
||||
const { longDateFormat, timeFormat, timeZone } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
@@ -174,6 +174,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
className={styles.age}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||
includeSeconds: true,
|
||||
timeZone,
|
||||
})}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
|
||||
@@ -21,6 +21,7 @@ import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import themes from 'Styles/Themes';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import timeZoneOptions from 'Utilities/Date/timeZoneOptions';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
@@ -218,6 +219,18 @@ function UISettings() {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('TimeZone')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="timeZone"
|
||||
values={timeZoneOptions}
|
||||
onChange={handleInputChange}
|
||||
{...settings.timeZone}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowRelativeDates')}</FormLabel>
|
||||
<FormInputGroup
|
||||
|
||||
26
frontend/src/Utilities/Date/convertToTimezone.ts
Normal file
26
frontend/src/Utilities/Date/convertToTimezone.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
export const convertToTimezone = (
|
||||
date: moment.MomentInput,
|
||||
timeZone: string
|
||||
) => {
|
||||
if (!date) {
|
||||
return moment();
|
||||
}
|
||||
|
||||
if (!timeZone) {
|
||||
return moment(date);
|
||||
}
|
||||
|
||||
try {
|
||||
return moment.tz(date, timeZone);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Error converting to timezone ${timeZone}. Using system timezone.`,
|
||||
error
|
||||
);
|
||||
return moment(date);
|
||||
}
|
||||
};
|
||||
|
||||
export default convertToTimezone;
|
||||
@@ -1,11 +1,15 @@
|
||||
import moment, { MomentInput } from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { convertToTimezone } from './convertToTimezone';
|
||||
import formatTime from './formatTime';
|
||||
import isToday from './isToday';
|
||||
import isTomorrow from './isTomorrow';
|
||||
import isYesterday from './isYesterday';
|
||||
|
||||
function getRelativeDay(date: MomentInput, includeRelativeDate: boolean) {
|
||||
function getRelativeDay(
|
||||
date: moment.MomentInput,
|
||||
includeRelativeDate: boolean
|
||||
) {
|
||||
if (!includeRelativeDate) {
|
||||
return '';
|
||||
}
|
||||
@@ -26,20 +30,23 @@ function getRelativeDay(date: MomentInput, includeRelativeDate: boolean) {
|
||||
}
|
||||
|
||||
function formatDateTime(
|
||||
date: MomentInput,
|
||||
date: moment.MomentInput,
|
||||
dateFormat: string,
|
||||
timeFormat: string,
|
||||
{ includeSeconds = false, includeRelativeDay = false } = {}
|
||||
{ includeSeconds = false, includeRelativeDay = false, timeZone = '' } = {}
|
||||
) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const relativeDay = getRelativeDay(date, includeRelativeDay);
|
||||
const formattedDate = moment(date).format(dateFormat);
|
||||
const formattedTime = formatTime(date, timeFormat, {
|
||||
const dateTime = convertToTimezone(date, timeZone);
|
||||
|
||||
const relativeDay = getRelativeDay(dateTime, includeRelativeDay);
|
||||
const formattedDate = dateTime.format(dateFormat);
|
||||
const formattedTime = formatTime(dateTime, timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
includeSeconds,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
if (relativeDay) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import moment, { MomentInput } from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import { convertToTimezone } from './convertToTimezone';
|
||||
|
||||
function formatTime(
|
||||
date: MomentInput,
|
||||
date: moment.MomentInput,
|
||||
timeFormat: string,
|
||||
{ includeMinuteZero = false, includeSeconds = false } = {}
|
||||
{ includeMinuteZero = false, includeSeconds = false, timeZone = '' } = {}
|
||||
) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const time = moment(date);
|
||||
const time = convertToTimezone(date, timeZone);
|
||||
|
||||
if (includeSeconds) {
|
||||
timeFormat = timeFormat.replace(/\(?:mm\)?/, ':mm:ss');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import moment from 'moment';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import isInNextWeek from 'Utilities/Date/isInNextWeek';
|
||||
import isToday from 'Utilities/Date/isToday';
|
||||
import isTomorrow from 'Utilities/Date/isTomorrow';
|
||||
import isYesterday from 'Utilities/Date/isYesterday';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { convertToTimezone } from './convertToTimezone';
|
||||
import formatDateTime from './formatDateTime';
|
||||
|
||||
interface GetRelativeDateOptions {
|
||||
@@ -12,6 +12,7 @@ interface GetRelativeDateOptions {
|
||||
shortDateFormat: string;
|
||||
showRelativeDates: boolean;
|
||||
timeFormat?: string;
|
||||
timeZone?: string;
|
||||
includeSeconds?: boolean;
|
||||
timeForToday?: boolean;
|
||||
includeTime?: boolean;
|
||||
@@ -22,6 +23,7 @@ function getRelativeDate({
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
timeFormat,
|
||||
timeZone = '',
|
||||
includeSeconds = false,
|
||||
timeForToday = false,
|
||||
includeTime = false,
|
||||
@@ -41,6 +43,7 @@ function getRelativeDate({
|
||||
? formatTime(date, timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
includeSeconds,
|
||||
timeZone,
|
||||
})
|
||||
: '';
|
||||
|
||||
@@ -49,7 +52,8 @@ function getRelativeDate({
|
||||
}
|
||||
|
||||
if (!showRelativeDates) {
|
||||
return moment(date).format(shortDateFormat);
|
||||
const dateTime = convertToTimezone(date, timeZone);
|
||||
return dateTime.format(shortDateFormat);
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
@@ -69,14 +73,18 @@ function getRelativeDate({
|
||||
}
|
||||
|
||||
if (isInNextWeek(date)) {
|
||||
const day = moment(date).format('dddd');
|
||||
const dateTime = convertToTimezone(date, timeZone);
|
||||
const day = dateTime.format('dddd');
|
||||
|
||||
return includeTime ? translate('DayOfWeekAt', { day, time }) : day;
|
||||
}
|
||||
|
||||
return includeTime && timeFormat
|
||||
? formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds })
|
||||
: moment(date).format(shortDateFormat);
|
||||
? formatDateTime(date, shortDateFormat, timeFormat, {
|
||||
includeSeconds,
|
||||
timeZone,
|
||||
})
|
||||
: convertToTimezone(date, timeZone).format(shortDateFormat);
|
||||
}
|
||||
|
||||
export default getRelativeDate;
|
||||
|
||||
192
frontend/src/Utilities/Date/timeZoneOptions.ts
Normal file
192
frontend/src/Utilities/Date/timeZoneOptions.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export const timeZoneOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: '',
|
||||
get value() {
|
||||
return translate('SystemDefault');
|
||||
},
|
||||
},
|
||||
|
||||
// UTC
|
||||
{ key: 'UTC', value: 'UTC' },
|
||||
|
||||
// Africa (Major cities and unique timezones)
|
||||
{ key: 'Africa/Abidjan', value: 'Africa/Abidjan' },
|
||||
{ key: 'Africa/Algiers', value: 'Africa/Algiers' },
|
||||
{ key: 'Africa/Cairo', value: 'Africa/Cairo' },
|
||||
{ key: 'Africa/Casablanca', value: 'Africa/Casablanca' },
|
||||
{ key: 'Africa/Johannesburg', value: 'Africa/Johannesburg' },
|
||||
{ key: 'Africa/Lagos', value: 'Africa/Lagos' },
|
||||
{ key: 'Africa/Nairobi', value: 'Africa/Nairobi' },
|
||||
{ key: 'Africa/Tripoli', value: 'Africa/Tripoli' },
|
||||
|
||||
// America - North America (Major US/Canada zones)
|
||||
{ key: 'America/New_York', value: 'America/New_York (Eastern)' },
|
||||
{ key: 'America/Chicago', value: 'America/Chicago (Central)' },
|
||||
{ key: 'America/Denver', value: 'America/Denver (Mountain)' },
|
||||
{ key: 'America/Los_Angeles', value: 'America/Los_Angeles (Pacific)' },
|
||||
{ key: 'America/Anchorage', value: 'America/Anchorage (Alaska)' },
|
||||
{ key: 'America/Adak', value: 'America/Adak (Hawaii-Aleutian)' },
|
||||
{ key: 'America/Phoenix', value: 'America/Phoenix (Arizona)' },
|
||||
{ key: 'America/Toronto', value: 'America/Toronto' },
|
||||
{ key: 'America/Vancouver', value: 'America/Vancouver' },
|
||||
{ key: 'America/Halifax', value: 'America/Halifax' },
|
||||
{ key: 'America/St_Johns', value: 'America/St_Johns (Newfoundland)' },
|
||||
|
||||
// America - Mexico
|
||||
{ key: 'America/Mexico_City', value: 'America/Mexico_City' },
|
||||
{ key: 'America/Cancun', value: 'America/Cancun' },
|
||||
{ key: 'America/Tijuana', value: 'America/Tijuana' },
|
||||
|
||||
// America - Central America
|
||||
{ key: 'America/Guatemala', value: 'America/Guatemala' },
|
||||
{ key: 'America/Costa_Rica', value: 'America/Costa_Rica' },
|
||||
{ key: 'America/Panama', value: 'America/Panama' },
|
||||
|
||||
// America - Caribbean
|
||||
{ key: 'America/Havana', value: 'America/Havana' },
|
||||
{ key: 'America/Jamaica', value: 'America/Jamaica' },
|
||||
{ key: 'America/Puerto_Rico', value: 'America/Puerto_Rico' },
|
||||
|
||||
// America - South America
|
||||
{ key: 'America/Bogota', value: 'America/Bogota' },
|
||||
{ key: 'America/Caracas', value: 'America/Caracas' },
|
||||
{ key: 'America/Guyana', value: 'America/Guyana' },
|
||||
{ key: 'America/La_Paz', value: 'America/La_Paz' },
|
||||
{ key: 'America/Lima', value: 'America/Lima' },
|
||||
{ key: 'America/Santiago', value: 'America/Santiago' },
|
||||
{ key: 'America/Asuncion', value: 'America/Asuncion' },
|
||||
{ key: 'America/Montevideo', value: 'America/Montevideo' },
|
||||
{
|
||||
key: 'America/Argentina/Buenos_Aires',
|
||||
value: 'America/Argentina/Buenos_Aires',
|
||||
},
|
||||
{ key: 'America/Sao_Paulo', value: 'America/Sao_Paulo' },
|
||||
{ key: 'America/Manaus', value: 'America/Manaus' },
|
||||
{ key: 'America/Fortaleza', value: 'America/Fortaleza' },
|
||||
{ key: 'America/Noronha', value: 'America/Noronha' },
|
||||
|
||||
// Antarctica (Research stations)
|
||||
{ key: 'Antarctica/McMurdo', value: 'Antarctica/McMurdo' },
|
||||
{ key: 'Antarctica/Palmer', value: 'Antarctica/Palmer' },
|
||||
|
||||
// Arctic
|
||||
{ key: 'Arctic/Longyearbyen', value: 'Arctic/Longyearbyen' },
|
||||
|
||||
// Asia - East Asia
|
||||
{ key: 'Asia/Tokyo', value: 'Asia/Tokyo' },
|
||||
{ key: 'Asia/Seoul', value: 'Asia/Seoul' },
|
||||
{ key: 'Asia/Shanghai', value: 'Asia/Shanghai' },
|
||||
{ key: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' },
|
||||
{ key: 'Asia/Taipei', value: 'Asia/Taipei' },
|
||||
{ key: 'Asia/Macau', value: 'Asia/Macau' },
|
||||
|
||||
// Asia - Southeast Asia
|
||||
{ key: 'Asia/Singapore', value: 'Asia/Singapore' },
|
||||
{ key: 'Asia/Kuala_Lumpur', value: 'Asia/Kuala_Lumpur' },
|
||||
{ key: 'Asia/Jakarta', value: 'Asia/Jakarta' },
|
||||
{ key: 'Asia/Manila', value: 'Asia/Manila' },
|
||||
{ key: 'Asia/Bangkok', value: 'Asia/Bangkok' },
|
||||
{ key: 'Asia/Ho_Chi_Minh', value: 'Asia/Ho_Chi_Minh' },
|
||||
|
||||
// Asia - South Asia
|
||||
{ key: 'Asia/Kolkata', value: 'Asia/Kolkata' },
|
||||
{ key: 'Asia/Dhaka', value: 'Asia/Dhaka' },
|
||||
{ key: 'Asia/Karachi', value: 'Asia/Karachi' },
|
||||
{ key: 'Asia/Kathmandu', value: 'Asia/Kathmandu' },
|
||||
{ key: 'Asia/Colombo', value: 'Asia/Colombo' },
|
||||
|
||||
// Asia - Central Asia
|
||||
{ key: 'Asia/Almaty', value: 'Asia/Almaty' },
|
||||
{ key: 'Asia/Tashkent', value: 'Asia/Tashkent' },
|
||||
{ key: 'Asia/Bishkek', value: 'Asia/Bishkek' },
|
||||
{ key: 'Asia/Dushanbe', value: 'Asia/Dushanbe' },
|
||||
|
||||
// Asia - Western Asia/Middle East
|
||||
{ key: 'Asia/Dubai', value: 'Asia/Dubai' },
|
||||
{ key: 'Asia/Riyadh', value: 'Asia/Riyadh' },
|
||||
{ key: 'Asia/Kuwait', value: 'Asia/Kuwait' },
|
||||
{ key: 'Asia/Qatar', value: 'Asia/Qatar' },
|
||||
{ key: 'Asia/Bahrain', value: 'Asia/Bahrain' },
|
||||
{ key: 'Asia/Jerusalem', value: 'Asia/Jerusalem' },
|
||||
{ key: 'Asia/Beirut', value: 'Asia/Beirut' },
|
||||
{ key: 'Asia/Damascus', value: 'Asia/Damascus' },
|
||||
{ key: 'Asia/Baghdad', value: 'Asia/Baghdad' },
|
||||
{ key: 'Asia/Tehran', value: 'Asia/Tehran' },
|
||||
|
||||
// Asia - Russia
|
||||
{ key: 'Europe/Moscow', value: 'Europe/Moscow' },
|
||||
{ key: 'Asia/Yekaterinburg', value: 'Asia/Yekaterinburg' },
|
||||
{ key: 'Asia/Novosibirsk', value: 'Asia/Novosibirsk' },
|
||||
{ key: 'Asia/Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
|
||||
{ key: 'Asia/Irkutsk', value: 'Asia/Irkutsk' },
|
||||
{ key: 'Asia/Yakutsk', value: 'Asia/Yakutsk' },
|
||||
{ key: 'Asia/Vladivostok', value: 'Asia/Vladivostok' },
|
||||
{ key: 'Asia/Sakhalin', value: 'Asia/Sakhalin' },
|
||||
{ key: 'Asia/Kamchatka', value: 'Asia/Kamchatka' },
|
||||
|
||||
// Atlantic
|
||||
{ key: 'Atlantic/Azores', value: 'Atlantic/Azores' },
|
||||
{ key: 'Atlantic/Canary', value: 'Atlantic/Canary' },
|
||||
{ key: 'Atlantic/Cape_Verde', value: 'Atlantic/Cape_Verde' },
|
||||
{ key: 'Atlantic/Reykjavik', value: 'Atlantic/Reykjavik' },
|
||||
|
||||
// Australia & New Zealand
|
||||
{ key: 'Australia/Sydney', value: 'Australia/Sydney' },
|
||||
{ key: 'Australia/Melbourne', value: 'Australia/Melbourne' },
|
||||
{ key: 'Australia/Brisbane', value: 'Australia/Brisbane' },
|
||||
{ key: 'Australia/Perth', value: 'Australia/Perth' },
|
||||
{ key: 'Australia/Adelaide', value: 'Australia/Adelaide' },
|
||||
{ key: 'Australia/Darwin', value: 'Australia/Darwin' },
|
||||
{ key: 'Australia/Hobart', value: 'Australia/Hobart' },
|
||||
{ key: 'Pacific/Auckland', value: 'Pacific/Auckland' },
|
||||
{ key: 'Pacific/Chatham', value: 'Pacific/Chatham' },
|
||||
|
||||
// Europe - Western Europe
|
||||
{ key: 'Europe/London', value: 'Europe/London' },
|
||||
{ key: 'Europe/Dublin', value: 'Europe/Dublin' },
|
||||
{ key: 'Europe/Paris', value: 'Europe/Paris' },
|
||||
{ key: 'Europe/Berlin', value: 'Europe/Berlin' },
|
||||
{ key: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
|
||||
{ key: 'Europe/Brussels', value: 'Europe/Brussels' },
|
||||
{ key: 'Europe/Zurich', value: 'Europe/Zurich' },
|
||||
{ key: 'Europe/Vienna', value: 'Europe/Vienna' },
|
||||
{ key: 'Europe/Rome', value: 'Europe/Rome' },
|
||||
{ key: 'Europe/Madrid', value: 'Europe/Madrid' },
|
||||
{ key: 'Europe/Lisbon', value: 'Europe/Lisbon' },
|
||||
|
||||
// Europe - Northern Europe
|
||||
{ key: 'Europe/Stockholm', value: 'Europe/Stockholm' },
|
||||
{ key: 'Europe/Oslo', value: 'Europe/Oslo' },
|
||||
{ key: 'Europe/Copenhagen', value: 'Europe/Copenhagen' },
|
||||
{ key: 'Europe/Helsinki', value: 'Europe/Helsinki' },
|
||||
|
||||
// Europe - Eastern Europe
|
||||
{ key: 'Europe/Warsaw', value: 'Europe/Warsaw' },
|
||||
{ key: 'Europe/Prague', value: 'Europe/Prague' },
|
||||
{ key: 'Europe/Budapest', value: 'Europe/Budapest' },
|
||||
{ key: 'Europe/Bucharest', value: 'Europe/Bucharest' },
|
||||
{ key: 'Europe/Sofia', value: 'Europe/Sofia' },
|
||||
{ key: 'Europe/Athens', value: 'Europe/Athens' },
|
||||
{ key: 'Europe/Istanbul', value: 'Europe/Istanbul' },
|
||||
{ key: 'Europe/Kiev', value: 'Europe/Kiev' },
|
||||
{ key: 'Europe/Minsk', value: 'Europe/Minsk' },
|
||||
|
||||
// Indian Ocean
|
||||
{ key: 'Indian/Mauritius', value: 'Indian/Mauritius' },
|
||||
{ key: 'Indian/Maldives', value: 'Indian/Maldives' },
|
||||
|
||||
// Pacific - Major Island Nations
|
||||
{ key: 'Pacific/Honolulu', value: 'Pacific/Honolulu' },
|
||||
{ key: 'Pacific/Fiji', value: 'Pacific/Fiji' },
|
||||
{ key: 'Pacific/Guam', value: 'Pacific/Guam' },
|
||||
{ key: 'Pacific/Tahiti', value: 'Pacific/Tahiti' },
|
||||
{ key: 'Pacific/Apia', value: 'Pacific/Apia' },
|
||||
{ key: 'Pacific/Tongatapu', value: 'Pacific/Tongatapu' },
|
||||
{ key: 'Pacific/Port_Moresby', value: 'Pacific/Port_Moresby' },
|
||||
{ key: 'Pacific/Noumea', value: 'Pacific/Noumea' },
|
||||
];
|
||||
|
||||
export default timeZoneOptions;
|
||||
@@ -4,6 +4,7 @@ export default interface UiSettings {
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
timeZone: string;
|
||||
firstDayOfWeek: number;
|
||||
enableColorImpairedMode: boolean;
|
||||
calendarWeekColumnHeader: string;
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.30.1",
|
||||
"moment-timezone": "0.6.0",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -94,6 +95,7 @@
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/moment-timezone": "0.5.30",
|
||||
"@types/mousetrap": "1.6.15",
|
||||
"@types/qs": "6.9.16",
|
||||
"@types/react-autosuggest": "10.1.11",
|
||||
|
||||
@@ -343,6 +343,13 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("TimeFormat", value); }
|
||||
}
|
||||
|
||||
public string TimeZone
|
||||
{
|
||||
get { return GetValue("TimeZone", ""); }
|
||||
|
||||
set { SetValue("TimeZone", value); }
|
||||
}
|
||||
|
||||
public bool ShowRelativeDates
|
||||
{
|
||||
get { return GetValueBoolean("ShowRelativeDates", true); }
|
||||
|
||||
@@ -68,6 +68,7 @@ namespace NzbDrone.Core.Configuration
|
||||
string ShortDateFormat { get; set; }
|
||||
string LongDateFormat { get; set; }
|
||||
string TimeFormat { get; set; }
|
||||
string TimeZone { get; set; }
|
||||
bool ShowRelativeDates { get; set; }
|
||||
bool EnableColorImpairedMode { get; set; }
|
||||
int UILanguage { get; set; }
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Sonarr.Api.V3.Config
|
||||
public string ShortDateFormat { get; set; }
|
||||
public string LongDateFormat { get; set; }
|
||||
public string TimeFormat { get; set; }
|
||||
public string TimeZone { get; set; }
|
||||
public bool ShowRelativeDates { get; set; }
|
||||
|
||||
public bool EnableColorImpairedMode { get; set; }
|
||||
@@ -32,6 +33,7 @@ namespace Sonarr.Api.V3.Config
|
||||
ShortDateFormat = model.ShortDateFormat,
|
||||
LongDateFormat = model.LongDateFormat,
|
||||
TimeFormat = model.TimeFormat,
|
||||
TimeZone = model.TimeZone,
|
||||
ShowRelativeDates = model.ShowRelativeDates,
|
||||
|
||||
EnableColorImpairedMode = model.EnableColorImpairedMode,
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -1395,6 +1395,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
|
||||
integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
|
||||
|
||||
"@types/moment-timezone@0.5.30":
|
||||
version "0.5.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7"
|
||||
integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg==
|
||||
dependencies:
|
||||
moment-timezone "*"
|
||||
|
||||
"@types/mousetrap@1.6.15":
|
||||
version "1.6.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.15.tgz#f144a0c539a4cef553a631824651d48267e53c86"
|
||||
@@ -4751,7 +4758,14 @@ mobile-detect@1.4.5:
|
||||
resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.5.tgz#da393c3c413ca1a9bcdd9ced653c38281c0fb6ad"
|
||||
integrity sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==
|
||||
|
||||
moment@2.30.1:
|
||||
moment-timezone@*, moment-timezone@0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.6.0.tgz#c5a6519171f31a64739ea75d33f5c136c08ff608"
|
||||
integrity sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==
|
||||
dependencies:
|
||||
moment "^2.29.4"
|
||||
|
||||
moment@2.30.1, moment@^2.29.4:
|
||||
version "2.30.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
|
||||
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
|
||||
|
||||
Reference in New Issue
Block a user