197 lines
8.6 KiB
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 %}
|
|
•
|
|
{{ share.download_count }} download{{ share.download_count|pluralize }}
|
|
{% if share.max_downloads %}/ {{ share.max_downloads }} max{% endif %}
|
|
{% if share.expires_at %}
|
|
• Expires {{ share.expires_at|date:"M d, Y" }}
|
|
{% endif %}
|
|
{% if share.is_password_protected %}
|
|
• 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>
|
|
• 0 downloads
|
|
${result.share.expires_at ? '• 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 %}
|