Files
Bjorn/web/attacks.html

1493 lines
78 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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 laction' : 'Activer laction';
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>&times;</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>