getTimestamp(); if ($diff < 90) { return 'used just now'; } foreach ([31536000 => 'y', 2592000 => 'mo', 604800 => 'w', 86400 => 'd', 3600 => 'h', 60 => 'min'] as $secs => $unit) { if ($diff >= $secs) { return 'used ' . floor($diff / $secs) . " $unit ago"; } } return 'used just now'; } // Hex color to [hue 0-360 (0 when achromatic), saturation 0-1, lightness 0-1]. function hex_hsl(?string $hex): array { if (!preg_match('/^#[0-9a-fA-F]{6}$/', (string)$hex)) { $hex = '#888888'; } $r = hexdec(substr($hex, 1, 2)) / 255; $g = hexdec(substr($hex, 3, 2)) / 255; $b = hexdec(substr($hex, 5, 2)) / 255; $max = max($r, $g, $b); $min = min($r, $g, $b); $d = $max - $min; $l = ($max + $min) / 2; $s = ($l == 0 || $l == 1) ? 0 : $d / (1 - abs(2 * $l - 1)); if ($d == 0) { $h = 0; } elseif ($max == $r) { $h = fmod(($g - $b) / $d, 6); } elseif ($max == $g) { $h = ($b - $r) / $d + 2; } else { $h = ($r - $g) / $d + 4; } return [fmod($h * 60 + 360, 360), $s, $l]; } // Sort key for ordering by color: colorful spools by hue then lightness, // grays (low saturation) grouped after them, dark to light. function hue_key(?string $hex): array { [$h, $s, $l] = hex_hsl($hex); return $s < 0.18 ? [1, $l, 0.0] : [0, $h, $l]; } // The remaining-amount pie inside the spool hub is always drawn white, so it // stays consistent and readable on the black hub regardless of filament color. function pie_color(?string $hex): string { return '#ffffff'; } // Percent of a spool's original (nominal) filament amount still on it. Rows // with original_g unset are assumed to be 1 kg spools. function spool_pct(array $s): int { $orig = (float)($s['original_g'] ?? 0); if ($orig <= 0) { $orig = 1000.0; } return max(0, min(100, (int)round(100 * (float)$s['remaining_g'] / $orig))); } // Whether a spool has been used up. The cutoff is half a gram rather than // zero so that the spools this reports as empty are exactly the ones the UI // already displays as "0 g left" — round() shows anything under 0.5 as 0 — // and so a deduction that takes the last of the filament still counts as // emptying the spool when floating-point leaves a sliver behind. function spool_is_empty(array $s): bool { return (float)$s['remaining_g'] < 0.5; } // Whether a spool should render as a "small" (physically smaller) swatch: // sub-250 g spools (the common 250 g / 100 g sizes) are auto-small, and the // small_spool flag lets any spool be marked small by hand on the edit form. function is_small_spool(array $s): bool { if (!empty($s['small_spool'])) { return true; } $orig = (float)($s['original_g'] ?? 0); return $orig > 0 && $orig <= 250; } // The filament types offered in the add/edit and bulk-edit type dropdowns. function filament_types(): array { return ['PLA', 'PETG', 'ASA', 'TPU', 'PLA+', 'PLA-CF', 'PETG-CF', 'PCTG', 'ABS', 'PC', 'PA (Nylon)', 'PA-CF', 'PVA', 'HIPS', 'PVB']; } // CSS classes for a spool's swatch: rainbow spools get the animated full-hue // ring, silk spools the recurring shine sweep, transparent spools a // checkerboard showing through the color, sparkle spools twinkling flecks. function swatch_class(array $s): string { $cls = 'swatch'; if (!empty($s['rainbow'])) { $cls .= ' rainbow'; } if (!empty($s['silk'])) { $cls .= ' silk'; } if (!empty($s['transparent'])) { $cls .= ' transparent'; } if (!empty($s['twotone'])) { $cls .= ' twotone'; } if (!empty($s['sparkle'])) { $cls .= ' sparkle'; } return $cls; } // A spool's swatch as HTML. Every page renders it through this so the markup // (the CSS custom properties and the sparkle layer) stays in one place. // Mini swatches (the color grid) are decorative — the button around them // carries the label — so they get aria-hidden instead of the role/title. function swatch_html(array $s, bool $mini = false, string $id = ''): string { $pct = spool_pct($s); $fc = $s['color_hex'] ?: '#888888'; $fc2 = !empty($s['color_hex2']) ? $s['color_hex2'] : $fc; $attrs = $mini ? ' aria-hidden="true"' : ' role="img" aria-label="' . $pct . '% of spool left" title="' . $pct . '% of spool left"'; // Small spools draw a smaller circle at both sizes; the CSS keeps the grid // cell around a mini one uniform so rows and columns still line up. $size = ($mini ? ' mini' : '') . (is_small_spool($s) ? ' small' : ''); return '' . ''; } // "Prusament PLA · Galaxy Black" — whichever of those the spool actually has. function spool_label(array $s): string { $label = trim($s['brand'] . ' ' . $s['type']); if (!empty($s['color_name'])) { $label .= ' · ' . $s['color_name']; } return $label; } // The HTML is sent no-store, but browsers and LiteSpeed happily hold on to a // stale assets/*.js — which serves new markup against old script. Key each // asset URL to its mtime so an edited file is always fetched fresh. function asset(string $path): string { $full = dirname(__DIR__) . '/' . $path; return e($path . '?v=' . (is_file($full) ? filemtime($full) : 0)); } // Some proxies (and LiteSpeed's page cache) ignore no-store and serve stale // HTML. Redirecting every plain GET to a cb=-stamped URL makes each // visit a unique URL that can never come from a cache. POSTs and requests // already carrying cb pass straight through; existing GET vars are kept. function cache_buster_redirect(): void { if (isset($_GET['cb']) || $_SERVER['REQUEST_METHOD'] === 'POST') { return; } $params = $_GET; $params['cb'] = bin2hex(random_bytes(6)); header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?') . '?' . http_build_query($params), true, 302); exit; } function page_header(string $title): void { $cfg = config(); // Every page is dynamic (inventory changes between visits), so forbid // caching by browsers, proxies, and LiteSpeed's server-side page cache. header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); header('X-LiteSpeed-Cache-Control: no-cache'); ?> <?= e($title) ?> · <?= e($cfg['site_name']) ?>
' . e($msg) . ''; } } // $extra names further scripts to load after app.js (e.g. 'assets/passkey.js'), // so pages that need one are not paid for by every other page. function page_footer(array $extra = []): void { echo '
' . "\n" . '' . "\n"; foreach ($extra as $src) { echo '' . "\n"; } echo "\n\n"; }