diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.tsx b/frontend/src/Calendar/Agenda/AgendaEvent.tsx
index f32b4b22b..0188be058 100644
--- a/frontend/src/Calendar/Agenda/AgendaEvent.tsx
+++ b/frontend/src/Calendar/Agenda/AgendaEvent.tsx
@@ -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) {
)}
>
- {formatTime(airDateUtc, timeFormat)} -{' '}
+ {formatTime(airDateUtc, timeFormat, { timeZone })} -{' '}
{formatTime(endTime.toISOString(), timeFormat, {
includeMinuteZero: true,
+ timeZone,
})}
diff --git a/frontend/src/Calendar/Events/CalendarEvent.tsx b/frontend/src/Calendar/Events/CalendarEvent.tsx
index 7c92a5e8c..64ed3d153 100644
--- a/frontend/src/Calendar/Events/CalendarEvent.tsx
+++ b/frontend/src/Calendar/Events/CalendarEvent.tsx
@@ -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}
- {formatTime(airDateUtc, timeFormat)} -{' '}
+ {formatTime(airDateUtc, timeFormat, { timeZone })} -{' '}
{formatTime(endTime.toISOString(), timeFormat, {
includeMinuteZero: true,
+ timeZone,
})}
diff --git a/frontend/src/Calendar/Events/CalendarEventGroup.tsx b/frontend/src/Calendar/Events/CalendarEventGroup.tsx
index ea2aa0567..1458011c1 100644
--- a/frontend/src/Calendar/Events/CalendarEventGroup.tsx
+++ b/frontend/src/Calendar/Events/CalendarEventGroup.tsx
@@ -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({
- {formatTime(airDateUtc, timeFormat)} -{' '}
+ {formatTime(airDateUtc, timeFormat, { timeZone })} -{' '}
{formatTime(endTime.toISOString(), timeFormat, {
includeMinuteZero: true,
+ timeZone,
})}
diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
index cf325c59d..7a83cee68 100644
--- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
+++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
@@ -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)}
diff --git a/frontend/src/Settings/UI/UISettings.tsx b/frontend/src/Settings/UI/UISettings.tsx
index 65d829c06..c5cb3d149 100644
--- a/frontend/src/Settings/UI/UISettings.tsx
+++ b/frontend/src/Settings/UI/UISettings.tsx
@@ -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() {
/>
+
+ {translate('TimeZone')}
+
+
+
+
{translate('ShowRelativeDates')}
{
+ 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;
diff --git a/frontend/src/Utilities/Date/formatDateTime.ts b/frontend/src/Utilities/Date/formatDateTime.ts
index 048296ba1..678f80538 100644
--- a/frontend/src/Utilities/Date/formatDateTime.ts
+++ b/frontend/src/Utilities/Date/formatDateTime.ts
@@ -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) {
diff --git a/frontend/src/Utilities/Date/formatTime.ts b/frontend/src/Utilities/Date/formatTime.ts
index f65bf28e4..8ba9c7d46 100644
--- a/frontend/src/Utilities/Date/formatTime.ts
+++ b/frontend/src/Utilities/Date/formatTime.ts
@@ -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');
diff --git a/frontend/src/Utilities/Date/getRelativeDate.ts b/frontend/src/Utilities/Date/getRelativeDate.ts
index 5a7218ecc..1a3d56096 100644
--- a/frontend/src/Utilities/Date/getRelativeDate.ts
+++ b/frontend/src/Utilities/Date/getRelativeDate.ts
@@ -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;
diff --git a/frontend/src/Utilities/Date/timeZoneOptions.ts b/frontend/src/Utilities/Date/timeZoneOptions.ts
new file mode 100644
index 000000000..0af94c6c6
--- /dev/null
+++ b/frontend/src/Utilities/Date/timeZoneOptions.ts
@@ -0,0 +1,192 @@
+import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
+import translate from 'Utilities/String/translate';
+
+export const timeZoneOptions: EnhancedSelectInputValue[] = [
+ {
+ 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;
diff --git a/frontend/src/typings/Settings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts
index c0f892939..328465293 100644
--- a/frontend/src/typings/Settings/UiSettings.ts
+++ b/frontend/src/typings/Settings/UiSettings.ts
@@ -4,6 +4,7 @@ export default interface UiSettings {
shortDateFormat: string;
longDateFormat: string;
timeFormat: string;
+ timeZone: string;
firstDayOfWeek: number;
enableColorImpairedMode: boolean;
calendarWeekColumnHeader: string;
diff --git a/package.json b/package.json
index d6bfd6988..3755bcfc4 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs
index 8b24be03c..cf5a35529 100644
--- a/src/NzbDrone.Core/Configuration/ConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/ConfigService.cs
@@ -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); }
diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs
index 2b5a32dfa..5ebb51b94 100644
--- a/src/NzbDrone.Core/Configuration/IConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/IConfigService.cs
@@ -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; }
diff --git a/src/Sonarr.Api.V3/Config/UiConfigResource.cs b/src/Sonarr.Api.V3/Config/UiConfigResource.cs
index 69e3fb318..ccdc02d15 100644
--- a/src/Sonarr.Api.V3/Config/UiConfigResource.cs
+++ b/src/Sonarr.Api.V3/Config/UiConfigResource.cs
@@ -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,
diff --git a/yarn.lock b/yarn.lock
index 02deec1dc..f25a18e31 100644
--- a/yarn.lock
+++ b/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==