diff --git a/resources/js/components/posts/editor/PostEditorComposer.vue b/resources/js/components/posts/editor/PostEditorComposer.vue index b6d4538f..094dfdac 100644 --- a/resources/js/components/posts/editor/PostEditorComposer.vue +++ b/resources/js/components/posts/editor/PostEditorComposer.vue @@ -23,6 +23,7 @@ import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { formatBytes } from '@/composables/useMedia'; import { getPlatformLabel, getPlatformLogo } from '@/composables/usePlatformLogo'; +import date from '@/date'; import { classify, isDocument, isVideo, MediaType } from '@/lib/mediaType'; import type { MediaItem } from '@/types/media'; @@ -86,13 +87,6 @@ const openPreview = (item: MediaItem) => { ); }; -const formatDuration = (seconds: number): string => { - const total = Math.round(seconds); - const m = Math.floor(total / 60); - const s = total % 60; - return `${m}:${s.toString().padStart(2, '0')}`; -}; - const limitsWithUsage = computed(() => props.platformLimits.map((p) => { const used = content.value.length; @@ -267,7 +261,7 @@ const canRegenerateWithAi = (item: MediaItem): boolean => props.allowAiRegenerat class="inline-flex items-center gap-0.5 rounded-md bg-black/65 px-1.5 py-0.5 backdrop-blur-sm" > - {{ formatDuration(item.meta.duration) }} + {{ date.formatClock(item.meta.duration) }} { return bytes + ' B'; }; -const formatDuration = (seconds: number): string => { - const s = Math.round(seconds); - if (s < 60) return `${s}s`; - const m = Math.floor(s / 60); - const rem = s % 60; - return rem === 0 ? `${m}min` : `${m}min ${rem}s`; -}; - const formatAspect = (ratio: number): string => ratio.toFixed(2); /** @@ -104,7 +97,7 @@ export const getMediaValidationWarning = ( if (rules.maxVideoDurationSec && duration > rules.maxVideoDurationSec) { return { key: 'video_too_long', - params: { max: formatDuration(rules.maxVideoDurationSec), current: formatDuration(duration) }, + params: { max: date.formatDurationWords(rules.maxVideoDurationSec), current: date.formatDurationWords(duration) }, }; } } else if (rules.maxImageBytes && size > rules.maxImageBytes) { diff --git a/resources/js/date.ts b/resources/js/date.ts index 09095988..fb91ad2c 100644 --- a/resources/js/date.ts +++ b/resources/js/date.ts @@ -214,4 +214,50 @@ export default { } return `${m}min`; }, + + /** + * Format seconds as a clock (m:ss, or h:mm:ss past one hour). + * For media badges and stopwatches (e.g. "5:30", "1:05:30"). + */ + formatClock(seconds: number): string { + const d = dayjs.duration(Math.round(seconds), 'seconds'); + + return d.asHours() >= 1 ? d.format('H:mm:ss') : d.format('m:ss'); + }, + + /** + * Format seconds as short words (e.g. "45s", "5min", "5min 30s"). + */ + formatDurationWords(seconds: number): string { + const s = Math.round(seconds); + if (s < 60) { + return `${s}s`; + } + + const m = Math.floor(s / 60); + const rem = s % 60; + + return rem === 0 ? `${m}min` : `${m}min ${rem}s`; + }, + + /** + * Format a duration in milliseconds (e.g. "—", "500ms", "1.5s", "2m 30s"). + */ + formatDurationMs(ms: number | null): string { + if (ms === null) { + return '—'; + } + if (ms < 1000) { + return `${ms}ms`; + } + + const seconds = ms / 1000; + if (seconds < 60) { + return `${seconds.toFixed(1)}s`; + } + + const minutes = Math.floor(seconds / 60); + + return `${minutes}m ${Math.round(seconds % 60)}s`; + }, }; diff --git a/resources/js/pages/automations/Invocations.vue b/resources/js/pages/automations/Invocations.vue index 151fd6b8..d9a0eb8a 100644 --- a/resources/js/pages/automations/Invocations.vue +++ b/resources/js/pages/automations/Invocations.vue @@ -100,14 +100,6 @@ const summary = (invocation: Invocation): string => { return trans('automations.invocations.summary.pending'); }; -const formatDuration = (ms: number | null): string => { - if (ms === null) return '—'; - if (ms < 1000) return `${ms}ms`; - const seconds = ms / 1000; - if (seconds < 60) return `${seconds.toFixed(1)}s`; - const minutes = Math.floor(seconds / 60); - return `${minutes}m ${Math.round(seconds % 60)}s`; -}; const stepsLabel = (count: number): string => transChoice('automations.invocations.steps', count, { @@ -362,7 +354,7 @@ const toggleExpand = async (invocation: Invocation) => { {{ - formatDuration( + date.formatDurationMs( invocation.duration_ms, ) }} { - if (ms === null) return '—'; - if (ms < 1000) return `${ms}ms`; - const seconds = ms / 1000; - if (seconds < 60) return `${seconds.toFixed(1)}s`; - const minutes = Math.floor(seconds / 60); - return `${minutes}m ${Math.round(seconds % 60)}s`; -}; - const cards = computed(() => [ { key: 'runs', label: 'automations.metrics.cards.runs', value: String(props.metrics.totals.runs) }, { key: 'completed', label: 'automations.metrics.cards.completed', value: String(props.metrics.totals.completed) }, { key: 'failed', label: 'automations.metrics.cards.failed', value: String(props.metrics.totals.failed) }, { key: 'in_progress', label: 'automations.metrics.cards.in_progress', value: String(props.metrics.totals.in_progress) }, { key: 'success_rate', label: 'automations.metrics.cards.success_rate', value: props.metrics.totals.success_rate === null ? '—' : `${props.metrics.totals.success_rate}%` }, - { key: 'avg_duration', label: 'automations.metrics.cards.avg_duration', value: formatDuration(props.metrics.totals.avg_duration_ms) }, + { key: 'avg_duration', label: 'automations.metrics.cards.avg_duration', value: date.formatDurationMs(props.metrics.totals.avg_duration_ms) }, { key: 'posts_created', label: 'automations.metrics.cards.posts_created', value: String(props.metrics.totals.posts_created) }, ]); diff --git a/resources/js/pages/posts/ai/Loading.vue b/resources/js/pages/posts/ai/Loading.vue index ca3c9b02..40bc0245 100644 --- a/resources/js/pages/posts/ai/Loading.vue +++ b/resources/js/pages/posts/ai/Loading.vue @@ -6,6 +6,7 @@ import { trans } from 'laravel-vue-i18n'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { Button } from '@/components/ui/button'; +import date from '@/date'; import AppLayout from '@/layouts/AppLayout.vue'; import { calendar as calendarRoute } from '@/routes/app'; import { create as createPostRoute, edit as editPostRoute } from '@/routes/app/posts'; @@ -53,11 +54,7 @@ const currentTip = computed(() => trans(tipKeys[tipIndex.value % tipKeys.length] const elapsed = ref(0); let elapsedTimer: ReturnType | null = null; -const elapsedLabel = computed(() => { - const minutes = Math.floor(elapsed.value / 60); - const seconds = elapsed.value % 60; - return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; -}); +const elapsedLabel = computed(() => date.formatClock(elapsed.value)); const progress = computed(() => { const ratio = elapsed.value / estimatedSeconds.value;