Consolidate duplicate utility functions into shared modules

Extract repeated helper functions from components into shared utils
(colors.ts, date.ts, file.ts) to reduce duplication:
- isLightColor, parseFilamentColor, hexToColorName → utils/colors.ts
- formatMediaTime, formatDurationFromHours → utils/date.ts
- formatFileSize guard hardened for NaN/negative → utils/file.ts
- formatBytes (ToastContext), formatTime (Timelapse*) deduplicated

Functions with subtly different behavior are intentionally kept local
(formatFilament, formatStorageSize, formatBytes, formatUptime,
formatDateTime, formatDate, formatTimeAgo) to preserve exact UI output.
This commit is contained in:
AneoPsy
2026-03-03 04:20:21 -10:00
parent 15521deb0a
commit abe40f1ed0
15 changed files with 507 additions and 577 deletions
+1 -11
View File
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Droplets, Link2, Copy, Check, Settings2, ExternalLink, Package, Unlink } from 'lucide-react';
import { isLightColor } from '../utils/colors';
interface FilamentData {
vendor: 'Bambu Lab' | 'Generic';
@@ -136,17 +137,6 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
return '#22c55e'; // green
};
// Determine if color is light (for text contrast on swatch)
const isLightColor = (hex: string | null): boolean => {
if (!hex) return false;
const cleanHex = hex.replace('#', '');
const r = parseInt(cleanHex.slice(0, 2), 16);
const g = parseInt(cleanHex.slice(2, 4), 16);
const b = parseInt(cleanHex.slice(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6;
};
const colorHex = data.colorHex ? `#${data.colorHex.replace('#', '')}` : null;
const assignedRemainingWeight = inventory?.assignedSpool?.remainingWeightGrams ?? null;
@@ -247,7 +247,6 @@ function formatStorageSize(bytes: number): string {
return `${mb.toFixed(0)} MB`;
}
function getFileIcon(filename: string, isDirectory: boolean) {
if (isDirectory) return Folder;
@@ -37,6 +37,12 @@ import { ConfirmModal } from './ConfirmModal';
import { useToast } from '../contexts/ToastContext';
import { formatRelativeTime } from '../utils/date';
function formatDateTime(dateStr: string | null): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString();
}
interface StatusBadgeProps {
status: string | null;
}
@@ -66,12 +72,6 @@ function StatusBadge({ status }: StatusBadgeProps) {
);
}
function formatDateTime(dateStr: string | null): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString();
}
export function GitHubBackupSettings() {
const queryClient = useQueryClient();
const { showToast } = useToast();
@@ -5,17 +5,16 @@ import { api } from '../api/client';
import type { SpoolUsageRecord } from '../api/client';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
interface SpoolUsageHistoryProps {
spoolId: number;
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: '2-digit' }) +
' ' + date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}
interface SpoolUsageHistoryProps {
spoolId: number;
}
const STATUS_COLORS: Record<string, string> = {
completed: 'text-bambu-green',
failed: 'text-red-400',
@@ -18,6 +18,7 @@ import {
import { Button } from './Button';
import { api } from '../api/client';
import { useToast } from '../contexts/ToastContext';
import { formatMediaTime } from '../utils/date';
interface TimelapseEditorModalProps {
archiveId: number;
@@ -28,12 +29,6 @@ interface TimelapseEditorModalProps {
const SPEED_OPTIONS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4];
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
export function TimelapseEditorModal({
archiveId,
timelapseSrc,
@@ -321,7 +316,7 @@ export function TimelapseEditorModal({
<Scissors className="w-4 h-4" />
<span>Trim</span>
<span className="ml-auto">
{formatTime(trimStart)} - {formatTime(trimEnd)} ({formatTime(trimmedDuration)})
{formatMediaTime(trimStart)} - {formatMediaTime(trimEnd)} ({formatMediaTime(trimmedDuration)})
</span>
</div>
@@ -435,7 +430,7 @@ export function TimelapseEditorModal({
<div className="flex items-center gap-2 text-sm text-bambu-gray">
<Gauge className="w-4 h-4" />
<span>Speed</span>
<span className="ml-auto">{speed}x (output: {formatTime(outputDuration)})</span>
<span className="ml-auto">{speed}x (output: {formatMediaTime(outputDuration)})</span>
</div>
<div className="flex gap-1">
{SPEED_OPTIONS.map((s) => (
@@ -523,10 +518,10 @@ export function TimelapseEditorModal({
{/* Summary */}
<div className="p-3 bg-bambu-dark rounded-lg text-sm space-y-1">
<p className="text-bambu-gray">
<span className="text-white">Original:</span> {formatTime(duration)} @ {videoInfo?.width}x{videoInfo?.height}
<span className="text-white">Original:</span> {formatMediaTime(duration)} @ {videoInfo?.width}x{videoInfo?.height}
</p>
<p className="text-bambu-gray">
<span className="text-white">Output:</span> {formatTime(outputDuration)} @ {speed}x speed
<span className="text-white">Output:</span> {formatMediaTime(outputDuration)} @ {speed}x speed
{audioFile && ` + music overlay`}
</p>
</div>
+3 -8
View File
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
import { X, Download, Film, Play, Pause, SkipBack, SkipForward, Pencil } from 'lucide-react';
import { Button } from './Button';
import { TimelapseEditorModal } from './TimelapseEditorModal';
import { formatMediaTime } from '../utils/date';
interface TimelapseViewerProps {
src: string;
@@ -97,12 +98,6 @@ export function TimelapseViewer({
video.currentTime = Math.min(duration, video.currentTime + 5);
};
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const handleDownload = () => {
const link = document.createElement('a');
link.href = src;
@@ -154,7 +149,7 @@ export function TimelapseViewer({
{/* Progress bar */}
<div className="flex items-center gap-3">
<span className="text-xs text-bambu-gray w-12 text-right">
{formatTime(currentTime)}
{formatMediaTime(currentTime)}
</span>
<input
type="range"
@@ -168,7 +163,7 @@ export function TimelapseViewer({
[&::-webkit-slider-thumb]:cursor-pointer"
/>
<span className="text-xs text-bambu-gray w-12">
{formatTime(duration)}
{formatMediaTime(duration)}
</span>
</div>