Files
2025-10-10 16:48:35 -04:00

485 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload Server</title>
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--background-color: #f8f9fa;
--text-color: #333;
--error-color: #e74c3c;
--success-color: #2ecc71;
--focus-outline: 3px solid #f39c12;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1, h2 {
color: var(--primary-color);
}
.container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.file-input {
border: 2px dashed #ddd;
padding: 20px;
text-align: center;
cursor: pointer;
border-radius: 5px;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
.file-input:hover, .file-input:focus-within {
border-color: var(--secondary-color);
background-color: #edf2f7;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
input[type="file"] {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.file-label {
display: block;
cursor: pointer;
font-weight: normal;
padding: 10px;
}
.btn {
display: inline-block;
background-color: var(--secondary-color);
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
margin-right: 10px;
}
.btn:hover, .btn:focus {
background-color: #2980b9;
}
.btn:focus {
outline: var(--focus-outline);
outline-offset: 2px;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-error {
background-color: #fdecea;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.alert-success {
background-color: #d4f6e6;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.skip-link {
position: absolute;
top: -40px;
left: 0;
color: white;
background-color: var(--secondary-color);
padding: 8px;
z-index: 100;
transition: top 0.3s ease;
}
.skip-link:focus {
top: 0;
}
/* For screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Status announcer for screen readers */
#status-announcer {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
pointer-events: none;
}
/* Upload sections */
.upload-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.upload-section:last-child {
border-bottom: none;
}
/* Upload feedback styles */
.btn:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.upload-status {
margin-top: 10px;
font-weight: bold;
color: var(--primary-color);
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Announcer for screen readers -->
<div id="status-announcer" aria-live="polite"></div>
<div class="container" id="main-content">
<h1>File Upload Server</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- File Upload Section -->
<section class="upload-section" aria-labelledby="file-section-heading">
<h2 id="file-section-heading">Upload Files</h2>
<p>Upload game ROMs and other individual files.</p>
<form action="/upload" method="POST" enctype="multipart/form-data">
<div class="form-group">
<div class="file-input" id="file-drop-area">
<label for="file-upload" class="file-label">
Choose files or drag and drop here
<span class="sr-only">Select one or more files to upload</span>
</label>
<input id="file-upload" type="file" name="file" multiple aria-describedby="file-help">
<p id="file-help" class="help-text">Selected files: <span id="selected-files">None</span></p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn" id="upload-button">Upload Files</button>
<div id="upload-status" class="upload-status" style="display: none;" aria-live="polite"></div>
</div>
</form>
</section>
<!-- Music Folder Upload Section -->
<section class="upload-section" aria-labelledby="music-section-heading">
<h2 id="music-section-heading">Upload Music Folder</h2>
<p>Upload an entire music folder with its structure directly to your Music directory.</p>
<form action="/upload_music" method="POST" enctype="multipart/form-data">
<div class="form-group">
<div class="file-input" id="folder-drop-area">
<label for="folder-upload" class="file-label">
Select a music folder to upload
<span class="sr-only">Select a folder containing music files</span>
</label>
<input id="folder-upload" type="file" name="folder" webkitdirectory directory multiple aria-describedby="folder-help">
<p id="folder-help" class="help-text">Selected folder: <span id="selected-folder">None</span></p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn" id="upload-folder-button">Upload Music Folder</button>
<div id="upload-folder-status" class="upload-status" style="display: none;" aria-live="polite"></div>
</div>
</form>
</section>
<!-- Audiobook Upload Section -->
<section class="upload-section" aria-labelledby="audiobook-section-heading">
<h2 id="audiobook-section-heading">Upload Audiobooks</h2>
<p>Upload audiobook files (.m4b, .epub, or .zip) to your Library directory.</p>
<form action="/upload" method="POST" enctype="multipart/form-data">
<div class="form-group">
<div class="file-input" id="audiobook-drop-area">
<label for="audiobook-upload" class="file-label">
Choose audiobook files or drag and drop here
<span class="sr-only">Select one or more audiobook files to upload</span>
</label>
<input id="audiobook-upload" type="file" name="file" multiple accept=".m4b,.epub,.zip" aria-describedby="audiobook-help">
<p id="audiobook-help" class="help-text">Selected files: <span id="selected-audiobooks">None</span></p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn" id="upload-audiobook-button">Upload Audiobooks</button>
<div id="upload-audiobook-status" class="upload-status" style="display: none;" aria-live="polite"></div>
</div>
</form>
</section>
<div class="form-group" style="margin-top: 30px; text-align: right;">
<a href="/shutdown" class="btn btn-secondary" style="background-color: #e74c3c;">Shutdown Server</a>
</div>
</div>
<script>
// Make the entire area clickable for file selection
document.getElementById('file-drop-area').addEventListener('click', function(e) {
if (e.target !== this) return;
document.getElementById('file-upload').click();
});
// Make the entire area clickable for folder selection
document.getElementById('folder-drop-area').addEventListener('click', function(e) {
if (e.target !== this) return;
document.getElementById('folder-upload').click();
});
// Make the entire area clickable for audiobook selection
document.getElementById('audiobook-drop-area').addEventListener('click', function(e) {
if (e.target !== this) return;
document.getElementById('audiobook-upload').click();
});
// Show selected files
document.getElementById('file-upload').addEventListener('change', function(e) {
const fileList = e.target.files;
const fileNames = Array.from(fileList).map(file => file.name);
const selectedFiles = document.getElementById('selected-files');
if (fileNames.length > 0) {
selectedFiles.textContent = fileNames.join(', ');
// Announce to screen readers
document.getElementById('status-announcer').textContent =
`Selected ${fileNames.length} files: ${fileNames.join(', ')}`;
} else {
selectedFiles.textContent = 'None';
document.getElementById('status-announcer').textContent = 'No files selected';
}
});
// Show selected folder
document.getElementById('folder-upload').addEventListener('change', function(e) {
const fileList = e.target.files;
const fileCount = fileList.length;
const selectedFolder = document.getElementById('selected-folder');
if (fileCount > 0) {
// Get the common folder path from the first file
const firstFile = fileList[0];
const folderPath = firstFile.webkitRelativePath.split('/')[0];
selectedFolder.textContent = `${folderPath} (${fileCount} files)`;
// Announce to screen readers
document.getElementById('status-announcer').textContent =
`Selected folder: ${folderPath} containing ${fileCount} files`;
} else {
selectedFolder.textContent = 'None';
document.getElementById('status-announcer').textContent = 'No folder selected';
}
});
// Show selected audiobooks
document.getElementById('audiobook-upload').addEventListener('change', function(e) {
const fileList = e.target.files;
const fileNames = Array.from(fileList).map(file => file.name);
const selectedAudiobooks = document.getElementById('selected-audiobooks');
if (fileNames.length > 0) {
selectedAudiobooks.textContent = fileNames.join(', ');
// Announce to screen readers
document.getElementById('status-announcer').textContent =
`Selected ${fileNames.length} audiobook files: ${fileNames.join(', ')}`;
} else {
selectedAudiobooks.textContent = 'None';
document.getElementById('status-announcer').textContent = 'No audiobook files selected';
}
});
// Handle drag and drop for files
const fileDropArea = document.getElementById('file-drop-area');
setupDragDrop(fileDropArea, 'file-upload');
// Handle drag and drop for folders
const folderDropArea = document.getElementById('folder-drop-area');
setupDragDrop(folderDropArea, 'folder-upload');
// Handle drag and drop for audiobooks
const audiobookDropArea = document.getElementById('audiobook-drop-area');
setupDragDrop(audiobookDropArea, 'audiobook-upload');
function setupDragDrop(dropArea, inputId) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, function() {
this.style.borderColor = 'var(--secondary-color)';
this.style.backgroundColor = '#edf2f7';
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, function() {
this.style.borderColor = '#ddd';
this.style.backgroundColor = '#f8f9fa';
}, false);
});
dropArea.addEventListener('drop', function(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (inputId === 'file-upload' || inputId === 'audiobook-upload') {
document.getElementById(inputId).files = files;
// Trigger change event
const event = new Event('change');
document.getElementById(inputId).dispatchEvent(event);
} else {
// Show message that folder drop might not be supported
document.getElementById('status-announcer').textContent =
'For folder upload, please use the file picker';
document.getElementById('selected-folder').textContent =
'Please use the folder selection button';
}
}, false);
}
// Upload progress handling
function setupUploadProgress(formSelector, buttonId, statusId) {
const form = document.querySelector(formSelector);
const button = document.getElementById(buttonId);
const status = document.getElementById(statusId);
const announcer = document.getElementById('status-announcer');
form.addEventListener('submit', function(e) {
// Disable button and show status
button.disabled = true;
button.textContent = 'Uploading...';
status.style.display = 'block';
status.textContent = 'Upload in progress, please wait...';
// Announce to screen readers
announcer.textContent = 'Upload started, please wait while files are being processed';
// Add a small delay to ensure the status is visible before form submits
setTimeout(() => {
// Form will submit naturally
}, 100);
});
}
// Set up progress handling for all forms
setupUploadProgress('form[action="/upload"]', 'upload-button', 'upload-status');
setupUploadProgress('form[action="/upload_music"]', 'upload-folder-button', 'upload-folder-status');
// Handle audiobook form separately (also uses /upload action but different button)
const audiobookForm = document.getElementById('upload-audiobook-button').closest('form');
const audiobookButton = document.getElementById('upload-audiobook-button');
const audiobookStatus = document.getElementById('upload-audiobook-status');
const announcer = document.getElementById('status-announcer');
audiobookForm.addEventListener('submit', function(e) {
audiobookButton.disabled = true;
audiobookButton.textContent = 'Uploading...';
audiobookStatus.style.display = 'block';
audiobookStatus.textContent = 'Upload in progress, please wait...';
announcer.textContent = 'Audiobook upload started, please wait while files are being processed';
});
// Check if the client is on local network
fetch('/check_local')
.then(response => {
if (!response.ok) {
throw new Error("Not on local network");
}
return response.json();
})
.then(data => {
if (!data.local) {
document.body.innerHTML = '<div class="container"><h1>Error</h1><p>Access denied: Local network only</p></div>';
}
})
.catch(error => {
console.error("Error checking local network:", error);
});
</script>
</body>
</html>