clearance
This commit is contained in:
parent
17bb747542
commit
e46dec38ca
@ -1,110 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Binary file viewer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-3">Viewing File: {{ filename }}</h1>
|
||||
<p class="text-muted">Tipo MIME: {{ mime_type }}</p>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h4>File Info</h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><strong>File Name:</strong> {{ filename }}</li>
|
||||
<li class="list-group-item"><strong>Size:</strong> {{ size }} bytes</li>
|
||||
<li class="list-group-item"><strong>Uploaded by:</strong> {{ owner.username }}</li>
|
||||
<li class="list-group-item"><strong>Upload Date:</strong> {{ paste.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>Acciones</h4>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="{{ url_for('download_paste', id=paste.id) }}" class="btn btn-success mb-2">
|
||||
<i class="fas fa-download me-2"></i> Descargar Archivo
|
||||
</a>
|
||||
{% if 'admin' in session %}
|
||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal" aria-label="Delete Paste">
|
||||
<i class="fas fa-trash-alt me-2"></i> Delete Paste
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Download from smartphone</h5>
|
||||
<p class="card-text">Scan this code from your mobile device:</p>
|
||||
<div id="qrcode" class="my-3 mx-auto" style="width:200px; height:200px;"></div>
|
||||
<button id="download-qr" class="btn btn-outline-primary mt-2">
|
||||
<i class="fas fa-download me-2"></i> Download QR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if 'admin' in session %}
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<form action="{{ url_for('admin_delete_paste', paste_id=paste.id) }}" method="POST">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirmar Eliminación</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
¿Are you sure? This action cannot be undone.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pasteId = "{{ paste.id }}";
|
||||
const downloadUrl = "{{ url_for('download_paste', id=paste.id, _external=True) }}";
|
||||
|
||||
const qrCodeContainer = document.getElementById('qrcode');
|
||||
|
||||
new QRCode(qrCodeContainer, {
|
||||
text: downloadUrl,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
correctLevel : QRCode.CorrectLevel.H
|
||||
});
|
||||
|
||||
const downloadQrButton = document.getElementById('download-qr');
|
||||
downloadQrButton.addEventListener('click', () => {
|
||||
const canvas = qrCodeContainer.querySelector('canvas');
|
||||
if (canvas) {
|
||||
const pngUrl = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream");
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = pngUrl;
|
||||
downloadLink.download = `qr_code_paste_${pasteId}.png`;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
alert('QR descargado exitosamente!');
|
||||
} else {
|
||||
alert('Error al generar el código QR.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,73 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Media Viewer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1 class="text-center">Viewing Media: {{ filename }}</h1>
|
||||
<p class="text-muted text-center">MIME Type: {{ mime_type }}</p>
|
||||
|
||||
<div class="media-container text-center">
|
||||
{% if mime_type.startswith("image/") %}
|
||||
<img src="{{ url_for('get_file', filename=filename) }}" alt="Image" class="img-fluid" />
|
||||
{% elif mime_type.startswith("video/") %}
|
||||
<video controls class="w-100">
|
||||
<source src="{{ url_for('get_file', filename=filename) }}" type="{{ mime_type }}">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% elif mime_type.startswith("audio/") %}
|
||||
<audio controls class="w-100">
|
||||
<source src="{{ url_for('get_file', filename=filename) }}" type="{{ mime_type }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
{% elif mime_type == "application/pdf" %}
|
||||
<embed src="{{ url_for('get_file', filename=filename) }}" type="{{ mime_type }}" width="100%" height="600px">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<button class="btn btn-primary me-2" onclick="toggleMetadata()">Toggle Metadata</button>
|
||||
<a href="{{ url_for('download_paste', id=paste_id) }}" class="btn btn-success">📥 Download</a>
|
||||
</div>
|
||||
|
||||
<div class="metadata mt-4">
|
||||
<div id="metadata-content" class="metadata-content" style="display: none;">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if metadata %}
|
||||
{% for track in metadata %}
|
||||
{% for key, value in track.items() %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2">No metadata available for this file.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function toggleMetadata() {
|
||||
const content = document.getElementById("metadata-content");
|
||||
if (content.style.display === "none") {
|
||||
content.style.display = "block";
|
||||
} else {
|
||||
content.style.display = "none";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,139 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Media Viewer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1 class="text-center">Viewing Media: {{ filename }}</h1>
|
||||
<p class="text-muted text-center">MIME Type: {{ mime_type }}</p>
|
||||
|
||||
<div class="media-container text-center">
|
||||
{% if mime_type.startswith("image/") %}
|
||||
<img src="{{ url_for('get_file', filename=filename) }}" alt="Image" class="img-fluid" />
|
||||
{% elif mime_type.startswith("video/") %}
|
||||
<video controls class="w-100">
|
||||
<source src="{{ url_for('get_file', filename=filename) }}" type="{{ mime_type }}">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% elif mime_type.startswith("audio/") %}
|
||||
<audio controls class="w-100">
|
||||
<source src="{{ url_for('get_file', filename=filename) }}" type="{{ mime_type }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
{% elif mime_type == "application/pdf" %}
|
||||
<embed src="{{ url_for('get_file', filename=filename) }}" type="{{ mime_type }}" width="100%" height="600px">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<button class="btn btn-primary me-2" onclick="toggleMetadata()">Toggle Metadata</button>
|
||||
<a href="{{ url_for('download_paste', id=paste_id) }}" class="btn btn-primary me-2">📥 Download</a>
|
||||
<a href="{{ url_for('get_file', filename=filename) }}" class="btn btn-primary me-2" target="_blank">🌐 View Raw</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Botón de favoritos, con clase "btn btn-sm btn-primary" para mantener la consistencia -->
|
||||
<button id="favorite-button" class="btn btn-sm btn-primary">
|
||||
❤️ Add to Favorites
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="metadata mt-4">
|
||||
<div id="metadata-content" class="metadata-content" style="display: none;">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if metadata %}
|
||||
{% for track in metadata %}
|
||||
{% for key, value in track.items() %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2">No metadata available for this file.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor para el Toast (puede ir en base.html o aquí) -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1100">
|
||||
<div id="liveToast" class="toast align-items-center text-white bg-primary border-0" role="alert" aria-live="assertive" aria-atomic="true" style="display: none;">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<!-- Mensaje dinámico -->
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleMetadata() {
|
||||
const content = document.getElementById("metadata-content");
|
||||
if (content.style.display === "none") {
|
||||
content.style.display = "block";
|
||||
} else {
|
||||
content.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Solo configuramos el listener si el usuario está autenticado
|
||||
{% if current_user.is_authenticated %}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const favoriteButton = document.getElementById('favorite-button');
|
||||
if (favoriteButton) {
|
||||
favoriteButton.addEventListener('click', function() {
|
||||
fetch(`/paste/{{ paste_id }}/favorite`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer {{ token }}'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to toggle favorite');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const message = data.message || 'Action completed!';
|
||||
showToast(message, 'bg-success');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showToast('Error adding to favorites.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Función para mostrar el toast
|
||||
function showToast(message, bgColor = 'bg-primary') {
|
||||
const toastElement = document.getElementById('liveToast');
|
||||
const toastBody = toastElement.querySelector('.toast-body');
|
||||
|
||||
toastBody.textContent = message;
|
||||
toastElement.className = `toast align-items-center text-white ${bgColor} border-0`;
|
||||
toastElement.style.display = 'block'; // Mostrar el contenedor
|
||||
|
||||
const toast = new bootstrap.Toast(toastElement);
|
||||
toast.show();
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
{{ super() }} <!-- Esto conserva el contenido original del footer definido en base.html -->
|
||||
{% endblock %}
|
@ -1,198 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Paste {{ paste.id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
{% if paste.owner_id == current_user.id %}
|
||||
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Share Paste</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<input type="text" id="share-username" placeholder="Username" class="form-control">
|
||||
<div id="user-suggestions" class="list-group position-absolute d-none" style="z-index: 1050;"></div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="share-can-edit" class="form-check-input" checked>
|
||||
<label class="form-check-label" for="share-can-edit">Allow Edit</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="share-submit" class="btn btn-success">Share Paste</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<h1 class="mb-0">Paste {{ paste.id }}</h1>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if paste.owner_id == current_user.id %}
|
||||
<button id="share-button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#shareModal" title="Share Paste">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<button id="favorite-button" class="btn btn-sm btn-secondary" title="Toggle Favorite">
|
||||
<i class="fas fa-heart"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<a id="edit-button" href="{{ url_for('edit_paste_web', id=paste.id) }}" class="btn btn-sm btn-warning" title="Edit Paste">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button id="copy-url-button" class="btn btn-sm btn-secondary" title="Copy URL">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 🔥 Botón de IA fuera del grupo de botones -->
|
||||
<button id="ai-generate-code" class="btn btn-sm btn-outline-primary magic-btn" title="AI Suggestions">
|
||||
✨
|
||||
</button>
|
||||
|
||||
<!-- Código Mejorado por IA -->
|
||||
<div id="generated-code-container" class="mt-3 d-none">
|
||||
<h5 class="text-primary">💻 AI-Enhanced Code:</h5>
|
||||
<div id="generated-code-box">
|
||||
<pre id="generated-code"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Botón IA como icono */
|
||||
.magic-btn {
|
||||
font-size: 18px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.magic-btn:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Contenedor de código */
|
||||
#generated-code-box {
|
||||
background-color: #1e1e1e;
|
||||
color: #c9d1d9;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-size: 14px;
|
||||
border: 1px solid #444;
|
||||
padding: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p class="text-muted small">
|
||||
{% if paste.filename and '.' in paste.filename %}
|
||||
<strong>Extension:</strong> {{ paste.filename.split('.')[-1] }} |
|
||||
{% endif %}
|
||||
<strong>MIME Type:</strong> {{ paste.content_type }} |
|
||||
<strong>Language:</strong> {{ paste.language }} |
|
||||
<strong>Size:</strong>
|
||||
{% if paste.size < 1024 %}
|
||||
{{ paste.size }} bytes
|
||||
{% elif paste.size < 1024 * 1024 %}
|
||||
{{ '%.2f' | format(paste.size / 1024) }} KB
|
||||
{% else %}
|
||||
{{ '%.2f' | format(paste.size / (1024 * 1024)) }} MB
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap">
|
||||
<div class="mb-2">
|
||||
{% if not is_markdown %}
|
||||
<button id="copy-button" class="btn btn-sm btn-secondary me-2" title="Copy Content">
|
||||
<i class="fas fa-clipboard"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary" href="{{ url_for('download_paste', id=paste.id) }}" download title="Download Paste">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pygments-style" class="form-label me-2 mb-0">Style:</label>
|
||||
<select id="pygments-style" class="form-select form-select-sm style-selector d-inline-block w-auto">
|
||||
{% for style in pygments_styles %}
|
||||
<option value="{{ style }}" {% if style == default_pygments_style %}selected{% endif %}>{{ style.replace('-', ' ').capitalize() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_markdown %}
|
||||
<div class="markdown-body">{{ md_html_code|safe }}</div>
|
||||
{% else %}
|
||||
<div class="highlight"><code class="language-{{ paste.language|lower }}">{{ html_code|safe }}</code></div>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-3">View the raw version <a href="{{ url_for('get_paste_raw', id=paste.id) }}">here</a>.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='js/share-paste.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/share-autocomplete.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/copy-paste.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/pygments-style.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/unshare.js') }}" defer></script>
|
||||
|
||||
<script>
|
||||
document.getElementById("ai-generate-code").addEventListener("click", function () {
|
||||
document.getElementById("generated-code").textContent = ""; // Limpiar salida previa
|
||||
document.getElementById("generated-code-container").classList.remove("d-none");
|
||||
|
||||
fetch("{{ url_for('generate_code_from_paste', id=paste.id) }}")
|
||||
.then(response => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
const outputElement = document.getElementById("generated-code");
|
||||
|
||||
function readChunk() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunkText = decoder.decode(value, { stream: true });
|
||||
|
||||
// Limpiar texto si contiene estructura JSON
|
||||
chunkText = chunkText.replace(/data: \{"id":.*?"choices":\[\{"delta":\{"content":"(.*?)"\}\}\]\}/g, "$1");
|
||||
|
||||
text += chunkText;
|
||||
outputElement.textContent = text;
|
||||
outputElement.scrollTop = outputElement.scrollHeight; // Hacer scroll automático
|
||||
|
||||
readChunk(); // Continuar leyendo los datos
|
||||
});
|
||||
}
|
||||
readChunk();
|
||||
})
|
||||
.catch(error => alert("Error generando código: " + error));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,484 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Paste {{ paste.id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- Encabezado del Paste -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<h1 class="mb-0">Paste {{ paste.id }}</h1>
|
||||
<!-- Barra de botones -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if paste.owner_id == current_user.id %}
|
||||
<!-- Botón para compartir -->
|
||||
<button id="share-button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#shareModal">
|
||||
Share
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Botón de favoritos -->
|
||||
<button id="favorite-button" class="btn btn-sm btn-secondary">
|
||||
❤️ {% if paste in current_user.favorite_pastes %}Remove from Favorites{% else %}Add to Favorites{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<!-- Botón para editar (solo si se puede editar) -->
|
||||
{% if can_edit %}
|
||||
<a id="edit-button" href="{{ url_for('edit_paste_web', id=paste.id) }}" class="btn btn-sm btn-warning">
|
||||
Edit Paste
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Compartir Paste (solo si eres el propietario) -->
|
||||
<!-- Modal de Compartir Paste (solo si eres el propietario) -->
|
||||
{% if paste.owner_id == current_user.id %}
|
||||
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Share Paste</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3 position-relative">
|
||||
<input type="text"
|
||||
id="share-username"
|
||||
placeholder="Username"
|
||||
class="form-control mb-3"
|
||||
autocomplete="off">
|
||||
<div id="user-suggestions" class="list-group position-absolute d-none" style="z-index: 1050;"></div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="share-can-edit" class="form-check-input" checked>
|
||||
<label class="form-check-label" for="share-can-edit">Allow Edit</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="share-submit" class="btn btn-success">Share Paste</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sección principal: contenido del Paste -->
|
||||
<div class="mb-3">
|
||||
<!-- Botones de copiar y descargar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap">
|
||||
<div class="mb-2">
|
||||
{% if not is_markdown %}
|
||||
<button id="copy-button" class="btn btn-sm btn-secondary me-2">📋 Copy</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary" href="{{ url_for('download_paste', id=paste.id) }}" download>
|
||||
📥 Download
|
||||
</a>
|
||||
</div>
|
||||
<!-- Desplegable de Estilos de Pygments -->
|
||||
<div>
|
||||
<label for="pygments-style" class="form-label me-2 mb-0">Style:</label>
|
||||
<select id="pygments-style" class="form-select form-select-sm style-selector d-inline-block w-auto">
|
||||
{% for style in pygments_styles %}
|
||||
<option value="{{ style }}"
|
||||
{% if style == default_pygments_style %}selected{% endif %}>
|
||||
{{ style.replace('-', ' ').capitalize() }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mostrar contenido según Markdown o texto plano -->
|
||||
{% if is_markdown %}
|
||||
<div class="markdown-body">
|
||||
{{ md_html_code|safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="highlight">
|
||||
<code class="language-{{ paste.language|lower }}">{{ html_code|safe }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-3">
|
||||
View the raw version <a href="{{ url_for('get_paste_raw', id=paste.id) }}">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Botón para ver la lista de usuarios con acceso (oculto por defecto en un collapse) -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<button
|
||||
class="btn btn-sm btn-info mb-2"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sharedWithCollapse"
|
||||
aria-expanded="false"
|
||||
aria-controls="sharedWithCollapse">
|
||||
See users with access
|
||||
</button>
|
||||
|
||||
<div class="collapse" id="sharedWithCollapse">
|
||||
<div class="card card-body">
|
||||
<h5>Shared With:</h5>
|
||||
<ul class="list-group">
|
||||
<ul class="list-group">
|
||||
{% for user, can_edit in shared_with %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>{{ user }}</span>
|
||||
<span>
|
||||
{% if can_edit %}
|
||||
<span class="badge bg-success">Can Edit</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Read Only</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Botón “Unshare” que abrirá el modal -->
|
||||
<button class="btn btn-sm btn-danger ms-2"
|
||||
onclick="showUnshareModal('{{ paste.id }}', '{{ user }}')">
|
||||
Unshare
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- Modal de confirmación (oculto por defecto) -->
|
||||
<div class="modal fade" id="unshareModal" tabindex="-1" aria-labelledby="unshareModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="unshareModalLabel">Confirm Unshare</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Texto dinámico que pondremos por JS -->
|
||||
<p id="unshareModalMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<!-- Botón de confirmación -->
|
||||
<button type="button" id="confirmUnshareBtn" class="btn btn-danger">Yes, Unshare</button>
|
||||
<!-- Botón para cancelar/cerrar -->
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Variables globales para guardar pasteId y username a descompartir
|
||||
let unsharePasteId = null;
|
||||
let unshareUsername = null;
|
||||
|
||||
function showUnshareModal(pasteId, username) {
|
||||
unsharePasteId = pasteId;
|
||||
unshareUsername = username;
|
||||
|
||||
// Cambiar el texto que aparece en el modal
|
||||
const messageElem = document.getElementById('unshareModalMessage');
|
||||
messageElem.textContent = `Are you sure you want to remove access for "${username}"?`;
|
||||
|
||||
// Mostrar el modal
|
||||
const modalElement = document.getElementById('unshareModal');
|
||||
const modalInstance = new bootstrap.Modal(modalElement);
|
||||
modalInstance.show();
|
||||
}
|
||||
|
||||
// Botón “Yes, Unshare” dentro del modal
|
||||
document.getElementById('confirmUnshareBtn').addEventListener('click', function () {
|
||||
fetch(`/paste/${unsharePasteId}/unshare`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ "username": unshareUsername })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Cerrar el modal
|
||||
const modalElement = document.getElementById('unshareModal');
|
||||
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
modalInstance.hide();
|
||||
|
||||
// Mostrar resultado en un toast
|
||||
if (data.message) {
|
||||
showToast(data.message, 'bg-success');
|
||||
} else if (data.error) {
|
||||
showToast(data.error, 'bg-danger');
|
||||
}
|
||||
|
||||
// Recargar la página o eliminar el <li> para ese usuario
|
||||
location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Error while unsharing paste.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Funcionalidad de Compartir
|
||||
const shareUsernameInput = document.getElementById('share-username');
|
||||
const suggestionsBox = document.getElementById('user-suggestions');
|
||||
const shareSubmitButton = document.getElementById('share-submit');
|
||||
|
||||
if (shareUsernameInput && suggestionsBox && shareSubmitButton) {
|
||||
// Función para buscar usuarios
|
||||
async function fetchUsers(query) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`);
|
||||
const users = await response.json();
|
||||
return users;
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar sugerencias
|
||||
function showSuggestions(users) {
|
||||
suggestionsBox.innerHTML = '';
|
||||
if (users.length === 0) {
|
||||
suggestionsBox.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(user => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.classList.add('list-group-item', 'list-group-item-action');
|
||||
item.textContent = user.username;
|
||||
item.addEventListener('click', () => {
|
||||
shareUsernameInput.value = user.username;
|
||||
suggestionsBox.classList.add('d-none');
|
||||
});
|
||||
suggestionsBox.appendChild(item);
|
||||
});
|
||||
|
||||
suggestionsBox.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Evento de entrada
|
||||
shareUsernameInput.addEventListener('input', async function () {
|
||||
const query = this.value.trim();
|
||||
if (query.length >= 2) { // Solo buscar si hay al menos 2 caracteres
|
||||
const users = await fetchUsers(query);
|
||||
showSuggestions(users);
|
||||
} else {
|
||||
suggestionsBox.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Ocultar sugerencias si se hace clic fuera del input
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!shareUsernameInput.contains(event.target) && !suggestionsBox.contains(event.target)) {
|
||||
suggestionsBox.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener para el botón de compartir
|
||||
shareSubmitButton.addEventListener('click', () => {
|
||||
const username = shareUsernameInput.value.trim();
|
||||
const canEdit = document.getElementById('share-can-edit').checked;
|
||||
|
||||
if (!username) {
|
||||
showToast('Por favor, ingresa un nombre de usuario.', 'bg-warning');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/paste/{{ paste.id }}/share`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ username, can_edit: canEdit })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.message) {
|
||||
showToast(data.message, 'bg-success');
|
||||
const shareModal = bootstrap.Modal.getInstance(document.getElementById('shareModal'));
|
||||
shareModal?.hide();
|
||||
// Limpia campos
|
||||
shareUsernameInput.value = '';
|
||||
document.getElementById('share-can-edit').checked = true;
|
||||
} else if (data.error) {
|
||||
showToast(data.error, 'bg-danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sharing paste:', error);
|
||||
showToast('Error sharing paste.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Funcionalidad de Favoritos
|
||||
const favoriteButton = document.getElementById('favorite-button');
|
||||
if (favoriteButton) {
|
||||
favoriteButton.addEventListener('click', function () {
|
||||
const action = "{{ 'unfavorite' if paste in current_user.favorite_pastes else 'favorite' }}";
|
||||
const url = `/paste/{{ paste.id }}/${action}`;
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.message.includes('added')) {
|
||||
favoriteButton.innerHTML = '❤️ Remove from Favorites';
|
||||
} else {
|
||||
favoriteButton.innerHTML = '❤️ Add to Favorites';
|
||||
}
|
||||
if (data.success) {
|
||||
showToast(data.message, 'bg-success');
|
||||
} else {
|
||||
showToast(data.message || 'Error updating favorites.', 'bg-danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating favorites:', error);
|
||||
showToast('Error updating favorites.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Funcionalidad de Toggle Editable
|
||||
{% if can_edit and paste.editable %}
|
||||
const toggleEditableButton = document.getElementById('toggle-editable-button');
|
||||
if (toggleEditableButton) {
|
||||
toggleEditableButton.addEventListener('click', function() {
|
||||
const url = `/paste/{{ paste.id }}/toggle_editable`;
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.editable) {
|
||||
toggleEditableButton.innerHTML = 'Disable Editable';
|
||||
toggleEditableButton.classList.remove('btn-success');
|
||||
toggleEditableButton.classList.add('btn-warning');
|
||||
const editButton = document.getElementById('edit-button');
|
||||
if (editButton) editButton.style.display = 'inline-block';
|
||||
} else {
|
||||
toggleEditableButton.innerHTML = 'Enable Editable';
|
||||
toggleEditableButton.classList.remove('btn-warning');
|
||||
toggleEditableButton.classList.add('btn-success');
|
||||
const editButton = document.getElementById('edit-button');
|
||||
if (editButton) editButton.style.display = 'none';
|
||||
}
|
||||
showToast(data.message, 'bg-success');
|
||||
} else {
|
||||
showToast(data.error || 'Action failed', 'bg-danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating editable:', error);
|
||||
showToast('Error updating editable.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Funcionalidad de Copiar al Portapapeles (si NO es Markdown)
|
||||
{% if not is_markdown %}
|
||||
const copyButton = document.getElementById('copy-button');
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', function () {
|
||||
const codeElement = document.querySelector('.highlight code');
|
||||
if (!codeElement) {
|
||||
showToast('Error: code snippet not found.', 'bg-danger');
|
||||
return;
|
||||
}
|
||||
|
||||
let code = codeElement.innerText.replace(/^\s*\d+\s/gm, '').trim();
|
||||
|
||||
navigator.clipboard.writeText(code)
|
||||
.then(() => {
|
||||
showToast('¡Copied to clipboard!', 'bg-success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error copying to clipboard:', err);
|
||||
showToast('Error copying code.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Funcionalidad del Selector de Estilos de Pygments
|
||||
const pygmentsStyleSelector = document.getElementById('pygments-style');
|
||||
if (pygmentsStyleSelector) {
|
||||
pygmentsStyleSelector.addEventListener('change', function () {
|
||||
const selectedStyle = this.value;
|
||||
document.querySelectorAll('.highlight code').forEach(function (codeElement) {
|
||||
// Remover clases relacionadas con estilos anteriores
|
||||
[...codeElement.classList].forEach(cls => {
|
||||
if (cls.startsWith('bg-') || cls.startsWith('hljs-') || cls.startsWith('theme-')) {
|
||||
codeElement.classList.remove(cls);
|
||||
}
|
||||
});
|
||||
// Añadir la clase del estilo seleccionado
|
||||
codeElement.classList.add(selectedStyle);
|
||||
});
|
||||
showToast('Pygments style changed locally.', 'bg-success');
|
||||
});
|
||||
}
|
||||
|
||||
// Funcionalidad de Unshare Modal
|
||||
const confirmUnshareBtn = document.getElementById('confirmUnshareBtn');
|
||||
if (confirmUnshareBtn) {
|
||||
let unsharePasteId = null;
|
||||
let unshareUsername = null;
|
||||
|
||||
confirmUnshareBtn.addEventListener('click', function () {
|
||||
fetch(`/paste/${unsharePasteId}/unshare`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ "username": unshareUsername })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Cerrar el modal
|
||||
const modalElement = document.getElementById('unshareModal');
|
||||
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
modalInstance.hide();
|
||||
|
||||
// Mostrar resultado en un toast
|
||||
if (data.message) {
|
||||
showToast(data.message, 'bg-success');
|
||||
} else if (data.error) {
|
||||
showToast(data.error, 'bg-danger');
|
||||
}
|
||||
|
||||
// Recargar la página o eliminar el <li> para ese usuario
|
||||
location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Error sharing paste.', 'bg-danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user