- Settings → Slicer: pick Bambu Studio, OrcaSlicer, PrusaSlicer, Cura, a custom open-link template, or plain download (remembered per browser) - Bambu Studio / OrcaSlicer launch via their URL schemes with a 10-minute signed download link (/dl/<token>/<name>) that needs no session cookie - PrusaSlicer and Cura fall back to a download (they can't open our links) - 3MF projects report which app saved them; opening one in a different slicer asks first - "slicer" action on every file row and archive entry - Drag model cards (file, or zip of all files) and file/entry rows out to the desktop via Chromium's DownloadURL Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
568 lines
26 KiB
HTML
568 lines
26 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="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">
|
||
<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="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">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">(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" id="regenThumbBtn">Regenerate thumbnail</button>
|
||
</div>
|
||
<div id="slicerNotice" class="slicer-notice" role="status" hidden></div>
|
||
</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>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-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>
|
||
|
||
<!-- 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="app.js"></script>
|
||
</body>
|
||
</html>
|