Merging dev into main

This commit is contained in:
Tyler
2026-04-20 00:54:02 -04:00
committed by GitHub
parent 5b9a2b7a53
commit a9022bb5e3
42 changed files with 10305 additions and 0 deletions
+796
View File
@@ -0,0 +1,796 @@
/**
* Enhanced PacShare Module
* Handles bulk uploads and single file uploads seamlessly
*/
class PacShareEnhanced {
constructor() {
this.selectedFiles = [];
this.uploadResults = [];
this.settings = {
enable2FA: false,
autoClearPasswords: true,
autoCopyLinks: true,
showUploadProgress: true,
scrollToResults: true,
maxUploadSizeMB: 25,
validateFileTypes: false,
concurrentUploads: 1,
enableFilePreview: true,
rememberAlgorithm: true
};
this.setupEventListeners();
this.loadSettings();
}
setupEventListeners() {
// Drag & Drop Zone
const dropZone = document.getElementById('pacshare-drop-zone');
const fileInput = document.getElementById('upload-file');
const fileSelect = document.getElementById('pacshare-file-select');
console.log('PacShare setup - dropZone:', dropZone, 'fileInput:', fileInput, 'fileSelect:', fileSelect);
if (dropZone && fileInput) {
console.log('Setting up PacShare drag & drop events');
// Drag & Drop Events
dropZone.addEventListener('dragover', this.handleDragOver.bind(this));
dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this));
dropZone.addEventListener('drop', this.handleDrop.bind(this));
dropZone.addEventListener('click', () => {
console.log('PacShare drop zone clicked, opening file input');
fileInput.click();
});
// File Input Events
fileInput.addEventListener('change', this.handleFileSelect.bind(this));
} else {
console.error('PacShare elements not found - dropZone:', dropZone, 'fileInput:', fileInput);
}
if (fileSelect) {
console.log('Setting up PacShare file select button');
fileSelect.addEventListener('click', (e) => {
console.log('PacShare file select button clicked');
e.stopPropagation();
fileInput.click();
});
} else {
console.error('PacShare file select button not found');
}
// Clear files button
const clearBtn = document.getElementById('pacshare-clear-files');
if (clearBtn) {
clearBtn.addEventListener('click', this.clearFiles.bind(this));
}
// Enhanced form submission
const uploadForm = document.getElementById('upload-form');
if (uploadForm) {
// Remove existing event listener first
uploadForm.replaceWith(uploadForm.cloneNode(true));
const newForm = document.getElementById('upload-form');
newForm.addEventListener('submit', this.handleEnhancedSubmit.bind(this));
}
// Settings modal controls
this.setupSettingsModal();
}
handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('pacshare-drop-zone').classList.add('drag-over');
}
handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('pacshare-drop-zone').classList.remove('drag-over');
}
handleDrop(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('pacshare-drop-zone').classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files);
this.addFiles(files);
}
handleFileSelect(e) {
const files = Array.from(e.target.files);
this.addFiles(files);
}
addFiles(newFiles) {
// Filter out duplicates
newFiles = newFiles.filter(newFile =>
!this.selectedFiles.some(existingFile =>
existingFile.name === newFile.name && existingFile.size === newFile.size
)
);
this.selectedFiles.push(...newFiles);
this.updateFileDisplay();
this.updateUI();
}
updateFileDisplay() {
const fileListContainer = document.getElementById('pacshare-file-list');
const filesContainer = document.getElementById('pacshare-files-container');
const uploadBtn = document.getElementById('pacshare-upload-btn');
if (!filesContainer || !fileListContainer) return;
if (this.selectedFiles.length === 0) {
fileListContainer.style.display = 'none';
if (uploadBtn) uploadBtn.textContent = 'Upload and Generate Link';
return;
}
fileListContainer.style.display = 'block';
filesContainer.innerHTML = '';
// Update button text based on file count
if (uploadBtn) {
uploadBtn.textContent = this.selectedFiles.length === 1
? 'Upload and Generate Link'
: `Upload ${this.selectedFiles.length} Files and Generate Links`;
}
this.selectedFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<div class="file-actions">
<button type="button" onclick="pacShareEnhanced.previewFile(${index})" style="padding: 5px 10px; font-size: 0.8em;">Preview</button>
<button type="button" onclick="pacShareEnhanced.removeFile(${index})" class="danger-button" style="padding: 5px 10px; font-size: 0.8em;">Remove</button>
</div>
`;
filesContainer.appendChild(fileItem);
});
}
async previewFile(index) {
const file = this.selectedFiles[index];
if (!file) return;
const previewContainer = document.createElement('div');
previewContainer.className = 'file-preview-container';
const header = document.createElement('div');
header.className = 'file-preview-header';
header.textContent = `Preview: ${file.name}`;
const content = document.createElement('div');
content.className = 'file-preview-content';
// Handle different file types
if (file.type.startsWith('text/') || this.isTextFile(file.name)) {
try {
const text = await this.readFileAsText(file);
content.textContent = text.length > 2000 ? text.substring(0, 2000) + '...' : text;
} catch (error) {
content.textContent = 'Error reading file: ' + error.message;
}
} else if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.className = 'image-preview';
img.src = URL.createObjectURL(file);
img.onload = () => URL.revokeObjectURL(img.src);
content.appendChild(img);
} else {
content.innerHTML = `
<div style="color: #888;">
File Type: ${file.type || 'Unknown'}<br>
Size: ${this.formatFileSize(file.size)}<br>
Preview not available for this file type.
</div>
`;
}
previewContainer.appendChild(header);
previewContainer.appendChild(content);
// Remove existing preview
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
// Add new preview after the file list
const fileList = document.getElementById('pacshare-files-container');
if (fileList) {
fileList.parentNode.insertBefore(previewContainer, fileList.nextSibling);
}
}
removeFile(index) {
this.selectedFiles.splice(index, 1);
this.updateFileDisplay();
// Remove preview if it exists
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
}
clearFiles() {
this.selectedFiles = [];
this.updateFileDisplay();
// Clear file input
const fileInput = document.getElementById('upload-file');
if (fileInput) fileInput.value = '';
// Remove preview if it exists
const existingPreview = document.querySelector('.file-preview-container');
if (existingPreview) {
existingPreview.remove();
}
this.hideResults();
}
async handleEnhancedSubmit(e) {
e.preventDefault();
if (this.selectedFiles.length === 0) {
alert('Please select at least one file to upload.');
return;
}
const algorithm = document.getElementById('share-algorithm')?.value;
const encPassword = document.querySelector('input[name="enc_password"]')?.value;
const pickupPassword = document.querySelector('input[name="pickup_password"]')?.value;
const enable2FA = this.settings.enable2FA;
if (!algorithm || !encPassword || !pickupPassword) {
alert('Please fill in all required fields.');
return;
}
if (this.selectedFiles.length === 1) {
// Single file - use existing logic
await this.uploadSingleFile(this.selectedFiles[0], algorithm, encPassword, pickupPassword, enable2FA);
} else {
// Multiple files - use bulk upload
await this.uploadMultipleFiles(algorithm, encPassword, pickupPassword, enable2FA);
}
}
async uploadSingleFile(file, algorithm, encPassword, pickupPassword, enable2FA) {
const formData = new FormData();
formData.append('file', file);
formData.append('algorithm', algorithm);
formData.append('enc_password', encPassword);
formData.append('pickup_password', pickupPassword);
if (enable2FA) formData.append('enable_2fa', 'on');
try {
const response = await fetch('/', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
if (data.success && data.pickup_url) {
this.showSingleResult(data);
}
} catch (error) {
alert('Error uploading file: ' + error.message);
}
}
async uploadMultipleFiles(algorithm, encPassword, pickupPassword, enable2FA) {
this.uploadResults = [];
this.showProgress();
// Upload files sequentially to avoid overwhelming the server
for (let i = 0; i < this.selectedFiles.length; i++) {
const file = this.selectedFiles[i];
this.updateFileProgress(i, 'uploading');
try {
const formData = new FormData();
formData.append('file', file);
formData.append('algorithm', algorithm);
formData.append('enc_password', encPassword);
formData.append('pickup_password', pickupPassword);
if (enable2FA) formData.append('enable_2fa', 'on');
const response = await fetch('/', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
this.uploadResults.push({ file, error: data.error, success: false });
this.updateFileProgress(i, 'error');
} else if (data.success && data.pickup_url) {
this.uploadResults.push({ file, data, success: true });
this.updateFileProgress(i, 'completed');
}
} catch (error) {
this.uploadResults.push({ file, error: error.message, success: false });
this.updateFileProgress(i, 'error');
}
this.updateOverallProgress(i + 1, this.selectedFiles.length);
}
this.showResults();
}
showProgress() {
const progressSection = document.getElementById('pacshare-progress');
if (progressSection) {
progressSection.style.display = 'block';
}
this.updateOverallProgress(0, this.selectedFiles.length);
this.initializeFileProgress();
}
initializeFileProgress() {
const progressContainer = document.getElementById('pacshare-file-progress');
if (!progressContainer) return;
progressContainer.innerHTML = '';
this.selectedFiles.forEach((file, index) => {
const progressItem = document.createElement('div');
progressItem.className = 'file-progress-item';
progressItem.innerHTML = `
<div class="file-progress-name">${file.name}</div>
<div class="file-progress-status" id="pacshare-progress-${index}">Waiting</div>
`;
progressContainer.appendChild(progressItem);
});
}
updateFileProgress(index, status) {
const statusElement = document.getElementById(`pacshare-progress-${index}`);
if (!statusElement) return;
statusElement.className = `file-progress-status status-${status}`;
switch (status) {
case 'uploading':
statusElement.textContent = 'Uploading...';
break;
case 'completed':
statusElement.textContent = 'Completed';
break;
case 'error':
statusElement.textContent = 'Error';
break;
default:
statusElement.textContent = 'Waiting';
}
}
updateOverallProgress(completed, total) {
const progressBar = document.getElementById('pacshare-overall-bar');
const progressText = document.getElementById('pacshare-overall-text');
if (progressBar) {
const percentage = total > 0 ? (completed / total) * 100 : 0;
progressBar.style.width = `${percentage}%`;
}
if (progressText) {
progressText.textContent = `${completed} / ${total} files uploaded`;
}
}
showSingleResult(data) {
// Use existing single result display logic
const shareLink = document.getElementById('share-link');
const shareLinkContainer = document.getElementById('share-link-container');
if (shareLink && shareLinkContainer) {
shareLink.href = data.pickup_url;
shareLink.textContent = data.pickup_url;
shareLinkContainer.style.display = 'flex';
// Handle 2FA if enabled
if (data.qr_code_url) {
this.showTwoFactorSetup(data.qr_code_url, data.service_name, data.totp_secret);
}
// Clear form
this.clearForm();
// Scroll to results
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
}
}
showResults() {
const resultsSection = document.getElementById('pacshare-results');
const resultsList = document.getElementById('pacshare-results-list');
if (!resultsSection || !resultsList) return;
resultsSection.style.display = 'block';
resultsList.innerHTML = '';
const successCount = this.uploadResults.filter(r => r.success).length;
const totalCount = this.uploadResults.length;
// Add summary
const summary = document.createElement('div');
summary.style.cssText = 'padding: 15px; border-bottom: 1px solid #333; background-color: #1a1a1a; font-weight: bold;';
summary.innerHTML = `
<div style="color: #00ff99;">Upload Complete</div>
<div style="font-size: 0.9em; color: #ccc; margin-top: 5px;">
${successCount} successful, ${totalCount - successCount} failed out of ${totalCount} files
</div>
`;
resultsList.appendChild(summary);
// Add individual results
this.uploadResults.forEach((result, index) => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
if (result.success) {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">✅ ${result.file.name}</div>
<div class="result-details">
<a href="${result.data.pickup_url}" target="_blank" style="color: #00ff99;">${result.data.pickup_url}</a>
</div>
</div>
<div class="result-actions">
<button type="button" onclick="pacShareEnhanced.copyLink('${result.data.pickup_url}')" style="padding: 5px 10px; font-size: 0.8em;">Copy Link</button>
</div>
`;
} else {
resultItem.innerHTML = `
<div class="result-info">
<div class="result-name">❌ ${result.file.name}</div>
<div class="result-details">${result.error}</div>
</div>
`;
}
resultsList.appendChild(resultItem);
});
// Clear form and scroll to results
this.clearForm();
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
copyLink(url) {
navigator.clipboard.writeText(url).then(() => {
this.showToast('Link copied to clipboard!');
}).catch(() => {
// Fallback
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
this.showToast('Link copied to clipboard!');
});
}
showToast(message) {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: #00ff99;
color: #000;
padding: 10px 20px;
border-radius: 5px;
font-weight: bold;
z-index: 10000;
opacity: 1;
transition: opacity 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000);
}
clearForm() {
// Clear passwords but keep algorithm
const encPassword = document.querySelector('input[name="enc_password"]');
const pickupPassword = document.querySelector('input[name="pickup_password"]');
const enable2FA = document.getElementById('enable-2fa');
if (encPassword) encPassword.value = '';
if (pickupPassword) pickupPassword.value = '';
if (enable2FA) enable2FA.checked = false;
// Clear selected files
this.clearFiles();
}
hideResults() {
const sections = ['pacshare-results', 'pacshare-progress', 'share-link-container'];
sections.forEach(id => {
const section = document.getElementById(id);
if (section) section.style.display = 'none';
});
}
updateUI() {
// Hide results when new files are selected
this.hideResults();
}
// Utility methods
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
isTextFile(filename) {
const textExtensions = ['.txt', '.md', '.js', '.html', '.css', '.json', '.xml', '.csv', '.log', '.py', '.java', '.c', '.cpp', '.h'];
return textExtensions.some(ext => filename.toLowerCase().endsWith(ext));
}
readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
}
showTwoFactorSetup(qrCodeUrl, serviceName, totpSecret) {
const container = document.getElementById('tfa-setup-container');
const qrImage = document.getElementById('tfa-qr-image');
const tfaString = document.getElementById('tfa-string');
if (container && qrImage && tfaString) {
qrImage.src = qrCodeUrl;
tfaString.value = totpSecret;
container.style.display = 'block';
container.scrollIntoView({ behavior: 'smooth' });
}
}
// Settings Modal Methods
setupSettingsModal() {
const settingsBtn = document.getElementById("pacshare-settings-btn");
const modal = document.getElementById("pacshare-settings-modal");
const closeBtn = document.getElementById("close-pacshare-settings");
const applyBtn = document.getElementById("apply-pacshare-settings");
const resetBtn = document.getElementById("reset-pacshare-settings");
if (settingsBtn && modal) {
settingsBtn.addEventListener("click", () => {
modal.style.display = "flex";
this.updateSettingsModal();
});
}
if (closeBtn && modal) {
closeBtn.addEventListener("click", () => {
modal.style.display = "none";
});
}
// Close modal when clicking outside
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.style.display = "none";
}
});
}
if (applyBtn && modal) {
applyBtn.addEventListener("click", () => {
this.applySettings();
modal.style.display = "none";
});
}
if (resetBtn) {
resetBtn.addEventListener("click", () => {
this.resetSettings();
});
}
// Input validation
const uploadSizeInput = document.getElementById("max-upload-size-input");
const concurrentInput = document.getElementById("concurrent-uploads-input");
if (uploadSizeInput) {
uploadSizeInput.addEventListener("input", () => {
let value = parseInt(uploadSizeInput.value);
if (value < 1) uploadSizeInput.value = 1;
else if (value > 1000) uploadSizeInput.value = 1000;
this.updateSettingsSummary();
});
}
if (concurrentInput) {
concurrentInput.addEventListener("input", () => {
let value = parseInt(concurrentInput.value);
if (value < 1) concurrentInput.value = 1;
else if (value > 10) concurrentInput.value = 10;
this.updateSettingsSummary();
});
}
// Update summary when checkboxes change
const checkboxIds = [
"enable-2fa-setting", "auto-clear-passwords", "auto-copy-links",
"show-upload-progress", "scroll-to-results", "validate-file-types",
"enable-file-preview", "remember-algorithm"
];
checkboxIds.forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.addEventListener("change", () => {
this.updateSettingsSummary();
});
}
});
}
updateSettingsModal() {
// Set checkbox values
const checkboxMap = {
"enable-2fa-setting": "enable2FA",
"auto-clear-passwords": "autoClearPasswords",
"auto-copy-links": "autoCopyLinks",
"show-upload-progress": "showUploadProgress",
"scroll-to-results": "scrollToResults",
"validate-file-types": "validateFileTypes",
"enable-file-preview": "enableFilePreview",
"remember-algorithm": "rememberAlgorithm"
};
Object.entries(checkboxMap).forEach(([id, setting]) => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.checked = this.settings[setting];
}
});
// Set number inputs
const uploadSizeInput = document.getElementById("max-upload-size-input");
const concurrentInput = document.getElementById("concurrent-uploads-input");
if (uploadSizeInput) uploadSizeInput.value = this.settings.maxUploadSizeMB;
if (concurrentInput) concurrentInput.value = this.settings.concurrentUploads;
this.updateSettingsSummary();
}
updateSettingsSummary() {
const summary = document.getElementById("pacshare-settings-summary");
if (!summary) return;
const enable2FA = document.getElementById("enable-2fa-setting")?.checked || this.settings.enable2FA;
const autoClearPasswords = document.getElementById("auto-clear-passwords")?.checked || this.settings.autoClearPasswords;
const maxSize = document.getElementById("max-upload-size-input")?.value || this.settings.maxUploadSizeMB;
const concurrent = document.getElementById("concurrent-uploads-input")?.value || this.settings.concurrentUploads;
summary.innerHTML = `
• 2FA: ${enable2FA ? 'Enabled' : 'Disabled'}<br>
• Auto-clear passwords: ${autoClearPasswords ? 'Yes' : 'No'}<br>
• Max file size: ${maxSize} MB<br>
• Upload mode: ${concurrent == 1 ? 'Sequential' : `${concurrent} concurrent`}<br>
• File preview: ${this.settings.enableFilePreview ? 'Enabled' : 'Disabled'}
`;
}
applySettings() {
// Get checkbox values
const checkboxMap = {
"enable-2fa-setting": "enable2FA",
"auto-clear-passwords": "autoClearPasswords",
"auto-copy-links": "autoCopyLinks",
"show-upload-progress": "showUploadProgress",
"scroll-to-results": "scrollToResults",
"validate-file-types": "validateFileTypes",
"enable-file-preview": "enableFilePreview",
"remember-algorithm": "rememberAlgorithm"
};
Object.entries(checkboxMap).forEach(([id, setting]) => {
const checkbox = document.getElementById(id);
if (checkbox) {
this.settings[setting] = checkbox.checked;
}
});
// Get number inputs
const uploadSizeInput = document.getElementById("max-upload-size-input");
const concurrentInput = document.getElementById("concurrent-uploads-input");
if (uploadSizeInput) this.settings.maxUploadSizeMB = parseInt(uploadSizeInput.value) || 25;
if (concurrentInput) this.settings.concurrentUploads = parseInt(concurrentInput.value) || 1;
this.saveSettings();
this.showToast("PacShare settings applied successfully!");
}
resetSettings() {
this.settings = {
enable2FA: false,
autoClearPasswords: true,
autoCopyLinks: true,
showUploadProgress: true,
scrollToResults: true,
maxUploadSizeMB: 25,
validateFileTypes: false,
concurrentUploads: 1,
enableFilePreview: true,
rememberAlgorithm: true
};
this.updateSettingsModal();
this.showToast("Settings reset to defaults!");
}
loadSettings() {
try {
const saved = localStorage.getItem('paccrypt-pacshare-settings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
} catch (error) {
console.warn('Failed to load PacShare settings:', error);
}
}
saveSettings() {
try {
localStorage.setItem('paccrypt-pacshare-settings', JSON.stringify(this.settings));
} catch (error) {
console.warn('Failed to save PacShare settings:', error);
}
}
}
// Initialize enhanced PacShare when DOM is loaded
let pacShareEnhanced;
document.addEventListener('DOMContentLoaded', () => {
pacShareEnhanced = new PacShareEnhanced();
// Make available globally for onclick handlers
window.pacShareEnhanced = pacShareEnhanced;
});