mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user