Added more missing files.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Classify Files</title>
|
||||
<style>
|
||||
/* Same CSS as index.html */
|
||||
/* ... (include all CSS from index.html) ... */
|
||||
|
||||
/* Additional styles for classification */
|
||||
.file-item {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"]:focus + label {
|
||||
outline: var(--focus-outline);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</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>Classify Unknown Files</h1>
|
||||
|
||||
<p>Please select the file type for each uploaded file:</p>
|
||||
|
||||
<form action="/classify" method="POST">
|
||||
{% for file in unknown_files %}
|
||||
<div class="file-item">
|
||||
<div class="file-name" id="file-{{ loop.index }}">{{ file.name }}</div>
|
||||
<fieldset aria-labelledby="file-{{ loop.index }}">
|
||||
<legend class="sr-only">Select file type for {{ file.name }}</legend>
|
||||
<div class="radio-group" role="radiogroup">
|
||||
{% for file_type in file_types %}
|
||||
<div class="radio-option">
|
||||
<input type="radio"
|
||||
id="{{ file.name }}-{{ file_type }}"
|
||||
name="{{ file.name }}"
|
||||
value="{{ file_type }}"
|
||||
required
|
||||
{% if file_type == 'other' %}checked{% endif %}>
|
||||
<label for="{{ file.name }}-{{ file_type }}">{{ file_type|title }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn">Save and Continue</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-group" style="margin-top: 30px; text-align: right;">
|
||||
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Ensure at least one option is selected for each file
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
const fileItems = document.querySelectorAll('.file-item');
|
||||
let isValid = true;
|
||||
|
||||
fileItems.forEach(item => {
|
||||
const fileName = item.querySelector('.file-name').textContent;
|
||||
const selectedOption = document.querySelector(`input[name="${fileName}"]:checked`);
|
||||
|
||||
if (!selectedOption) {
|
||||
isValid = false;
|
||||
document.getElementById('status-announcer').textContent =
|
||||
`Please select a file type for ${fileName}`;
|
||||
|
||||
// Highlight the unselected file item
|
||||
item.style.borderColor = 'var(--error-color)';
|
||||
setTimeout(() => {
|
||||
item.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error</title>
|
||||
<style>
|
||||
/* Same CSS as index.html */
|
||||
/* ... (include CSS from index.html) ... */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Error</h1>
|
||||
<div class="alert alert-error" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<div style="text-align: right; margin-top: 20px;">
|
||||
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,377 @@
|
||||
<!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;
|
||||
}
|
||||
</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>
|
||||
</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>
|
||||
</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();
|
||||
});
|
||||
|
||||
// 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';
|
||||
}
|
||||
});
|
||||
|
||||
// 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');
|
||||
|
||||
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') {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Shutdown Server</title>
|
||||
<style>
|
||||
/* Same CSS as index.html */
|
||||
/* ... (include CSS from index.html) ... */
|
||||
|
||||
/* Additional styles */
|
||||
.shutdown-warning {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #95a5a6;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<div class="container" id="main-content">
|
||||
<h1>Shutdown Server</h1>
|
||||
|
||||
<p class="shutdown-warning" role="alert">Warning: This will completely shut down the upload server.</p>
|
||||
<p>Are you sure you want to shut down the server? You can restart it from the system menu if you want to use it again.</p>
|
||||
|
||||
<div class="button-group">
|
||||
<a href="/" class="btn btn-secondary">Cancel</a>
|
||||
<form action="/shutdown" method="POST">
|
||||
<button type="submit" class="btn btn-danger">Confirm Shutdown</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Shutting Down</title>
|
||||
<style>
|
||||
/* Same CSS as index.html */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Additional styles */
|
||||
.shutdown-message {
|
||||
color: #3498db;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 6px solid #f3f3f3;
|
||||
border-top: 6px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 2s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Server Shutting Down</h1>
|
||||
|
||||
<div class="spinner" role="status" aria-label="Server is shutting down"></div>
|
||||
|
||||
<p class="shutdown-message" aria-live="polite">
|
||||
The upload server is shutting down.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Close the window automatically
|
||||
window.onload = function() {
|
||||
// Try multiple approaches to close the window
|
||||
setTimeout(function() {
|
||||
// Method 1: Try to close the window
|
||||
window.close();
|
||||
|
||||
// Method 2: Add a fallback message if window doesn't close
|
||||
setTimeout(function() {
|
||||
document.querySelector('.shutdown-message').textContent =
|
||||
'Server has shut down. You can close this window and return to the menu.';
|
||||
|
||||
// Method 3: Redirect to about:blank as a last resort
|
||||
setTimeout(function() {
|
||||
window.location.href = 'about:blank';
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,308 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Upload Complete</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;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-item.success {
|
||||
background-color: rgba(46, 204, 113, 0.1);
|
||||
}
|
||||
|
||||
.file-item.error {
|
||||
background-color: rgba(231, 76, 60, 0.1);
|
||||
}
|
||||
|
||||
.file-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back-link:hover, .back-link:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #e7f4fd;
|
||||
color: var(--secondary-color);
|
||||
border: 1px solid var(--secondary-color);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.file-count {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* For displaying folder structure */
|
||||
.folder-path {
|
||||
font-family: monospace;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<div class="container" id="main-content">
|
||||
<h1>Upload Complete</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 %}
|
||||
|
||||
<h2>Files Processed</h2>
|
||||
|
||||
{% if uploaded_files %}
|
||||
<div class="file-count">
|
||||
Showing <span id="showing-count">1-{{ uploaded_files|length if uploaded_files|length < 10 else 10 }}</span> of {{ uploaded_files|length }} files
|
||||
</div>
|
||||
|
||||
<div class="file-list" role="table" aria-label="Uploaded files">
|
||||
<div class="file-item" role="row">
|
||||
<div role="columnheader" style="font-weight: bold; width: 60%;">File Name</div>
|
||||
<div role="columnheader" style="font-weight: bold; width: 20%;">File Type</div>
|
||||
<div role="columnheader" style="font-weight: bold; width: 20%;">Status</div>
|
||||
</div>
|
||||
|
||||
<div id="file-items-container">
|
||||
{% for file in uploaded_files[:10] %}
|
||||
<div class="file-item {{ 'success' if file.success else 'error' }}" role="row">
|
||||
<div role="cell" style="width: 60%;">
|
||||
{{ file.name }}
|
||||
{% if file.error %}
|
||||
<div class="folder-path">Error: {{ file.error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div role="cell" style="width: 20%;">{{ file.type|title }}</div>
|
||||
<div role="cell" style="width: 20%;" class="file-status {{ 'status-success' if file.success else 'status-error' }}">
|
||||
{{ 'Success' if file.success else 'Failed' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if uploaded_files|length > 10 %}
|
||||
<div class="pagination">
|
||||
<button id="prev-btn" disabled>Previous</button>
|
||||
<button id="next-btn">Next</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
// Simple pagination for large file lists
|
||||
const allFiles = [
|
||||
{% for file in uploaded_files %}
|
||||
{
|
||||
name: "{{ file.name|safe }}",
|
||||
type: "{{ file.type|title }}",
|
||||
success: {{ 'true' if file.success else 'false' }},
|
||||
{% if file.error %}
|
||||
error: "{{ file.error|safe }}"
|
||||
{% endif %}
|
||||
}{{ ',' if not loop.last }}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const itemsPerPage = 10;
|
||||
let currentPage = 0;
|
||||
|
||||
function updateDisplay() {
|
||||
const container = document.getElementById('file-items-container');
|
||||
const start = currentPage * itemsPerPage;
|
||||
const end = Math.min(start + itemsPerPage, allFiles.length);
|
||||
|
||||
// Update displayed count
|
||||
document.getElementById('showing-count').textContent =
|
||||
`${start + 1}-${end}`;
|
||||
|
||||
// Clear current items
|
||||
container.innerHTML = '';
|
||||
|
||||
// Add items for current page
|
||||
for (let i = start; i < end; i++) {
|
||||
const file = allFiles[i];
|
||||
const item = document.createElement('div');
|
||||
item.className = `file-item ${file.success ? 'success' : 'error'}`;
|
||||
item.setAttribute('role', 'row');
|
||||
|
||||
const nameCell = document.createElement('div');
|
||||
nameCell.setAttribute('role', 'cell');
|
||||
nameCell.style.width = '60%';
|
||||
nameCell.textContent = file.name;
|
||||
|
||||
if (file.error) {
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'folder-path';
|
||||
errorDiv.textContent = `Error: ${file.error}`;
|
||||
nameCell.appendChild(errorDiv);
|
||||
}
|
||||
|
||||
const typeCell = document.createElement('div');
|
||||
typeCell.setAttribute('role', 'cell');
|
||||
typeCell.style.width = '20%';
|
||||
typeCell.textContent = file.type;
|
||||
|
||||
const statusCell = document.createElement('div');
|
||||
statusCell.setAttribute('role', 'cell');
|
||||
statusCell.style.width = '20%';
|
||||
statusCell.className = `file-status ${file.success ? 'status-success' : 'status-error'}`;
|
||||
statusCell.textContent = file.success ? 'Success' : 'Failed';
|
||||
|
||||
item.appendChild(nameCell);
|
||||
item.appendChild(typeCell);
|
||||
item.appendChild(statusCell);
|
||||
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
// Update button states
|
||||
const prevBtn = document.getElementById('prev-btn');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
|
||||
if (prevBtn && nextBtn) {
|
||||
prevBtn.disabled = currentPage === 0;
|
||||
nextBtn.disabled = end >= allFiles.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up pagination buttons if they exist
|
||||
const prevBtn = document.getElementById('prev-btn');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
|
||||
if (prevBtn && nextBtn) {
|
||||
prevBtn.addEventListener('click', function() {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
updateDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', function() {
|
||||
if ((currentPage + 1) * itemsPerPage < allFiles.length) {
|
||||
currentPage++;
|
||||
updateDisplay();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% else %}
|
||||
<p>No files were processed.</p>
|
||||
{% endif %}
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 20px;">
|
||||
<a href="/" class="back-link">Back to main page</a>
|
||||
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Voxin Voice Installation</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;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-item.success {
|
||||
background-color: rgba(46, 204, 113, 0.1);
|
||||
}
|
||||
|
||||
.file-item.error {
|
||||
background-color: rgba(231, 76, 60, 0.1);
|
||||
}
|
||||
|
||||
.file-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back-link:hover, .back-link:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #e7f4fd;
|
||||
color: var(--secondary-color);
|
||||
border: 1px solid var(--secondary-color);
|
||||
}
|
||||
|
||||
.details-container {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.details-toggle {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--secondary-color);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 0.9em;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<div class="container" id="main-content">
|
||||
<h1>Voxin Voice Installation</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 %}
|
||||
|
||||
<h2>Installation Results</h2>
|
||||
|
||||
{% if results %}
|
||||
<div class="file-list" role="table" aria-label="Voice installation results">
|
||||
<div class="file-item" role="row">
|
||||
<div role="columnheader" style="font-weight: bold; width: 50%;">Voice Package</div>
|
||||
<div role="columnheader" style="font-weight: bold; width: 25%;">Status</div>
|
||||
<div role="columnheader" style="font-weight: bold; width: 25%;">Message</div>
|
||||
</div>
|
||||
|
||||
{% for result in results %}
|
||||
<div class="file-item {{ 'success' if result.success else 'error' }}" role="row">
|
||||
<div role="cell" style="width: 50%;">{{ result.name }}</div>
|
||||
<div role="cell" style="width: 25%;" class="file-status {{ 'status-success' if result.success else 'status-error' }}">
|
||||
{{ 'Success' if result.success else 'Failed' }}
|
||||
</div>
|
||||
<div role="cell" style="width: 25%;">
|
||||
{{ result.message }}
|
||||
{% if result.details %}
|
||||
<div>
|
||||
<button class="details-toggle"
|
||||
onclick="toggleDetails('details-{{ loop.index }}')"
|
||||
aria-expanded="false"
|
||||
aria-controls="details-{{ loop.index }}">
|
||||
Show details
|
||||
</button>
|
||||
<div id="details-{{ loop.index }}" class="details-container" style="display: none;">
|
||||
{{ result.details }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No voice packages were processed.</p>
|
||||
{% endif %}
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 20px;">
|
||||
<a href="/" class="back-link">Back to main page</a>
|
||||
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDetails(id) {
|
||||
const details = document.getElementById(id);
|
||||
const button = document.querySelector(`[aria-controls="${id}"]`);
|
||||
|
||||
if (details.style.display === 'none') {
|
||||
details.style.display = 'block';
|
||||
button.textContent = 'Hide details';
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
details.style.display = 'none';
|
||||
button.textContent = 'Show details';
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user