-
-
+
+
+
{Math.round(status.progress || 0)}%
+
+
+ {status.remaining_time != null && status.remaining_time > 0 && (
+ <>
+
+
+ {formatDuration(status.remaining_time * 60)}
+
+
+ ETA {formatETA(status.remaining_time, timeFormat, t)}
+
+ >
+ )}
+ {status.layer_num != null && status.total_layers != null && status.total_layers > 0 && (
+
+
+ {status.layer_num}/{status.total_layers}
+
+ )}
-
{t('queue.printingInProgress')}
)}
diff --git a/frontend/src/pages/StreamOverlayPage.tsx b/frontend/src/pages/StreamOverlayPage.tsx
index aed6f53c9..cd1e06fb8 100644
--- a/frontend/src/pages/StreamOverlayPage.tsx
+++ b/frontend/src/pages/StreamOverlayPage.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Layers, Clock, Timer, Printer } from 'lucide-react';
import { api } from '../api/client';
import type { PrinterStatus } from '../api/client';
+import { formatDuration, formatETA, type TimeFormat } from '../utils/date';
type TFunction = (key: string, options?: Record
) => string;
@@ -46,31 +47,6 @@ function parseConfig(params: URLSearchParams): OverlayConfig {
};
}
-function formatTime(seconds: number): string {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
-}
-
-function formatETA(remainingMinutes: number, t: TFunction): string {
- const now = new Date();
- const eta = new Date(now.getTime() + remainingMinutes * 60 * 1000);
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- const etaDay = new Date(eta);
- etaDay.setHours(0, 0, 0, 0);
-
- const timeStr = eta.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
-
- if (etaDay.getTime() === today.getTime()) {
- return timeStr;
- } else if (etaDay.getTime() === today.getTime() + 86400000) {
- return `${t('streamOverlay.tomorrow')} ${timeStr}`;
- } else {
- return eta.toLocaleDateString([], { weekday: 'short' }) + ' ' + timeStr;
- }
-}
-
function getStatusText(status: PrinterStatus, t: TFunction): string {
if (status.stg_cur_name) return status.stg_cur_name;
@@ -146,6 +122,14 @@ export function StreamOverlayPage() {
refetchInterval: 2000,
});
+ // Fetch settings info
+ const { data: settings } = useQuery({
+ queryKey: ['settings'],
+ queryFn: api.getSettings,
+ });
+
+ const timeFormat: TimeFormat = settings?.time_format || 'system';
+
// WebSocket for real-time updates
useEffect(() => {
if (!id) return;
@@ -298,14 +282,14 @@ export function StreamOverlayPage() {
- {formatTime(status.remaining_time * 60)}
+ {formatDuration(status.remaining_time * 60)}
- {t('streamOverlay.eta')} {formatETA(status.remaining_time, t)}
+ {t('streamOverlay.eta')} {formatETA(status.remaining_time, timeFormat, t)}
>
diff --git a/frontend/src/utils/amsHelpers.ts b/frontend/src/utils/amsHelpers.ts
index e1af13e06..3180da48d 100644
--- a/frontend/src/utils/amsHelpers.ts
+++ b/frontend/src/utils/amsHelpers.ts
@@ -94,17 +94,6 @@ export function getGlobalTrayId(
return amsId * 4 + trayId;
}
-/**
- * Format seconds to human readable time string.
- */
-export function formatTime(seconds: number | null | undefined): string {
- if (!seconds) return '';
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- if (hours > 0) return `${hours}h ${minutes}m`;
- return `${minutes}m`;
-}
-
/**
* Get minimum datetime for scheduling (now + 1 minute).
* Returns ISO string format for datetime-local input.
diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts
index 1d53de926..4798cd54f 100644
--- a/frontend/src/utils/date.ts
+++ b/frontend/src/utils/date.ts
@@ -376,3 +376,51 @@ export function formatTimeOnly(
const finalOptions = applyTimeFormat({ ...defaultOptions, ...options }, timeFormat);
return date.toLocaleTimeString([], finalOptions);
}
+
+/**
+ * Calculate and format an ETA based on remaining minutes from now.
+ *
+ * @param remainingMinutes - Minutes until completion
+ * @param timeFormat - Time format setting ('system', '12h', '24h')
+ * @param t - Optional i18n translation function
+ * @returns Formatted ETA string (e.g., "3:45 PM", "Tomorrow 9:30 AM", "Wed 2:00 PM")
+ */
+export function formatETA(
+ remainingMinutes: number,
+ timeFormat: 'system' | '12h' | '24h' = 'system',
+ t?: (key: string) => string
+): string {
+ const now = new Date();
+ const eta = new Date(now.getTime() + remainingMinutes * 60 * 1000);
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const etaDay = new Date(eta);
+ etaDay.setHours(0, 0, 0, 0);
+
+ const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
+ if (timeFormat === '12h') timeOptions.hour12 = true;
+ else if (timeFormat === '24h') timeOptions.hour12 = false;
+
+ const timeStr = eta.toLocaleTimeString([], timeOptions);
+ const dayDiff = Math.floor((etaDay.getTime() - today.getTime()) / 86400000);
+
+ if (dayDiff === 0) return timeStr;
+ if (dayDiff === 1) return `${t?.('common.tomorrow') ?? 'Tomorrow'} ${timeStr}`;
+ return `${eta.toLocaleDateString([], { weekday: 'short' })} ${timeStr}`;
+}
+
+/**
+ * Format a duration in seconds to a human-readable string, with null handling.
+ *
+ * @param seconds - Duration in seconds, or null/undefined
+ * @returns Formatted string (e.g., "2h 30m", "45m") or "--" if no value
+ */
+export function formatDuration(seconds: number | null | undefined): string {
+ if (seconds == null || seconds < 0) return '--';
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
+}
\ No newline at end of file