This commit is contained in:
2026-02-19 11:00:53 +09:00
commit ad91226e3f
11 changed files with 810 additions and 0 deletions

301
static/index.html Normal file
View File

@@ -0,0 +1,301 @@
<!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>