Printers move out of Settings into their own table and page: each has an optional nickname (the model is shown when blank), a model from the Bambu lineup with a line drawing that follows the selection, an AMS type suggested from the model, and its address, serial and access code. Access codes are never returned to the browser. The printer set up in Settings is migrated automatically and its settings rows removed. Background sync covers every printer. Grams left are filled in only from an AMS that measures (AMS, AMS 2 Pro, AMS HT), never from an AMS lite. Spools now show their RFID tag ID, product code and filament code, and say which printer they're loaded in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
684 lines
32 KiB
HTML
684 lines
32 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>Library</h3>
|
||
<div id="libraryList" class="projects-list">
|
||
<div class="project-item active" data-scope="all">
|
||
<span>All Models</span>
|
||
<span class="count" id="allFilesCount">0</span>
|
||
</div>
|
||
<div class="project-item" data-scope="duplicates">
|
||
<span>Duplicates</span>
|
||
<span class="count" id="duplicatesCount">0</span>
|
||
</div>
|
||
<div class="project-item" data-scope="ai">
|
||
<span>AI & auto-tags</span>
|
||
<span class="count ai-busy" id="aiBusy" hidden title="AI tagging is running">●</span>
|
||
</div>
|
||
<div class="project-item" data-scope="watch">
|
||
<span>Watched</span>
|
||
<span class="count" id="watchCount">0</span>
|
||
</div>
|
||
<div class="project-item" data-scope="filament">
|
||
<span>Filament</span>
|
||
<span class="count" id="spoolCount">0</span>
|
||
</div>
|
||
<div class="project-item" data-scope="printers">
|
||
<span>Printers</span>
|
||
<span class="count" id="printerCount">0</span>
|
||
</div>
|
||
<div class="project-item" data-scope="trash">
|
||
<span>Trash</span>
|
||
<span class="count" id="trashCount">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section">
|
||
<h3>Collections</h3>
|
||
<div id="projectsList" class="projects-list"></div>
|
||
</div>
|
||
|
||
<div class="sidebar-section">
|
||
<div class="section-head">
|
||
<h3>Tags</h3>
|
||
<div class="tag-tools">
|
||
<button type="button" class="btn-link" id="clearTags" hidden>Clear</button>
|
||
<div class="seg" id="tagMatch" title="When several tags are included, match any or all of them">
|
||
<button type="button" data-match="any">Any</button>
|
||
<button type="button" data-match="all">All</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="tagsList" class="tags-list"></div>
|
||
<p class="form-hint">Click a tag to include it, again to exclude it.</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="content">
|
||
<div class="content-header">
|
||
<h2 id="contentTitle">All Models</h2>
|
||
<div class="content-actions">
|
||
<span id="resultCount" class="result-count"></span>
|
||
<button type="button" id="emptyTrashBtn" class="btn btn-danger btn-sm" hidden>Empty trash</button>
|
||
<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>
|
||
|
||
<div id="filterBar" class="filter-bar">
|
||
<input type="search" id="searchInput" class="search-input"
|
||
placeholder="Search names, tags, notes, files, designers… ( / )" aria-label="Search models">
|
||
<div class="seg search-mode" id="searchMode" title="Match the words you type, or find models by what they are">
|
||
<button type="button" data-mode="words">Words</button>
|
||
<button type="button" data-mode="meaning">Meaning</button>
|
||
</div>
|
||
<select id="filterFormat" aria-label="Filter by format"><option value="">Any format</option></select>
|
||
<select id="filterSource" aria-label="Filter by source"><option value="">Any source</option></select>
|
||
<select id="filterLicense" aria-label="Filter by license">
|
||
<option value="">Any license</option>
|
||
<option value="sellable">Sellable</option>
|
||
<option value="noncommercial">Non-commercial</option>
|
||
<option value="unknown">License unclear</option>
|
||
</select>
|
||
<select id="filterPrintable" aria-label="Filter by what you can print">
|
||
<option value="">Any filament</option>
|
||
<option value="yes">Printable now</option>
|
||
<option value="no">Missing filament</option>
|
||
</select>
|
||
<select id="sortBy" aria-label="Sort">
|
||
<option value="newest">Newest first</option>
|
||
<option value="oldest">Oldest first</option>
|
||
<option value="name">Name A–Z</option>
|
||
<option value="name-desc">Name Z–A</option>
|
||
<option value="size">Largest first</option>
|
||
<option value="modified">Recently edited</option>
|
||
</select>
|
||
<select id="groupBy" aria-label="Group">
|
||
<option value="">No grouping</option>
|
||
<option value="source">Group by source</option>
|
||
<option value="format">Group by format</option>
|
||
</select>
|
||
</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">Slicer</h3>
|
||
<div class="form-group">
|
||
<p class="form-hint">
|
||
“Open in slicer” uses <strong id="settingsSlicerName">no slicer yet</strong>. This choice is
|
||
remembered in this browser only, since each computer has its own slicers installed.
|
||
</p>
|
||
<button type="button" class="btn btn-secondary btn-sm" id="chooseSlicerBtn">Choose slicer…</button>
|
||
</div>
|
||
|
||
<h3 class="settings-group-title" id="aiSettings">AI tagging</h3>
|
||
<div class="ai-grid">
|
||
<div class="form-group">
|
||
<label for="aiProvider">Provider</label>
|
||
<select id="aiProvider" data-setting="ai_provider">
|
||
<option value="deepseek">DeepSeek</option>
|
||
<option value="openrouter">OpenRouter</option>
|
||
<option value="openai">OpenAI</option>
|
||
<option value="custom">Other OpenAI-compatible (Ollama, LM Studio…)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="aiApiKey">API key</label>
|
||
<input type="password" id="aiApiKey" data-setting="ai_api_key" placeholder="Paste your API key" autocomplete="off">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="aiModel">Model</label>
|
||
<input type="text" id="aiModel" data-setting="ai_model" data-clear-blank autocomplete="off" spellcheck="false">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="aiBaseUrl">API address</label>
|
||
<input type="text" id="aiBaseUrl" data-setting="ai_base_url" data-clear-blank autocomplete="off" spellcheck="false">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="aiPriceInput">Input price ($ per million tokens)</label>
|
||
<input type="number" id="aiPriceInput" data-setting="ai_price_input" data-clear-blank min="0" step="0.001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="aiPriceOutput">Output price ($ per million tokens)</label>
|
||
<input type="number" id="aiPriceOutput" data-setting="ai_price_output" data-clear-blank min="0" step="0.001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="aiSendImages">Send thumbnails</label>
|
||
<select id="aiSendImages" data-setting="ai_send_images">
|
||
<option value="true">Yes: better tags, slightly higher cost</option>
|
||
<option value="false">No: names and files only</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<p class="form-hint">
|
||
Key status: <span class="setting-status" data-status="ai_api_key">—</span>.
|
||
<a id="aiKeyLink" target="_blank" rel="noopener" hidden></a>
|
||
The key is stored on your server and only sent to the provider. Leave model, address and prices
|
||
blank to use the provider's defaults (shown greyed out). Prices are only used to show what runs cost.
|
||
</p>
|
||
<div class="ai-test-row">
|
||
<button type="button" class="btn btn-secondary btn-sm" id="aiTestBtn">Save & test connection</button>
|
||
<button type="button" class="btn btn-link clear-setting" data-setting="ai_api_key">Clear key</button>
|
||
</div>
|
||
<div id="aiTestResult" class="form-hint ai-test-result"></div>
|
||
|
||
<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. In a browser signed in to
|
||
makerworld.com, open the developer tools (Application / Storage → Cookies → makerworld.com)
|
||
and copy the value of the <code>token</code> cookie. It expires after a while: paste a new one if
|
||
MakerWorld downloads stop working.
|
||
</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">(models, archives, pictures, PDFs & readmes — multiple allowed)</span></label>
|
||
<input type="file" id="fileInput" accept=".stl,.3mf,.obj,.fbx,.step,.stp,.3dm,.gcode,.gco,.zip,.7z,.rar,.tar,.tgz,.gz,.jpg,.jpeg,.png,.gif,.webp,.bmp,.svg,.pdf,.txt,.md,.mtl" 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="detailPlates" class="plate-chips" hidden></div>
|
||
<div id="detailFileTabs" class="file-tabs"></div>
|
||
<div class="preview-actions">
|
||
<span id="slicerBar" class="slicer-bar" hidden>
|
||
<button type="button" class="btn btn-primary btn-sm" id="openInSlicerBtn">Open in slicer</button>
|
||
<button type="button" class="link-btn slicer-change" id="changeSlicerBtn">change slicer</button>
|
||
</span>
|
||
<button type="button" class="btn btn-secondary btn-sm drag-out-btn" id="dragOutBtn" draggable="true" hidden>⠿ Drag out</button>
|
||
<button type="button" class="btn btn-secondary btn-sm" id="regenThumbBtn">Regenerate thumbnail</button>
|
||
</div>
|
||
<div id="slicerNotice" class="slicer-notice" role="status" hidden></div>
|
||
</div>
|
||
<div class="file-detail-info">
|
||
<div id="detailRenamed" class="renamed-note" hidden></div>
|
||
<div class="info-section">
|
||
<h3>Source</h3>
|
||
<div id="detailSource"></div>
|
||
</div>
|
||
<div class="info-section" id="detailPrintableSection">
|
||
<h3>Printable now</h3>
|
||
<div id="detailPrintable"></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>Notes <span id="notesStatus" class="notes-status"></span></h3>
|
||
<textarea id="detailNotes" rows="3" placeholder="Your notes — settings that worked, tweaks, who it's for…"></textarea>
|
||
</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" id="detailFilamentsSection" hidden>
|
||
<h3>Filaments</h3>
|
||
<div id="detailFilaments" class="filament-list"></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,.fbx,.step,.stp,.3dm,.gcode,.gco,.zip,.7z,.rar,.tar,.tgz,.gz,.jpg,.jpeg,.png,.gif,.webp,.bmp,.svg,.pdf,.txt,.md,.mtl">
|
||
<div class="form-actions" id="detailActions">
|
||
<button class="btn btn-danger" id="deleteFileBtn">Move to trash</button>
|
||
<button class="btn btn-secondary" id="addFilesBtn">Add files</button>
|
||
<button class="btn btn-secondary" id="aiTagBtn" title="Suggest tags and a description with AI">✦ Tag with AI</button>
|
||
<button class="btn btn-primary" id="editModelBtn">Edit</button>
|
||
</div>
|
||
<div class="form-actions" id="trashActions" hidden>
|
||
<button class="btn btn-danger" id="purgeModelBtn">Delete forever</button>
|
||
<button class="btn btn-primary" id="restoreModelBtn">Restore</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MakerWorld token prompt (shown when MakerWorld refuses the saved token) -->
|
||
<div id="tokenModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Update your MakerWorld token</h2>
|
||
<button class="close-btn">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="tokenForm">
|
||
<p id="tokenReason" class="token-reason"></p>
|
||
<ol class="token-steps">
|
||
<li>In this browser, sign in to <a href="https://makerworld.com" target="_blank" rel="noopener">makerworld.com</a>.</li>
|
||
<li>Open the developer tools (<kbd>⌥⌘I</kbd> on a Mac, <kbd>F12</kbd> elsewhere) and go to
|
||
Application (Storage in Firefox) → Cookies → https://makerworld.com.</li>
|
||
<li>Copy the value of the <code>token</code> cookie and paste it below.</li>
|
||
</ol>
|
||
<div class="form-group">
|
||
<label for="tokenInput">MakerWorld auth token</label>
|
||
<input type="password" id="tokenInput" autocomplete="off" spellcheck="false" required>
|
||
</div>
|
||
<div id="tokenStatus" class="form-hint token-status" role="status"></div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" id="cancelToken">Not now</button>
|
||
<button type="submit" class="btn btn-primary" id="tokenSubmit">Save & download files</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Slicer choice (per browser) -->
|
||
<div id="slicerModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Choose your slicer</h2>
|
||
<button class="close-btn">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="slicerForm">
|
||
<p class="form-hint slicer-intro">Your browser can't tell which apps are installed, so pick the slicer this computer uses.</p>
|
||
<div id="slicerOptions" class="slicer-options"></div>
|
||
<div class="form-group" id="slicerTemplateGroup" hidden>
|
||
<label for="slicerTemplate">Open-link template</label>
|
||
<input type="text" id="slicerTemplate" placeholder="myslicer://open?file={url}" autocomplete="off" spellcheck="false">
|
||
<p class="form-hint">
|
||
<code>{url}</code> becomes the file's download link, URL-encoded (<code>{rawurl}</code> leaves it as is).
|
||
The link works for 10 minutes.
|
||
</p>
|
||
</div>
|
||
<div id="slicerFormError" class="slicer-error"></div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" id="cancelSlicer">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Save</button>
|
||
</div>
|
||
</form>
|
||
</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="printer-art.js"></script>
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|