mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-13 16:14:57 +00:00
1493 lines
78 KiB
HTML
1493 lines
78 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta
|
||
name="viewport"
|
||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||
/>
|
||
<title>Bjorn Cyberviking - Management Interface 1.0</title>
|
||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon" />
|
||
<link rel="stylesheet" href="web/css/global.css" />
|
||
<link rel="manifest" href="manifest.json" />
|
||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png" />
|
||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||
<meta name="mobile-web-app-capable" content="yes" />
|
||
<meta name="theme-color" content="#333" />
|
||
<script src="web/js/global.js" defer></script>
|
||
|
||
<style>
|
||
/* ===== bridge vars -> global.css tokens ===== */
|
||
:root{
|
||
--_bg: var(--bg, #0b0c0f);
|
||
--_panel: var(--c-panel-2, rgba(16,22,22,.7));
|
||
--_panel-hi: color-mix(in oklab, var(--_panel) 96%, transparent);
|
||
--_panel-lo: color-mix(in oklab, var(--_panel) 86%, transparent);
|
||
--_border: var(--c-border, rgba(255,255,255,.08));
|
||
--_ink: var(--ink, #e6fff7);
|
||
--_muted: var(--muted, #8affc1cc);
|
||
--_acid: var(--acid, #00ff9a);
|
||
--_acid2: var(--acid-2, #18f0ff);
|
||
--_shadow: var(--shadow, 0 12px 28px rgba(0,0,0,.35));
|
||
--tile-min: 160px;
|
||
|
||
--ok: #22c55e; /* green */
|
||
--ko: #ef4444; /* red */
|
||
--ok-glow: rgba(34,197,94,.45);
|
||
--ko-glow: rgba(239,68,68,.45);
|
||
}
|
||
body{ color:var(--_ink); background:var(--_bg); }
|
||
|
||
/* ===== sidebar / tabs ===== */
|
||
.tabs-container{
|
||
display:flex; gap:4px; margin-bottom:16px; padding-bottom:8px;
|
||
border-bottom:1px solid var(--_border);
|
||
}
|
||
.tab-btn{
|
||
flex:1; padding:10px 8px; border:none; cursor:pointer; font-size:14px; font-weight:700;
|
||
border-radius:10px 10px 0 0; color:var(--_ink);
|
||
background: var(--_panel-lo); transition:.2s; border:1px solid var(--_border); border-bottom:none;
|
||
}
|
||
.tab-btn:hover{ background:var(--_panel-hi); transform: translateY(-1px); }
|
||
.tab-btn.active{
|
||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 18%, transparent), color-mix(in oklab, var(--_acid2) 12%, transparent));
|
||
color:var(--_ink);
|
||
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||
}
|
||
|
||
/* ===== unified list ===== */
|
||
.unified-list{ list-style:none; margin:0; padding:0; }
|
||
.unified-list .card{
|
||
display:flex; align-items:center; gap:12px; padding:10px; margin-bottom:6px; cursor:pointer;
|
||
border-radius:12px; background:var(--_panel-lo); transition:.2s;
|
||
border:1px solid var(--_border); box-shadow: none;
|
||
}
|
||
.unified-list .card:hover{ background:var(--_panel-hi); transform: translateY(-1px); box-shadow: var(--_shadow); }
|
||
.unified-list .card.selected{
|
||
background: color-mix(in oklab, var(--_acid2) 16%, var(--_panel-hi));
|
||
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
||
}
|
||
.unified-list .card img{
|
||
height:50px; width:50px; border-radius:10px; object-fit:cover; background:#0b0e13; border:1px solid var(--_border);
|
||
}
|
||
.unified-list .card span{ flex:1; font-weight:700; color:var(--_ink); }
|
||
|
||
/* === enable/disable dot === */
|
||
.enable-dot{
|
||
--size: 14px;
|
||
width: var(--size);
|
||
height: var(--size);
|
||
border-radius: 999px;
|
||
border: 1px solid var(--_border);
|
||
background: var(--ko);
|
||
box-shadow: 0 0 0 0 var(--ko-glow);
|
||
transition: .18s ease;
|
||
flex: 0 0 auto;
|
||
cursor: pointer;
|
||
}
|
||
.enable-dot.on{
|
||
background: var(--ok);
|
||
box-shadow: 0 0 0 4px var(--ok-glow);
|
||
border-color: color-mix(in oklab, var(--ok) 45%, var(--_border));
|
||
}
|
||
.enable-dot:focus-visible{
|
||
outline: none;
|
||
box-shadow: 0 0 0 4px color-mix(in oklab, var(--_acid2) 45%, transparent);
|
||
}
|
||
|
||
/* ===== page visibility ===== */
|
||
.page-content{ display:none; overflow: auto;height: -webkit-fill-available;}
|
||
.page-content.active{ display:block; }
|
||
|
||
/* ===== attacks page ===== */
|
||
.editor-textarea-container{ display:flex; flex-direction:column; height:100%; gap:12px; }
|
||
.editor-header{ display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; }
|
||
.editor-buttons{ display:flex; gap:8px; }
|
||
.editor-textarea{
|
||
flex:1; min-height:400px; resize:vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:14px;
|
||
color:var(--_ink); background:var(--_panel-lo); border:1px solid var(--_border); border-radius:12px; padding:14px;
|
||
box-shadow: inset 0 0 0 1px transparent; transition:.2s;
|
||
}
|
||
.editor-textarea:focus{
|
||
outline:none; border-color: color-mix(in oklab, var(--_acid2) 30%, var(--_border));
|
||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 18%, transparent);
|
||
background: var(--_panel-hi);
|
||
}
|
||
|
||
/* ===== images page ===== */
|
||
.actions-bar{
|
||
display:flex; flex-wrap:wrap; gap:10px; margin-bottom:20px; position:sticky; top:0; z-index:10;
|
||
background:var(--_panel); padding:10px; border-radius:12px; border:1px solid var(--_border); backdrop-filter: blur(10px);
|
||
}
|
||
.actions-bar button, .chip, .select, .sort-toggle{
|
||
border-radius:10px; border:1px solid var(--_border); color:var(--_ink); background:var(--_panel-lo);
|
||
padding:10px 12px; cursor:pointer; transition:.2s; font-weight:700;
|
||
}
|
||
.actions-bar button:hover, .chip:hover, .select:hover, .sort-toggle:hover{ background:var(--_panel-hi); transform: translateY(-1px); }
|
||
.actions-bar button.danger{ background: color-mix(in oklab, var(--_acid) 12%, var(--_panel-lo)); }
|
||
.actions-bar button.danger:hover{ background: color-mix(in oklab, var(--_acid) 18%, var(--_panel-hi)); }
|
||
.chip{ border-radius:999px; }
|
||
.field{ position:relative; min-width:190px; }
|
||
.input{
|
||
width:100%; padding:10px 12px 10px 36px; color:var(--_ink); background:var(--_panel-lo);
|
||
border:1px solid var(--_border); border-radius:10px; outline:none; transition:.2s;
|
||
}
|
||
.input:focus{
|
||
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 14%, transparent);
|
||
background: var(--_panel-hi);
|
||
}
|
||
.field .icon{ position:absolute; left:10px; top:9px; opacity:.7; pointer-events:none; }
|
||
.select{ appearance:none; }
|
||
.sort-toggle{ min-width:42px; text-align:center; }
|
||
|
||
.range-wrap{ display:flex; align-items:center; gap:8px; }
|
||
.range{ accent-color: var(--_acid); }
|
||
|
||
.image-container{
|
||
display:grid; gap:10px;
|
||
grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr));
|
||
padding-bottom:140px;
|
||
}
|
||
.image-item{
|
||
position:relative; border-radius:12px; overflow:hidden; cursor:pointer; aspect-ratio:1/1; transition:.2s;
|
||
background:var(--_panel-lo); border:1px solid var(--_border);
|
||
}
|
||
.image-item:hover{ transform: translateY(-2px); box-shadow: var(--_shadow); background:var(--_panel-hi); }
|
||
.image-item img{ width:100%; height:100%; display:block; object-fit:contain; background:#0b0e13; image-rendering:pixelated; }
|
||
.image-info{
|
||
position:absolute; inset:auto 0 0 0; padding:6px 8px; text-align:center; font-size:12px; color:var(--_ink);
|
||
background: linear-gradient(180deg, transparent, rgba(0,0,0,.75));
|
||
}
|
||
.select-ring{
|
||
position:absolute; inset:0; pointer-events:none; border:3px solid transparent; border-radius:12px; transition:.2s;
|
||
}
|
||
.image-item.selectable:hover .select-ring{ border-color: color-mix(in oklab, var(--_acid2) 35%, transparent); }
|
||
.image-item.selected .select-ring{ border-color: var(--_acid2); box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--_acid2) 35%, transparent); }
|
||
|
||
.tick-overlay{
|
||
position:absolute; top:8px; right:8px; width:26px; height:26px; border-radius:50%;
|
||
background: color-mix(in oklab, var(--_acid) 80%, white); color:#001; font-weight:900; display:none;
|
||
align-items:center; justify-content:center; box-shadow: var(--_shadow);
|
||
}
|
||
.image-item.selected .tick-overlay{ display:flex; }
|
||
|
||
.skeleton{
|
||
border-radius:12px; aspect-ratio:1/1;
|
||
background: linear-gradient(90deg, rgba(255,255,255,.03) 25%, rgba(255,255,255,.08) 37%, rgba(255,255,255,.03) 63%);
|
||
background-size:400% 100%; animation: shimmer 1.1s infinite;
|
||
border:1px solid var(--_border);
|
||
}
|
||
@keyframes shimmer{0%{background-position:100% 0}100%{background-position:0 0}}
|
||
|
||
.edit-only{ display:none; }
|
||
.edit-mode .edit-only{ display:inline-flex; }
|
||
.status-only{ display:none; }
|
||
.static-only{ display:none; }
|
||
.status-mode .status-only{ display:inline-block; }
|
||
.static-mode .static-only{ display:inline-block; }
|
||
|
||
/* NEW: context buttons for Web/Icons sections */
|
||
.web-only{ display:none; }
|
||
.icons-only{ display:none; }
|
||
.web-mode .web-only{ display:inline-block; }
|
||
.icons-mode .icons-only{ display:inline-block; }
|
||
|
||
/* ===== comments page ===== */
|
||
.main{ display:flex; flex-direction:column; overflow:hidden; }
|
||
.buttons-container{ flex:0 0 auto; margin-bottom:10px;top: 0;position: sticky; }
|
||
.comments-container{ display:flex; flex:1 1 auto; min-height:0; }
|
||
.comments-editor{
|
||
flex:1 1 auto; min-width:0; min-height:0; overflow:auto; white-space:pre; word-wrap:normal;
|
||
background:var(--_panel-lo); color:var(--_ink);
|
||
border:1px solid var(--_border); border-radius:12px; padding:16px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:14px;
|
||
}
|
||
.comment-line{ display:block; width:100%; }
|
||
.comment-line:nth-child(odd){ color:var(--_ink); }
|
||
.comment-line:nth-child(even){ color: var(--_acid); }
|
||
|
||
/* ===== modals ===== */
|
||
.modal-action{
|
||
display:none; position:fixed; inset:0; z-index:1000; padding:10px;
|
||
background: rgba(0,0,0,.6); justify-content:center; align-items:center;
|
||
}
|
||
.modal-content{
|
||
position:relative; width:100%; max-width:520px; max-height:90vh; overflow-y:auto;
|
||
background:var(--_panel-hi); padding:20px; border-radius:14px; border:1px solid var(--_border); box-shadow: var(--_shadow);
|
||
}
|
||
.modal-header h3{ margin:0 0 10px 0; color:var(--_ink); }
|
||
.modal-body{ margin-bottom:20px; }
|
||
.modal-footer{ display:flex; justify-content:flex-end; gap:10px; }
|
||
.close{ position:absolute; right:10px; top:10px; font-size:24px; cursor:pointer; color:var(--_muted); }
|
||
|
||
.form-group{ margin-bottom:15px; }
|
||
.form-group label{ display:block; margin-bottom:6px; color:var(--_muted); font-weight:700; }
|
||
.form-group input[type="text"],
|
||
.form-group input[type="number"],
|
||
.form-group input[type="file"]{
|
||
width:100%; padding:10px 12px; color:var(--_ink);
|
||
background:var(--_panel-lo); border:1px solid var(--_border); border-radius:10px; outline:none; transition:.2s;
|
||
}
|
||
.form-group input:focus{
|
||
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 14%, transparent);
|
||
background: var(--_panel-hi);
|
||
}
|
||
|
||
/* ===== responsive ===== */
|
||
@media (max-width: 480px){
|
||
.tabs-container{ gap:2px; }
|
||
.tab-btn{ font-size:13px; padding:8px 6px; }
|
||
.actions-bar{ gap:8px; }
|
||
}
|
||
.action-btn-container{
|
||
padding: 2px;
|
||
gap: 2px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
align-content: center;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.hero-btn {
|
||
border-radius: 16px;
|
||
background: var(--grid), var(--grad-hero);
|
||
position: sticky;
|
||
bottom: 0;
|
||
border: 1px solid var(--c-border);
|
||
box-shadow: var(--shadow);
|
||
display: grid;
|
||
align-items: center;
|
||
justify-items: center;
|
||
text-align: center;
|
||
padding: 6px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidehead">
|
||
<div class="sidetitle">Management</div>
|
||
<div class="spacer"></div>
|
||
<button class="btn" id="hideSidebar"><span class="icon">⟵</span><span class="label">Hide</span></button>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="sideheader" id="sideheader">
|
||
<div class="tabs-container">
|
||
<button class="tab-btn active" data-page="attacks">Attacks</button>
|
||
<button class="tab-btn" data-page="comments">Comments</button>
|
||
<button class="tab-btn" data-page="images">Images</button>
|
||
</div>
|
||
<div class="action-btn-container">
|
||
<button class="btn" id="add-action-btn">Add Action</button>
|
||
<button class="btn danger" id="delete-action-btn" disabled>Delete Action</button>
|
||
<button class="btn" id="sync-missing-btn">Sync Missing</button>
|
||
<button class="btn danger" id="restore-default-actions-btn">Restore Default</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar content -->
|
||
<div class="sidecontent" id="sidecontent">
|
||
<!-- Attacks content -->
|
||
<div id="attacks-sidebar" class="sidebar-page" style="display:block">
|
||
<ul class="unified-list" id="attacks-list"></ul>
|
||
<div class="hero-btn">
|
||
<button class="btn" id="add-attack-btn">Add Attack</button>
|
||
<button class="btn danger" id="remove-attack-btn">Remove Attack</button>
|
||
</div>
|
||
<div id="empty-attacks-hint" style="display:none;opacity:.8;margin-top:8px;font-size:.95em">
|
||
No attacks found. Import a .py attack with "Add Attack".
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Images content -->
|
||
<div id="images-sidebar" class="sidebar-page" style="display:none">
|
||
<h3 style="margin:8px 0">Characters</h3>
|
||
<div id="characters-page" class="page-content"></div>
|
||
<ul class="unified-list" id="character-list"></ul>
|
||
<div class="chips" style="margin:8px 0 16px 0">
|
||
<button class="btn" id="create-character-btn">Create Character</button>
|
||
<button class="btn danger" id="delete-character-btn">Del Character</button>
|
||
</div>
|
||
|
||
<!-- NEW: Web Images + Actions Images (before Static Images) -->
|
||
<h3 style="margin:8px 0">Web Images</h3>
|
||
<ul class="unified-list" id="web-images-list"></ul>
|
||
|
||
<h3 style="margin:8px 0">Actions Images</h3>
|
||
<ul class="unified-list" id="actions-icons-list"></ul>
|
||
<!-- /NEW -->
|
||
|
||
<h3 style="margin:8px 0">Static Images</h3>
|
||
<ul class="unified-list" id="library-list"></ul>
|
||
<h3 style="margin:8px 0">Status Images</h3>
|
||
<ul class="unified-list" id="action-list"></ul>
|
||
</div>
|
||
|
||
<!-- Comments content -->
|
||
<div id="comments-sidebar" class="sidebar-page" style="display:none">
|
||
<ul class="unified-list" id="section-list"></ul>
|
||
<div class="hero-btn">
|
||
<button class="btn" id="add-section-btn">Add Section</button>
|
||
<button class="btn danger" id="delete-section-btn" disabled>Delete Section</button>
|
||
<button class="btn danger" id="restore-default-btn">Restore Default</button>
|
||
</div>
|
||
<div id="empty-comments-hint" style="display:none;opacity:.8;margin-top:8px;font-size:.95em">No comments found.</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<main class="main" id="main">
|
||
<!-- Attacks Page -->
|
||
<div id="attacks-page" class="page-content active">
|
||
<div class="editor-textarea-container">
|
||
<div class="editor-header">
|
||
<h2 id="editor-title" style="margin:0">Select an Attack</h2>
|
||
<div class="editor-buttons">
|
||
<button class="btn" id="save-attack-btn">Save</button>
|
||
<button class="btn" id="restore-attack-btn">Restore Default</button>
|
||
</div>
|
||
</div>
|
||
<textarea id="editor-textarea" class="editor-textarea" disabled></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Images Page -->
|
||
<div id="images-page" class="page-content">
|
||
<div class="actions-bar">
|
||
<span class="chip -brand" id="edit-mode-toggle-btn">Enter Edit Mode</span>
|
||
<select id="sort-key" class="select" title="Sort by">
|
||
<option value="name">Sort: Name</option>
|
||
<option value="dim">Sort: Dimensions</option>
|
||
</select>
|
||
<button id="sort-dir" class="sort-toggle" title="Toggle ascending/descending">↑</button>
|
||
<div class="field" title="Search by filename">
|
||
<span class="icon">🔎</span>
|
||
<input id="search-input" class="input" placeholder="Search images…">
|
||
</div>
|
||
<div class="range-wrap" title="Grid density">
|
||
<span style="font-size:12px;opacity:.8">Density</span>
|
||
<input id="density" type="range" min="120" max="260" value="160" class="range">
|
||
</div>
|
||
<button id="rename-image-btn" class="edit-only">Rename Image</button>
|
||
<button id="replace-image-btn" class="edit-only">Replace Image</button>
|
||
<button id="resize-images-btn" class="edit-only">Resize Selected Images</button>
|
||
|
||
<button id="add-characters-btn" class="status-only">Add Character Images</button>
|
||
<button id="add-status-image-btn" class="status-only">Add Status Image</button>
|
||
<button id="add-static-image-btn" class="static-only">Add Static Image</button>
|
||
|
||
<!-- NEW: context buttons -->
|
||
<button id="add-web-image-btn" class="web-only">Add Web Image</button>
|
||
<button id="add-icon-image-btn" class="icons-only">Add Action Icon</button>
|
||
<!-- /NEW -->
|
||
|
||
<button id="delete-images-btn" class="edit-only danger">Del Selected Images</button>
|
||
</div>
|
||
<div class="image-container" id="image-container"></div>
|
||
</div>
|
||
|
||
<!-- Comments Page -->
|
||
<div id="comments-page" class="page-content">
|
||
<div class="buttons-container">
|
||
<h2 id="section-title" style="margin:0 0 10px 0;">Comments</h2>
|
||
<button class="btn" id="save-comments-btn">Save</button>
|
||
<button class="btn" id="select-all-btn">Select All</button>
|
||
</div>
|
||
<div class="comments-container">
|
||
<div class="comments-editor" id="comments-editor" contenteditable="true" data-placeholder="Comments will be displayed here..." role="textbox" aria-multiline="true"></div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- ===== Modals ===== -->
|
||
<div id="add-action-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Add New Action</h3>
|
||
<div class="form-group">
|
||
<label for="new-action-name">Action Name:</label>
|
||
<input type="text" id="new-action-name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-attack-file">Python file (.py):</label>
|
||
<input type="file" id="new-attack-file" accept=".py" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-status-icon">Select Status Image:</label>
|
||
<input type="file" id="new-status-icon" accept=".bmp,.jpg,.jpeg,.png" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-character-images">Select Character Images:</label>
|
||
<input type="file" id="new-character-images" accept=".bmp,.jpg,.jpeg,.png" multiple>
|
||
</div>
|
||
<button id="create-action-btn">Create</button>
|
||
<button class="cancel-btn" id="cancel-action-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="add-static-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Add Static Image</h3>
|
||
<div class="form-group">
|
||
<label for="static-image-file">Select Image:</label>
|
||
<input type="file" id="static-image-file" accept=".bmp,.jpg,.jpeg,.png" required>
|
||
</div>
|
||
<button id="upload-static-image-btn">Upload Image</button>
|
||
<button class="cancel-btn" id="cancel-static-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- NEW: Web Images upload modal -->
|
||
<div id="add-web-image-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Add Web Image</h3>
|
||
<div class="form-group">
|
||
<label for="web-image-file">Select Image:</label>
|
||
<input type="file" id="web-image-file" accept=".bmp,.jpg,.jpeg,.png,.gif,.ico,.webp" required>
|
||
</div>
|
||
<button id="upload-web-image-btn">Upload Web Image</button>
|
||
<button class="cancel-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- NEW: Actions Icons upload modal -->
|
||
<div id="add-icon-image-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Add Action Icon</h3>
|
||
<div class="form-group">
|
||
<label for="icon-image-file">Select Icon:</label>
|
||
<input type="file" id="icon-image-file" accept=".bmp,.jpg,.jpeg,.png,.gif,.ico,.webp" required>
|
||
</div>
|
||
<button id="upload-icon-image-btn">Upload Icon</button>
|
||
<button class="cancel-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
<!-- /NEW -->
|
||
|
||
<div id="rename-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Rename Image</h3>
|
||
<div class="form-group">
|
||
<label for="new-name">New Name:</label>
|
||
<input type="text" id="new-name" required>
|
||
</div>
|
||
<button id="rename-image-confirm-btn">Rename</button>
|
||
<button class="cancel-btn" id="cancel-rename-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="replace-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Replace Image</h3>
|
||
<div class="form-group">
|
||
<label for="replace-image-file">Select New Image:</label>
|
||
<input type="file" id="replace-image-file" accept=".bmp,.jpg,.jpeg,.png" required>
|
||
</div>
|
||
<button id="replace-image-confirm-btn">Replace</button>
|
||
<button class="cancel-btn" id="cancel-replace-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="add-characters-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Add Character Images</h3>
|
||
<div class="form-group">
|
||
<label for="character-images-upload">Select Images:</label>
|
||
<input type="file" id="character-images-upload" accept=".bmp,.jpg,.jpeg,.png" multiple required>
|
||
</div>
|
||
<button id="upload-characters-btn">Upload</button>
|
||
<button class="cancel-btn" id="cancel-characters-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="add-status-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Add Status Image</h3>
|
||
<div class="form-group">
|
||
<label for="status-image-file">Select Status Image:</label>
|
||
<input type="file" id="status-image-file" accept=".bmp,.jpg,.jpeg,.png" required>
|
||
</div>
|
||
<button id="upload-status-image-btn">Upload Status Image</button>
|
||
<button class="cancel-btn" id="cancel-status-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="resize-images-modal" class="modal-action">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h3>Resize Selected Images</h3>
|
||
<div class="form-group">
|
||
<label for="resize-width-input">Width:</label>
|
||
<input type="number" id="resize-width-input" value="100" min="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="resize-height-input">Height:</label>
|
||
<input type="number" id="resize-height-input" value="100" min="1">
|
||
</div>
|
||
<button id="resize-images-confirm-btn">Resize</button>
|
||
<button class="cancel-btn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Inline app script (UPDATED) -->
|
||
<script>
|
||
function toast(msg, ms){ if (typeof window.showToast === 'function') showToast(msg, ms); else console.log('[toast]', msg); }
|
||
const actionIconCache = new Map();
|
||
async function getActionIconURL(actionName){
|
||
if (actionIconCache.has(actionName)) return actionIconCache.get(actionName);
|
||
for (const url of iconCandidateURLs(actionName)){
|
||
try{
|
||
const r = await fetch(url, { cache: 'force-cache' });
|
||
if (!r.ok) continue;
|
||
const blob = await r.blob();
|
||
const objectURL = URL.createObjectURL(blob);
|
||
actionIconCache.set(actionName, objectURL);
|
||
return objectURL;
|
||
}catch(_){}
|
||
}
|
||
return '/web/images/attack.png';
|
||
}
|
||
function iconCandidateURLs(actionName){
|
||
const n = encodeURIComponent(actionName);
|
||
return [
|
||
`/actions_icons/${n}.png`,
|
||
`/get_status_icon?action=${n}`,
|
||
`/images/status/${n}/${n}.bmp`,
|
||
`/resources/images/status/${n}/${n}.bmp`,
|
||
];
|
||
}
|
||
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', function(){
|
||
const page = this.dataset.page;
|
||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||
this.classList.add('active');
|
||
document.querySelectorAll('.sidebar-page').forEach(sp=>sp.style.display='none');
|
||
document.getElementById(page+'-sidebar').style.display='block';
|
||
document.querySelectorAll('.page-content').forEach(pc=>pc.classList.remove('active'));
|
||
document.getElementById(page+'-page')?.classList.add('active');
|
||
if(page==='attacks') loadAttacks?.();
|
||
if(page==='images') loadActions?.();loadCharacters?.();
|
||
if(page==='comments') loadSections?.();
|
||
});
|
||
});
|
||
|
||
/* === ATTACKS === */
|
||
const attackList = document.getElementById('attacks-list');
|
||
const addAttackBtn = document.getElementById('add-attack-btn');
|
||
const removeAttackBtn = document.getElementById('remove-attack-btn');
|
||
const saveAttackBtn = document.getElementById('save-attack-btn');
|
||
const restoreAttackBtn = document.getElementById('restore-attack-btn');
|
||
const attackEditor = document.getElementById('editor-textarea');
|
||
const editorTitle = document.getElementById('editor-title');
|
||
const emptyAttacksHint = document.getElementById('empty-attacks-hint');
|
||
let currentAttack = null;
|
||
|
||
function normalizeAttacks(data){
|
||
// support {attacks:[{name, image, enabled}] } ou la simple liste
|
||
if (Array.isArray(data)) return data;
|
||
if (data && Array.isArray(data.attacks)) return data.attacks;
|
||
return [];
|
||
}
|
||
|
||
function setDot(el, enabled){
|
||
el.dataset.enabled = enabled ? '1' : '0';
|
||
el.classList.toggle('on', !!enabled);
|
||
el.title = enabled ? 'Désactiver l’action' : 'Activer l’action';
|
||
el.setAttribute('aria-pressed', enabled ? 'true' : 'false');
|
||
}
|
||
|
||
async function toggleEnabled(name, dot){
|
||
const target = dot.dataset.enabled !== '1'; // on veut passer à 1 si off
|
||
const prev = dot.dataset.enabled === '1';
|
||
setDot(dot, target); // optimistic
|
||
|
||
try{
|
||
const r = await fetch('/actions/set_enabled', {
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ action_name: name, enabled: target ? 1 : 0 })
|
||
});
|
||
const d = await r.json();
|
||
if(d.status !== 'success') throw new Error(d.message || 'set_enabled failed');
|
||
toast(`${name} ${target ? 'activée' : 'désactivée'}`, 1800);
|
||
}catch(e){
|
||
// rollback
|
||
setDot(dot, prev);
|
||
console.error(e);
|
||
toast('❌ Erreur lors du changement d’état', 2600);
|
||
}
|
||
}
|
||
|
||
function loadAttacks(){
|
||
fetch('/get_attacks')
|
||
.then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
||
.then(async data=>{
|
||
const attacks = normalizeAttacks(data)
|
||
.map(a=>({name: a.name || a.id || 'Unnamed', enabled: Number(a.enabled ?? a.b_enabled ?? 0)}))
|
||
.sort((a,b)=>a.name.localeCompare(b.name, undefined, {sensitivity:'base', numeric:true}));
|
||
|
||
attackList.innerHTML = '';
|
||
emptyAttacksHint.style.display = attacks.length ? 'none' : 'block';
|
||
for (const attack of attacks){
|
||
const name = attack.name;
|
||
const li = document.createElement('li'); li.className='card'; li.dataset.attackName=name;
|
||
|
||
const img = document.createElement('img');
|
||
getActionIconURL(name).then(url => img.src = url);
|
||
|
||
const span = document.createElement('span'); span.textContent=name;
|
||
|
||
const dot = document.createElement('button');
|
||
dot.type = 'button';
|
||
dot.className = 'enable-dot';
|
||
setDot(dot, !!attack.enabled);
|
||
dot.addEventListener('click', (e)=>{ e.stopPropagation(); toggleEnabled(name, dot); });
|
||
|
||
li.append(img, span, dot);
|
||
li.addEventListener('click', ()=>selectAttack(name, li));
|
||
attackList.appendChild(li);
|
||
}
|
||
})
|
||
.catch(err=>{
|
||
console.error('Error fetching attacks:', err);
|
||
attackList.innerHTML=''; emptyAttacksHint.style.display='block';
|
||
emptyAttacksHint.textContent='Failed to load attacks. Check /get_attacks.';
|
||
});
|
||
}
|
||
|
||
function selectAttack(attackName, node){
|
||
document.querySelectorAll('#attacks-list .card').forEach(n=>n.classList.remove('selected'));
|
||
if(node) node.classList.add('selected');
|
||
currentAttack = attackName; editorTitle.textContent = attackName; attackEditor.disabled = false;
|
||
fetch(`/get_attack_content?name=${encodeURIComponent(attackName)}`)
|
||
.then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
||
.then(data=>{ if(data && data.status==='success'){ attackEditor.value = data.content ?? ''; } else { toast('❌ '+(data?.message||'Unknown error'), 3200); } })
|
||
.catch(err=>{ console.error('Error fetching attack content:', err); attackEditor.value=''; });
|
||
}
|
||
addAttackBtn?.addEventListener('click', ()=>{
|
||
const fileInput = document.createElement('input'); fileInput.type='file'; fileInput.accept='.py';
|
||
fileInput.onchange = ()=>{
|
||
const file = fileInput.files[0]; if(!file) return;
|
||
const formData = new FormData(); formData.append('attack_file', file);
|
||
fetch('/add_attack', { method:'POST', body:formData })
|
||
.then(r=>r.json()).then(data=>{
|
||
if(data.status==='success'){ toast('Attack imported successfully.',2400); loadAttacks(); }
|
||
else { toast('❌ Error: '+(data.message||'Unknown'),3200); }
|
||
})
|
||
.catch(()=>toast('❌ Error importing attack.',3000));
|
||
};
|
||
fileInput.click();
|
||
});
|
||
removeAttackBtn?.addEventListener('click', ()=>{
|
||
if(!currentAttack) return toast('⚠️ Please select an attack to remove.',2200);
|
||
if(!confirm(`Remove attack "${currentAttack}"?`)) return;
|
||
fetch('/remove_attack', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:currentAttack}) })
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
if(data.status==='success'){
|
||
toast('✅ Attack removed.',2400);
|
||
attackEditor.value=''; attackEditor.disabled=true; editorTitle.textContent='Select an Attack'; currentAttack=null; loadAttacks();
|
||
} else { toast('❌ Error: '+(data.message||'Unknown'),3200); }
|
||
})
|
||
.catch(()=>toast('❌ Error removing attack.',3000));
|
||
});
|
||
saveAttackBtn?.addEventListener('click', ()=>{
|
||
if(!currentAttack) return toast('⚠️ No attack selected.',2200);
|
||
const content = attackEditor.value;
|
||
fetch('/save_attack', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ name: currentAttack, content }) })
|
||
.then(r=>r.json())
|
||
.then(data=>{ if(data.status==='success'){ toast(`✅ Attack <b>${currentAttack}</b> saved.`,2400); } else { toast('❌ Error: '+(data.message||'Unknown'),3200); } })
|
||
.catch(()=>toast('❌ Error saving attack.',3000));
|
||
});
|
||
restoreAttackBtn?.addEventListener('click', ()=>{
|
||
if(!currentAttack) return toast('⚠️ No attack selected.',2200);
|
||
if(!confirm(`Restore "${currentAttack}" to default?`)) return;
|
||
fetch('/restore_attack', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ name: currentAttack }) })
|
||
.then(r=>r.json())
|
||
.then(data=>{ if(data.status==='success'){ toast(`✅ Attack <b>${currentAttack}</b> restored to default.`,2400); selectAttack(currentAttack); } else { toast('❌ Error: '+(data.message||'Unknown'),3200); } })
|
||
.catch(()=>toast('❌ Error restoring attack.',3000));
|
||
});
|
||
|
||
/* === IMAGES === */
|
||
let editMode=false, selectedItem=null, selectedType=null, selectedImages=new Set();
|
||
let cacheImages=[], cacheSrcResolver=null; let sortKey='name', sortDir=1; const grid=document.getElementById('image-container');
|
||
|
||
function skeletonGrid(n=12){ grid.innerHTML=''; for(let i=0;i<n;i++){ const sk=document.createElement('div'); sk.className='skeleton'; grid.appendChild(sk);} }
|
||
function sortImages(list){
|
||
const cmpName=(a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true,sensitivity:'base'})*sortDir;
|
||
const area=im=>(im.width||0)*(im.height||0);
|
||
const cmpDim=(a,b)=>(area(a)-area(b))*sortDir||cmpName(a,b);
|
||
return [...list].sort(sortKey==='name'?cmpName:cmpDim);
|
||
}
|
||
function renderImages(images, srcForName){
|
||
cacheImages = images.map(im => {
|
||
const nm = (typeof im === 'string') ? im : (im.name || im.filename || im.file || String(im));
|
||
const name = (nm || '').trim();
|
||
return { name, width: im.width, height: im.height };
|
||
});
|
||
cacheSrcResolver = srcForName;
|
||
renderCache();
|
||
}
|
||
function renderCache(){
|
||
const q = (document.getElementById('search-input')?.value || '').trim().toLowerCase();
|
||
const list = sortImages(cacheImages).filter(im => !q || (im.name||'').toLowerCase().includes(q));
|
||
grid.innerHTML = '';
|
||
list.forEach(im => {
|
||
const name = im.name;
|
||
const tile = document.createElement('div'); tile.className='image-item'; tile.dataset.imageName=name;
|
||
const img = document.createElement('img'); img.src = cacheSrcResolver(name);
|
||
const info = document.createElement('div'); info.className='image-info';
|
||
info.textContent = (im.width&&im.height) ? `${name} (${im.width}x${im.height})` : name;
|
||
const ring=document.createElement('div'); ring.className='select-ring';
|
||
const tick=document.createElement('div'); tick.className='tick-overlay'; tick.textContent='✓';
|
||
tile.append(img,info,ring,tick);
|
||
tile.addEventListener('click',()=>{ if(!editMode) return;
|
||
tile.classList.toggle('selected');
|
||
tile.classList.contains('selected') ? selectedImages.add(name) : selectedImages.delete(name);
|
||
});
|
||
tile.addEventListener('dblclick',()=>{
|
||
const m=document.createElement('div'); m.className='modal'; m.style.display='flex';
|
||
m.innerHTML=`<div class="modal-content"><span class="close" data-x>×</span>
|
||
<img src="${cacheSrcResolver(name)}" style="width:100%;height:auto;display:block;background:#0b0e13;object-fit:contain;border-radius:8px">
|
||
</div>`;
|
||
document.body.appendChild(m);
|
||
m.addEventListener('click',e=>{ if(e.target.classList.contains('modal')||e.target.hasAttribute('data-x')) m.remove(); });
|
||
});
|
||
grid.appendChild(tile);
|
||
});
|
||
document.querySelectorAll('.image-item').forEach(it=>it.classList.toggle('selectable',editMode));
|
||
}
|
||
function updateButtonVisibility(){
|
||
const body=document.body;
|
||
body.classList.remove('status-mode','static-mode','web-mode','icons-mode');
|
||
if(selectedType==='action') body.classList.add('status-mode');
|
||
else if(selectedType==='static') body.classList.add('static-mode');
|
||
else if(selectedType==='web') body.classList.add('web-mode');
|
||
else if(selectedType==='icons') body.classList.add('icons-mode');
|
||
|
||
// Optionally hide resize button for web/icons
|
||
const rb = document.getElementById('resize-images-btn');
|
||
if (rb) rb.style.display = (selectedType==='web' || selectedType==='icons') ? 'none' : 'inline-block';
|
||
}
|
||
function updateDeleteActionButton(){
|
||
const btn=document.getElementById('delete-action-btn');
|
||
if(btn) btn.disabled=!(selectedType==='action'&&selectedItem);
|
||
}
|
||
function exitEditMode(){
|
||
editMode=false; document.body.classList.remove('edit-mode'); selectedImages.clear();
|
||
document.querySelectorAll('.image-item.selected').forEach(i=>i.classList.remove('selected'));
|
||
const b=document.getElementById('edit-mode-toggle-btn');
|
||
if(b){ b.textContent='Enter Edit Mode'; b.classList.remove('edit-mode-active'); }
|
||
document.querySelectorAll('.edit-only').forEach(btn=>btn.style.display='none');
|
||
document.querySelectorAll('.image-item').forEach(it=>it.classList.toggle('selectable',false));
|
||
}
|
||
function toggleEditMode(){
|
||
if(!selectedItem){ alert('Please select an action or image section.'); return; }
|
||
editMode=!editMode; const b=document.getElementById('edit-mode-toggle-btn');
|
||
if(editMode){
|
||
document.body.classList.add('edit-mode'); b.textContent='Exit Edit Mode'; b.classList.add('edit-mode-active');
|
||
document.querySelectorAll('.edit-only').forEach(btn=>btn.style.display='inline-block');
|
||
} else { exitEditMode(); }
|
||
}
|
||
function selectItem(item){
|
||
document.querySelectorAll('#action-list .card, #library-list .card, #web-images-list .card, #actions-icons-list .card').forEach(i=>i.classList.remove('selected'));
|
||
item.classList.add('selected'); selectedItem=item; selectedType=item.getAttribute('data-type'); selectedImages.clear();
|
||
updateButtonVisibility(); updateDeleteActionButton(); exitEditMode();
|
||
if(selectedType==='action') loadActionImages(item.getAttribute('data-name'));
|
||
else if(selectedType==='static') loadStaticImagesIntoContainer();
|
||
else if(selectedType==='web') loadWebImagesIntoContainer();
|
||
else if(selectedType==='icons') loadActionsIconsIntoContainer();
|
||
}
|
||
function loadActions(){
|
||
fetch('/get_actions')
|
||
.then(r=>r.json())
|
||
.then(async data=>{
|
||
// Status Images (per-action folders)
|
||
const list = document.getElementById('action-list'); list.innerHTML='';
|
||
data.actions
|
||
.slice()
|
||
.sort((a,b)=>a.name.localeCompare(b.name, undefined,{sensitivity:'base', numeric:true}))
|
||
.forEach(action=>{
|
||
const li=document.createElement('li'); li.className='card'; li.setAttribute('data-name', action.name); li.setAttribute('data-type','action');
|
||
const img=document.createElement('img'); getActionIconURL(action.name).then(url => img.src = url);
|
||
const span=document.createElement('span'); span.textContent=action.name;
|
||
li.append(img, span);
|
||
li.addEventListener('click', function(){ selectItem(li); loadActionImages(action.name); });
|
||
list.appendChild(li);
|
||
});
|
||
|
||
// NEW: Web Images (single card)
|
||
const webList=document.getElementById('web-images-list'); webList.innerHTML='';
|
||
const webLi=document.createElement('li'); webLi.className='card'; webLi.setAttribute('data-name','web_images'); webLi.setAttribute('data-type','web');
|
||
const webImg=document.createElement('img'); webImg.src='/web/images/icon-192x192.png'; webImg.onerror=function(){ this.src='/web/images/attack.png' };
|
||
const webSpan=document.createElement('span'); webSpan.textContent='Web Images';
|
||
webLi.append(webImg, webSpan);
|
||
webLi.addEventListener('click', function(){ selectItem(webLi); loadWebImagesIntoContainer(); });
|
||
webList.appendChild(webLi);
|
||
|
||
// NEW: Actions Images (single card)
|
||
const iconsList=document.getElementById('actions-icons-list'); iconsList.innerHTML='';
|
||
const iconsLi=document.createElement('li'); iconsLi.className='card'; iconsLi.setAttribute('data-name','actions_icons'); iconsLi.setAttribute('data-type','icons');
|
||
const iconsImg=document.createElement('img'); iconsImg.src='/web/images/attack.png';
|
||
const iconsSpan=document.createElement('span'); iconsSpan.textContent='Actions Images';
|
||
iconsLi.append(iconsImg, iconsSpan);
|
||
iconsLi.addEventListener('click', function(){ selectItem(iconsLi); loadActionsIconsIntoContainer(); });
|
||
iconsList.appendChild(iconsLi);
|
||
|
||
// Static Images (single card)
|
||
const libraryList=document.getElementById('library-list'); libraryList.innerHTML='';
|
||
const staticLi=document.createElement('li'); staticLi.className='card'; staticLi.setAttribute('data-name','static_images'); staticLi.setAttribute('data-type','static');
|
||
const staticImg=document.createElement('img'); staticImg.src='/web/images/static_icon.png'; staticImg.onerror=function(){ this.src='/web/images/attack.png' };
|
||
const staticSpan=document.createElement('span'); staticSpan.textContent='Static Images';
|
||
staticLi.append(staticImg, staticSpan);
|
||
staticLi.addEventListener('click', function(){ selectItem(staticLi); loadStaticImagesIntoContainer(); });
|
||
libraryList.appendChild(staticLi);
|
||
})
|
||
.catch(e=>{ console.error('Error loading actions:', e); alert('An error occurred while loading actions.'); });
|
||
}
|
||
document.getElementById('delete-character-btn')?.addEventListener('click',function(){
|
||
fetch('/list_characters')
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
const characters=data.characters;
|
||
const current=data.current_character;
|
||
const deletable=characters.filter(c=>c.name!=='BJORN');
|
||
if(!deletable.length){ alert('No characters available for deletion.'); return; }
|
||
const names=deletable.map(c=>c.name);
|
||
const ask=prompt('Enter the name of the character to delete:\n'+names.join('\n'));
|
||
if(ask&&names.includes(ask)){
|
||
if(ask===current){ if(!confirm(`You are about to delete the current character '${ask}'. Continue?`)) return; }
|
||
deleteCharacter(ask);
|
||
}else{ alert('Invalid character name.'); }
|
||
});
|
||
});
|
||
function resizeSelectedImages(){
|
||
const w=parseInt(document.getElementById('resize-width-input').value,10);
|
||
const h=parseInt(document.getElementById('resize-height-input').value,10);
|
||
if(!(w>0&&h>0)){ alert('Dimensions must be positive numbers.'); return; }
|
||
const payload={ type:selectedType, action:selectedType==='action'?selectedItem.getAttribute('data-name'):null, image_names:Array.from(selectedImages), width:w, height:h };
|
||
fetch('/resize_images',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){
|
||
hideModal('resize-images-modal'); selectedImages.clear(); exitEditMode();
|
||
if(selectedType==='action') loadActionImages(selectedItem.getAttribute('data-name'));
|
||
else if(selectedType==='static') loadStaticImagesIntoContainer();
|
||
else if(selectedType==='web') loadWebImagesIntoContainer();
|
||
else if(selectedType==='icons') loadActionsIconsIntoContainer();
|
||
} else { alert(`Error resizing images: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error resizing images:',e); alert('An error occurred while resizing images.'); });
|
||
}
|
||
function loadCharacters(){
|
||
fetch('/list_characters')
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
const ul = document.getElementById('character-list');
|
||
ul.innerHTML = '';
|
||
const current = data.current_character;
|
||
data.characters
|
||
.slice()
|
||
.sort((a,b)=>a.name.localeCompare(b.name, undefined, {sensitivity:'base'}))
|
||
.forEach(character=>{
|
||
const li = document.createElement('li');
|
||
li.className='card';
|
||
li.dataset.name=character.name;
|
||
const img=document.createElement('img');
|
||
img.src='/get_character_icon?character='+encodeURIComponent(character.name)+'&t='+Date.now();
|
||
img.onerror=function(){ this.src='/web/images/default_character_icon.png' };
|
||
const span=document.createElement('span');
|
||
span.textContent=character.name;
|
||
li.append(img,span);
|
||
if(character.name===current){
|
||
const check=document.createElement('span');
|
||
check.style.color='var(--_acid)';
|
||
check.style.fontSize='18px';
|
||
check.textContent='✔';
|
||
li.appendChild(check);
|
||
}
|
||
li.addEventListener('click',()=>selectCharacter(li));
|
||
ul.appendChild(li);
|
||
});
|
||
})
|
||
.catch(e=>{ console.error('Error loading characters:',e); alert('An error occurred while loading characters.'); });
|
||
}
|
||
function selectCharacter(li){
|
||
const name=li.getAttribute('data-name');
|
||
if(confirm(`Do you want to switch to character '${name}'? Any unsaved changes to the current character will be saved.`)){
|
||
switchCharacter(name);
|
||
}
|
||
}
|
||
function switchCharacter(characterName){
|
||
fetch('/switch_character',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({character_name:characterName})})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){ alert('Character switched successfully.'); loadCharacters(); loadActions(); loadStaticImagesIntoContainer(); }
|
||
else { alert(`Error switching character: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error switching character:',e); alert('An error occurred while switching character.'); });
|
||
}
|
||
document.getElementById('create-character-btn')?.addEventListener('click', function(){
|
||
const name=prompt('Enter a name for the new character:');
|
||
if(name) createCharacter(name);
|
||
});
|
||
function createCharacter(characterName){
|
||
fetch('/create_character',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({character_name:characterName})})
|
||
.then(r=>r.json())
|
||
.then(d=>{ if(d.status==='success'){ alert('Character created successfully.'); loadCharacters(); } else { alert(`Error creating character: ${d.message}`); } })
|
||
.catch(e=>{ console.error('Error creating character:',e); alert('An error occurred while creating character.'); });
|
||
}
|
||
function deleteCharacter(characterName){
|
||
if(!confirm(`Are you sure you want to delete character '${characterName}'? This action cannot be undone.`)) return;
|
||
fetch('/delete_character',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({character_name:characterName})})
|
||
.then(r=>r.json())
|
||
.then(d=>{ if(d.status==='success'){ alert('Character deleted successfully.'); loadCharacters(); loadActions(); } else { alert(`Error deleting character: ${d.message}`); } })
|
||
.catch(e=>{ console.error('Error deleting character:',e); alert('An error occurred while deleting the character.'); });
|
||
}
|
||
function loadActionImages(actionName){
|
||
skeletonGrid();
|
||
fetch('/get_action_images?action=' + encodeURIComponent(actionName))
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.status !== 'success') throw new Error(data.message || 'Error loading action images.');
|
||
renderImages(
|
||
data.images,
|
||
name => `/images/status/${encodeURIComponent(actionName)}/${encodeURIComponent(name)}`
|
||
);
|
||
})
|
||
.catch(e => { console.error('Error loading action images:', e); alert('An error occurred while loading action images.'); });
|
||
}
|
||
function loadStaticImagesIntoContainer(){
|
||
skeletonGrid();
|
||
fetch('/list_static_images_with_dimensions')
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
if(data.status!=='success') throw new Error(data.message||'Error loading static images.');
|
||
renderImages(data.images, name=>'/static_images/'+encodeURIComponent(name));
|
||
})
|
||
.catch(e=>{ console.error('Error loading static images:',e); alert('An error occurred while loading static images.'); });
|
||
}
|
||
|
||
// NEW: loaders for Web Images & Actions Icons
|
||
function loadWebImagesIntoContainer(){
|
||
skeletonGrid();
|
||
fetch('/list_web_images')
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
if(data.status!=='success') throw new Error(data.message||'Error loading web images.');
|
||
renderImages(data.images, name=>'/web/images/'+encodeURIComponent(name));
|
||
})
|
||
.catch(e=>{ console.error('Error loading web images:',e); alert('An error occurred while loading web images.'); });
|
||
}
|
||
function loadActionsIconsIntoContainer(){
|
||
skeletonGrid();
|
||
fetch('/list_actions_icons')
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
if(data.status!=='success') throw new Error(data.message||'Error loading action icons.');
|
||
renderImages(data.images, name=>'/actions_icons/'+encodeURIComponent(name));
|
||
})
|
||
.catch(e=>{ console.error('Error loading action icons:',e); alert('An error occurred while loading action icons.'); });
|
||
}
|
||
|
||
function showModal(id){ document.getElementById(id).style.display='flex'; }
|
||
function hideModal(target){
|
||
const el = (typeof target === 'string') ? document.getElementById(target) : target;
|
||
if (el && el.style) el.style.display = 'none';
|
||
}
|
||
window.addEventListener('click', e=>{ if(e.target.classList && e.target.classList.contains('modal')) e.target.style.display='none'; });
|
||
(function setupImagesUI(){
|
||
document.getElementById('delete-action-btn')?.addEventListener('click', deleteAction);
|
||
document.getElementById('add-action-btn')?.addEventListener('click', ()=>showModal('add-action-modal'));
|
||
document.getElementById('add-static-image-btn')?.addEventListener('click', ()=>showModal('add-static-modal'));
|
||
document.getElementById('add-status-image-btn')?.addEventListener('click', ()=>showModal('add-status-modal'));
|
||
|
||
// NEW buttons
|
||
document.getElementById('add-web-image-btn')?.addEventListener('click', ()=>showModal('add-web-image-modal'));
|
||
document.getElementById('add-icon-image-btn')?.addEventListener('click', ()=>showModal('add-icon-image-modal'));
|
||
|
||
document.getElementById('edit-mode-toggle-btn')?.addEventListener('click', toggleEditMode);
|
||
document.getElementById('rename-image-btn')?.addEventListener('click', ()=>{ if(selectedImages.size===1) showModal('rename-modal'); else alert('Please select exactly one image to rename.'); });
|
||
document.getElementById('replace-image-btn')?.addEventListener('click', ()=>{ if(selectedImages.size===1) showModal('replace-modal'); else alert('Please select exactly one image to replace.'); });
|
||
document.getElementById('resize-images-btn')?.addEventListener('click', ()=>{ if(selectedImages.size>0) showModal('resize-images-modal'); else alert('Please select at least one image to resize.'); });
|
||
document.getElementById('delete-images-btn')?.addEventListener('click', deleteSelectedImages);
|
||
document.getElementById('sync-missing-btn')?.addEventListener('click', syncMissing);
|
||
document.getElementById('add-characters-btn')?.addEventListener('click', ()=>{ if(selectedType==='action'&&selectedItem) showModal('add-characters-modal'); else alert('Please select an action before adding character images.'); });
|
||
document.getElementById('restore-default-actions-btn')?.addEventListener('click', function(){
|
||
if(confirm('Restore ALL defaults? (actions, images, comments)')){ restoreDefaultActionsBundle(); }
|
||
});
|
||
document.querySelectorAll('.modal-action .close').forEach(el => {
|
||
el.addEventListener('click', function () {
|
||
hideModal(this.closest('.modal-action'));
|
||
});
|
||
});
|
||
document.querySelectorAll('.modal-action .cancel-btn').forEach(btn => {
|
||
btn.addEventListener('click', function () {
|
||
hideModal(this.closest('.modal-action'));
|
||
});
|
||
});
|
||
document.addEventListener('click', e => {
|
||
if (e.target.matches('.modal [data-x]')) hideModal(e.target.closest('.modal'));
|
||
});
|
||
|
||
document.getElementById('create-action-btn')?.addEventListener('click', createNewAction);
|
||
document.getElementById('upload-static-image-btn')?.addEventListener('click', uploadStaticImage);
|
||
document.getElementById('upload-status-image-btn')?.addEventListener('click', uploadStatusImage);
|
||
document.getElementById('rename-image-confirm-btn')?.addEventListener('click', renameImage);
|
||
document.getElementById('replace-image-confirm-btn')?.addEventListener('click', replaceImage);
|
||
document.getElementById('upload-characters-btn')?.addEventListener('click', uploadCharacterImages);
|
||
document.getElementById('resize-images-confirm-btn')?.addEventListener('click', resizeSelectedImages);
|
||
|
||
// NEW upload handlers
|
||
document.getElementById('upload-web-image-btn')?.addEventListener('click', uploadWebImage);
|
||
document.getElementById('upload-icon-image-btn')?.addEventListener('click', uploadActionIcon);
|
||
|
||
document.querySelectorAll('#resize-images-modal .cancel-btn').forEach(btn=>btn.addEventListener('click', ()=>hideModal('resize-images-modal')));
|
||
document.querySelectorAll('.cancel-btn').forEach(btn=>btn.addEventListener('click', function(){ hideModal(this.closest('.modal').id); }));
|
||
const density=document.getElementById('density');
|
||
if(density){
|
||
document.documentElement.style.setProperty('--tile-min', density.value+'px');
|
||
density.addEventListener('input', e=>document.documentElement.style.setProperty('--tile-min', e.target.value+'px'));
|
||
}
|
||
const searchInput=document.getElementById('search-input'); searchInput?.addEventListener('input', renderCache);
|
||
const sortKeySel=document.getElementById('sort-key'); const sortDirBtn=document.getElementById('sort-dir');
|
||
sortKeySel?.addEventListener('change', ()=>{ sortKey=sortKeySel.value; renderCache(); });
|
||
sortDirBtn?.addEventListener('click', ()=>{ sortDir*=-1; sortDirBtn.textContent = sortDir===1?'↑':'↓'; renderCache(); });
|
||
})();
|
||
async function restoreDefaultActionsBundle(){
|
||
try{
|
||
const r = await fetch('/actions/restore_defaults', { method:'POST' });
|
||
const d = await r.json();
|
||
if(d.status !== 'success') throw new Error(d.message||'Restore failed');
|
||
for(const url of actionIconCache.values()) URL.revokeObjectURL(url);
|
||
actionIconCache.clear();
|
||
selectedItem=null; selectedType=null; selectedImages.clear();
|
||
grid.innerHTML=''; updateButtonVisibility(); updateDeleteActionButton();
|
||
loadActions?.(); loadAttacks?.(); loadSections?.();
|
||
alert('Defaults restored (actions, images, comments).');
|
||
}catch(e){
|
||
console.error(e); alert('Error restoring defaults: '+e.message);
|
||
}
|
||
}
|
||
async function fetchJSON(url){
|
||
const r = await fetch(url);
|
||
if(!r.ok) throw new Error(`${url} -> HTTP ${r.status}`);
|
||
return r.json();
|
||
}
|
||
function makePlaceholderIconBlob(actionName){
|
||
const size = 128;
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = size; canvas.height = size;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.fillStyle = '#0b0e13'; ctx.fillRect(0,0,size,size);
|
||
ctx.lineWidth = 8; ctx.strokeStyle = '#59b6ff';
|
||
ctx.beginPath(); ctx.arc(size/2, size/2, size/2 - 8, 0, Math.PI*2); ctx.stroke();
|
||
const initials = (actionName || 'A').split(/[^A-Za-z0-9]+/).filter(Boolean).slice(0,2).map(s=>s[0]).join('').toUpperCase() || 'A';
|
||
ctx.fillStyle = '#59b6ff'; ctx.font = 'bold 56px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(initials, size/2, size/2 + 4);
|
||
return new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||
}
|
||
async function statusIconExists(actionName){
|
||
for (const url of iconCandidateURLs(actionName)){
|
||
try{ const r = await fetch(url, { cache: 'no-cache' }); if (r.ok) return true; }catch(_){}
|
||
}
|
||
return false;
|
||
}
|
||
async function actionHasCharacterImages(actionName){
|
||
try{ const data = await fetchJSON('/get_action_images?action=' + encodeURIComponent(actionName));
|
||
return data.status === 'success' && Array.isArray(data.images) && data.images.length > 0;
|
||
}catch(_){ return false; }
|
||
}
|
||
async function fetchActionIconBlob(actionName){
|
||
for (const url of iconCandidateURLs(actionName)){
|
||
try{ const r = await fetch(url, { cache: 'no-cache' }); if (r.ok) return await r.blob(); }catch(_){}
|
||
}
|
||
return await makePlaceholderIconBlob(actionName);
|
||
}
|
||
async function ensureStatusImageFromIcon(actionName){
|
||
const bmpPath = `/images/status/${encodeURIComponent(actionName)}/${encodeURIComponent(actionName)}.bmp`;
|
||
try { const r = await fetch(bmpPath, { cache: 'no-cache' }); if (r.ok) return false; } catch(_) {}
|
||
const blob = await fetchActionIconBlob(actionName);
|
||
const fd = new FormData();
|
||
fd.append('type', 'action');
|
||
fd.append('action_name', actionName);
|
||
fd.append('status_image', new File([blob], `${actionName}.bmp`, { type: 'image/bmp' }));
|
||
const r = await fetch('/upload_status_image', { method:'POST', body: fd });
|
||
const d = await r.json();
|
||
if (d.status !== 'success') throw new Error(d.message || 'upload_status_image failed');
|
||
return true;
|
||
}
|
||
async function ensureAtLeastOneCharacterImageFromIcon(actionName){
|
||
const hasChar = await actionHasCharacterImages(actionName);
|
||
if (hasChar) return false;
|
||
const blob = await fetchActionIconBlob(actionName);
|
||
const fd = new FormData();
|
||
fd.append('action_name', actionName);
|
||
fd.append('character_images', new File([blob], `${actionName}.png`, { type: blob.type || 'image/png' }));
|
||
const r = await fetch('/upload_character_images', { method:'POST', body: fd });
|
||
const d = await r.json();
|
||
if (d.status !== 'success') throw new Error(d.message || 'upload_character_images failed');
|
||
return true;
|
||
}
|
||
async function ensureCommentsSection(sectionName, sectionsSet){
|
||
if (sectionsSet.has(sectionName)) return;
|
||
await fetch('/save_comments', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ section: sectionName, comments: ["Add comment for this action"] })
|
||
});
|
||
sectionsSet.add(sectionName);
|
||
}
|
||
async function syncMissing(){
|
||
try{
|
||
const attacksResp = await fetchJSON('/get_attacks');
|
||
const attacks = Array.isArray(attacksResp) ? attacksResp : (Array.isArray(attacksResp.attacks) ? attacksResp.attacks : []);
|
||
const attackNames = attacks.map(a => a.name || a.id).filter(Boolean);
|
||
if (!attackNames.length){ toast('No attacks to sync.', 2200); return; }
|
||
const sectionsResp = await fetchJSON('/get_sections');
|
||
const sectionsSet = new Set((sectionsResp.sections || []).map(String));
|
||
let createdComments = 0, createdStatus = 0, createdChars = 0;
|
||
for (const name of attackNames){
|
||
if (!sectionsSet.has(name)){ await ensureCommentsSection(name, sectionsSet); createdComments++; }
|
||
if (await ensureStatusImageFromIcon(name)) createdStatus++;
|
||
if (await ensureAtLeastOneCharacterImageFromIcon(name)) createdChars++;
|
||
}
|
||
actionIconCache.clear();
|
||
loadActions?.(); loadSections?.();
|
||
toast(`Sync done. New comments: ${createdComments}, status images: ${createdStatus}, character images: ${createdChars}.`, 3800);
|
||
} catch(e){
|
||
console.error('Sync Missing error:', e);
|
||
alert('Sync Missing failed: ' + e.message);
|
||
}
|
||
}
|
||
async function createNewAction(){
|
||
const name = document.getElementById('new-action-name').value.trim();
|
||
const py = document.getElementById('new-attack-file').files[0];
|
||
const icon = document.getElementById('new-status-icon').files[0];
|
||
const chars= document.getElementById('new-character-images').files[0] ? document.getElementById('new-character-images').files : [];
|
||
if(!name || !py || !icon){ alert('Please provide Action Name, Python .py, and a Status Image.'); return; }
|
||
const fd = new FormData();
|
||
fd.append('action_name', name);
|
||
fd.append('attack_file', py);
|
||
fd.append('status_icon', icon);
|
||
Array.from(chars||[]).forEach(f => fd.append('character_images', f));
|
||
fd.append('create_comments_section', '1');
|
||
try{
|
||
const r = await fetch('/action/create', { method:'POST', body:fd });
|
||
const d = await r.json();
|
||
if(d.status !== 'success') throw new Error(d.message||'Create failed');
|
||
hideModal('add-action-modal'); resetForm('add-action-modal');
|
||
actionIconCache.delete(name); getActionIconURL(name);
|
||
loadActions?.(); loadAttacks?.(); loadSections?.();
|
||
setTimeout(()=>{
|
||
const it=[...document.querySelectorAll('#action-list .card')].find(i=>i.getAttribute('data-name')===name);
|
||
if(it){ selectItem(it); }
|
||
}, 200);
|
||
}catch(e){ console.error(e); alert('Error creating action: '+e.message); }
|
||
}
|
||
function uploadStaticImage(){
|
||
const file=document.getElementById('static-image-file').files[0];
|
||
if(!file){ alert('Please select an image to upload.'); return; }
|
||
const fd=new FormData(); fd.append('static_image',file);
|
||
fetch('/upload_static_image',{method:'POST',body:fd})
|
||
.then(r=>r.json()).then(d=>{
|
||
if(d.status==='success'){ hideModal('add-static-modal'); loadStaticImagesIntoContainer(); resetForm('add-static-modal'); }
|
||
else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error uploading static image:',e); alert('An error occurred while uploading the static image.'); });
|
||
}
|
||
function uploadStatusImage(){
|
||
if(!selectedItem||selectedType!=='action'){ alert('Please select an action before adding a status image.'); return; }
|
||
const file=document.getElementById('status-image-file').files[0];
|
||
if(!file){ alert('Please select a status image to upload.'); return; }
|
||
const fd=new FormData(); fd.append('type','action'); fd.append('action_name',selectedItem.getAttribute('data-name')); fd.append('status_image',file);
|
||
fetch('/upload_status_image',{method:'POST',body:fd})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){
|
||
hideModal('add-status-modal'); loadActions(); loadActionImages(selectedItem.getAttribute('data-name')); resetForm('add-status-modal');
|
||
actionIconCache.delete(selectedItem.getAttribute('data-name')); getActionIconURL(selectedItem.getAttribute('data-name'));
|
||
} else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error uploading status image:',e); alert('An error occurred while uploading the status image.'); });
|
||
}
|
||
function uploadCharacterImages(){
|
||
if(!selectedItem||selectedType!=='action'){ alert('Please select an action before adding character images.'); return; }
|
||
const files=document.getElementById('character-images-upload').files;
|
||
if(!files.length){ alert('Please select at least one character image to upload.'); return; }
|
||
const fd=new FormData(); fd.append('action_name',selectedItem.getAttribute('data-name')); Array.from(files).forEach(f=>fd.append('character_images',f));
|
||
fetch('/upload_character_images',{method:'POST',body:fd})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){ hideModal('add-characters-modal'); loadActionImages(selectedItem.getAttribute('data-name')); resetForm('add-characters-modal'); }
|
||
else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error uploading character images:',e); alert('An error occurred while uploading character images.'); });
|
||
}
|
||
function uploadWebImage(){
|
||
const file=document.getElementById('web-image-file').files[0];
|
||
if(!file){ alert('Please select an image to upload.'); return; }
|
||
const fd=new FormData(); fd.append('web_image',file);
|
||
fetch('/upload_web_image',{method:'POST',body:fd})
|
||
.then(r=>r.json()).then(d=>{
|
||
if(d.status==='success'){ hideModal('add-web-image-modal'); loadWebImagesIntoContainer(); resetForm('add-web-image-modal'); }
|
||
else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error uploading web image:',e); alert('An error occurred while uploading the web image.'); });
|
||
}
|
||
function uploadActionIcon(){
|
||
const file=document.getElementById('icon-image-file').files[0];
|
||
if(!file){ alert('Please select an image to upload.'); return; }
|
||
const fd=new FormData(); fd.append('icon_image',file);
|
||
fetch('/upload_actions_icon',{method:'POST',body:fd})
|
||
.then(r=>r.json()).then(d=>{
|
||
if(d.status==='success'){ hideModal('add-icon-image-modal'); loadActionsIconsIntoContainer(); resetForm('add-icon-image-modal'); }
|
||
else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error uploading action icon:',e); alert('An error occurred while uploading the action icon.'); });
|
||
}
|
||
function renameImage(){
|
||
const newName=document.getElementById('new-name').value.trim(); if(!newName){ alert('New name cannot be empty.'); return; }
|
||
const oldName=Array.from(selectedImages)[0];
|
||
let entityType = (selectedType==='action') ? 'image'
|
||
: (selectedType==='static') ? 'static'
|
||
: (selectedType==='web') ? 'web'
|
||
: (selectedType==='icons') ? 'icons' : 'static';
|
||
const payload={ type:entityType, action:selectedType==='action'?selectedItem.getAttribute('data-name'):null, old_name:oldName, new_name:newName };
|
||
fetch('/rename_image',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){
|
||
hideModal('rename-modal'); selectedImages.clear();
|
||
if(selectedType==='action') loadActionImages(selectedItem.getAttribute('data-name'));
|
||
else if(selectedType==='static') loadStaticImagesIntoContainer();
|
||
else if(selectedType==='web') loadWebImagesIntoContainer();
|
||
else if(selectedType==='icons') loadActionsIconsIntoContainer();
|
||
resetForm('rename-modal');
|
||
} else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error renaming image:',e); alert('An error occurred while renaming the image.'); });
|
||
}
|
||
function replaceImage(){
|
||
const file=document.getElementById('replace-image-file').files[0]; if(!file){ alert('Please select a new image to replace with.'); return; }
|
||
if(selectedImages.size!==1){ alert('Please select exactly one image to replace.'); return; }
|
||
const imageName=Array.from(selectedImages)[0];
|
||
const fd=new FormData(); fd.append('type',selectedType); fd.append('image_name',imageName); if(selectedType==='action') fd.append('action',selectedItem.getAttribute('data-name')); fd.append('new_image',file);
|
||
fetch('/replace_image',{method:'POST',body:fd})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){
|
||
hideModal('replace-modal'); selectedImages.clear();
|
||
if(selectedType==='action') loadActionImages(selectedItem.getAttribute('data-name'));
|
||
else if(selectedType==='static') loadStaticImagesIntoContainer();
|
||
else if(selectedType==='web') loadWebImagesIntoContainer();
|
||
else if(selectedType==='icons') loadActionsIconsIntoContainer();
|
||
resetForm('replace-modal');
|
||
} else { alert(`Error: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error replacing image:',e); alert('An error occurred while replacing the image.'); });
|
||
}
|
||
function deleteSelectedImages(){
|
||
if(!selectedImages.size){ alert('Please select at least one image to delete.'); return; }
|
||
if(!confirm('Are you sure you want to delete the selected images?')) return;
|
||
const payload={ type:selectedType, action:selectedType==='action'?selectedItem.getAttribute('data-name'):null, image_names:Array.from(selectedImages) };
|
||
fetch('/delete_images',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='success'){
|
||
if(selectedType==='action') loadActionImages(selectedItem.getAttribute('data-name'));
|
||
else if(selectedType==='static') loadStaticImagesIntoContainer();
|
||
else if(selectedType==='web') loadWebImagesIntoContainer();
|
||
else if(selectedType==='icons') loadActionsIconsIntoContainer();
|
||
selectedImages.clear();
|
||
} else { alert(`Error deleting images: ${d.message}`); }
|
||
})
|
||
.catch(e=>{ console.error('Error deleting images:',e); alert('An error occurred while deleting the images.'); });
|
||
}
|
||
async function deleteAction(){
|
||
if(!selectedItem||selectedType!=='action'){ alert('Please select an action to delete.'); return; }
|
||
const actionName=selectedItem.getAttribute('data-name');
|
||
if(!confirm(`Delete action '${actionName}' (script, images, comments)?`)) return;
|
||
try{
|
||
const r = await fetch('/action/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action_name:actionName})});
|
||
const d = await r.json();
|
||
if(d.status!=='success') throw new Error(d.message||'Delete failed');
|
||
if(actionIconCache.has(actionName)){ URL.revokeObjectURL(actionIconCache.get(actionName)); actionIconCache.delete(actionName); }
|
||
selectedItem=null; selectedType=null; selectedImages.clear(); grid.innerHTML=''; updateButtonVisibility(); updateDeleteActionButton();
|
||
loadActions?.(); loadAttacks?.(); loadSections?.();
|
||
}catch(e){ console.error('Error deleting action:',e); alert('Error deleting action: '+e.message); }
|
||
}
|
||
function resetForm(modalId){
|
||
const modal=document.getElementById(modalId); if(!modal) return;
|
||
modal.querySelectorAll('input').forEach(input=>{
|
||
if(input.type==='text') input.value='';
|
||
else if(input.type==='file') input.value=null;
|
||
else if(input.type==='checkbox') input.checked=false;
|
||
});
|
||
}
|
||
|
||
// =============== COMMENTS (all functions) ===============
|
||
const commentsEditor = document.getElementById('comments-editor');
|
||
const sectionTitleEl = document.getElementById('section-title');
|
||
const sectionListEl = document.getElementById('section-list');
|
||
const addSectionBtn = document.getElementById('add-section-btn');
|
||
const deleteSectionBtn = document.getElementById('delete-section-btn');
|
||
const restoreDefaultBtn = document.getElementById('restore-default-btn');
|
||
const saveCommentsBtn = document.getElementById('save-comments-btn');
|
||
const selectAllBtn = document.getElementById('select-all-btn');
|
||
|
||
function applyPlaceholder(){ if(!commentsEditor.innerText.trim()){ commentsEditor.classList.add('placeholder'); commentsEditor.textContent = commentsEditor.dataset.placeholder || 'Comments will be displayed here...'; } }
|
||
function ensureStructureOnFirstEdit(){ if(commentsEditor.classList.contains('placeholder')){ commentsEditor.classList.remove('placeholder'); commentsEditor.innerHTML = '<div class="comment-line"><br></div>'; } }
|
||
function normalizeLinesAfterInput(){
|
||
if(commentsEditor.classList.contains('placeholder')) return;
|
||
const nodes = Array.from(commentsEditor.childNodes);
|
||
nodes.forEach(node=>{
|
||
if(node.nodeType===Node.TEXT_NODE){
|
||
const div=document.createElement('div'); div.className='comment-line'; div.textContent=node.textContent; commentsEditor.replaceChild(div,node);
|
||
} else if(node.nodeType===Node.ELEMENT_NODE && !node.classList.contains('comment-line')){
|
||
const wrap=document.createElement('div'); wrap.className='comment-line'; wrap.appendChild(node.cloneNode(true)); commentsEditor.replaceChild(wrap,node);
|
||
}
|
||
});
|
||
}
|
||
applyPlaceholder();
|
||
|
||
commentsEditor?.addEventListener('input', ()=>{
|
||
if(!commentsEditor.textContent.trim()){ commentsEditor.innerHTML=''; applyPlaceholder(); return; }
|
||
normalizeLinesAfterInput();
|
||
});
|
||
commentsEditor?.addEventListener('click', ()=>ensureStructureOnFirstEdit());
|
||
commentsEditor?.addEventListener('keydown', (e)=>{
|
||
if(e.key!=="Enter") return;
|
||
e.preventDefault();
|
||
ensureStructureOnFirstEdit();
|
||
const sel=window.getSelection(); if(!sel||sel.rangeCount===0) return;
|
||
const range=sel.getRangeAt(0);
|
||
const current = range.startContainer.nodeType===3 ? range.startContainer.parentElement : range.startContainer;
|
||
const newLine=document.createElement('div'); newLine.className='comment-line'; newLine.innerHTML='<br>';
|
||
const line = current && current.closest ? current.closest('.comment-line') : null;
|
||
if(line && line.parentNode){ line.parentNode.insertBefore(newLine,line.nextSibling); } else { commentsEditor.appendChild(newLine); }
|
||
const r=document.createRange(); r.selectNodeContents(newLine); r.collapse(true);
|
||
sel.removeAllRanges(); sel.addRange(r);
|
||
});
|
||
|
||
selectAllBtn?.addEventListener('click', ()=>{
|
||
const range=document.createRange(); range.selectNodeContents(commentsEditor);
|
||
const sel=window.getSelection(); sel.removeAllRanges(); sel.addRange(range); commentsEditor.scrollTop=0;
|
||
});
|
||
|
||
function attachSectionEvents(){
|
||
document.querySelectorAll('#section-list .card').forEach(card=>{
|
||
card.removeEventListener('click', onSectionClick);
|
||
card.addEventListener('click', onSectionClick);
|
||
});
|
||
}
|
||
function onSectionClick(){
|
||
document.querySelectorAll('#section-list .card').forEach(it=>it.classList.remove('selected'));
|
||
this.classList.add('selected'); loadComments(this.dataset.section); updateDeleteSectionButton();
|
||
}
|
||
function updateDeleteSectionButton(){
|
||
const selected=document.querySelector('#section-list .card.selected');
|
||
if(deleteSectionBtn) deleteSectionBtn.disabled=!selected;
|
||
}
|
||
|
||
function loadSections(){
|
||
fetch('/get_sections').then(r=>r.json()).then(async data=>{
|
||
if(data.status!=='success') throw new Error(data.message||'get_sections failed');
|
||
sectionListEl.innerHTML=''; const hint=document.getElementById('empty-comments-hint');
|
||
const sections = (Array.isArray(data.sections)?data.sections:[]).slice()
|
||
.sort((a,b)=>String(a).localeCompare(String(b), undefined, {sensitivity:'base', numeric:true}));
|
||
|
||
if(!sections.length){ hint.style.display='block'; updateDeleteSectionButton(); return; }
|
||
hint.style.display='none';
|
||
|
||
for (const section of sections){
|
||
const li=document.createElement('li'); li.className='card'; li.dataset.section=section;
|
||
const img=document.createElement('img'); getActionIconURL(section).then(url => img.src = url);
|
||
const title=document.createElement('span'); title.textContent=section;
|
||
li.append(img, title); sectionListEl.appendChild(li);
|
||
}
|
||
attachSectionEvents();
|
||
if(!document.querySelector('#section-list .card.selected')){
|
||
const first=document.querySelector('#section-list .card'); if(first) first.click();
|
||
}
|
||
updateDeleteSectionButton();
|
||
}).catch(err=>{ console.error('Error loading sections:', err); toast('Error loading sections'); });
|
||
}
|
||
|
||
function loadComments(sectionName){
|
||
fetch('/get_comments?section='+encodeURIComponent(sectionName)).then(r=>r.json()).then(data=>{
|
||
if(data.status!=='success'){ console.warn('get_comments failed:', data.message); return; }
|
||
commentsEditor.classList.remove('placeholder'); commentsEditor.innerHTML='';
|
||
if(sectionTitleEl) sectionTitleEl.textContent='Comments — '+sectionName;
|
||
(data.comments||[]).forEach(line=>{
|
||
const div=document.createElement('div'); div.className='comment-line'; div.textContent=(line&&line.length)?line:''; commentsEditor.appendChild(div);
|
||
});
|
||
if(!commentsEditor.innerText.trim()){ commentsEditor.innerHTML=''; applyPlaceholder(); }
|
||
}).catch(err=>{ console.error('Error loading comments:', err); toast('Error loading comments'); });
|
||
}
|
||
|
||
function saveComments(sectionName, comments){
|
||
if(saveCommentsBtn) saveCommentsBtn.disabled=true;
|
||
fetch('/save_comments',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({section:sectionName,comments}) })
|
||
.then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
||
.then(data=>{ if(data.status==='success') toast('Comments saved successfully'); else toast('Error: '+(data.message||'Save failed')); })
|
||
.catch(err=>{ console.error('Error saving comments:',err); toast('Error during save'); })
|
||
.finally(()=>{ if(saveCommentsBtn) saveCommentsBtn.disabled=false; });
|
||
}
|
||
|
||
function deleteSection(sectionName){
|
||
fetch('/delete_comment_section',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({section:sectionName}) })
|
||
.then(r=>r.json())
|
||
.then(data=>{
|
||
if(data.status==='success'){ loadSections(); if(sectionTitleEl) sectionTitleEl.textContent='Comments'; commentsEditor.innerHTML=''; applyPlaceholder(); updateDeleteSectionButton(); }
|
||
else { toast('Error deleting section: '+(data.message||'')); }
|
||
})
|
||
.catch(err=>{ console.error('Error deleting section:',err); toast('An error occurred while deleting the section.'); });
|
||
}
|
||
|
||
function restoreDefaultComments(){
|
||
fetch('/restore_default_comments',{method:'POST'}).then(r=>r.json()).then(data=>{
|
||
if(data.status==='success'){ toast('Comments restored successfully.'); loadSections(); if(sectionTitleEl) sectionTitleEl.textContent='Comments'; commentsEditor.innerHTML=''; applyPlaceholder(); updateDeleteSectionButton(); }
|
||
else { toast('Error restoring comments: '+(data.message||'')); }
|
||
}).catch(err=>{ console.error('Error restoring comments:',err); toast('An error occurred while restoring comments.'); });
|
||
}
|
||
|
||
addSectionBtn?.addEventListener('click', ()=>{
|
||
const name=prompt('Enter the name of the new section:'); if(!name) return;
|
||
saveComments(name, []); setTimeout(loadSections, 600);
|
||
});
|
||
deleteSectionBtn?.addEventListener('click', ()=>{
|
||
const selected=document.querySelector('#section-list .card.selected'); if(!selected) return;
|
||
const sectionName=selected.dataset.section;
|
||
if(confirm(`Are you sure you want to delete the section "${sectionName}"?`)){ deleteSection(sectionName); }
|
||
});
|
||
restoreDefaultBtn?.addEventListener('click', ()=>{
|
||
if(confirm('Restore default comments? This will overwrite current modifications.')){ restoreDefaultComments(); }
|
||
});
|
||
saveCommentsBtn?.addEventListener('click', ()=>{
|
||
const selected=document.querySelector('#section-list .card.selected'); if(!selected){ toast('Please select a section.'); return; }
|
||
const sectionName=selected.dataset.section;
|
||
if(commentsEditor.classList.contains('placeholder')){ toast('Nothing to save.'); return; }
|
||
const lines=Array.from(commentsEditor.querySelectorAll('.comment-line')).map(div=>div.innerText.replace(//g,'').trim());
|
||
const comments=lines.filter(Boolean);
|
||
saveComments(sectionName, comments);
|
||
});
|
||
|
||
// =============== Boot ===============
|
||
document.addEventListener('DOMContentLoaded', ()=>{ loadAttacks(); });
|
||
</script>
|
||
</body>
|
||
</html>
|