Files
website/templates/itemeditor_command_storage.html
2026-01-07 16:32:38 +01:00

753 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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.
{% extends "base.html" %}
{% block title %}Item Editor - Command Storage - Devanturas{% endblock %}
{% block description %}Store commands longer than 256 characters temporarily for Minecraft Item Editor plugin{% endblock %}
{% block content %}
<section class="page-header">
<div class="container">
<div class="project-breadcrumb">
<a href="{{ url_for('projects') }}">Projects</a> / Item Editor / Command Storage
</div>
<h1>Item Editor Command Storage</h1>
<p>Store long commands (>256 chars) temporarily and generate a retrievable link</p>
</div>
</section>
<section class="command-storage-section">
<div class="container">
<div class="storage-grid">
<!-- Left: Command Input -->
<div class="storage-card">
<div class="card-header">
<i class="fas fa-terminal"></i>
<h2>Store Command</h2>
</div>
<p class="card-description">
Paste your Minecraft command below. Commands longer than 256 characters will be stored
for 30 minutes and a unique retrieval link will be generated.
</p>
<form id="commandForm">
<div class="form-group">
<label for="command">Minecraft Command</label>
<textarea
id="command"
name="command"
rows="6"
placeholder="Paste your command here..."
required
></textarea>
<div class="char-counter">
<span id="charCount">0</span> characters
<span id="charStatus"></span>
</div>
</div>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save"></i> Store Command
</button>
</form>
<!-- Available Placeholders Section (moved here) -->
<div class="info-card" style="margin-top:2.5rem;">
<div class="info-icon">
<i class="fas fa-percent"></i>
</div>
<h3>Available Placeholders</h3>
<ul class="info-list">
<li><code>%player%</code> Name of the player using the item</li>
<li><code>%target%</code> Name of the target player</li>
<li><code>%x%</code> Block X coordinate of the player</li>
<li><code>%y%</code> Block Y coordinate of the player</li>
<li><code>%z%</code> Block Z coordinate of the player</li>
</ul>
</div>
<!-- Success Message -->
<div id="successMessage" style="display: none;" class="success-box">
<div class="success-header">
<i class="fas fa-check-circle"></i>
<h3>Command Stored Successfully!</h3>
</div>
<p class="success-description">Your command has been stored for 30 minutes.</p>
<div class="link-display">
<label>Retrieval Link (JSON):</label>
<div class="link-box">
<input type="text" id="generatedLink" readonly>
<button class="btn btn-secondary btn-copy" onclick="copyLink()">
<i class="fas fa-copy"></i> Copy
</button>
</div>
<small>Use this link in your Item Editor plugin to load the command.</small>
</div>
<div class="expiry-info">
<i class="fas fa-clock"></i>
<span>Expires in: <strong id="expiryTime">30 minutes</strong></span>
</div>
<button class="btn btn-outline" onclick="resetForm()">
<i class="fas fa-plus"></i> Store Another Command
</button>
</div>
<!-- Recently Stored Commands Section -->
<div id="recentCommandsSection" class="recent-commands-section" style="margin-top:2.5rem;">
<h3 style="color:#00d4ff; margin-bottom:1rem;">Recently Stored Commands</h3>
<div id="recentCommandsList" class="recent-commands-list"></div>
</div>
</div>
<!-- Right: Info & Stats -->
<div class="info-sidebar">
<div class="info-card">
<div class="info-icon">
<i class="fas fa-info-circle"></i>
</div>
<h3>How It Works</h3>
<ol class="info-steps">
<li>Paste your long Minecraft command</li>
<li>Click "Store Command" to generate a link</li>
<li>Copy the generated JSON link</li>
<li>Use the link in your Item Editor plugin</li>
</ol>
</div>
<div class="info-card">
<div class="info-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h3>Storage Limits</h3>
<ul class="info-list">
<li><i class="fas fa-check"></i> <strong>Duration:</strong> 30 minutes</li>
<li><i class="fas fa-check"></i> <strong>Max Length:</strong> 10,000 characters</li>
<li><i class="fas fa-check"></i> <strong>Format:</strong> JSON response</li>
<li><i class="fas fa-check"></i> <strong>Access:</strong> Anyone with link</li>
</ul>
</div>
<div class="info-card">
<div class="info-icon">
<i class="fas fa-server"></i>
</div>
<h3>Self-Hosting</h3>
<p class="code-description">Host this system on your own webserver with this simple PHP script:</p>
<pre class="code-block" style="font-size: 0.75rem; line-height: 1.4;">&lt;?php
header('Content-Type: application/json');
$storage_file = 'commands.json';
// Store command (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$command = $data['command'] ?? '';
if (strlen($command) > 10000) {
http_response_code(400);
echo json_encode(['error' => 'Too long']);
exit;
}
$uuid = bin2hex(random_bytes(16));
$expires = time() + 1800; // 30 min
$storage = file_exists($storage_file)
? json_decode(file_get_contents($storage_file), true)
: [];
$storage[$uuid] = [
'command' => $command,
'expires_at' => $expires
];
file_put_contents($storage_file, json_encode($storage));
echo json_encode([
'success' => true,
'url' => "https://yourserver.com/storage.php?id=$uuid"
]);
exit;
}
// Retrieve command (GET)
if (isset($_GET['id'])) {
$storage = file_exists($storage_file)
? json_decode(file_get_contents($storage_file), true)
: [];
$uuid = $_GET['id'];
if (!isset($storage[$uuid])) {
http_response_code(404);
echo json_encode(['error' => 'Not found']);
exit;
}
if ($storage[$uuid]['expires_at'] < time()) {
unset($storage[$uuid]);
file_put_contents($storage_file, json_encode($storage));
http_response_code(410);
echo json_encode(['error' => 'Expired']);
exit;
}
echo json_encode($storage[$uuid]);
exit;
}
?&gt;</pre>
<p style="color: #cfcfcf; font-size: 0.9rem; margin-top: 1rem;">
<strong>Setup:</strong> Save as <code>storage.php</code> on your webserver.
Make sure the directory is writable for <code>commands.json</code>.
</p>
</div>
</div>
</div>
</div>
</section>
<style>
.page-header {
padding: 110px 0 40px;
background: #0f0f0f;
border-bottom: 1px solid #1f1f1f;
}
.command-storage-section {
padding: 60px 0;
background: #0a0a0a;
}
.storage-grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 2rem;
align-items: start;
}
.storage-card, .info-card {
background: linear-gradient(145deg, #161616, #1e1e1e);
border: 1px solid #2a2a2a;
border-radius: 16px;
padding: 2rem;
}
.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.card-header i {
color: #00d4ff;
font-size: 2rem;
}
.card-header h2 {
color: #fff;
font-size: 1.8rem;
margin: 0;
}
.card-description {
color: #cfcfcf;
margin-bottom: 2rem;
line-height: 1.6;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
color: #00d4ff;
font-weight: 600;
margin-bottom: 0.5rem;
}
.form-group textarea {
width: 100%;
background: #121212;
border: 1px solid #2a2a2a;
border-radius: 10px;
padding: 1rem;
color: #fff;
font-family: 'Courier New', monospace;
font-size: 0.95rem;
resize: vertical;
transition: border-color 0.3s;
}
.form-group textarea:focus {
outline: none;
border-color: #00d4ff;
}
.char-counter {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
font-size: 0.9rem;
}
#charCount {
color: #00d4ff;
font-weight: 700;
}
#charStatus {
color: #999;
}
#charStatus.warning {
color: #ffaa00;
}
#charStatus.error {
color: #ff5555;
}
.success-box {
margin-top: 2rem;
padding: 2rem;
background: rgba(0, 255, 166, 0.05);
border: 1px solid rgba(0, 255, 166, 0.2);
border-radius: 12px;
}
.success-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.success-header i {
color: #00ffa6;
font-size: 2rem;
}
.success-header h3 {
color: #00ffa6;
margin: 0;
}
.success-description {
color: #cfcfcf;
margin-bottom: 1.5rem;
}
.link-display {
margin-bottom: 1.5rem;
}
.link-display label {
display: block;
color: #00d4ff;
font-weight: 600;
margin-bottom: 0.5rem;
}
.link-box {
display: flex;
gap: 0.5rem;
}
.link-box input {
flex: 1;
background: #121212;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 0.75rem;
color: #00d4ff;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.btn-copy {
padding: 0.75rem 1rem;
white-space: nowrap;
}
.link-display small {
display: block;
color: #999;
margin-top: 0.5rem;
}
.expiry-info {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cfcfcf;
margin-bottom: 1.5rem;
}
.expiry-info i {
color: #ffaa00;
}
.info-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.info-icon {
width: 50px;
height: 50px;
background: rgba(0, 212, 255, 0.1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.info-icon i {
color: #00d4ff;
font-size: 1.5rem;
}
.info-card h3 {
color: #fff;
margin-bottom: 1rem;
}
.info-steps {
padding-left: 1.5rem;
color: #cfcfcf;
line-height: 1.8;
}
.info-list {
list-style: none;
padding: 0;
}
.info-list li {
color: #cfcfcf;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.info-list i {
color: #00ffa6;
font-size: 0.9rem;
}
.code-description {
color: #cfcfcf;
margin-bottom: 1rem;
}
.code-block {
background: #0a0a0a;
border: 1px solid #1f1f1f;
border-radius: 8px;
padding: 1rem;
color: #00d4ff;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
overflow-x: auto;
line-height: 1.6;
}
.recent-commands-section {
background: #181818;
border:1px solid #232323;
border-radius: 14px;
padding: 1.5rem;
margin-top: 2.5rem;
}
.recent-commands-list {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.recent-command-card {
background: #222;
border-radius: 10px;
padding: 1rem 1.2rem;
}
.recent-command-row {
display: flex;
gap: .7rem;
align-items: center;
margin-bottom: .3rem;
}
.recent-command-label {
color: #00d4ff;
font-size: .95rem;
min-width: 80px;
}
.recent-command-value {
color: #fff;
font-size: .97rem;
}
.recent-command-link {
color: #00ffa6;
font-size: .97rem;
word-break: break-all;
}
.recent-command-timer {
color: #ffaa00;
font-size: .97rem;
}
.recent-command-text {
font-family: 'Courier New', monospace;
}
@media (max-width: 992px) {
.storage-grid {
grid-template-columns: 1fr;
}
}
</style>
<script>
const textarea = document.getElementById('command');
const charCount = document.getElementById('charCount');
const charStatus = document.getElementById('charStatus');
const commandForm = document.getElementById('commandForm');
const submitBtn = document.getElementById('submitBtn');
const successMessage = document.getElementById('successMessage');
// Character counter
textarea.addEventListener('input', function() {
const length = this.value.length;
charCount.textContent = length;
if (length === 0) {
charStatus.textContent = '';
charStatus.className = '';
} else if (length <= 256) {
charStatus.textContent = '(within chat limit)';
charStatus.className = '';
} else if (length <= 10000) {
charStatus.textContent = '(exceeds chat limit - storage required)';
charStatus.className = 'warning';
} else {
charStatus.textContent = '(exceeds maximum length)';
charStatus.className = 'error';
}
});
// Form submission
commandForm.addEventListener('submit', async function(e) {
e.preventDefault();
const command = textarea.value.trim();
if (!command) return;
if (command.length > 10000) {
alert('Command exceeds maximum length of 10,000 characters');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Storing...';
try {
const response = await fetch('/projects/itemeditor/storage', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ command: command })
});
const data = await response.json();
if (data.success) {
// Hide form, show success
commandForm.style.display = 'none';
successMessage.style.display = 'block';
// Set generated link
document.getElementById('generatedLink').value = data.url;
// Start countdown
startCountdown(data.expires_at);
} else {
alert('Error: ' + (data.error || 'Failed to store command'));
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save"></i> Store Command';
}
} catch (error) {
console.error('Error:', error);
alert('Failed to store command. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save"></i> Store Command';
}
});
function copyLink() {
const linkInput = document.getElementById('generatedLink');
linkInput.select();
document.execCommand('copy');
const copyBtn = event.target.closest('.btn-copy');
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
}, 2000);
}
function resetForm() {
commandForm.style.display = 'block';
successMessage.style.display = 'none';
textarea.value = '';
charCount.textContent = '0';
charStatus.textContent = '';
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save"></i> Store Command';
document.getElementById('generatedLink').value = '';
document.getElementById('expiryTime').textContent = '30 minutes';
}
function startCountdown(expiresAt) {
const expiryTime = document.getElementById('expiryTime');
const endTime = new Date(expiresAt).getTime();
// Update countdown immediately
function updateCountdown() {
const now = new Date().getTime();
const distance = endTime - now;
if (distance < 0) {
expiryTime.innerHTML = '<span style="color: #ff5555;">Expired</span>';
return false;
}
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
expiryTime.textContent = `${minutes}m ${seconds}s`;
return true;
}
// Update immediately
if (!updateCountdown()) return;
// Then update every second
const interval = setInterval(() => {
if (!updateCountdown()) {
clearInterval(interval);
}
}, 1000);
}
// --- Recent Commands Storage (localStorage) ---
function getRecentCommands() {
let cmds = [];
try {
cmds = JSON.parse(localStorage.getItem('recentCommands') || '[]');
} catch {}
return Array.isArray(cmds) ? cmds : [];
}
function setRecentCommands(cmds) {
localStorage.setItem('recentCommands', JSON.stringify(cmds));
}
function addRecentCommand(cmd) {
let cmds = getRecentCommands();
cmds.unshift(cmd);
cmds = cmds.filter(c => c && c.expires_at && new Date(c.expires_at).getTime() > Date.now());
if (cmds.length > 5) cmds = cmds.slice(0,5);
setRecentCommands(cmds);
}
function renderRecentCommands() {
const list = document.getElementById('recentCommandsList');
const cmds = getRecentCommands();
if (!cmds.length) {
list.innerHTML = '<div style="color:#888;">No recent commands.</div>';
return;
}
list.innerHTML = '';
cmds.forEach((cmd, idx) => {
const div = document.createElement('div');
div.className = 'recent-command-card';
div.innerHTML = `
<div class="recent-command-row">
<span class="recent-command-label">Command:</span>
<span class="recent-command-value recent-command-text">${cmd.command.length > 60 ? cmd.command.slice(0,60)+'...' : cmd.command}</span>
</div>
<div class="recent-command-row">
<span class="recent-command-label">Link:</span>
<a href="${cmd.url}" target="_blank" class="recent-command-link">${cmd.url}</a>
</div>
<div class="recent-command-row">
<span class="recent-command-label">Expires in:</span>
<span class="recent-command-timer" id="recent-timer-${idx}"></span>
</div>
`;
list.appendChild(div);
startRecentCountdown(cmd.expires_at, `recent-timer-${idx}`, cmd.url);
});
}
function startRecentCountdown(expiresAt, elemId, url) {
const el = document.getElementById(elemId);
function update() {
const now = Date.now();
const end = new Date(expiresAt).getTime();
const dist = end - now;
if (dist < 0) {
el.innerHTML = '<span style="color:#ff5555;">Expired</span>';
// Remove from localStorage
let cmds = getRecentCommands().filter(c => c.url !== url);
setRecentCommands(cmds);
renderRecentCommands();
return false;
}
const m = Math.floor((dist % (1000*60*60))/(1000*60));
const s = Math.floor((dist % (1000*60))/1000);
el.textContent = `${m}m ${s}s`;
return true;
}
if (!update()) return;
const interval = setInterval(() => {
if (!update()) clearInterval(interval);
}, 1000);
}
// On page load
renderRecentCommands();
// On successful store, add to recent
const origSuccessHandler = function(data) {
addRecentCommand({
command: textarea.value.trim(),
url: data.url,
expires_at: data.expires_at
});
renderRecentCommands();
};
// Patch the form submit handler to call origSuccessHandler
const origSubmit = commandForm.onsubmit;
commandForm.onsubmit = function(e) {
if (origSubmit) origSubmit.call(this, e);
};
// Patch the fetch success in the submit handler
const origFetch = window.fetch;
window.fetch = async function() {
const res = await origFetch.apply(this, arguments);
if (arguments[0] && arguments[0].toString().includes('/projects/itemeditor/storage')) {
try {
const clone = res.clone();
const data = await clone.json();
if (data.success && textarea && textarea.value) {
origSuccessHandler(data);
}
} catch {}
}
return res;
};
</script>
{% endblock %}