* fix(gallery): clear only Prompt Manager cache entries (#222) The legacy gallery's Clear cache deleted every localStorage key starting pm_ or promptManager_ (the workspace theme and the dismissed update notice among them) and every CacheStorage cache whose name held gallery or thumbnail, which on ComfyUI's shared origin can belong to another tool. web/js/cache-ownership.js names what a clear may remove from an inventory of the real writers: every key Prompt Manager writes is a preference and no page opens CacheStorage or IndexedDB, so nothing is removed and the toast says so. A test fails when a new storage writer is not classified. The server thumbnail clear stays its own request; a refused or unreachable server changes nothing and is reported, and refused browser storage is reported as such. * feat(system): run reviewed maintenance natively System › Maintenance is native: the concept's notice and its Database and Images & cache surfaces replace the hand-off. - Integrity & relationship checks runs the health check; the report's Review missing links reviews a fresh Missing-file links plan. - Review plan shows the server's read-only plan (D100): scope, protected, backup policy and media lines, the listed records, how long the review holds and the backup box; Apply reviewed operation sends only its token and opens the job in Activity. busy names the running job, plan-used links the job that applied it, an expired or replaced review is reviewed again, and a lost answer says Outcome not confirmed and points to Activity. - Find duplicates starts the search; its view offers Review groups (D101): ten groups at a time, Keep radios per copy, each keeper sent as {group_id, root_index, path}; the dialog says History keeps the removed paths in its run records. - Thumbnail cache opens the thumbnail dialog; Metadata cache is disabled with its reason while nothing is owned (D102, Q5). - Activity descriptors for maintenance, duplicates and duplicate_cleanup. - The legacy duplicates dialog's View Full names its source (?root=). * test(system): compare Maintenance with the concept system-maintenance-concept.spec compares System › Maintenance, the plan dialog of each operation and the duplicates dialog with the concept in both themes and both viewports, mapped only by D100–D102, and proves a mutated row, label, danger mark or token is noticed. The duplicates dialog's Review duplicate cleanup is the concept's plain button. * test(system): Maintenance no longer hands off; raise coverage floors The System shell test expected Maintenance's D94 hand-off; the section is native now. coverage:update sets the new modules' floors from the measured run (cache-ownership 94, maintenance-review 94, system-maintenance 92 lines) and raises the floors other files passed. * test(admin): close the preview worker after the last job in teardown A scan publishes its last event, which ends the stream the test reads, before it hands its missing previews to the automatic worker. That hand-off can run after tearDownAsync closed the worker, so it starts a new one whose .part files were still open when the final drain checked for open handles (py3.10 on ubuntu, test_scan_reads_metadata_on_several_threads). The drain now closes the worker for good once every job has ended. * fix(system): address the S7 final review - I1: an apply answer is a refusal only when our server sent it (a known code, or a 4xx whose body says success:false); a proxy's 5xx, an unreadable 2xx or a dropped connection says Outcome not confirmed and reads Activity again. ApiError gains 'refused'; the server's own pre-start failure is coded 'apply-failed'. - M1: the lost-response test lets the apply run, then drops its answer. - M2: a failed apply records its code in the job's scope; Activity offers Review again for an overtaken plan (Maintenance for a duplicate cleanup whose sources changed). - M3: a failed duplicate cleanup toasts the server's reason. - M4: a review button holds while its preview is read (one preview per double press). - M5: the storage guard also catches bracket and property assignment and CacheStorage/IndexedDB opens; why the Toolkit build is skipped is written down. - M6: a refused storage listing says so on the Metadata cache row. - M7, M8: D100 text corrected; workflow_storage has its title. - M9: a busy start names the job the server named. * test: follow #319 in the Maintenance tests - The thumbnail dialog has only Scope since #319. - The admin teardown refuses new preview workers on its own API instead of closing with final=True, which since #319 also shuts the process-wide encoders that later test modules use. * fix(system): keep the server's search-again line on a duplicate review A duplicate cleanup review the server refuses with 409 sources-changed showed the generic "could not be reviewed" message; it now shows the server's "The sources changed since the search. Search for duplicates again." * fix(system): a plan with nothing to do offers only Close A reviewed maintenance plan whose scope is 0 records ("No duplicate prompt text") offered Apply reviewed operation, which started an empty job. It now shows its rows, "There is nothing to apply; nothing was changed." and a Close button. * fix(system): wrap long copy paths in the duplicates review A Keep label is the copy's source and path, and dataset folders often have long names with no space to break at, so the review scrolled sideways. The label now wraps anywhere inside its compare item (D101).
844 lines
53 KiB
HTML
844 lines
53 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PromptManager - Gallery</title>
|
|
<script src="js/theme-bridge.js"></script>
|
|
<script src="js/html-escape.js"></script>
|
|
<script src="js/image-helpers.js"></script>
|
|
<script src="js/gallery-settings.js"></script>
|
|
<script src="js/cache-ownership.js"></script>
|
|
<script src="js/scan-progress.js"></script>
|
|
<script src="js/api-client.js"></script>
|
|
<script src="js/png-metadata.js"></script>
|
|
<script src="js/list-state.js"></script>
|
|
<script src="js/sse-stream.js"></script>
|
|
<script src="js/data-actions.js"></script>
|
|
<link rel="stylesheet" href="lib/tailwind/comfyui-theme.css">
|
|
<link rel="stylesheet" href="lib/tailwind/styles.css">
|
|
<link rel="stylesheet" href="lib/viewerjs/viewer.min.css">
|
|
<script src="lib/viewerjs/viewer.min.js"></script>
|
|
<style>
|
|
/* Column count from Display Settings. An id selector inside the desktop
|
|
media query outranks the Tailwind breakpoint classes on #galleryGrid. */
|
|
@media (min-width: 1280px) {
|
|
#galleryGrid.gallery-columns-custom {
|
|
grid-template-columns: var(--gallery-columns, repeat(8, minmax(0, 1fr)));
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-pm-primary text-pm min-h-screen">
|
|
<div class="min-h-screen flex flex-col">
|
|
<!-- Header -->
|
|
<header class="bg-pm-surface border-b border-pm px-4 py-2">
|
|
<div class="max-w-7xl mx-auto">
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-sm font-semibold text-pm">PromptManager Gallery</h1>
|
|
<div class="hidden sm:flex items-center gap-2">
|
|
<div class="w-2 h-2 bg-pm-success rounded-full animate-pulse"></div>
|
|
<span class="text-pm-success text-xs font-medium">Online</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Controls -->
|
|
<div class="bg-pm-surface border-b border-pm-subtle px-4 py-2">
|
|
<div class="max-w-7xl mx-auto">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex items-center gap-2">
|
|
<label class="text-xs text-pm-secondary">Show:</label>
|
|
<select id="limitSelector" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
|
|
<option value="50">50</option>
|
|
<option value="100" selected>100</option>
|
|
<option value="200">200</option>
|
|
<option value="500">500</option>
|
|
</select>
|
|
<span class="text-xs text-pm-secondary">images</span>
|
|
<span class="text-pm-secondary mx-1">|</span>
|
|
<label class="text-xs text-pm-secondary">Size:</label>
|
|
<button id="thumbSmBtn" class="px-2 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs rounded-pm-sm transition-colors" title="Small thumbnails">S</button>
|
|
<button id="thumbMdBtn" class="px-2 py-1 bg-pm-accent text-pm-accent-fg text-xs rounded-pm-sm transition-colors" title="Medium thumbnails">M</button>
|
|
<button id="thumbLgBtn" class="px-2 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs rounded-pm-sm transition-colors" title="Large thumbnails">L</button>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button id="refreshBtn"
|
|
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
|
|
Refresh
|
|
</button>
|
|
<button id="gridViewBtn"
|
|
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Grid View
|
|
</button>
|
|
<button id="listViewBtn"
|
|
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
List View
|
|
</button>
|
|
<button id="settingsBtn"
|
|
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Settings
|
|
</button>
|
|
<button id="autoTagBtn"
|
|
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
|
|
Auto Tag
|
|
</button>
|
|
<button id="addPromptBtn"
|
|
class="px-3 py-1 bg-pm-success hover:bg-pm-success text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Add Prompt
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Bar -->
|
|
<div class="bg-pm-surface px-4 py-2 border-b border-pm-subtle">
|
|
<div class="max-w-7xl mx-auto">
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="text-pm-secondary">
|
|
Showing <span id="showingStart">0</span> to <span id="showingEnd">0</span> of <span id="totalImages">0</span> images
|
|
</span>
|
|
<span class="text-pm-secondary" id="loadingStatus">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gallery Content -->
|
|
<div class="flex-1 overflow-hidden">
|
|
<div class="max-w-full mx-auto p-3 flex gap-0" style="height: calc(100vh - 140px);">
|
|
<!-- Folder Tree Panel — sticky, scrolls independently -->
|
|
<div id="folderTreePanel" class="hidden flex-shrink-0 bg-pm-surface border border-pm rounded-pm-sm overflow-y-auto" style="width: 240px; min-width: 160px;">
|
|
<div class="px-3 py-2 border-b border-pm-subtle flex items-center justify-between sticky top-0 bg-pm-surface z-10">
|
|
<span class="text-sm font-semibold text-pm">Folders</span>
|
|
<button id="clearFolderFilterBtn" class="text-sm text-pm-secondary hover:text-pm transition-colors" title="Clear filter">×</button>
|
|
</div>
|
|
<div id="folderTree" class="py-1 text-sm">
|
|
<!-- Tree nodes inserted by JS -->
|
|
</div>
|
|
</div>
|
|
<!-- Resize Handle -->
|
|
<div id="folderResizeHandle" class="hidden w-1 cursor-col-resize bg-transparent hover:bg-pm-accent/30 flex-shrink-0"></div>
|
|
|
|
<!-- Main Gallery Area — scrolls independently -->
|
|
<div id="galleryScroll" class="flex-1 min-w-0 overflow-y-auto pl-3">
|
|
<!-- Loading State -->
|
|
<div id="loadingState" class="flex items-center justify-center py-12">
|
|
<div class="text-center">
|
|
<div class="w-8 h-8 border-4 border-pm-accent-tint border-t-pm-accent rounded-full animate-spin mx-auto mb-3"></div>
|
|
<p class="text-pm-secondary text-xs">Loading gallery...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="errorState" class="hidden flex items-center justify-center py-12">
|
|
<div class="text-center">
|
|
<h3 class="text-sm font-semibold text-pm-error mb-2">Error Loading Gallery</h3>
|
|
<p class="text-pm-secondary text-xs mb-3" id="errorMessage">Something went wrong while loading the images.</p>
|
|
<button id="retryBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gallery Grid -->
|
|
<div id="galleryGrid" class="hidden grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 gap-2">
|
|
<!-- Images will be inserted here -->
|
|
</div>
|
|
|
|
<!-- Gallery List -->
|
|
<div id="galleryList" class="hidden space-y-2">
|
|
<!-- Images will be inserted here -->
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div id="paginationControls" class="hidden mt-4 flex items-center justify-center gap-2">
|
|
<button id="prevPageBtn" class="px-3 py-1 bg-pm-surface hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
Previous
|
|
</button>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-pm-secondary text-xs">Page</span>
|
|
<input id="pageInput" type="number" min="1" class="w-16 px-2 py-1 bg-pm-input text-pm text-xs text-center rounded-pm-sm border border-pm focus:border-pm-accent focus:outline-none" value="1">
|
|
<span id="pageInfo" class="text-pm-secondary text-xs">of 1</span>
|
|
</div>
|
|
<button id="nextPageBtn" class="px-3 py-1 bg-pm-surface hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div> <!-- /Main Gallery Area -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Modal -->
|
|
<div id="settingsModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md max-w-2xl w-full max-h-[90vh] overflow-hidden mx-4 border border-pm">
|
|
<div class="p-3 border-b border-pm">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-pm">Gallery Settings</h3>
|
|
<button id="closeSettingsBtn" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
|
|
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 overflow-y-auto max-h-[calc(90vh-200px)] space-y-4">
|
|
|
|
<!-- Performance Settings -->
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-pm">Performance Settings</h4>
|
|
|
|
<!-- Thumbnail Generation -->
|
|
<div class="bg-pm-input rounded-pm-sm p-3">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div>
|
|
<h5 class="text-pm text-xs font-medium">Generate Thumbnails</h5>
|
|
<p class="text-xs text-pm-secondary">Create optimized thumbnails for faster loading</p>
|
|
</div>
|
|
<button id="generateThumbnailsBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs rounded-pm-sm transition-colors">
|
|
Generate
|
|
</button>
|
|
</div>
|
|
<div class="text-xs text-pm-muted mb-2">
|
|
Status: <span id="thumbnailStatus">Not generated</span>
|
|
<div class="text-xs text-pm-muted mt-1">
|
|
Thumbnails speed up gallery loading. Original images always used for viewing and metadata.
|
|
</div>
|
|
</div>
|
|
<!-- Check thumbnails at startup -->
|
|
<div class="flex items-center justify-between py-2 border-t border-pm">
|
|
<div>
|
|
<label class="text-xs text-pm-secondary">Check thumbnails at startup</label>
|
|
<p class="text-xs text-pm-muted">Prompt to generate thumbnails if missing when opening gallery</p>
|
|
</div>
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" id="checkThumbnailsAtStartup" class="sr-only peer" checked>
|
|
<div class="w-11 h-6 bg-pm-input peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-pm-accent rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-pm after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-pm-accent"></div>
|
|
</label>
|
|
</div>
|
|
<!-- Progress bar (hidden by default) -->
|
|
<div id="thumbnailProgress" class="hidden">
|
|
<div class="flex items-center justify-between text-xs text-pm-secondary mb-1">
|
|
<span id="progressText">Processing...</span>
|
|
<div class="flex items-center gap-2">
|
|
<span id="progressPercent">0%</span>
|
|
<button id="cancelThumbnailsBtn" class="text-pm-error hover:text-pm-error text-xs px-2 py-1 bg-pm-error-tint rounded-pm-sm hidden">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-pm-input rounded-full h-2">
|
|
<div id="progressBar" class="bg-pm-accent h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
<div class="flex items-center justify-between text-xs text-pm-muted mt-1">
|
|
<span id="progressDetails">Scanning images...</span>
|
|
<span id="progressETA" class="text-pm-muted"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Loading -->
|
|
<div class="bg-pm-input rounded-pm-sm p-3">
|
|
<h5 class="text-pm text-xs font-medium mb-2">Image Loading</h5>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Lazy Loading</label>
|
|
<input type="checkbox" id="lazyLoadingToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Image Quality</label>
|
|
<select id="imageQualitySelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
|
|
<option value="high">High (Original)</option>
|
|
<option value="medium" selected>Medium (Optimized)</option>
|
|
<option value="low">Low (Fast)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Display Settings -->
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-pm">Display Settings</h4>
|
|
|
|
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Default View Mode</label>
|
|
<select id="defaultViewModeSelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
|
|
<option value="grid" selected>Grid View</option>
|
|
<option value="list">List View</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Items per Page</label>
|
|
<select id="defaultLimitSelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
|
|
<option value="50">50</option>
|
|
<option value="100" selected>100</option>
|
|
<option value="200">200</option>
|
|
<option value="500">500</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Grid Columns (Desktop)</label>
|
|
<select id="gridColumnsSelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
|
|
<option value="4">4 Columns</option>
|
|
<option value="6">6 Columns</option>
|
|
<option value="8" selected>8 Columns</option>
|
|
<option value="10">10 Columns</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Show Media Info</label>
|
|
<input type="checkbox" id="showImageInfoToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Collapse Sidebar by Default</label>
|
|
<input type="checkbox" id="sidebarCollapsedByDefaultToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<label class="text-xs text-pm-secondary">Infinite Scrolling</label>
|
|
<p class="text-[11px] text-pm-muted">Load the next page as you scroll instead of showing page controls</p>
|
|
</div>
|
|
<input type="checkbox" id="infiniteScrollToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Settings -->
|
|
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
|
|
<h5 class="text-pm text-xs font-medium mb-2">Video Settings</h5>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Autoplay Videos</label>
|
|
<input type="checkbox" id="videoAutoplayToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Mute Videos by Default</label>
|
|
<input type="checkbox" id="videoMuteToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Loop Videos</label>
|
|
<input type="checkbox" id="videoLoopToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata Settings -->
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-pm">Metadata Settings</h4>
|
|
|
|
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Auto-load Metadata</label>
|
|
<input type="checkbox" id="autoLoadMetadataToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Cache Metadata</label>
|
|
<input type="checkbox" id="cacheMetadataToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Show File Paths</label>
|
|
<input type="checkbox" id="showFilePathsToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Maintenance -->
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-pm">Maintenance</h4>
|
|
|
|
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h5 class="text-pm text-xs font-medium">Clear Cache</h5>
|
|
<p class="text-xs text-pm-secondary">Clear cached metadata and thumbnails</p>
|
|
</div>
|
|
<button id="clearCacheBtn" class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs rounded-pm-sm transition-colors">
|
|
Clear
|
|
</button>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h5 class="text-pm text-xs font-medium">Rescan Output Folder</h5>
|
|
<p class="text-xs text-pm-secondary">Refresh the image list from disk</p>
|
|
</div>
|
|
<button id="rescanFolderBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs rounded-pm-sm transition-colors">
|
|
Rescan
|
|
</button>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h5 class="text-pm text-xs font-medium">Find Duplicate Images</h5>
|
|
<p class="text-xs text-pm-secondary">Scan for duplicate images based on file content</p>
|
|
</div>
|
|
<button id="scanDuplicatesBtn" class="px-3 py-1 bg-pm-warning hover:bg-pm-warning text-pm text-xs rounded-pm-sm transition-colors">
|
|
Scan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Settings -->
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-pm">Advanced</h4>
|
|
|
|
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">Debug Mode</label>
|
|
<input type="checkbox" id="debugModeToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs text-pm-secondary">API Timeout (seconds)</label>
|
|
<input type="number" id="apiTimeoutInput" value="30" min="5" max="300" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs w-20">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="p-3 border-t border-pm flex justify-end gap-2">
|
|
<button id="resetSettingsBtn" class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Reset to Defaults
|
|
</button>
|
|
<button id="saveSettingsBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
|
|
Save Settings
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Minimized duplicate scan: shown while it runs with its modal hidden, or when results are waiting -->
|
|
<button id="dupScanPill" type="button" title="Show duplicate scan"
|
|
class="hidden fixed bottom-4 right-4 z-40 px-3 py-1.5 bg-pm-surface border border-pm shadow-pm rounded-pm-md text-xs text-pm hover:bg-pm-hover transition-colors">
|
|
<span class="inline-block w-1.5 h-1.5 rounded-full bg-pm-accent mr-1.5 align-middle animate-pulse"></span><span id="dupScanPillText" class="align-middle">Scanning...</span>
|
|
</button>
|
|
|
|
<!-- Duplicates Modal -->
|
|
<div id="duplicatesModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md max-w-4xl w-full max-h-[90vh] overflow-hidden mx-4 border border-pm">
|
|
<div class="p-3 border-b border-pm">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-pm">Duplicate Images Found</h3>
|
|
<button id="closeDuplicatesBtn" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
|
|
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 overflow-y-auto max-h-[calc(90vh-200px)]">
|
|
<div id="duplicatesScanStatus" class="py-6">
|
|
<div class="max-w-md mx-auto bg-pm-input rounded-pm-md p-3">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<span class="text-pm text-xs font-medium">Scanning for duplicate images</span>
|
|
<span id="dupProgressText" class="text-pm-secondary text-xs">Starting...</span>
|
|
</div>
|
|
<div class="w-full bg-pm-primary rounded-full h-1.5">
|
|
<div id="dupProgressBar" class="bg-pm-accent h-1.5 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
<p class="text-[11px] text-pm-muted mt-2">Every file is hashed, so this takes a while on a large library. You can close this window; the scan keeps running and the results will be waiting.</p>
|
|
</div>
|
|
</div>
|
|
<div id="duplicatesContent" class="hidden">
|
|
<div class="mb-3 p-3 bg-pm-error-tint border border-pm-accent rounded-pm-sm">
|
|
<div class="flex items-start">
|
|
<svg class="w-4 h-4 text-pm-error mt-0.5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<div>
|
|
<h4 class="text-pm-error text-xs font-medium">Warning: Removing duplicate images cannot be undone!</h4>
|
|
<p class="text-pm-secondary text-xs mt-1">When you remove a duplicate image, the selected file will be permanently deleted from disk. Choose carefully which images to keep.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="duplicatesList" class="space-y-3">
|
|
<!-- Duplicates will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="duplicatesFooter" class="hidden p-3 border-t border-pm flex justify-between items-center">
|
|
<div class="text-xs text-pm-secondary">
|
|
<span id="duplicatesCount">0</span> duplicate image groups found
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button id="selectAllBtn" class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Select All to Delete
|
|
</button>
|
|
<button id="removeDuplicatesBtn" class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Delete Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auto Tag Modal -->
|
|
<div id="autoTagModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md p-3 max-w-lg w-full mx-4 border border-pm">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-semibold text-pm">Auto Tag Images</h3>
|
|
<button onclick="hideModal('autoTagModal')" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
|
|
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Model Selection -->
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-medium text-pm-secondary mb-2">Model Selection</label>
|
|
<div class="space-y-2">
|
|
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
|
|
<div class="flex items-center">
|
|
<input type="radio" name="autoTagModel" value="gguf" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
|
|
<div class="ml-3">
|
|
<span class="text-pm text-xs font-medium">GGUF Model</span>
|
|
<span class="text-pm-secondary text-xs ml-2">(Recommended, ~4.5GB)</span>
|
|
</div>
|
|
</div>
|
|
<div id="ggufModelStatus" class="flex items-center gap-2">
|
|
<span class="text-pm-secondary text-xs">Checking...</span>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
|
|
<div class="flex items-center">
|
|
<input type="radio" name="autoTagModel" value="hf" class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
|
|
<div class="ml-3">
|
|
<span class="text-pm text-xs font-medium">HuggingFace Model</span>
|
|
<span class="text-pm-secondary text-xs ml-2">(~16GB)</span>
|
|
</div>
|
|
</div>
|
|
<div id="hfModelStatus" class="flex items-center gap-2">
|
|
<span class="text-pm-secondary text-xs">Checking...</span>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
|
|
<div class="flex items-center">
|
|
<input type="radio" name="autoTagModel" value="wd14-swinv2" class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
|
|
<div class="ml-3">
|
|
<span class="text-pm text-xs font-medium">WD14 SwinV2</span>
|
|
<span class="text-pm-secondary text-xs ml-2">(~400MB, fast classifier)</span>
|
|
</div>
|
|
</div>
|
|
<div id="wd14SwinV2ModelStatus" class="flex items-center gap-2">
|
|
<span class="text-pm-secondary text-xs">Checking...</span>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
|
|
<div class="flex items-center">
|
|
<input type="radio" name="autoTagModel" value="wd14-vit" class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
|
|
<div class="ml-3">
|
|
<span class="text-pm text-xs font-medium">WD14 ViT</span>
|
|
<span class="text-pm-secondary text-xs ml-2">(~400MB, ViT architecture)</span>
|
|
</div>
|
|
</div>
|
|
<div id="wd14VitModelStatus" class="flex items-center gap-2">
|
|
<span class="text-pm-secondary text-xs">Checking...</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keep Model in Memory Option -->
|
|
<div class="mb-3">
|
|
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="keepModelInMemory" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
<div class="ml-3">
|
|
<span class="text-pm text-xs font-medium">Keep model in memory</span>
|
|
<span class="text-pm-secondary text-xs ml-2">(Faster processing, uses ~4-16GB VRAM)</span>
|
|
</div>
|
|
</div>
|
|
<button id="unloadModelBtn" class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs rounded-pm-sm transition-colors hidden">
|
|
Unload Model
|
|
</button>
|
|
</label>
|
|
<p id="modelLoadedStatus" class="text-pm-muted text-xs mt-1 hidden">
|
|
<span class="text-pm-success">●</span> Model currently loaded: <span id="loadedModelType">none</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Prompt Template (LLM models only) -->
|
|
<div id="promptSection" class="mb-3">
|
|
<label class="block text-xs font-medium text-pm-secondary mb-2">Tag Generation Prompt</label>
|
|
<textarea id="autoTagPrompt" rows="6"
|
|
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs font-mono resize-none"
|
|
placeholder="Enter prompt template...">Generate only comma-separated Danbooru tags (lowercase_underscores, no spaces).
|
|
Include: subject_count (1girl/1boy/etc), gender, ethnicity (asian, caucasian, etc), age_approx (teen, young_woman), body_attributes, face_features, hairstyle, appearance, clothing, accessories, pose, expression, action, camera_angle, camera_shot, scene_composition (wide_shot, long_shot, dutch_angle), environment, background_elements, lighting, mood.
|
|
Prioritize visual accuracy.
|
|
No sentences, no commentary, no captions. Only tags. Keep length short but descriptive.</textarea>
|
|
</div>
|
|
|
|
<!-- WD14 Threshold Controls -->
|
|
<div id="wd14ThresholdSection" class="mb-3 hidden">
|
|
<label class="block text-xs font-medium text-pm-secondary mb-2">Tag Confidence Thresholds</label>
|
|
<div class="space-y-3">
|
|
<div class="p-2.5 bg-pm-input rounded-pm-sm border border-pm">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-pm text-xs">General tags</span>
|
|
<span id="wd14GeneralThresholdValue" class="text-pm-accent text-xs font-mono">0.35</span>
|
|
</div>
|
|
<input type="range" id="wd14GeneralThreshold" min="0" max="1" step="0.05" value="0.35"
|
|
class="w-full h-1.5 bg-pm-primary rounded-lg appearance-none cursor-pointer accent-[var(--pm-accent)]">
|
|
</div>
|
|
<div class="p-2.5 bg-pm-input rounded-pm-sm border border-pm">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-pm text-xs">Character tags</span>
|
|
<span id="wd14CharacterThresholdValue" class="text-pm-accent text-xs font-mono">0.85</span>
|
|
</div>
|
|
<input type="range" id="wd14CharacterThreshold" min="0" max="1" step="0.05" value="0.85"
|
|
class="w-full h-1.5 bg-pm-primary rounded-lg appearance-none cursor-pointer accent-[var(--pm-accent)]">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex justify-end gap-2">
|
|
<button onclick="hideModal('autoTagModal')"
|
|
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button id="startReviewBtn"
|
|
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
|
|
Review
|
|
</button>
|
|
<button id="startAutoTagBtn"
|
|
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
|
|
Auto Tag
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auto Tag Review Modal -->
|
|
<div id="autoTagReviewModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md p-3 max-w-4xl w-full mx-4 border border-pm">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-semibold text-pm">
|
|
Review Tags
|
|
<span class="text-pm-secondary text-xs font-normal">(<span id="reviewCurrentIndex">1</span> of <span id="reviewTotalCount">0</span>)</span>
|
|
</h3>
|
|
<button onclick="window.gallery.cancelReview()" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
|
|
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<!-- Image Preview -->
|
|
<div class="bg-pm-primary rounded-pm-sm p-3">
|
|
<img id="reviewImage" src="" alt="Review image" class="w-full h-64 object-contain rounded-pm-sm">
|
|
<p id="reviewImageName" class="text-pm-secondary text-xs mt-2 text-center truncate">image_name.png</p>
|
|
</div>
|
|
|
|
<!-- Generated Tags -->
|
|
<div>
|
|
<label class="block text-xs font-medium text-pm-secondary mb-2">Generated Tags (click to remove)</label>
|
|
<div id="reviewTagsContainer" class="flex flex-wrap gap-2 p-3 bg-pm-primary rounded-pm-sm min-h-[200px] max-h-[250px] overflow-y-auto">
|
|
<!-- Tag chips will be inserted here -->
|
|
</div>
|
|
<p class="text-pm-muted text-xs mt-1">Click the x on tags to remove them before applying</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex justify-between mt-3">
|
|
<button id="skipReviewBtn"
|
|
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Skip
|
|
</button>
|
|
<div class="flex gap-2">
|
|
<button onclick="window.gallery.cancelReview()"
|
|
class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Cancel All
|
|
</button>
|
|
<button id="applyReviewBtn"
|
|
class="px-3 py-1 bg-pm-success hover:bg-pm-success text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Apply & Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auto Tag Progress Modal -->
|
|
<div id="autoTagProgressModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md p-3 max-w-md w-full mx-4 border border-pm">
|
|
<h3 class="text-sm font-semibold text-pm mb-3">Auto Tagging Progress</h3>
|
|
|
|
<!-- Current File -->
|
|
<div class="mb-3">
|
|
<p class="text-pm-secondary text-xs">Processing:</p>
|
|
<p id="autoTagCurrentFile" class="text-pm text-xs font-medium truncate">Initializing...</p>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="mb-3">
|
|
<div class="flex justify-between text-xs text-pm-secondary mb-1">
|
|
<span>Progress</span>
|
|
<span id="autoTagProgressPercent">0%</span>
|
|
</div>
|
|
<div class="w-full bg-pm-input rounded-full h-2">
|
|
<div id="autoTagProgressBar" class="bg-pm-accent h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="grid grid-cols-3 gap-2 mb-3">
|
|
<div class="bg-pm-input rounded-pm-sm p-2 text-center">
|
|
<div class="text-sm font-semibold text-pm-accent" id="autoTagProcessed">0</div>
|
|
<div class="text-xs text-pm-secondary">Processed</div>
|
|
</div>
|
|
<div class="bg-pm-input rounded-pm-sm p-2 text-center">
|
|
<div class="text-sm font-semibold text-pm-success" id="autoTagApplied">0</div>
|
|
<div class="text-xs text-pm-secondary">Tags Applied</div>
|
|
</div>
|
|
<div class="bg-pm-input rounded-pm-sm p-2 text-center">
|
|
<div class="text-sm font-semibold text-pm-warning" id="autoTagSkipped">0</div>
|
|
<div class="text-xs text-pm-secondary">Skipped</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cancel Button -->
|
|
<div class="flex justify-center">
|
|
<button id="cancelAutoTagBtn"
|
|
class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auto Tag Download Modal -->
|
|
<div id="autoTagDownloadModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md p-3 max-w-md w-full mx-4 border border-pm">
|
|
<h3 class="text-sm font-semibold text-pm mb-3">Downloading Model</h3>
|
|
|
|
<!-- Model Name -->
|
|
<div class="mb-3">
|
|
<p class="text-pm-secondary text-xs">Downloading:</p>
|
|
<p id="downloadModelName" class="text-pm text-xs font-medium">GGUF Model</p>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="mb-3">
|
|
<div class="flex justify-between text-xs text-pm-secondary mb-1">
|
|
<span id="downloadStatus">Preparing...</span>
|
|
<span id="downloadProgressPercent">0%</span>
|
|
</div>
|
|
<div class="w-full bg-pm-input rounded-full h-2">
|
|
<div id="downloadProgressBar" class="bg-pm-accent h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cancel Button -->
|
|
<div class="flex justify-center">
|
|
<button id="cancelDownloadBtn"
|
|
class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Prompt Modal -->
|
|
<div id="addPromptModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
<div class="bg-pm-surface rounded-pm-md p-3 max-w-2xl w-full mx-4 border border-pm max-h-[90vh] overflow-y-auto">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-semibold text-pm">Add New Prompt</h3>
|
|
<button onclick="window.gallery.hideAddPromptModal()" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
|
|
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Prompt Text -->
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-medium text-pm-secondary mb-1">Prompt Text <span class="text-pm-error">*</span></label>
|
|
<textarea id="addPromptText" rows="4"
|
|
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs resize-none"
|
|
placeholder="Enter your prompt text here..."></textarea>
|
|
</div>
|
|
|
|
<!-- Category and Rating Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
|
|
<!-- Category -->
|
|
<div>
|
|
<label class="block text-xs font-medium text-pm-secondary mb-1">Category</label>
|
|
<div class="relative">
|
|
<input type="text" id="addPromptCategory" list="categoryList"
|
|
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs"
|
|
placeholder="Select or type category...">
|
|
<datalist id="categoryList"></datalist>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rating -->
|
|
<div>
|
|
<label class="block text-xs font-medium text-pm-secondary mb-1">Rating</label>
|
|
<div class="flex items-center gap-1" id="addPromptRating">
|
|
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="1">☆</button>
|
|
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="2">☆</button>
|
|
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="3">☆</button>
|
|
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="4">☆</button>
|
|
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="5">☆</button>
|
|
<button type="button" class="ml-2 text-xs text-pm-muted hover:text-pm-secondary" id="clearRatingBtn">Clear</button>
|
|
</div>
|
|
<input type="hidden" id="addPromptRatingValue" value="">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-medium text-pm-secondary mb-1">Tags</label>
|
|
<div class="relative">
|
|
<div id="addPromptTagsContainer" class="flex flex-wrap gap-2 p-2 bg-pm-input border border-pm rounded-pm-sm min-h-[44px]">
|
|
<input type="text" id="addPromptTagInput"
|
|
class="flex-1 min-w-[150px] bg-transparent border-none outline-none text-pm text-xs placeholder-pm"
|
|
placeholder="Type to add tags...">
|
|
</div>
|
|
<div id="tagSuggestions" class="absolute left-0 right-0 top-full mt-1 bg-pm-surface border border-pm rounded-pm-sm shadow-pm hidden z-10 max-h-48 overflow-y-auto"></div>
|
|
</div>
|
|
<p class="text-pm-muted text-xs mt-1">Press Enter or comma to add a tag. Click a tag to remove it.</p>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-medium text-pm-secondary mb-1">Notes</label>
|
|
<textarea id="addPromptNotes" rows="2"
|
|
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs resize-none"
|
|
placeholder="Optional notes about this prompt..."></textarea>
|
|
</div>
|
|
|
|
<!-- Protection Option -->
|
|
<div class="mb-3">
|
|
<label class="flex items-start p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
|
|
<input type="checkbox" id="addPromptProtected" checked class="w-4 h-4 mt-0.5 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
|
<div class="ml-3">
|
|
<span class="text-pm text-xs font-medium">Protect from orphan cleanup</span>
|
|
<p class="text-pm-secondary text-xs mt-1">Keep this prompt even if it has no linked images. Uncheck if you want this prompt to be automatically removed during maintenance when it has no images.</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex justify-end gap-2">
|
|
<button onclick="window.gallery.hideAddPromptModal()"
|
|
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button id="saveNewPromptBtn"
|
|
class="px-3 py-1 bg-pm-success hover:bg-pm-success text-pm text-xs font-medium rounded-pm-sm transition-colors">
|
|
Add Prompt
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script src="js/comfy-metadata.js"></script>
|
|
<script src="js/gallery.js"></script>
|
|
</body>
|
|
</html> |