Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MLBB Collage Maker & Skin Manager</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css"> | |
| <link rel="stylesheet" href="style.css"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet"> | |
| </head> | |
| <body data-theme="light"> | |
| <div class="app-container"> | |
| <aside id="control-panel" class="glass-panel"> | |
| <header class="panel-header"> | |
| <div class="logo-icon"> | |
| <svg width="48" height="48" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect width="32" height="32" rx="8" fill="url(#gradient)"/> | |
| <path d="M16 8L24 24H8L16 8Z" fill="white" fill-opacity="0.9"/> | |
| <defs> | |
| <linearGradient id="gradient" x1="0" y1="0" x2="32" y2="32"> | |
| <stop stop-color="#0f766e"/> | |
| <stop offset="1" stop-color="#0ea5e9"/> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| </div> | |
| <div class="header-text"> | |
| <h1>MLBB Collage Maker</h1> | |
| <p class="subtitle">Skin Collection Manager</p> | |
| </div> | |
| <button type="button" id="btnToggleSidebar" class="btn panel-toggle-btn" title="Toggle sidebar"> | |
| <span class="btn-icon">☰</span> | |
| Panel | |
| </button> | |
| </header> | |
| <div class="tab-switcher"> | |
| <button type="button" id="btnTabCollage" class="btn btn-primary tab-btn active">Collage</button> | |
| <button type="button" id="btnTabOcr" class="btn tab-btn">OCR Stack</button> | |
| <button type="button" id="btnTabProfile" class="btn tab-btn">Profile</button> | |
| </div> | |
| <nav id="tabCollage" class="control-sections tab-panel active"> | |
| <section class="control-section file-ops-section"> | |
| <h3>File Operations</h3> | |
| <div class="button-grid"> | |
| <button type="button" id="btnLoadImages" class="btn btn-primary"> | |
| <span class="btn-icon">📷</span> | |
| Load Images | |
| </button> | |
| <button type="button" id="btnLoadZip" class="btn"> | |
| <span class="btn-icon">📦</span> | |
| Load ZIP | |
| </button> | |
| </div> | |
| <div class="button-grid one-col"> | |
| <button type="button" id="btnLoadProjectImage" class="btn"> | |
| <span class="btn-icon">🔗</span> | |
| Load Collage Project | |
| </button> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnSaveTemplate" class="btn"> | |
| <span class="btn-icon">💾</span> | |
| Save Template | |
| </button> | |
| <button type="button" id="btnLoadTemplate" class="btn"> | |
| <span class="btn-icon">📂</span> | |
| Load Template | |
| </button> | |
| </div> | |
| <div class="button-grid one-col"> | |
| <button type="button" id="btnDownload" class="btn btn-primary"> | |
| <span class="btn-icon">⬇</span> | |
| Download | |
| </button> | |
| </div> | |
| <div class="button-grid one-col"> | |
| <button type="button" id="btnStitchProfile" class="btn"> | |
| <span class="btn-icon">🧵</span> | |
| Stitch Profile + Collage | |
| </button> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnSetStartup" class="btn"> | |
| <span class="btn-icon">⚙</span> | |
| Set Startup | |
| </button> | |
| <button type="button" id="btnClearStartup" class="btn"> | |
| <span class="btn-icon">✖</span> | |
| Clear Startup | |
| </button> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnFetchEndpoint" class="btn btn-primary"> | |
| <span class="btn-icon">📶</span> | |
| Fetch from Endpoint | |
| </button> | |
| <button type="button" id="btnResetFromCache" class="btn"> | |
| <span class="btn-icon">⟳</span> | |
| Reset from Saved ZIP | |
| </button> | |
| </div> | |
| </section> | |
| <section class="control-section"> | |
| <h3>Grid & Layout</h3> | |
| <div class="input-group"> | |
| <label for="gridCols">Columns</label> | |
| <input type="number" id="gridCols" min="1" max="200" value="5"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="gridRows">Rows</label> | |
| <input type="number" id="gridRows" min="1" max="5000" value="1"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="skinW">Skin Width (px)</label> | |
| <input type="number" id="skinW" min="20" max="1000" value="335"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="skinH">Skin Height (px)</label> | |
| <input type="number" id="skinH" min="20" max="1000" value="300"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="gapSize">Spacing: <span id="gapValue">0px</span></label> | |
| <input type="range" id="gapSize" min="0" max="40" step="1" value="0"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="bgColor">Background Color</label> | |
| <input type="color" id="bgColor" value="#ffffff"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="zoomLevel">Zoom: <span id="zoomValue">50%</span></label> | |
| <input type="range" id="zoomLevel" min="0.1" max="2" step="0.05" value="0.5"> | |
| </div> | |
| </section> | |
| <section class="control-section"> | |
| <h3>Display</h3> | |
| <div class="input-group"> | |
| <label for="groupFilter">Group Filter</label> | |
| <select id="groupFilter"> | |
| <option value="all">All Groups</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label for="groupOrderList">Group Sequence</label> | |
| <div id="groupOrderList" class="group-order-list"></div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="groupPosSlider">Move Group Position</label> | |
| <input type="range" id="groupPosSlider" min="1" max="1" value="1"> | |
| <div id="groupPosLabel" class="hint-text">Position: -</div> | |
| </div> | |
| <div class="button-grid two-col"> | |
| <button type="button" id="btnGroupUp" class="btn">Move Up</button> | |
| <button type="button" id="btnGroupDown" class="btn">Move Down</button> | |
| </div> | |
| <div class="input-group"> | |
| <label for="themeSelect">Theme</label> | |
| <select id="themeSelect"> | |
| <option value="light">Light</option> | |
| <option value="night">Night</option> | |
| <option value="ocean">Ocean</option> | |
| <option value="sunset">Sunset</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label>UI Toggles</label> | |
| <div class="ui-switches"> | |
| <label class="ui-switch"> | |
| <input type="checkbox" id="toggleCompactUi"> | |
| <span>Compact Layout</span> | |
| </label> | |
| <label class="ui-switch"> | |
| <input type="checkbox" id="toggleStrongContrast"> | |
| <span>High Contrast</span> | |
| </label> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="control-section"> | |
| <h3>Editing</h3> | |
| <div class="button-grid one-col"> | |
| <button type="button" id="btnInsertImage" class="btn btn-primary"> | |
| <span class="btn-icon">➕</span> | |
| Insert Image (Cropper) | |
| </button> | |
| </div> | |
| <div class="input-group"> | |
| <label for="multiOrder">Make Auto Collage</label> | |
| <select id="multiOrder"> | |
| <option value="asc">Paste Ascending</option> | |
| <option value="desc">Paste Descending</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label for="autoOrderInput">Name List (one per line)</label> | |
| <textarea id="autoOrderInput" rows="6" placeholder="Paste names from JSON, one per line"></textarea> | |
| </div> | |
| <div class="input-group"> | |
| <label for="insertNameInput">Insert Name at Position</label> | |
| <div class="insert-position-grid"> | |
| <input type="text" id="insertNameInput" placeholder="New image name"> | |
| <input type="number" id="insertPositionInput" min="1" value="1" placeholder="Position"> | |
| </div> | |
| <div class="insert-position-grid"> | |
| <input type="text" id="insertGroupInput" placeholder="Group (optional)"> | |
| <button type="button" id="btnInsertNameAtPos" class="btn">Insert & Shift</button> | |
| </div> | |
| </div> | |
| <div class="button-grid three-col"> | |
| <button type="button" id="btnApplyAutoOrder" class="btn btn-primary">Apply Auto Collage</button> | |
| <button type="button" id="btnImportOcr" class="btn">OCR Import</button> | |
| <button type="button" id="btnClearAutoOrder" class="btn">Clear List</button> | |
| </div> | |
| <div class="button-grid three-col"> | |
| <button type="button" id="btnMulti" class="btn"> | |
| <span class="btn-icon">☑</span> | |
| Multi | |
| </button> | |
| <button type="button" id="btnSwap" class="btn" disabled> | |
| <span class="btn-icon">⇆</span> | |
| Swap | |
| </button> | |
| <button type="button" id="btnDelete" class="btn btn-danger" disabled> | |
| <span class="btn-icon">🗑</span> | |
| Delete | |
| </button> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnFillEmpty" class="btn" disabled> | |
| <span class="btn-icon">⤋</span> | |
| Fill Empty Slots | |
| </button> | |
| <button type="button" id="btnOpenDrawer" class="btn"> | |
| <span class="btn-icon">🔍</span> | |
| Search & Bin | |
| </button> | |
| </div> | |
| <div class="input-group"> | |
| <label for="autoOrderMissing">Missing (not matched)</label> | |
| <textarea id="autoOrderMissing" rows="4" readonly placeholder="Missing names will appear here"></textarea> | |
| <div id="autoSuggestionList" class="auto-suggestion-list"></div> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnUndo" class="btn" disabled> | |
| <span class="btn-icon">↶</span> | |
| Undo | |
| </button> | |
| <button type="button" id="btnRedo" class="btn" disabled> | |
| <span class="btn-icon">↷</span> | |
| Redo | |
| </button> | |
| </div> | |
| </section> | |
| <section class="control-section danger-zone"> | |
| <h3>Danger Zone</h3> | |
| <button type="button" id="btnClearAll" class="btn btn-danger full-width"> | |
| <span class="btn-icon">⚠</span> | |
| Clear All Images | |
| </button> | |
| </section> | |
| </nav> | |
| <section id="tabOcr" class="control-sections tab-panel"> | |
| <div class="control-section"> | |
| <h3>OCR Stack Tool</h3> | |
| <div class="input-group"> | |
| <label for="ocrBatchLoader">Upload Images</label> | |
| <button type="button" id="btnOcrLoadImages" class="btn btn-primary full-width">Choose Images</button> | |
| <input type="file" id="ocrBatchLoader" style="display:none" multiple accept="image/*"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="ocrBatchSize">Batch Size</label> | |
| <input type="number" id="ocrBatchSize" min="1" max="100" value="5"> | |
| </div> | |
| <div class="hint-text">Crop is fixed: cut top 25% and bottom 30% (keeps 25%-70%), then stacked vertically.</div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnProcessOcrStack" class="btn btn-primary">Process</button> | |
| <button type="button" id="btnDownloadOcrZip" class="btn" disabled>Download ZIP</button> | |
| </div> | |
| <div id="ocrStatus" class="hint-text">No OCR stack output yet.</div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Gemini AI</h3> | |
| <div class="input-group"> | |
| <label for="geminiApiKey">Gemini API Key</label> | |
| <input type="password" id="geminiApiKey" placeholder="AIza..." autocomplete="off"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="geminiModel">Model</label> | |
| <input type="text" id="geminiModel" value="gemini-3-flash-preview" placeholder="gemini-3-flash-preview" list="geminiModelList"> | |
| <datalist id="geminiModelList"></datalist> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnFetchGeminiModels" class="btn">Fetch Models</button> | |
| </div> | |
| <div id="geminiModelInfo" class="hint-text">Model list not loaded yet.</div> | |
| <div class="input-group"> | |
| <label for="geminiModelLimits">Free-tier Model Limits</label> | |
| <textarea id="geminiModelLimits" rows="6" readonly placeholder="Fetch models to view limits"></textarea> | |
| </div> | |
| <div class="input-group"> | |
| <label for="geminiPrompt">Prompt</label> | |
| <textarea id="geminiPrompt" rows="4" placeholder="Explain and process this ZIP. Return structured output."></textarea> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnChooseGeminiZip" class="btn">Choose ZIP</button> | |
| <button type="button" id="btnSendGemini" class="btn btn-primary">Send to Gemini</button> | |
| </div> | |
| <div class="input-group"> | |
| <label class="ui-switch"> | |
| <input type="checkbox" id="geminiAutoApplyCollage" checked> | |
| <span>Auto apply Gemini result to collage</span> | |
| </label> | |
| </div> | |
| <input type="file" id="geminiZipLoader" style="display:none" accept=".zip,application/zip,image/png,image/jpeg,.png,.jpg,.jpeg"> | |
| <div id="geminiZipInfo" class="hint-text">Uses processed OCR image (all-at-once) by default, or choose your own file.</div> | |
| <div class="input-group"> | |
| <label for="geminiResult">Gemini Response</label> | |
| <textarea id="geminiResult" rows="8" readonly placeholder="Response will appear here"></textarea> | |
| </div> | |
| <div class="button-grid"> | |
| <button type="button" id="btnDownloadGeminiResult" class="btn" disabled>Download Response</button> | |
| <button type="button" id="btnToggleMarkdownPreview" class="btn">Markdown Preview</button> | |
| </div> | |
| <div id="geminiMarkdownPreview" class="markdown-preview hidden"></div> | |
| </div> | |
| </section> | |
| <section id="tabProfile" class="control-sections tab-panel"> | |
| <div class="control-section"> | |
| <h3>Profile Studio</h3> | |
| <div class="hint-text">Loads the profile tool from `../profile/try.html` inside this tab.</div> | |
| <div class="button-grid one-col"> | |
| <button type="button" id="btnOpenProfileStandalone" class="btn">Open Profile Standalone</button> | |
| </div> | |
| </div> | |
| </section> | |
| <input type="file" id="imgLoader" style="display:none" multiple accept="image/*"> | |
| <input type="file" id="zipLoader" style="display:none" accept=".zip"> | |
| <input type="file" id="insertImageLoader" style="display:none" accept="image/*"> | |
| <input type="file" id="projectImageLoader" style="display:none" accept="image/*"> | |
| <input type="file" id="templateLoader" style="display:none" accept=".json"> | |
| <input type="file" id="startupLoader" style="display:none" accept=".json"> | |
| <input type="file" id="ocrLoader" style="display:none" accept="image/*"> | |
| </aside> | |
| <main id="viewport"> | |
| <div id="activeSkinsBadge" class="active-skins-badge">Active Skins: 0</div> | |
| <div id="collage-area"></div> | |
| <div id="empty-state" class="empty-state"> | |
| <div class="empty-icon">🎨</div> | |
| <h2>No Images Loaded</h2> | |
| <p>Load images or a ZIP file to get started</p> | |
| <button type="button" id="btnQuickLoad" class="btn btn-primary">Load Sample Data</button> | |
| </div> | |
| </main> | |
| <section id="ocr-workspace" class="ocr-workspace hidden"> | |
| <div class="ocr-workspace-header"> | |
| <h2>OCR Batch Output</h2> | |
| <p>Cropped (25%-70%), sharpened, and stacked in batch order.</p> | |
| </div> | |
| <div id="ocrResults" class="ocr-results"></div> | |
| </section> | |
| <section id="profile-workspace" class="profile-workspace hidden"> | |
| <iframe id="profileFrame" class="profile-frame" src="../profile/try.html?v=profile4" title="Profile Studio"></iframe> | |
| </section> | |
| </div> | |
| <div id="floating-panel" class="floating-panel glass-panel"> | |
| <div class="selection-info"> | |
| <span id="selection-status"></span> | |
| </div> | |
| <div class="button-group"> | |
| <button type="button" id="btnFloatDelete" class="btn btn-danger"> | |
| <span class="btn-icon">🗑</span> | |
| Delete | |
| </button> | |
| <button type="button" id="btnFloatDeselect" class="btn"> | |
| <span class="btn-icon">✖</span> | |
| Deselect All | |
| </button> | |
| </div> | |
| </div> | |
| <div id="utilityScrim" class="drawer-scrim"></div> | |
| <aside id="utilityDrawer" class="utility-drawer glass-panel"> | |
| <div class="utility-drawer-header"> | |
| <h3>Search & Bin</h3> | |
| <button type="button" id="btnCloseDrawer" class="modal-close">✕</button> | |
| </div> | |
| <section class="utility-section"> | |
| <label for="drawerSearch">Search Skin</label> | |
| <div class="drawer-search-row"> | |
| <input type="text" id="drawerSearch" placeholder="Type skin name..."> | |
| <button type="button" id="btnClearSearch" class="btn">Clear</button> | |
| </div> | |
| <div id="drawerSearchStatus" class="hint-text">Search is hidden here. Matches are highlighted in blue.</div> | |
| </section> | |
| <section class="utility-section"> | |
| <div class="utility-row"> | |
| <h4>Bin (<span id="binCount">0</span>)</h4> | |
| <div class="utility-actions"> | |
| <button type="button" id="btnPlaceBinSelected" class="btn">Place Selected</button> | |
| <button type="button" id="btnRestoreBinAll" class="btn">Restore All</button> | |
| <button type="button" id="btnEmptyBin" class="btn btn-danger">Empty Bin</button> | |
| </div> | |
| </div> | |
| <div id="binList" class="bin-list"></div> | |
| </section> | |
| </aside> | |
| <div id="image-processor-modal" class="modal-overlay"> | |
| <div class="modal-content glass-panel"> | |
| <div class="modal-header"> | |
| <h3 id="cropModalTitle">Crop Images</h3> | |
| <button type="button" id="btnCloseCrop" class="modal-close">✕</button> | |
| </div> | |
| <div class="processor-preview"> | |
| <img id="image-processor-preview" alt="Image preview for cropping"> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="input-row"> | |
| <div class="input-group"> | |
| <label for="cropW">Crop Width</label> | |
| <input type="number" id="cropW" min="1" max="2000"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="cropH">Crop Height</label> | |
| <input type="number" id="cropH" min="1" max="2000"> | |
| </div> | |
| <button type="button" id="btnApplyCrop" class="btn">Set Size</button> | |
| </div> | |
| <p id="cropModalHint" class="modal-hint">Adjust crop, then click "Confirm & Map" to apply to all images</p> | |
| <div id="insertMetaPanel" class="insert-meta-panel hidden"> | |
| <div class="input-group"> | |
| <label for="insertCropName">Image Name</label> | |
| <input type="text" id="insertCropName" placeholder="Skin name"> | |
| </div> | |
| <div class="insert-position-grid"> | |
| <div class="input-group"> | |
| <label for="insertCropPosition">Insert Position</label> | |
| <input type="number" id="insertCropPosition" min="1" value="1"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="insertCropGroupSelect">Group</label> | |
| <select id="insertCropGroupSelect"></select> | |
| </div> | |
| </div> | |
| <div class="input-group hidden" id="insertCropNewGroupWrap"> | |
| <label for="insertCropNewGroup">New Group Name</label> | |
| <input type="text" id="insertCropNewGroup" placeholder="Type new group name"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" id="btnCancelCrop" class="btn">Cancel</button> | |
| <button type="button" id="btnConfirmCrop" class="btn btn-primary">Confirm & Map</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="stitchPreviewModal" class="modal-overlay"> | |
| <div class="modal-content glass-panel"> | |
| <div class="modal-header"> | |
| <h3>Stitched Preview</h3> | |
| <button type="button" id="btnCloseStitchPreview" class="modal-close">✕</button> | |
| </div> | |
| <div class="processor-preview stitch-preview"> | |
| <img id="stitchPreviewImage" alt="Stitched result preview"> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" id="btnCloseStitchPreviewSecondary" class="btn">Close</button> | |
| <button type="button" id="btnDownloadStitchPreview" class="btn btn-primary">Download Stitched Image</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="loading-overlay" class="modal-overlay"> | |
| <div class="loader-container"> | |
| <div class="spinner"></div> | |
| <div class="loading-text">PROCESSING...</div> | |
| <div id="loading-progress-label" class="loading-progress-label">Starting...</div> | |
| <div class="loading-progress-track"> | |
| <div id="loading-progress-bar" class="loading-progress-bar"></div> | |
| </div> | |
| <div id="loading-progress-detail" class="loading-progress-detail"></div> | |
| </div> | |
| </div> | |
| <div id="sidebarScrim" class="sidebar-scrim"></div> | |
| <button type="button" id="btnToggleSidebarFab" class="btn btn-primary floating-action"> | |
| <span class="btn-icon">☰</span> | |
| Controls | |
| </button> | |
| <div id="mobileQuickBar" class="mobile-quickbar"> | |
| <button type="button" id="btnMobileControls" class="btn">☰</button> | |
| <button type="button" id="btnMobileZoomOut" class="btn">-</button> | |
| <button type="button" id="btnMobileZoomIn" class="btn">+</button> | |
| <button type="button" id="btnMobileFit" class="btn">⛶</button> | |
| <button type="button" id="btnMobileBin" class="btn">🔍</button> | |
| </div> | |
| <script id="embedded-collage-data" type="application/json"> | |
| { | |
| "skinWidth": 335, | |
| "skinHeight": 300, | |
| "cols": 5, | |
| "files": { | |
| "0": { "name": "- Layla_- Blue Specter.jpg", "group": "Premium" }, | |
| "1": { "name": "- Roger_- Fiend Haunter.jpg", "group": "Premium" }, | |
| "2": { "name": "- Rafaela_- Biomedic.jpg", "group": "Premium" }, | |
| "3": { "name": "- Nana_- Wind Fairy.jpg", "group": "Premium" }, | |
| "4": { "name": "- Paquito_- Manny Pacquiao.jpg", "group": "Premium" } | |
| } | |
| } | |
| </script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script> | |
| <script src="script.js"></script> | |
| </body> | |
| </html> | |