485 lines
19 KiB
HTML
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>
|