rfiles-online/portal/templates/portal/file_detail.html

197 lines
8.6 KiB
HTML

{% extends "portal/base.html" %}
{% block title %}{{ file.title|default:file.original_filename }}{% endblock %}
{% block extra_css %}
<style>
.file-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem; }
.file-header h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.file-header .meta { color: var(--text-muted); font-size: 0.875rem; }
.file-header .meta span { margin-right: 1rem; }
.section { margin-bottom: 2rem; }
.section h2 { font-size: 1rem; margin-bottom: 1rem; color: var(--text-muted); }
.share-list { display: grid; gap: 0.75rem; }
.share-item { display: flex; justify-content: space-between; align-items: center; padding: 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; }
.share-item.inactive { opacity: 0.5; }
.share-info { flex: 1; }
.share-url { font-family: monospace; font-size: 0.875rem; color: var(--primary); word-break: break-all; }
.share-meta { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem; }
.share-actions { display: flex; gap: 0.5rem; }
.new-share-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; padding: 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 1rem; }
.form-group label { display: block; font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.25rem; }
.form-group input, .form-group select { width: 100%; padding: 0.5rem; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 0.875rem; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary); }
.danger-zone { border-color: var(--error); }
.danger-zone h2 { color: var(--error); }
</style>
{% endblock %}
{% block content %}
<div class="file-header">
<div>
<h1>{{ file.title|default:file.original_filename }}</h1>
<div class="meta">
<span>{{ file.file_size|filesizeformat }}</span>
<span>{{ file.mime_type }}</span>
<span>Uploaded {{ file.created_at|date:"M d, Y H:i" }}</span>
</div>
{% if file.description %}
<p class="mt-1">{{ file.description }}</p>
{% endif %}
</div>
<div>
<a href="{{ file.file.url }}" class="btn btn-primary" download>Download</a>
</div>
</div>
<div class="section">
<h2>Create New Share Link</h2>
<form class="new-share-form" id="newShareForm">
<div class="form-group">
<label>Expires In</label>
<select name="expires_in_hours" id="expiresIn">
<option value="">Never</option>
<option value="1">1 hour</option>
<option value="24">1 day</option>
<option value="168" selected>1 week</option>
<option value="720">30 days</option>
</select>
</div>
<div class="form-group">
<label>Max Downloads</label>
<input type="number" name="max_downloads" id="maxDownloads" placeholder="Unlimited" min="1">
</div>
<div class="form-group">
<label>Password (optional)</label>
<input type="password" name="password" id="password" placeholder="Leave empty for no password">
</div>
<div class="form-group" style="display: flex; align-items: flex-end;">
<button type="submit" class="btn btn-primary">Create Share Link</button>
</div>
</form>
</div>
<div class="section">
<h2>Active Share Links ({{ shares.count }})</h2>
<div class="share-list" id="shareList">
{% for share in shares %}
<div class="share-item {% if not share.is_valid %}inactive{% endif %}" data-share-id="{{ share.id }}">
<div class="share-info">
<div class="share-url">{{ share.get_public_url }}</div>
<div class="share-meta">
{% if share.is_valid %}
<span class="text-success">Active</span>
{% else %}
<span class="text-error">Inactive</span>
{% endif %}
&bull;
{{ share.download_count }} download{{ share.download_count|pluralize }}
{% if share.max_downloads %}/ {{ share.max_downloads }} max{% endif %}
{% if share.expires_at %}
&bull; Expires {{ share.expires_at|date:"M d, Y" }}
{% endif %}
{% if share.is_password_protected %}
&bull; Password protected
{% endif %}
</div>
</div>
<div class="share-actions">
<button class="btn btn-ghost copy-btn" data-url="{{ share.get_public_url }}">Copy</button>
{% if share.is_valid %}
<button class="btn btn-ghost revoke-btn" data-share-id="{{ share.id }}">Revoke</button>
{% endif %}
</div>
</div>
{% empty %}
<p class="text-muted">No share links yet. Create one above.</p>
{% endfor %}
</div>
</div>
<div class="section card danger-zone">
<h2>Danger Zone</h2>
<p class="text-muted mb-1">Deleting this file will also remove all share links.</p>
<button class="btn btn-danger" id="deleteBtn">Delete File</button>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.getElementById('newShareForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
expires_in_hours: document.getElementById('expiresIn').value || null,
max_downloads: document.getElementById('maxDownloads').value || null,
password: document.getElementById('password').value || null,
};
const response = await fetch('{% url "portal:create_share" file.id %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (response.ok) {
const result = await response.json();
const shareList = document.getElementById('shareList');
const newItem = document.createElement('div');
newItem.className = 'share-item';
newItem.innerHTML = `
<div class="share-info">
<div class="share-url">${result.share.url}</div>
<div class="share-meta">
<span class="text-success">Active</span>
&bull; 0 downloads
${result.share.expires_at ? '&bull; Expires ' + new Date(result.share.expires_at).toLocaleDateString() : ''}
</div>
</div>
<div class="share-actions">
<button class="btn btn-ghost copy-btn" data-url="${result.share.url}">Copy</button>
<button class="btn btn-ghost revoke-btn" data-share-id="${result.share.id}">Revoke</button>
</div>
`;
shareList.insertBefore(newItem, shareList.firstChild);
navigator.clipboard.writeText(result.share.url);
alert('Share link created and copied to clipboard!');
e.target.reset();
} else {
alert('Failed to create share link');
}
});
document.addEventListener('click', (e) => {
if (e.target.classList.contains('copy-btn')) {
const url = e.target.dataset.url;
navigator.clipboard.writeText(url).then(() => {
const originalText = e.target.textContent;
e.target.textContent = 'Copied!';
setTimeout(() => e.target.textContent = originalText, 2000);
});
}
});
document.addEventListener('click', async (e) => {
if (e.target.classList.contains('revoke-btn')) {
if (!confirm('Are you sure you want to revoke this share link?')) return;
const shareId = e.target.dataset.shareId;
const response = await fetch(`/shares/${shareId}/revoke/`, { method: 'POST' });
if (response.ok) {
const item = e.target.closest('.share-item');
item.classList.add('inactive');
item.querySelector('.text-success')?.classList.replace('text-success', 'text-error');
item.querySelector('.text-success, .text-error').textContent = 'Inactive';
e.target.remove();
}
}
});
document.getElementById('deleteBtn').addEventListener('click', async () => {
if (!confirm('Are you sure you want to delete this file? This cannot be undone.')) return;
const response = await fetch('{% url "portal:delete_file" file.id %}', { method: 'POST' });
if (response.ok) {
window.location.href = '{% url "portal:files" %}';
} else {
alert('Failed to delete file');
}
});
</script>
{% endblock %}