302 lines
13 KiB
HTML
302 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Modern File Manager</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-color: #f0f2f5;
|
||
--primary-pastel: #a2d2ff;
|
||
--secondary-pastel: #ffafcc;
|
||
--accent-pastel: #bde0fe;
|
||
--success-pastel: #cdb4db;
|
||
--text-color: #4a4a4a;
|
||
--card-bg: #ffffff;
|
||
--shadow: 0 8px 30px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
* { box-sizing: border-box; font-family: 'Poppins', sans-serif; }
|
||
body { background-color: var(--bg-color); color: var(--text-color); margin: 0; padding: 40px 20px; }
|
||
.container { max-width: 1000px; margin: 0 auto; }
|
||
|
||
h1 { text-align: center; color: #6a6a6a; font-weight: 600; margin-bottom: 40px; }
|
||
h3 { font-weight: 600; margin-top: 0; color: #888; }
|
||
|
||
/* Card Style */
|
||
.card {
|
||
background: var(--card-bg);
|
||
border-radius: 20px;
|
||
padding: 30px;
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: 30px;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
/* Form Controls */
|
||
.form-group { margin-bottom: 15px; }
|
||
label { display: block; margin-bottom: 8px; font-size: 0.9rem; font-weight: 600; color: #999; }
|
||
input[type="text"], input[type="url"], input[type="file"] {
|
||
width: 100%; padding: 12px 15px; border: 2px solid #f1f1f1; border-radius: 12px;
|
||
outline: none; transition: border-color 0.3s;
|
||
}
|
||
input:focus { border-color: var(--primary-pastel); }
|
||
|
||
.btn-group { display: flex; gap: 10px; margin-top: 20px; }
|
||
button {
|
||
flex: 1; padding: 12px; border: none; border-radius: 12px; cursor: pointer;
|
||
font-weight: 600; transition: all 0.3s; color: white;
|
||
}
|
||
.btn-upload { background-color: var(--primary-pastel); color: #555; }
|
||
.btn-upload:hover { background-color: #89c2f7; transform: translateY(-2px); }
|
||
.btn-yt { background-color: var(--secondary-pastel); color: #555; }
|
||
.btn-yt:hover { background-color: #ff99bb; transform: translateY(-2px); }
|
||
button:disabled { background-color: #ccc; cursor: not-allowed; }
|
||
|
||
/* Video Player Area */
|
||
.video-player-container {
|
||
display: none; background: #000; border-radius: 20px; overflow: hidden;
|
||
margin-bottom: 30px; position: relative; box-shadow: 0 20px 50px rgba(0,0,0,0.2);
|
||
}
|
||
video { width: 100%; display: block; }
|
||
.close-player {
|
||
position: absolute; top: 15px; right: 15px; background: rgba(255,255,255,0.3);
|
||
border: none; color: white; padding: 5px 15px; border-radius: 20px; cursor: pointer; backdrop-filter: blur(5px);
|
||
}
|
||
|
||
/* Grid for Files */
|
||
.file-grid {
|
||
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px;
|
||
}
|
||
.file-card {
|
||
background: white; border-radius: 18px; padding: 20px; box-shadow: var(--shadow);
|
||
display: flex; flex-direction: column; justify-content: space-between; border: 1px solid #f9f9f9;
|
||
}
|
||
.file-info h4 { margin: 0 0 10px 0; font-size: 1.1rem; word-break: break-all; color: #555; }
|
||
.file-info p { margin: 5px 0; font-size: 0.85rem; color: #aaa; }
|
||
.file-tag {
|
||
display: inline-block; padding: 4px 10px; border-radius: 8px; font-size: 0.75rem;
|
||
margin-bottom: 15px; background: var(--accent-pastel); color: #666;
|
||
}
|
||
|
||
.file-actions { display: flex; gap: 10px; margin-top: 15px; }
|
||
.action-link {
|
||
text-decoration: none; font-size: 0.85rem; font-weight: 600;
|
||
padding: 8px 15px; border-radius: 10px; flex: 1; text-align: center;
|
||
}
|
||
.link-dl { background: #f0f0f0; color: #777; }
|
||
.link-play { background: var(--success-pastel); color: #555; }
|
||
.link-delete { background: #ffcfcf; color: #d65a5a; }
|
||
.link-delete:hover { background: #ffbaba; }
|
||
|
||
/* Loading Animation */
|
||
.loader {
|
||
border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-pastel);
|
||
border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite;
|
||
display: inline-block; vertical-align: middle; margin-right: 10px;
|
||
}
|
||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||
|
||
@media (max-width: 600px) {
|
||
.btn-group { flex-direction: column; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>✨ File Manager</h1>
|
||
|
||
<!-- Video Player -->
|
||
<div id="playerContainer" class="video-player-container card">
|
||
<button class="close-player" onclick="closePlayer()">Close ×</button>
|
||
<video id="videoPlayer" controls></video>
|
||
<div style="padding: 15px; background: white;">
|
||
<h4 id="playingTitle" style="margin:0; color: #555;">Playing...</h4>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
|
||
<!-- Upload Form -->
|
||
<div class="card" style="flex: 1; min-width: 300px;">
|
||
<h3>📁 Local Upload</h3>
|
||
<form id="uploadForm">
|
||
<div class="form-group">
|
||
<label>Select File</label>
|
||
<input type="file" id="fileInput" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Description</label>
|
||
<input type="text" id="description" placeholder="What is this?">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Uploader</label>
|
||
<input type="text" id="uploader" placeholder="Your name">
|
||
</div>
|
||
<button type="submit" class="btn-upload" id="upBtn">Upload to Cloud</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- YouTube Form -->
|
||
<div class="card" style="flex: 1; min-width: 300px;">
|
||
<h3>📺 YouTube Link</h3>
|
||
<form id="youtubeForm">
|
||
<div class="form-group">
|
||
<label>Video URL</label>
|
||
<input type="url" id="youtubeUrl" placeholder="https://youtube.com/..." required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Uploader Name</label>
|
||
<input type="text" id="ytUploader" placeholder="Your name">
|
||
</div>
|
||
<br>
|
||
<button type="submit" class="btn-yt" id="ytBtn">Download Video</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>📦 Library</h3>
|
||
<div class="file-grid" id="fileGrid">
|
||
<!-- Files will be injected here -->
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const uploadForm = document.getElementById('uploadForm');
|
||
const youtubeForm = document.getElementById('youtubeForm');
|
||
const fileGrid = document.getElementById('fileGrid');
|
||
|
||
async function fetchFiles() {
|
||
const response = await fetch('/api/files');
|
||
const files = await response.json();
|
||
fileGrid.innerHTML = '';
|
||
|
||
if (files.length === 0) {
|
||
fileGrid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: #ccc; margin-top: 40px;">No files found yet. Start uploading!</p>';
|
||
return;
|
||
}
|
||
|
||
files.reverse().forEach(file => {
|
||
const isVideo = file.type.startsWith('video/');
|
||
const card = document.createElement('div');
|
||
card.className = 'file-card';
|
||
card.innerHTML = `
|
||
<div class="file-info">
|
||
<span class="file-tag">${file.type.split('/')[1].toUpperCase()}</span>
|
||
<h4>${file.filename}</h4>
|
||
<p>👤 ${file.uploader || 'Anonymous'}</p>
|
||
<p>📅 ${new Date(file.upload_date).toLocaleDateString()}</p>
|
||
${file.description ? `<p style="color: #888; margin-top:10px; font-style: italic;">"${file.description}"</p>` : ''}
|
||
</div>
|
||
<div class="file-actions">
|
||
<a href="/api/files/download?filename=${encodeURIComponent(file.filename)}" class="action-link link-dl">Down</a>
|
||
${isVideo ? `<a href="#" onclick="playVideo('${encodeURIComponent(file.filename)}')" class="action-link link-play">Play</a>` : ''}
|
||
<a href="#" onclick="deleteFile('${encodeURIComponent(file.filename)}')" class="action-link link-delete">Del</a>
|
||
</div>
|
||
`;
|
||
fileGrid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function playVideo(filename) {
|
||
const playerContainer = document.getElementById('playerContainer');
|
||
const videoPlayer = document.getElementById('videoPlayer');
|
||
const playingTitle = document.getElementById('playingTitle');
|
||
|
||
playingTitle.innerText = 'Now Playing: ' + decodeURIComponent(filename);
|
||
videoPlayer.src = `/api/files/download?filename=${filename}&stream=true`;
|
||
playerContainer.style.display = 'block';
|
||
videoPlayer.play();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
function closePlayer() {
|
||
const playerContainer = document.getElementById('playerContainer');
|
||
const videoPlayer = document.getElementById('videoPlayer');
|
||
videoPlayer.pause();
|
||
videoPlayer.src = '';
|
||
playerContainer.style.display = 'none';
|
||
}
|
||
|
||
async function deleteFile(filename) {
|
||
if (!confirm(`Are you sure you want to delete "${decodeURIComponent(filename)}"?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/files?filename=${filename}`, {
|
||
method: 'DELETE'
|
||
});
|
||
if (response.ok) {
|
||
fetchFiles();
|
||
} else {
|
||
const err = await response.json();
|
||
alert('Delete failed: ' + err.detail);
|
||
}
|
||
} catch (error) {
|
||
alert('Connection error');
|
||
}
|
||
}
|
||
|
||
// Handle Local Upload
|
||
uploadForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const upBtn = document.getElementById('upBtn');
|
||
const originalText = upBtn.innerText;
|
||
upBtn.innerHTML = '<span class="loader"></span> Uploading...';
|
||
upBtn.disabled = true;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', document.getElementById('fileInput').files[0]);
|
||
formData.append('description', document.getElementById('description').value);
|
||
formData.append('uploader', document.getElementById('uploader').value);
|
||
|
||
try {
|
||
const response = await fetch('/api/files/upload', { method: 'POST', body: formData });
|
||
if (response.ok) {
|
||
uploadForm.reset();
|
||
fetchFiles();
|
||
} else {
|
||
const err = await response.json();
|
||
alert('Error: ' + err.detail);
|
||
}
|
||
} catch (error) {
|
||
alert('Connection error');
|
||
} finally {
|
||
upBtn.innerText = originalText;
|
||
upBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// Handle YouTube Download
|
||
youtubeForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const ytBtn = document.getElementById('ytBtn');
|
||
const originalText = ytBtn.innerText;
|
||
ytBtn.innerHTML = '<span class="loader"></span> Processing YouTube...';
|
||
ytBtn.disabled = true;
|
||
|
||
const formData = new FormData();
|
||
formData.append('url', document.getElementById('youtubeUrl').value);
|
||
formData.append('uploader', document.getElementById('ytUploader').value);
|
||
|
||
try {
|
||
const response = await fetch('/api/files/youtube', { method: 'POST', body: formData });
|
||
if (response.ok) {
|
||
youtubeForm.reset();
|
||
fetchFiles();
|
||
} else {
|
||
const err = await response.json();
|
||
alert('YouTube Error: ' + err.detail);
|
||
}
|
||
} catch (error) {
|
||
alert('Connection error');
|
||
} finally {
|
||
ytBtn.innerText = originalText;
|
||
ytBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
fetchFiles();
|
||
</script>
|
||
</body>
|
||
</html>
|