Files
fastapi_file_uploader/static/index.html
2026-02-19 11:00:53 +09:00

302 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>