mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-14 23:21:19 +00:00
feat: add 12-hour/24-hour timestamp format toggle option
- Add timestampFormat setting to global-settings.ts with enum type - Update formatTimestamp.ts to accept format parameter (12hour/24hour) - Add comprehensive tests for 12-hour format (AM/PM display) - Add time format dropdown in UI Settings when timestamps are enabled - Update i18n translations for the new setting Resolves: Issue #10539 - Request for 12hr/24hr time format toggle
This commit is contained in:
parent
58f707b23e
commit
8c81d0c92c
10 changed files with 226 additions and 65 deletions
|
|
@ -198,6 +198,11 @@ export const globalSettingsSchema = z.object({
|
|||
* @default false
|
||||
*/
|
||||
showTimestamps: z.boolean().optional(),
|
||||
/**
|
||||
* Format for displaying timestamps on chat messages
|
||||
* @default "24hour"
|
||||
*/
|
||||
timestampFormat: z.enum(["12hour", "24hour"]).optional(),
|
||||
profileThresholds: z.record(z.string(), z.number()).optional(),
|
||||
hasOpenedModeSelector: z.boolean().optional(),
|
||||
lastModeExportPath: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ export type ExtensionState = Pick<
|
|||
| "reasoningBlockCollapsed"
|
||||
| "enterBehavior"
|
||||
| "showTimestamps"
|
||||
| "timestampFormat"
|
||||
| "includeCurrentTime"
|
||||
| "includeCurrentCost"
|
||||
| "maxGitStatusFiles"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
|
|||
import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { formatPathTooltip } from "@src/utils/formatPathTooltip"
|
||||
import { formatTimestamp } from "@src/utils/formatTimestamp"
|
||||
import { formatTimestamp, TimestampFormat } from "@src/utils/formatTimestamp"
|
||||
|
||||
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
|
||||
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
|
||||
|
|
@ -168,8 +168,16 @@ export const ChatRowContent = ({
|
|||
}: ChatRowContentProps) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, showTimestamps } =
|
||||
useExtensionState()
|
||||
const {
|
||||
mcpServers,
|
||||
alwaysAllowMcp,
|
||||
currentCheckpoint,
|
||||
mode,
|
||||
apiConfiguration,
|
||||
clineMessages,
|
||||
showTimestamps,
|
||||
timestampFormat,
|
||||
} = useExtensionState()
|
||||
const { info: model } = useSelectedModel(apiConfiguration)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedContent, setEditedContent] = useState("")
|
||||
|
|
@ -385,7 +393,7 @@ export const ChatRowContent = ({
|
|||
// Timestamp element to be displayed on the right side of headers
|
||||
const timestampElement = showTimestamps ? (
|
||||
<span className="text-vscode-descriptionForeground ml-auto shrink-0" style={{ fontWeight: "normal" }}>
|
||||
{formatTimestamp(message.ts)}
|
||||
{formatTimestamp(message.ts, (timestampFormat ?? "24hour") as TimestampFormat)}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
reasoningBlockCollapsed,
|
||||
enterBehavior,
|
||||
showTimestamps,
|
||||
timestampFormat,
|
||||
includeCurrentTime,
|
||||
includeCurrentCost,
|
||||
maxGitStatusFiles,
|
||||
|
|
@ -412,6 +413,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
|
||||
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
|
||||
enterBehavior: enterBehavior ?? "send",
|
||||
showTimestamps: showTimestamps ?? false,
|
||||
timestampFormat: timestampFormat ?? "24hour",
|
||||
includeCurrentTime: includeCurrentTime ?? true,
|
||||
includeCurrentCost: includeCurrentCost ?? true,
|
||||
maxGitStatusFiles: maxGitStatusFiles ?? 0,
|
||||
|
|
@ -832,6 +835,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
|
||||
enterBehavior={enterBehavior ?? "send"}
|
||||
showTimestamps={showTimestamps ?? false}
|
||||
timestampFormat={timestampFormat ?? "24hour"}
|
||||
setCachedStateField={setCachedStateField}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { HTMLAttributes, useMemo } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Glasses } from "lucide-react"
|
||||
import { telemetryClient } from "@/utils/TelemetryClient"
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
|
|||
reasoningBlockCollapsed: boolean
|
||||
enterBehavior: "send" | "newline"
|
||||
showTimestamps: boolean
|
||||
timestampFormat: "12hour" | "24hour"
|
||||
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export const UISettings = ({
|
|||
reasoningBlockCollapsed,
|
||||
enterBehavior,
|
||||
showTimestamps,
|
||||
timestampFormat,
|
||||
setCachedStateField,
|
||||
...props
|
||||
}: UISettingsProps) => {
|
||||
|
|
@ -59,6 +61,15 @@ export const UISettings = ({
|
|||
})
|
||||
}
|
||||
|
||||
const handleTimestampFormatChange = (format: "12hour" | "24hour") => {
|
||||
setCachedStateField("timestampFormat", format)
|
||||
|
||||
// Track telemetry event
|
||||
telemetryClient.capture("ui_settings_timestamp_format_changed", {
|
||||
format,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
|
|
@ -110,6 +121,31 @@ export const UISettings = ({
|
|||
{t("settings:ui.showTimestamps.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timestamp Format Setting - only visible when timestamps are enabled */}
|
||||
{showTimestamps && (
|
||||
<div className="flex flex-col gap-1 ml-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{t("settings:ui.timestampFormat.label")}</span>
|
||||
<VSCodeDropdown
|
||||
value={timestampFormat}
|
||||
onChange={(e: any) =>
|
||||
handleTimestampFormatChange(e.target.value as "12hour" | "24hour")
|
||||
}
|
||||
data-testid="timestamp-format-dropdown">
|
||||
<VSCodeOption value="24hour">
|
||||
{t("settings:ui.timestampFormat.options.24hour")}
|
||||
</VSCodeOption>
|
||||
<VSCodeOption value="12hour">
|
||||
{t("settings:ui.timestampFormat.options.12hour")}
|
||||
</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
</div>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
{t("settings:ui.timestampFormat.description")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ describe("UISettings", () => {
|
|||
reasoningBlockCollapsed: false,
|
||||
enterBehavior: "send" as const,
|
||||
showTimestamps: false,
|
||||
timestampFormat: "24hour" as const,
|
||||
setCachedStateField: vi.fn(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setIncludeCurrentCost: (value: boolean) => void
|
||||
showTimestamps?: boolean
|
||||
setShowTimestamps: (value: boolean) => void
|
||||
timestampFormat?: "12hour" | "24hour"
|
||||
setTimestampFormat: (value: "12hour" | "24hour") => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -300,6 +302,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [includeCurrentTime, setIncludeCurrentTime] = useState(true)
|
||||
const [includeCurrentCost, setIncludeCurrentCost] = useState(true)
|
||||
const [showTimestamps, setShowTimestamps] = useState(false) // Default to false (timestamps hidden)
|
||||
const [timestampFormat, setTimestampFormat] = useState<"12hour" | "24hour">("24hour") // Default to 24-hour format
|
||||
|
||||
const setListApiConfigMeta = useCallback(
|
||||
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
|
|
@ -349,6 +352,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
if ((newState as any).showTimestamps !== undefined) {
|
||||
setShowTimestamps((newState as any).showTimestamps)
|
||||
}
|
||||
// Update timestampFormat if present in state message
|
||||
if ((newState as any).timestampFormat !== undefined) {
|
||||
setTimestampFormat((newState as any).timestampFormat)
|
||||
}
|
||||
// Handle marketplace data if present in state message
|
||||
if (newState.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(newState.marketplaceItems)
|
||||
|
|
@ -601,6 +608,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setIncludeCurrentCost,
|
||||
showTimestamps,
|
||||
setShowTimestamps,
|
||||
timestampFormat,
|
||||
setTimestampFormat,
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@
|
|||
"showTimestamps": {
|
||||
"label": "Show timestamps on messages",
|
||||
"description": "When enabled, timestamps will be displayed on the right side of message headers"
|
||||
},
|
||||
"timestampFormat": {
|
||||
"label": "Time format",
|
||||
"description": "Choose how timestamps are displayed",
|
||||
"options": {
|
||||
"12hour": "12-hour (2:34 PM)",
|
||||
"24hour": "24-hour (14:34)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
|
|
@ -12,64 +12,130 @@ describe("formatTimestamp", () => {
|
|||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("formats today's time in 24-hour format", () => {
|
||||
// Same day at 10:15
|
||||
const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("10:15")
|
||||
describe("24-hour format (default)", () => {
|
||||
it("formats today's time in 24-hour format", () => {
|
||||
// Same day at 10:15
|
||||
const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("10:15")
|
||||
})
|
||||
|
||||
it("pads single-digit hours and minutes", () => {
|
||||
const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("09:05")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous days", () => {
|
||||
// Previous day
|
||||
const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("Jan 8, 14:34")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous months", () => {
|
||||
// Previous month
|
||||
const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("Dec 25, 09:00")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous years", () => {
|
||||
// Previous year
|
||||
const timestamp = new Date("2025-06-15T18:45:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("Jun 15, 18:45")
|
||||
})
|
||||
|
||||
it("handles midnight correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("00:00")
|
||||
})
|
||||
|
||||
it("handles end of day correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("23:59")
|
||||
})
|
||||
|
||||
it("correctly abbreviates all months", () => {
|
||||
const months = [
|
||||
{ date: "2025-01-15", expected: "Jan" },
|
||||
{ date: "2025-02-15", expected: "Feb" },
|
||||
{ date: "2025-03-15", expected: "Mar" },
|
||||
{ date: "2025-04-15", expected: "Apr" },
|
||||
{ date: "2025-05-15", expected: "May" },
|
||||
{ date: "2025-06-15", expected: "Jun" },
|
||||
{ date: "2025-07-15", expected: "Jul" },
|
||||
{ date: "2025-08-15", expected: "Aug" },
|
||||
{ date: "2025-09-15", expected: "Sep" },
|
||||
{ date: "2025-10-15", expected: "Oct" },
|
||||
{ date: "2025-11-15", expected: "Nov" },
|
||||
{ date: "2025-12-15", expected: "Dec" },
|
||||
]
|
||||
|
||||
months.forEach(({ date, expected }) => {
|
||||
const timestamp = new Date(`${date}T12:00:00.000Z`).getTime()
|
||||
expect(formatTimestamp(timestamp)).toContain(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("pads single-digit hours and minutes", () => {
|
||||
const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("09:05")
|
||||
describe("12-hour format", () => {
|
||||
it("formats morning time with AM", () => {
|
||||
const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("10:15 AM")
|
||||
})
|
||||
|
||||
it("formats afternoon time with PM", () => {
|
||||
const timestamp = new Date("2026-01-09T14:30:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("2:30 PM")
|
||||
})
|
||||
|
||||
it("formats midnight as 12:00 AM", () => {
|
||||
const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("12:00 AM")
|
||||
})
|
||||
|
||||
it("formats noon as 12:00 PM", () => {
|
||||
const timestamp = new Date("2026-01-09T12:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("12:00 PM")
|
||||
})
|
||||
|
||||
it("formats end of day correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("11:59 PM")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous days with 12-hour format", () => {
|
||||
const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("Jan 8, 2:34 PM")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous months with 12-hour format", () => {
|
||||
const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("Dec 25, 9:00 AM")
|
||||
})
|
||||
|
||||
it("handles single-digit hours without padding", () => {
|
||||
const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("9:05 AM")
|
||||
})
|
||||
|
||||
it("formats 1 AM correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T01:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("1:00 AM")
|
||||
})
|
||||
|
||||
it("formats 1 PM correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T13:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "12hour")).toBe("1:00 PM")
|
||||
})
|
||||
})
|
||||
|
||||
it("includes date for messages from previous days", () => {
|
||||
// Previous day
|
||||
const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("Jan 8, 14:34")
|
||||
})
|
||||
describe("explicit 24-hour format parameter", () => {
|
||||
it("formats time same as default when explicitly set to 24hour", () => {
|
||||
const timestamp = new Date("2026-01-09T14:30:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "24hour")).toBe("14:30")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous months", () => {
|
||||
// Previous month
|
||||
const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("Dec 25, 09:00")
|
||||
})
|
||||
|
||||
it("includes date for messages from previous years", () => {
|
||||
// Previous year
|
||||
const timestamp = new Date("2025-06-15T18:45:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("Jun 15, 18:45")
|
||||
})
|
||||
|
||||
it("handles midnight correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("00:00")
|
||||
})
|
||||
|
||||
it("handles end of day correctly", () => {
|
||||
const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp)).toBe("23:59")
|
||||
})
|
||||
|
||||
it("correctly abbreviates all months", () => {
|
||||
const months = [
|
||||
{ date: "2025-01-15", expected: "Jan" },
|
||||
{ date: "2025-02-15", expected: "Feb" },
|
||||
{ date: "2025-03-15", expected: "Mar" },
|
||||
{ date: "2025-04-15", expected: "Apr" },
|
||||
{ date: "2025-05-15", expected: "May" },
|
||||
{ date: "2025-06-15", expected: "Jun" },
|
||||
{ date: "2025-07-15", expected: "Jul" },
|
||||
{ date: "2025-08-15", expected: "Aug" },
|
||||
{ date: "2025-09-15", expected: "Sep" },
|
||||
{ date: "2025-10-15", expected: "Oct" },
|
||||
{ date: "2025-11-15", expected: "Nov" },
|
||||
{ date: "2025-12-15", expected: "Dec" },
|
||||
]
|
||||
|
||||
months.forEach(({ date, expected }) => {
|
||||
const timestamp = new Date(`${date}T12:00:00.000Z`).getTime()
|
||||
expect(formatTimestamp(timestamp)).toContain(expected)
|
||||
it("formats midnight correctly with explicit 24-hour format", () => {
|
||||
const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime()
|
||||
expect(formatTimestamp(timestamp, "24hour")).toBe("00:00")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
export type TimestampFormat = "12hour" | "24hour"
|
||||
|
||||
/**
|
||||
* Formats a Unix timestamp (in milliseconds) to a human-readable time string.
|
||||
*
|
||||
* Requirements from Issue #10539:
|
||||
* - 24-hour format (14:34)
|
||||
* - Full date for messages from previous days (e.g., "Jan 7, 14:34")
|
||||
* - Configurable 12-hour (2:34 PM) or 24-hour (14:34) format
|
||||
* - Full date for messages from previous days (e.g., "Jan 7, 14:34" or "Jan 7, 2:34 PM")
|
||||
* - Text-size same as header row text
|
||||
*
|
||||
* @param ts - Unix timestamp in milliseconds
|
||||
* @param format - Time format: "12hour" for AM/PM, "24hour" for 24-hour format (default: "24hour")
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
export function formatTimestamp(ts: number): string {
|
||||
export function formatTimestamp(ts: number, format: TimestampFormat = "24hour"): string {
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
|
||||
|
|
@ -19,10 +22,8 @@ export function formatTimestamp(ts: number): string {
|
|||
date.getMonth() === now.getMonth() &&
|
||||
date.getFullYear() === now.getFullYear()
|
||||
|
||||
// Format hours and minutes in 24-hour format
|
||||
const hours = date.getHours().toString().padStart(2, "0")
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0")
|
||||
const time = `${hours}:${minutes}`
|
||||
// Format the time based on the selected format
|
||||
const time = formatTime(date, format)
|
||||
|
||||
if (isToday) {
|
||||
// Just show time for today's messages
|
||||
|
|
@ -36,3 +37,25 @@ export function formatTimestamp(ts: number): string {
|
|||
|
||||
return `${month} ${day}, ${time}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats just the time portion of a date.
|
||||
*
|
||||
* @param date - Date object to format
|
||||
* @param format - Time format: "12hour" for AM/PM, "24hour" for 24-hour format
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
function formatTime(date: Date, format: TimestampFormat): string {
|
||||
const hours24 = date.getHours()
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0")
|
||||
|
||||
if (format === "12hour") {
|
||||
const hours12 = hours24 % 12 || 12 // Convert 0 to 12 for midnight
|
||||
const period = hours24 < 12 ? "AM" : "PM"
|
||||
return `${hours12}:${minutes} ${period}`
|
||||
}
|
||||
|
||||
// 24-hour format
|
||||
const hours = hours24.toString().padStart(2, "0")
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue