All checks were successful
build-and-push / docker (push) Successful in 11s
On a fresh install (no integration settings yet), prompt once for the Thingiverse/MakerWorld/Printables tokens and FlareSolverr URL, with a "skip for now" option. /api/config exposes setupComplete (true if any setting is configured or setup was dismissed); POST /api/setup/complete records dismissal. DB password stays a deploy-time env var by necessity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
405 lines
17 KiB
HTML
405 lines
17 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>
|
|
|
|
<!-- 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>
|
|
|
|
<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">
|
|
</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>
|