357 lines
8.3 KiB
JavaScript
357 lines
8.3 KiB
JavaScript
let characters = {};
|
|
let currentEditName = null;
|
|
let searchTerm = '';
|
|
let characterImages = {};
|
|
|
|
function encodeName(name) {
|
|
return btoa(unescape(encodeURIComponent(name)));
|
|
}
|
|
|
|
function decodeName(b64) {
|
|
return decodeURIComponent(escape(atob(b64)));
|
|
}
|
|
|
|
async function loadCharacters() {
|
|
try {
|
|
const response = await fetch('/character_editor');
|
|
console.log('Response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Loaded characters:', data);
|
|
characters = data;
|
|
|
|
// Load image availability
|
|
await checkImages();
|
|
|
|
renderCharacters();
|
|
} catch (error) {
|
|
console.error('Load error:', error);
|
|
showStatus('Error loading characters: ' + error.message,
|
|
'error');
|
|
}
|
|
}
|
|
|
|
async function checkImages() {
|
|
for (const name of Object.keys(characters)) {
|
|
const response = await fetch(
|
|
`/character_editor/image/${encodeName(name)}`
|
|
);
|
|
characterImages[name] = response.ok;
|
|
}
|
|
}
|
|
|
|
function getImageUrl(name) {
|
|
return `/character_editor/image/${encodeName(name)}` +
|
|
`?t=${Date.now()}`;
|
|
}
|
|
|
|
function getSortedCharacterNames() {
|
|
return Object.keys(characters).sort((a, b) =>
|
|
a.toLowerCase().localeCompare(b.toLowerCase())
|
|
);
|
|
}
|
|
|
|
function renderCharacters() {
|
|
const grid = document.getElementById('characterGrid');
|
|
const emptyState = document.getElementById('emptyState');
|
|
grid.innerHTML = '';
|
|
|
|
const sortedNames = getSortedCharacterNames();
|
|
const filteredNames = sortedNames.filter(name =>
|
|
name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
if (filteredNames.length === 0) {
|
|
grid.style.display = 'none';
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
grid.style.display = 'grid';
|
|
emptyState.style.display = 'none';
|
|
|
|
for (const name of filteredNames) {
|
|
const card = document.createElement('div');
|
|
card.className = 'character-card';
|
|
const hasImage = characterImages[name];
|
|
if (hasImage) {
|
|
card.classList.add('has-image');
|
|
card.style.backgroundImage = `url(${getImageUrl(name)})`;
|
|
}
|
|
|
|
card.onclick = (e) => {
|
|
showEditModal(name);
|
|
};
|
|
|
|
card.innerHTML = `
|
|
${!hasImage ? '<div class="character-card-placeholder"></div>' : ''}
|
|
<div class="upload-hint">Drop image here</div>
|
|
<div class="character-card-name">${name}</div>
|
|
`;
|
|
|
|
setupDragAndDrop(card, name);
|
|
grid.appendChild(card);
|
|
}
|
|
}
|
|
|
|
function showEditModal(name) {
|
|
currentEditName = name;
|
|
const data = characters[name];
|
|
|
|
document.getElementById('editCharName').textContent = name;
|
|
document.getElementById('editCharacter').value =
|
|
data.character || '';
|
|
document.getElementById('editTop').value = data.top || '';
|
|
document.getElementById('editBottom').value = data.bottom || '';
|
|
document.getElementById('editNeg').value = data.neg || '';
|
|
|
|
const preview = document.getElementById('imagePreview');
|
|
const previewImg = document.getElementById('previewImg');
|
|
if (characterImages[name]) {
|
|
previewImg.src = getImageUrl(name);
|
|
preview.style.display = 'block';
|
|
} else {
|
|
preview.style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('editModal').classList.add('show');
|
|
}
|
|
|
|
function setupDragAndDrop(card, name) {
|
|
card.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
card.classList.add('drag-over');
|
|
});
|
|
|
|
card.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
card.classList.remove('drag-over');
|
|
});
|
|
|
|
card.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
card.classList.remove('drag-over');
|
|
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0 && files[0].type.startsWith('image/')) {
|
|
await processImage(files[0], name);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function processImage(file, name) {
|
|
return new Promise((resolve) => {
|
|
const reader = new FileReader();
|
|
reader.onload = async (e) => {
|
|
try {
|
|
const response = await fetch(
|
|
`/character_editor/image/` +
|
|
`${encodeName(name)}`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
image: e.target.result
|
|
})
|
|
}
|
|
);
|
|
|
|
if (response.ok) {
|
|
characterImages[name] = true;
|
|
renderCharacters();
|
|
showStatus('Image updated!', 'success');
|
|
} else {
|
|
throw new Error('Failed to upload image');
|
|
}
|
|
resolve();
|
|
} catch (error) {
|
|
showStatus('Error uploading image: ' +
|
|
error.message, 'error');
|
|
resolve();
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
async function saveCharacterData(name) {
|
|
try {
|
|
const response = await fetch('/character_editor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(characters)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to save');
|
|
}
|
|
} catch (error) {
|
|
showStatus('Error saving: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function removeImage() {
|
|
if (!currentEditName) return;
|
|
|
|
if (!confirm('Remove image from this character?')) return;
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`/character_editor/image/` +
|
|
`${encodeName(currentEditName)}`,
|
|
{
|
|
method: 'DELETE'
|
|
}
|
|
);
|
|
|
|
if (response.ok || response.status === 404) {
|
|
characterImages[currentEditName] = false;
|
|
document.getElementById('imagePreview').style.display =
|
|
'none';
|
|
renderCharacters();
|
|
showStatus('Image removed!', 'success');
|
|
} else {
|
|
throw new Error('Failed to delete image');
|
|
}
|
|
} catch (error) {
|
|
showStatus('Error removing image: ' + error.message,
|
|
'error');
|
|
}
|
|
}
|
|
|
|
function hideEditModal() {
|
|
document.getElementById('editModal').classList.remove('show');
|
|
currentEditName = null;
|
|
}
|
|
|
|
async function saveCharacter() {
|
|
if (!currentEditName) return;
|
|
|
|
characters[currentEditName] = {
|
|
character: document.getElementById('editCharacter').value,
|
|
top: document.getElementById('editTop').value,
|
|
bottom: document.getElementById('editBottom').value,
|
|
neg: document.getElementById('editNeg').value
|
|
};
|
|
|
|
const fileInput = document.getElementById('editImage');
|
|
if (fileInput.files.length > 0) {
|
|
await processImage(fileInput.files[0], currentEditName);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/character_editor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(characters)
|
|
});
|
|
|
|
if (response.ok) {
|
|
showStatus('Character saved successfully!', 'success');
|
|
renderCharacters();
|
|
hideEditModal();
|
|
} else {
|
|
throw new Error('Failed to save');
|
|
}
|
|
} catch (error) {
|
|
showStatus('Error saving character: ' + error.message,
|
|
'error');
|
|
}
|
|
}
|
|
|
|
async function deleteCurrentCharacter() {
|
|
if (!currentEditName) return;
|
|
|
|
if (!confirm(`Delete character "${currentEditName}"?`)) return;
|
|
|
|
delete characters[currentEditName];
|
|
|
|
try {
|
|
const response = await fetch('/character_editor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(characters)
|
|
});
|
|
|
|
if (response.ok) {
|
|
showStatus('Character deleted successfully!', 'success');
|
|
renderCharacters();
|
|
hideEditModal();
|
|
} else {
|
|
throw new Error('Failed to delete');
|
|
}
|
|
} catch (error) {
|
|
showStatus('Error deleting character: ' + error.message,
|
|
'error');
|
|
}
|
|
}
|
|
|
|
function showAddModal() {
|
|
document.getElementById('addModal').classList.add('show');
|
|
document.getElementById('newCharName').value = '';
|
|
}
|
|
|
|
function hideAddModal() {
|
|
document.getElementById('addModal').classList.remove('show');
|
|
}
|
|
|
|
function addCharacter() {
|
|
const name = document.getElementById('newCharName').value.trim();
|
|
|
|
if (!name) {
|
|
alert('Please enter a character name');
|
|
return;
|
|
}
|
|
|
|
if (characters[name]) {
|
|
alert('Character already exists');
|
|
return;
|
|
}
|
|
|
|
characters[name] = {
|
|
character: '',
|
|
top: '',
|
|
bottom: '',
|
|
neg: ''
|
|
};
|
|
|
|
renderCharacters();
|
|
hideAddModal();
|
|
showEditModal(name);
|
|
}
|
|
|
|
function showStatus(message, type) {
|
|
const status = document.getElementById('status');
|
|
status.textContent = message;
|
|
status.className = `status ${type} show`;
|
|
setTimeout(() => status.classList.remove('show'), 3000);
|
|
}
|
|
|
|
document.getElementById('searchInput').addEventListener('input',
|
|
(e) => {
|
|
searchTerm = e.target.value;
|
|
renderCharacters();
|
|
}
|
|
);
|
|
|
|
document.getElementById('addModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'addModal') hideAddModal();
|
|
});
|
|
|
|
document.getElementById('editModal').addEventListener('click',
|
|
(e) => {
|
|
if (e.target.id === 'editModal') hideEditModal();
|
|
}
|
|
);
|
|
|
|
loadCharacters();
|