All checks were successful
build-and-push / docker (push) Successful in 29s
Brand hexagon in amber on the dark graphite, linked as the icon + apple-touch-icon, plus a theme-color meta. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
462 lines
20 KiB
HTML
462 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Printhive</title>
|
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
<link rel="apple-touch-icon" href="/favicon.svg">
|
|
<meta name="theme-color" content="#15171c">
|
|
<link rel="stylesheet" href="styles.css">
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<!-- Auth screen (shown when signed out / first run / password reset) -->
|
|
<div id="authScreen" class="auth-screen" style="display:none">
|
|
<div class="auth-card">
|
|
<h1 class="brand auth-brand">
|
|
<svg class="logo" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
<path d="M12 2.5 20.5 7v10L12 21.5 3.5 17V7L12 2.5Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
|
<path d="M12 7.5 16.3 10v4L12 16.5 7.7 14v-4L12 7.5Z" fill="currentColor" opacity="0.85"/>
|
|
</svg>
|
|
<span>Print<span class="accent">hive</span></span>
|
|
</h1>
|
|
|
|
<form id="auth-login" class="auth-form">
|
|
<h2>Sign in</h2>
|
|
<input type="email" id="loginEmail" placeholder="Email" autocomplete="username" required>
|
|
<input type="password" id="loginPassword" placeholder="Password" autocomplete="current-password" required>
|
|
<button type="submit" class="btn btn-primary">Sign in</button>
|
|
<button type="button" class="btn btn-link auth-nav" data-auth="forgot">Forgot password?</button>
|
|
</form>
|
|
|
|
<form id="auth-register" class="auth-form">
|
|
<h2>Create your account</h2>
|
|
<p class="form-hint">First-time setup — this becomes your login.</p>
|
|
<input type="email" id="regEmail" placeholder="Email" autocomplete="username" required>
|
|
<input type="password" id="regPassword" placeholder="Password (min 8 characters)" autocomplete="new-password" required>
|
|
<button type="submit" class="btn btn-primary">Create account</button>
|
|
</form>
|
|
|
|
<form id="auth-forgot" class="auth-form">
|
|
<h2>Reset password</h2>
|
|
<p class="form-hint">Enter your email and we'll send a reset link (if email is configured).</p>
|
|
<input type="email" id="forgotEmail" placeholder="Email" required>
|
|
<button type="submit" class="btn btn-primary">Send reset link</button>
|
|
<button type="button" class="btn btn-link auth-nav" data-auth="login">Back to sign in</button>
|
|
</form>
|
|
|
|
<form id="auth-reset" class="auth-form">
|
|
<h2>Set a new password</h2>
|
|
<input type="password" id="resetPassword" placeholder="New password (min 8 characters)" autocomplete="new-password" required>
|
|
<button type="submit" class="btn btn-primary">Set new password</button>
|
|
</form>
|
|
|
|
<div id="authError" class="auth-error"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container" style="display:none">
|
|
<header>
|
|
<h1 class="brand">
|
|
<svg class="logo" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
<path d="M12 2.5 20.5 7v10L12 21.5 3.5 17V7L12 2.5Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
|
<path d="M12 7.5 16.3 10v4L12 16.5 7.7 14v-4L12 7.5Z" fill="currentColor" opacity="0.85"/>
|
|
</svg>
|
|
<span>Print<span class="accent">hive</span></span>
|
|
</h1>
|
|
<div class="header-actions">
|
|
<button id="logoutBtn" class="btn btn-secondary">Sign out</button>
|
|
<button id="settingsBtn" class="btn btn-secondary">Settings</button>
|
|
<button id="newProjectBtn" class="btn btn-secondary">New Collection</button>
|
|
<button id="importBtn" class="btn btn-secondary">Import from URL</button>
|
|
<button id="uploadBtn" class="btn btn-primary">Upload Files</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="main-content">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-section">
|
|
<h3>Collections</h3>
|
|
<div id="projectsList" class="projects-list">
|
|
<div class="project-item active" data-project-id="all">
|
|
<span>All Models</span>
|
|
<span class="count" id="allFilesCount">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<h3>Tags</h3>
|
|
<div id="tagsList" class="tags-list"></div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<h3>Search</h3>
|
|
<input type="text" id="searchInput" placeholder="Search models..." class="search-input">
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="content">
|
|
<div class="content-header">
|
|
<h2 id="contentTitle">All Models</h2>
|
|
<div class="view-options">
|
|
<button class="view-btn active" data-view="grid">Grid</button>
|
|
<button class="view-btn" data-view="list">List</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fileGrid" class="file-grid"></div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- First-run Setup Modal -->
|
|
<div id="setupModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Welcome to Printhive</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="form-hint" style="margin-bottom:18px">
|
|
Optionally connect import sources now — you can change these any time in Settings.
|
|
All are optional; you can also just upload files directly.
|
|
</p>
|
|
<form id="setupForm">
|
|
<div class="form-group">
|
|
<label for="setupThingiverse">Thingiverse API token</label>
|
|
<input type="password" id="setupThingiverse" data-setting="thingiverse_token" autocomplete="off"
|
|
placeholder="App Token from thingiverse.com/developers">
|
|
<p class="form-hint">Enables automatic Thingiverse imports.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="setupFlaresolverr">FlareSolverr URL</label>
|
|
<input type="text" id="setupFlaresolverr" data-setting="flaresolverr_url" autocomplete="off"
|
|
placeholder="http://192.168.1.124:8191">
|
|
<p class="form-hint">Lets imports read Cloudflare-protected sites like MakerWorld.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="setupMakerworld">MakerWorld auth token</label>
|
|
<input type="password" id="setupMakerworld" data-setting="makerworld_token" autocomplete="off"
|
|
placeholder="Bearer token from your logged-in session">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="setupPrintables">Printables auth token</label>
|
|
<input type="password" id="setupPrintables" data-setting="printables_token" autocomplete="off"
|
|
placeholder="Optional — for account-gated files">
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="skipSetup">Skip for now</button>
|
|
<button type="submit" class="btn btn-primary">Save & finish</button>
|
|
</div>
|
|
</form>
|
|
<div id="setupStatus" class="import-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Modal -->
|
|
<div id="settingsModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Settings</h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="settingsForm">
|
|
<h3 class="settings-group-title">Import sources</h3>
|
|
|
|
<div class="form-group">
|
|
<label for="thingiverseToken">Thingiverse API token</label>
|
|
<input type="password" id="thingiverseToken" data-setting="thingiverse_token"
|
|
placeholder="Paste your App Token" autocomplete="off">
|
|
<p class="form-hint">
|
|
Status: <span class="setting-status" data-status="thingiverse_token">—</span>.
|
|
Get a free token at
|
|
<a href="https://www.thingiverse.com/developers" target="_blank" rel="noopener">thingiverse.com/developers</a>
|
|
(create an app → copy the App Token). Leave blank to keep the current value; clear it with the button.
|
|
</p>
|
|
<button type="button" class="btn btn-link clear-setting" data-setting="thingiverse_token">Clear token</button>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="flaresolverrUrl">FlareSolverr URL</label>
|
|
<input type="text" id="flaresolverrUrl" data-setting="flaresolverr_url"
|
|
placeholder="http://192.168.1.124:8191" autocomplete="off">
|
|
<p class="form-hint">
|
|
Status: <span class="setting-status" data-status="flaresolverr_url">—</span>.
|
|
Lets imports read Cloudflare-protected sites like MakerWorld by solving the challenge
|
|
with a headless browser. Point this at your FlareSolverr instance.
|
|
</p>
|
|
<button type="button" class="btn btn-link clear-setting" data-setting="flaresolverr_url">Clear</button>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="makerworldToken">MakerWorld auth token</label>
|
|
<input type="password" id="makerworldToken" data-setting="makerworld_token"
|
|
placeholder="Bearer token from your logged-in session" autocomplete="off">
|
|
<p class="form-hint">
|
|
Status: <span class="setting-status" data-status="makerworld_token">—</span>.
|
|
Lets Printhive download files from MakerWorld using your account. Paste the bearer
|
|
token from your logged-in browser session (see instructions in chat).
|
|
</p>
|
|
<button type="button" class="btn btn-link clear-setting" data-setting="makerworld_token">Clear token</button>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="printablesToken">Printables auth token</label>
|
|
<input type="password" id="printablesToken" data-setting="printables_token"
|
|
placeholder="Bearer token from your logged-in session" autocomplete="off">
|
|
<p class="form-hint">
|
|
Status: <span class="setting-status" data-status="printables_token">—</span>.
|
|
Lets Printhive download files from Printables using your account. Paste the bearer
|
|
token from your logged-in browser session (see instructions in chat).
|
|
</p>
|
|
<button type="button" class="btn btn-link clear-setting" data-setting="printables_token">Clear token</button>
|
|
</div>
|
|
|
|
<h3 class="settings-group-title">Email (SMTP) — for password resets</h3>
|
|
<div class="print-settings">
|
|
<input type="text" data-setting="smtp_host" placeholder="SMTP host" autocomplete="off">
|
|
<input type="text" data-setting="smtp_port" placeholder="Port (e.g. 587)" autocomplete="off">
|
|
<input type="text" data-setting="smtp_secure" placeholder="SSL: true/false" autocomplete="off">
|
|
<input type="text" data-setting="smtp_from" placeholder="From address" autocomplete="off">
|
|
<input type="text" data-setting="smtp_user" placeholder="Username" autocomplete="off">
|
|
<input type="password" data-setting="smtp_pass" placeholder="Password" autocomplete="off">
|
|
</div>
|
|
<p class="form-hint">Status: <span class="setting-status" data-status="smtp_host">—</span>. Needed for "forgot password" emails.</p>
|
|
|
|
<h3 class="settings-group-title">License types — which are sellable</h3>
|
|
<div id="licenseRules" class="license-rules"></div>
|
|
<p class="form-hint">New licenses auto-register as you import. Changing one updates every model with that license.</p>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancelSettings">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</form>
|
|
<div id="settingsStatus" class="import-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import from URL Modal -->
|
|
<div id="importModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Import from URL</h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="importForm">
|
|
<div class="form-group">
|
|
<label for="importUrl">Model URL</label>
|
|
<input type="url" id="importUrl" placeholder="https://www.thingiverse.com/thing:12345" required>
|
|
<p class="form-hint" id="importHint"></p>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancelImport">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" id="importSubmit">Import</button>
|
|
</div>
|
|
</form>
|
|
<div id="importStatus" class="import-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div id="uploadModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Upload Model Files</h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="uploadForm">
|
|
<div class="form-group">
|
|
<label for="fileInput">Files * <span class="form-hint-inline">(stl, 3mf, obj, step, gcode, zip — multiple allowed)</span></label>
|
|
<input type="file" id="fileInput" accept=".stl,.3mf,.obj,.step,.stp,.gcode,.gco,.zip" multiple required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="fileName">Name *</label>
|
|
<input type="text" id="fileName" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="fileDescription">Description</label>
|
|
<textarea id="fileDescription" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="fileTags">Tags (comma-separated)</label>
|
|
<input type="text" id="fileTags" placeholder="miniature, terrain, hero">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="fileProjects">Collections</label>
|
|
<select id="fileProjects" multiple size="4"></select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Print Settings</label>
|
|
<div class="print-settings">
|
|
<input type="text" id="printMaterial" placeholder="Material (e.g., PLA)">
|
|
<input type="text" id="printLayerHeight" placeholder="Layer Height (e.g., 0.2mm)">
|
|
<input type="text" id="printInfill" placeholder="Infill (e.g., 20%)">
|
|
<input type="text" id="printSupports" placeholder="Supports (e.g., Yes/No)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancelUpload">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Upload</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collection Modal -->
|
|
<div id="projectModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>New Collection</h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="projectForm">
|
|
<div class="form-group">
|
|
<label for="projectName">Collection Name *</label>
|
|
<input type="text" id="projectName" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="projectDescription">Description</label>
|
|
<textarea id="projectDescription" rows="3"></textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancelProject">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create Collection</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model Detail Modal -->
|
|
<div id="fileDetailModal" class="modal">
|
|
<div class="modal-content large">
|
|
<div class="modal-header">
|
|
<h2 id="detailFileName"></h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="file-detail-content">
|
|
<div class="file-detail-preview">
|
|
<div id="detailViewer" class="model-viewer"></div>
|
|
<div id="detailFileTabs" class="file-tabs"></div>
|
|
<button type="button" class="btn btn-secondary btn-sm" id="regenThumbBtn">Regenerate thumbnail</button>
|
|
</div>
|
|
<div class="file-detail-info">
|
|
<div class="info-section">
|
|
<h3>Source</h3>
|
|
<div id="detailSource"></div>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>License</h3>
|
|
<div id="detailLicense"></div>
|
|
<button type="button" class="btn btn-secondary btn-sm" id="refreshLicenseBtn" style="margin-top:10px">Refresh from source</button>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>Description</h3>
|
|
<p id="detailDescription"></p>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>Tags</h3>
|
|
<div id="detailTags" class="tags"></div>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>Collections</h3>
|
|
<div id="detailProjects"></div>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>Print Settings</h3>
|
|
<div id="detailPrintSettings"></div>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>Files</h3>
|
|
<div id="detailFileInfo"></div>
|
|
<button type="button" class="btn btn-secondary btn-sm" id="downloadFilesBtn" style="margin-top:10px; display:none"></button>
|
|
</div>
|
|
<input type="file" id="addFilesInput" multiple style="display:none"
|
|
accept=".stl,.3mf,.obj,.step,.stp,.gcode,.gco,.zip">
|
|
<div class="form-actions">
|
|
<button class="btn btn-danger" id="deleteFileBtn">Delete</button>
|
|
<button class="btn btn-secondary" id="addFilesBtn">Add files</button>
|
|
<button class="btn btn-primary" id="editModelBtn">Edit</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Model Modal -->
|
|
<div id="editModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Edit model</h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editForm">
|
|
<div class="form-group">
|
|
<label for="editName">Name *</label>
|
|
<input type="text" id="editName" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editDescription">Description</label>
|
|
<textarea id="editDescription" rows="3"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editTags">Tags (comma-separated)</label>
|
|
<input type="text" id="editTags" placeholder="miniature, terrain, hero">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editProjects">Collections</label>
|
|
<select id="editProjects" multiple size="4"></select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editLicense">License</label>
|
|
<input type="text" id="editLicense">
|
|
<p class="form-hint">Sellable/non-commercial is set per license in Settings → License types.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Print Settings</label>
|
|
<div class="print-settings">
|
|
<input type="text" id="editMaterial" placeholder="Material (e.g., PLA)">
|
|
<input type="text" id="editLayerHeight" placeholder="Layer Height (e.g., 0.2mm)">
|
|
<input type="text" id="editInfill" placeholder="Infill (e.g., 20%)">
|
|
<input type="text" id="editSupports" placeholder="Supports (e.g., Yes/No)">
|
|
</div>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="cancelEdit">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module" src="viewer.js"></script>
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|