diff --git a/src/webui/www/private/scripts/addtorrent.js b/src/webui/www/private/scripts/addtorrent.js index 4660862e7..2ac888f8b 100644 --- a/src/webui/www/private/scripts/addtorrent.js +++ b/src/webui/www/private/scripts/addtorrent.js @@ -275,7 +275,7 @@ window.qBittorrent.AddTorrent ??= (() => { if (metadata.info?.length !== undefined) document.getElementById("size").textContent = window.qBittorrent.Misc.friendlyUnit(metadata.info.length, false); if ((metadata.creation_date !== undefined) && (metadata.creation_date > 1)) - document.getElementById("createdDate").textContent = new Date(metadata.creation_date * 1000).toLocaleString(); + document.getElementById("createdDate").textContent = window.qBittorrent.Misc.formatDate(new Date(metadata.creation_date * 1000)); if (metadata.comment !== undefined) document.getElementById("comment").textContent = metadata.comment; diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js index eb09b777b..fea74838a 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -69,6 +69,7 @@ window.qBittorrent.Client ??= (() => { "show_status_bar", "show_filters_sidebar", "hide_zero_status_filters", + "date_format", "color_scheme", "full_url_tracker_column", "use_alt_row_colors", diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index 826f87386..b36b896fd 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -1426,7 +1426,7 @@ window.qBittorrent.DynamicTable ??= (() => { // added on this.columns["added_on"].updateTd = function(td, row) { - const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); + const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000)); td.textContent = date; td.title = date; }; @@ -1439,7 +1439,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.title = ""; } else { - const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); + const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000)); td.textContent = date; td.title = date; } @@ -1964,7 +1964,7 @@ window.qBittorrent.DynamicTable ??= (() => { }; const displayDate = function(td, row) { const value = this.getRowValue(row) * 1000; - const formattedValue = (Number.isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString()); + const formattedValue = (Number.isNaN(value) || (value <= 0)) ? "" : window.qBittorrent.Misc.formatDate(new Date(value)); td.textContent = formattedValue; td.title = formattedValue; }; @@ -3734,7 +3734,7 @@ window.qBittorrent.DynamicTable ??= (() => { initColumnsFunctions() { this.columns["timestamp"].updateTd = function(td, row) { - const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); + const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000)); td.textContent = date; td.title = date; }; @@ -3812,7 +3812,7 @@ window.qBittorrent.DynamicTable ??= (() => { this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true); this.columns["timestamp"].updateTd = function(td, row) { - const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); + const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000)); td.textContent = date; td.title = date; }; @@ -4036,7 +4036,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.title = ""; } else { - const date = new Date(val).toLocaleString(); + const date = window.qBittorrent.Misc.formatDate(new Date(val)); td.textContent = date; td.title = date; } diff --git a/src/webui/www/private/scripts/misc.js b/src/webui/www/private/scripts/misc.js index c1bad9e3e..364e51453 100644 --- a/src/webui/www/private/scripts/misc.js +++ b/src/webui/www/private/scripts/misc.js @@ -28,6 +28,108 @@ "use strict"; +/** + * @type Record + */ +const DateFormatOptions = { + "MM/dd/yyyy, h:mm:ss AM/PM": { + locale: "en-US", + options: { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + hour12: true + } + }, + "MM/dd/yyyy, HH:mm:ss": { + locale: "en-US", + options: { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + } + }, + "dd/MM/yyyy, HH:mm:ss": { + locale: "en-GB", + options: { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + } + }, + "yyyy-MM-dd HH:mm:ss": { + locale: "sv-SE", + options: { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + } + }, + "yyyy/MM/dd HH:mm:ss": { + locale: "ja-JP", + options: { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + } + }, + "dd.MM.yyyy, HH:mm:ss": { + locale: "en-GB", + options: { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + } + }, + "MMM dd, yyyy, h:mm:ss AM/PM": { + locale: "en-US", + options: { + year: "numeric", + month: "short", + day: "2-digit", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + hour12: true + } + }, + "dd MMM yyyy, HH:mm:ss": { + locale: "en-GB", + options: { + year: "numeric", + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + } + }, +}; + window.qBittorrent ??= {}; window.qBittorrent.Misc ??= (() => { const exports = () => { @@ -46,6 +148,7 @@ window.qBittorrent.Misc ??= (() => { containsAllTerms: containsAllTerms, sleep: sleep, downloadFile: downloadFile, + formatDate: formatDate, // variables FILTER_INPUT_DELAY: 400, MAX_ETA: 8640000 @@ -302,6 +405,21 @@ window.qBittorrent.Misc ??= (() => { } }; + /** + * @param {Date} date + * @param {string} format + * @returns {string} + */ + const formatDate = (date, format = window.parent.qBittorrent.ClientData.getCached("date_format")) => { + if ((format === "default") || !Object.keys(DateFormatOptions).includes(format)) + return date.toLocaleString(); + + const { locale, options } = DateFormatOptions[format]; + const formatter = new Intl.DateTimeFormat(locale, options); + const formatted = formatter.format(date).replace(" at ", ", "); + return format.includes(".") ? formatted.replaceAll("/", ".") : formatted; + }; + return exports(); })(); Object.freeze(window.qBittorrent.Misc); diff --git a/src/webui/www/private/scripts/prop-general.js b/src/webui/www/private/scripts/prop-general.js index fce3c84a6..abfdf6c8b 100644 --- a/src/webui/www/private/scripts/prop-general.js +++ b/src/webui/www/private/scripts/prop-general.js @@ -177,7 +177,7 @@ window.qBittorrent.PropGeneral ??= (() => { document.getElementById("reannounce").textContent = window.qBittorrent.Misc.friendlyDuration(data.reannounce); const lastSeen = (data.last_seen >= 0) - ? new Date(data.last_seen * 1000).toLocaleString() + ? window.qBittorrent.Misc.formatDate(new Date(data.last_seen * 1000)) : "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]"; document.getElementById("last_seen").textContent = lastSeen; @@ -195,17 +195,17 @@ window.qBittorrent.PropGeneral ??= (() => { document.getElementById("created_by").textContent = data.created_by; const additionDate = (data.addition_date >= 0) - ? new Date(data.addition_date * 1000).toLocaleString() + ? window.qBittorrent.Misc.formatDate(new Date(data.addition_date * 1000)) : "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; document.getElementById("addition_date").textContent = additionDate; const completionDate = (data.completion_date >= 0) - ? new Date(data.completion_date * 1000).toLocaleString() + ? window.qBittorrent.Misc.formatDate(new Date(data.completion_date * 1000)) : ""; document.getElementById("completion_date").textContent = completionDate; const creationDate = (data.creation_date >= 0) - ? new Date(data.creation_date * 1000).toLocaleString() + ? window.qBittorrent.Misc.formatDate(new Date(data.creation_date * 1000)) : ""; document.getElementById("creation_date").textContent = creationDate; diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index f05fca02f..a067affc5 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -1,10 +1,34 @@
- QBT_TR(Language)QBT_TR[CONTEXT=OptionsDialog] - - + QBT_TR(Localization)QBT_TR[CONTEXT=OptionsDialog] + + + + + + + + + + + +
+ +
+ +
@@ -2229,6 +2253,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD window.parent.qBittorrent.Cache.preferences.init() .then((pref) => { const clientData = window.parent.qBittorrent.ClientData; + const dateFormat = clientData.getCached("date_format"); const colorScheme = clientData.getCached("color_scheme"); const fullUrlTrackerColumn = clientData.getCached("full_url_tracker_column"); const useVirtualList = clientData.getCached("use_virtual_list"); @@ -2239,8 +2264,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD const useAltRowColors = clientData.getCached("use_alt_row_colors"); // Behavior tab - // Language + // Localization updateWebuiLocaleSelect(pref.locale); + const dateFormatSelect = document.getElementById("dateFormatSelect"); + if ((dateFormat?.length > 0) && ([...dateFormatSelect.options].find(o => o.value === dateFormat) !== undefined)) + dateFormatSelect.value = dateFormat; + + // Interface updateColoSchemeSelect(colorScheme); document.getElementById("statusBarExternalIP").checked = pref.status_bar_external_ip; @@ -2673,6 +2703,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD // Behavior tab // Language settings["locale"] = document.getElementById("locale_select").value; + clientData.date_format = document.getElementById("dateFormatSelect").value; const colorScheme = Number(document.getElementById("colorSchemeSelect").value); if (colorScheme === 0) clientData.color_scheme = null; diff --git a/src/webui/www/private/views/rss.html b/src/webui/www/private/views/rss.html index 57a92d3ed..40a5977e3 100644 --- a/src/webui/www/private/views/rss.html +++ b/src/webui/www/private/views/rss.html @@ -456,7 +456,7 @@ torrentDate.append(torrentDateDesc); const torrentDateData = document.createElement("span"); - torrentDateData.textContent = new Date(articleDate).toLocaleString(); + torrentDateData.textContent = window.qBittorrent.Misc.formatDate(new Date(articleDate)); torrentDate.append(torrentDateData); detailsView.append(torrentDate); diff --git a/src/webui/www/test/private/misc.test.js b/src/webui/www/test/private/misc.test.js index 30732d624..439e41abf 100644 --- a/src/webui/www/test/private/misc.test.js +++ b/src/webui/www/test/private/misc.test.js @@ -26,7 +26,7 @@ * exception statement from your version. */ -import { expect, test } from "vitest"; +import { vi, expect, test } from "vitest"; import "../../private/scripts/misc.js"; test("Test toFixedPointString()", () => { @@ -76,3 +76,57 @@ test("Test toFixedPointString()", () => { expect(toFixedPointString(-100.00, 1)).toBe("-100.0"); expect(toFixedPointString(-100.00, 2)).toBe("-100.00"); }); + +test("Test formatDate() - Format Coverage", () => { + const formatDate = window.qBittorrent.Misc.formatDate; + const testDate = new Date(2025, 7, 23, 22, 32, 46); // Aug 23, 2025 10:32:46 PM + + expect(formatDate(testDate, "MM/dd/yyyy, h:mm:ss AM/PM")).toBe("08/23/2025, 10:32:46 PM"); + expect(formatDate(testDate, "MM/dd/yyyy, HH:mm:ss")).toBe("08/23/2025, 22:32:46"); + expect(formatDate(testDate, "dd/MM/yyyy, HH:mm:ss")).toBe("23/08/2025, 22:32:46"); + expect(formatDate(testDate, "yyyy-MM-dd HH:mm:ss")).toBe("2025-08-23 22:32:46"); + expect(formatDate(testDate, "yyyy/MM/dd HH:mm:ss")).toBe("2025/08/23 22:32:46"); + expect(formatDate(testDate, "dd.MM.yyyy, HH:mm:ss")).toBe("23.08.2025, 22:32:46"); + expect(formatDate(testDate, "MMM dd, yyyy, h:mm:ss AM/PM")).toBe("Aug 23, 2025, 10:32:46 PM"); + expect(formatDate(testDate, "dd MMM yyyy, HH:mm:ss")).toBe("23 Aug 2025, 22:32:46"); +}); + +test("Test formatDate() - Fallback Behavior", () => { + // Mock ClientData.getCached + const mockGetCached = vi.fn().mockReturnValue("default"); + const originalParent = window.parent; + + window.parent = { + qBittorrent: { + ClientData: { + getCached: mockGetCached + } + } + }; + + const formatDate = window.qBittorrent.Misc.formatDate; + const testDate = new Date(2025, 7, 23, 22, 32, 46); // Aug 23, 2025 10:32:46 PM + const expectedDefault = testDate.toLocaleString(); + + // Test that "default" format uses toLocaleString() + expect(formatDate(testDate, "default")).toBe(expectedDefault); + + // Test default behavior when no format argument is provided + expect(mockGetCached).toHaveBeenCalledTimes(0); + expect(formatDate(testDate)).toBe(expectedDefault); + expect(mockGetCached).toHaveBeenCalledWith("date_format"); + expect(mockGetCached).toHaveBeenCalledTimes(1); + + // Test with unknown/invalid format strings + expect(formatDate(testDate, "invalid-format")).toBe(expectedDefault); + expect(formatDate(testDate, "")).toBe(expectedDefault); + expect(formatDate(testDate, null)).toBe(expectedDefault); + + expect(mockGetCached).toHaveBeenCalledTimes(1); + expect(formatDate(testDate, undefined)).toBe(expectedDefault); + expect(mockGetCached).toHaveBeenCalledWith("date_format"); + expect(mockGetCached).toHaveBeenCalledTimes(2); + + // Restore original window.parent + window.parent = originalParent; +});