485 lines
19 KiB
Plaintext
485 lines
19 KiB
Plaintext
{% 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 %}
|
|
|