A modern web interface for Luanti (Minetest) server management with ContentDB integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
429 lines
16 KiB
Plaintext
429 lines
16 KiB
Plaintext
<%
|
|
const body = `
|
|
<div class="page-header">
|
|
<h2>ContentDB Installer</h2>
|
|
<p>Install mods, games, and texture packs by pasting ContentDB URLs</p>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Install from URL</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="/contentdb/install-url" id="installForm">
|
|
${typeof csrfToken !== 'undefined' && csrfToken ? `<input type="hidden" name="_csrf" value="${csrfToken}">` : ''}
|
|
<div class="form-group">
|
|
<label for="packageUrl">ContentDB Package URL*</label>
|
|
<input type="text"
|
|
id="packageUrl"
|
|
name="packageUrl"
|
|
class="form-control"
|
|
placeholder="https://content.luanti.org/packages/author/package_name/"
|
|
required>
|
|
<small class="form-text text-muted">
|
|
Paste any ContentDB package URL or use format: author/package_name
|
|
</small>
|
|
<div id="urlValidation" class="mt-2"></div>
|
|
</div>
|
|
|
|
<div class="form-row" id="locationSelectionGroup">
|
|
<div class="form-group">
|
|
<label for="installLocation">Install Location</label>
|
|
<select name="installLocation" id="installLocation" class="form-control" required>
|
|
<option value="global">Global</option>
|
|
<option value="world">Specific World</option>
|
|
</select>
|
|
<small class="form-text text-muted" id="locationHelp">Choose where to install this content</small>
|
|
</div>
|
|
|
|
<div class="form-group" id="worldSelectionGroup" style="display: none;">
|
|
<label for="worldName">Target World</label>
|
|
<select name="worldName" id="worldName" class="form-control">
|
|
<option value="">Select a world...</option>
|
|
<!-- Will be populated dynamically -->
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" id="dependencyGroup">
|
|
<div class="checkbox-wrapper">
|
|
<input type="checkbox" id="installDeps" name="installDeps" checked>
|
|
<label for="installDeps" class="checkbox-label">
|
|
Install dependencies automatically
|
|
</label>
|
|
</div>
|
|
<small class="form-text text-muted" id="depsHelp">
|
|
Recommended: Automatically download and install required dependencies
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<button type="submit" class="btn btn-success btn-lg" id="installBtn">
|
|
📦 Install Package
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" onclick="clearForm()">
|
|
Clear
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="installStatus" style="display: none;">
|
|
<div class="alert" id="statusAlert">
|
|
<div id="statusMessage"></div>
|
|
<div id="installProgress" class="mt-2">
|
|
<div class="spinner"></div>
|
|
<span>Installing package...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>How to Use</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<h4>📋 Step 1: Copy URL</h4>
|
|
<p>Go to <a href="https://content.luanti.org" target="_blank">content.luanti.org</a> and copy the URL of any content (mods, games, texture packs).</p>
|
|
|
|
<h4>📍 Step 2: Auto-Detection</h4>
|
|
<p><strong>Games</strong> install automatically to games directory.<br>
|
|
<strong>Mods</strong> let you choose global or world-specific.<br>
|
|
<strong>Texture packs</strong> install automatically to textures directory.</p>
|
|
|
|
<h4>⚡ Step 3: Install</h4>
|
|
<p>Click install and dependencies will be resolved automatically.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>🔄 Package Updates</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p>Check for updates to your installed packages:</p>
|
|
<a href="/contentdb/updates" class="btn btn-primary btn-block">Check for Updates</a>
|
|
<a href="/contentdb/installed" class="btn btn-outline-primary btn-block">View Installed</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>📝 Supported URL Formats</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="url-examples">
|
|
<h4>✅ Supported Formats:</h4>
|
|
<ul>
|
|
<li><code>https://content.luanti.org/packages/author/package_name/</code></li>
|
|
<li><code>content.luanti.org/packages/author/package_name/</code></li>
|
|
<li><code>author/package_name</code> (direct format)</li>
|
|
</ul>
|
|
|
|
<h4>📋 Example URLs:</h4>
|
|
<ul>
|
|
<li><strong>Mod:</strong> <code>https://content.luanti.org/packages/VanessaE/basic_materials/</code></li>
|
|
<li><strong>Game:</strong> <code>https://content.luanti.org/packages/GreenXenith/nodecore/</code></li>
|
|
<li><strong>Texture Pack:</strong> <code>https://content.luanti.org/packages/author/texture_pack/</code></li>
|
|
<li><strong>Direct:</strong> <code>VanessaE/basic_materials</code></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.form-row {
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.form-row .form-group {
|
|
flex: 1;
|
|
min-width: 250px;
|
|
}
|
|
|
|
.url-examples ul {
|
|
margin-left: 1.5rem;
|
|
}
|
|
|
|
.url-examples li {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.url-examples code {
|
|
background: var(--bg-accent);
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-block {
|
|
width: 100%;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
#urlValidation {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.validation-success {
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.validation-error {
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.validation-info {
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
#installProgress {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.form-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-row .form-group {
|
|
min-width: auto;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const packageUrlInput = document.getElementById('packageUrl');
|
|
const installLocationSelect = document.getElementById('installLocation');
|
|
const worldSelectionGroup = document.getElementById('worldSelectionGroup');
|
|
const worldNameSelect = document.getElementById('worldName');
|
|
const installForm = document.getElementById('installForm');
|
|
const installBtn = document.getElementById('installBtn');
|
|
const installStatus = document.getElementById('installStatus');
|
|
const urlValidation = document.getElementById('urlValidation');
|
|
const locationSelectionGroup = document.getElementById('locationSelectionGroup');
|
|
const dependencyGroup = document.getElementById('dependencyGroup');
|
|
const locationHelp = document.getElementById('locationHelp');
|
|
const depsHelp = document.getElementById('depsHelp');
|
|
|
|
let currentPackageType = null;
|
|
|
|
// Load available worlds
|
|
loadWorlds();
|
|
|
|
// Show/hide world selection based on install location
|
|
installLocationSelect.addEventListener('change', function() {
|
|
if (this.value === 'world') {
|
|
worldSelectionGroup.style.display = 'block';
|
|
worldNameSelect.required = true;
|
|
} else {
|
|
worldSelectionGroup.style.display = 'none';
|
|
worldNameSelect.required = false;
|
|
}
|
|
});
|
|
|
|
// Real-time URL validation
|
|
let validationTimeout;
|
|
packageUrlInput.addEventListener('input', function() {
|
|
clearTimeout(validationTimeout);
|
|
validationTimeout = setTimeout(() => {
|
|
validateUrl(this.value);
|
|
}, 500);
|
|
});
|
|
|
|
// Form submission
|
|
installForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const url = packageUrlInput.value.trim();
|
|
if (!url) {
|
|
showError('Please enter a package URL');
|
|
return;
|
|
}
|
|
|
|
// Show installation status
|
|
installBtn.disabled = true;
|
|
installBtn.textContent = '⏳ Installing...';
|
|
showStatus('Installing package...', 'info', true);
|
|
|
|
try {
|
|
const formData = new FormData(this);
|
|
const params = new URLSearchParams();
|
|
|
|
// Convert FormData to URLSearchParams for proper encoding
|
|
for (let [key, value] of formData.entries()) {
|
|
params.append(key, value);
|
|
}
|
|
|
|
const response = await fetch('/contentdb/install-url', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: params
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showStatus(result.message + ' ✅', 'success', false);
|
|
clearForm();
|
|
|
|
// Auto-hide success message after 5 seconds
|
|
setTimeout(() => {
|
|
installStatus.style.display = 'none';
|
|
}, 5000);
|
|
} else {
|
|
showStatus(result.error || 'Installation failed', 'error', false);
|
|
}
|
|
} catch (error) {
|
|
console.error('Installation error:', error);
|
|
showStatus('Installation failed: ' + error.message, 'error', false);
|
|
} finally {
|
|
installBtn.disabled = false;
|
|
installBtn.textContent = '📦 Install Package';
|
|
}
|
|
});
|
|
|
|
async function loadWorlds() {
|
|
try {
|
|
const response = await fetch('/api/worlds');
|
|
const worlds = await response.json();
|
|
|
|
worldNameSelect.innerHTML = '<option value="">Select a world...</option>';
|
|
worlds.forEach(world => {
|
|
const option = document.createElement('option');
|
|
option.value = world.name;
|
|
option.textContent = world.displayName || world.name;
|
|
worldNameSelect.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load worlds:', error);
|
|
}
|
|
}
|
|
|
|
async function validateUrl(url) {
|
|
if (!url.trim()) {
|
|
urlValidation.innerHTML = '';
|
|
resetUIForPackageType();
|
|
return;
|
|
}
|
|
|
|
// Parse URL client-side for immediate feedback
|
|
const parsed = parseContentDBUrl(url);
|
|
|
|
if (parsed.author && parsed.name) {
|
|
// Show valid URL format - type detection happens during installation
|
|
urlValidation.innerHTML = '<span class="validation-success">✅ Valid: ' + parsed.author + '/' + parsed.name + ' (type will be detected during installation)</span>';
|
|
resetUIForPackageType();
|
|
} else {
|
|
urlValidation.innerHTML = '<span class="validation-error">❌ Invalid URL format</span>';
|
|
resetUIForPackageType();
|
|
}
|
|
}
|
|
|
|
function parseContentDBUrl(url) {
|
|
// Remove protocol and clean up
|
|
url = url.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');
|
|
|
|
// Match patterns
|
|
const patterns = [
|
|
/^content\\.luanti\\.org\\/packages\\/([^/]+)\\/([^/]+)$/,
|
|
/^([^/]+)\\/([^/]+)$/
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = url.match(pattern);
|
|
if (match) {
|
|
return { author: match[1], name: match[2] };
|
|
}
|
|
}
|
|
|
|
return { author: null, name: null };
|
|
}
|
|
|
|
function updateUIForPackageType(packageType, author, name, title) {
|
|
const typeDisplayName = packageType === 'game' ? 'Game' :
|
|
packageType === 'txp' ? 'Texture Pack' : 'Mod';
|
|
const typeEmoji = packageType === 'game' ? '🎮' :
|
|
packageType === 'txp' ? '🎨' : '📦';
|
|
|
|
urlValidation.innerHTML = \`<span class="validation-success">✅ \${typeEmoji} \${typeDisplayName}: \${title || (author + '/' + name)}</span>\`;
|
|
|
|
if (packageType === 'game') {
|
|
// Games go to games directory - no location choice
|
|
locationSelectionGroup.style.display = 'none';
|
|
dependencyGroup.style.display = 'none';
|
|
installBtn.innerHTML = '🎮 Install Game';
|
|
} else if (packageType === 'txp') {
|
|
// Texture packs go to textures directory - no location choice
|
|
locationSelectionGroup.style.display = 'none';
|
|
dependencyGroup.style.display = 'none';
|
|
installBtn.innerHTML = '🎨 Install Texture Pack';
|
|
} else {
|
|
// Mods can be installed globally or per-world
|
|
locationSelectionGroup.style.display = 'block';
|
|
dependencyGroup.style.display = 'block';
|
|
locationHelp.textContent = 'Choose where to install this mod';
|
|
depsHelp.textContent = 'Recommended: Automatically download and install required dependencies';
|
|
installBtn.innerHTML = '📦 Install Mod';
|
|
}
|
|
}
|
|
|
|
function resetUIForPackageType() {
|
|
currentPackageType = null;
|
|
locationSelectionGroup.style.display = 'block';
|
|
dependencyGroup.style.display = 'block';
|
|
installBtn.innerHTML = '📦 Install Package';
|
|
locationHelp.textContent = 'Choose where to install this content';
|
|
depsHelp.textContent = 'Recommended: Automatically download and install required dependencies';
|
|
}
|
|
|
|
function showStatus(message, type, showProgress) {
|
|
const statusAlert = document.getElementById('statusAlert');
|
|
const statusMessage = document.getElementById('statusMessage');
|
|
const installProgress = document.getElementById('installProgress');
|
|
|
|
// Map alert types to Bootstrap classes
|
|
const alertClass = type === 'error' ? 'alert-danger' : type === 'info' ? 'alert-info' : 'alert-' + type;
|
|
|
|
statusAlert.className = 'alert ' + alertClass;
|
|
statusMessage.textContent = message;
|
|
installProgress.style.display = showProgress ? 'flex' : 'none';
|
|
installStatus.style.display = 'block';
|
|
|
|
// Scroll to status for better visibility
|
|
installStatus.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
function showError(message) {
|
|
showStatus(message, 'danger', false);
|
|
}
|
|
|
|
window.clearForm = function() {
|
|
installForm.reset();
|
|
urlValidation.innerHTML = '';
|
|
installStatus.style.display = 'none';
|
|
worldSelectionGroup.style.display = 'none';
|
|
worldNameSelect.required = false;
|
|
resetUIForPackageType();
|
|
};
|
|
});
|
|
</script>
|
|
`;
|
|
%>
|
|
|
|
<%- include('../layout', { body: body, currentPage: 'contentdb', title: title }) %> |