Added more missing files.

This commit is contained in:
Storm Dragon
2025-07-17 00:20:15 -04:00
parent d2e82fadf0
commit 2b4f369c8b
12 changed files with 1957 additions and 0 deletions
@@ -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>