printhive/public/index.html
dlawler489 a37db15a3f Initial commit: Printhive 3D-print file library
Self-hosted Express + Postgres app for organizing STL/3MF/OBJ/etc. with:
- Model/file library, tags, collections, print settings
- URL import: Thingiverse (API), Printables (GraphQL), MakerWorld
  (FlareSolverr + token), cubee3d/other (metadata + manual files)
- In-browser 3D viewer; 3MF rendered via Python/trimesh GLB conversion
- Auto thumbnails, license/commercial-use tracking, in-app editing
- Settings (API tokens / FlareSolverr) stored in DB with .env fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:15:17 +10:00

361 lines
15 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="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>
<div class="container">
<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="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>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Settings</h2>
<button class="close-btn">&times;</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>
<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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">
</div>
<div class="form-group">
<label for="editCommercial">Commercial use (sellable?)</label>
<select id="editCommercial">
<option value="">Unknown</option>
<option value="true">Allowed — sellable</option>
<option value="false">Non-commercial only</option>
</select>
</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>