Files
knock-gui/desktop-angular/src/main/save-dialog.html

606 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data:;" />
<title>Save File</title>
<style>
:root {
--bg: #2d3748;
--text: #ffffff;
--border: rgba(255,255,255,0.2);
--hover-bg: rgba(255,255,255,0.1);
--selected-bg: rgba(255,255,255,0.2);
--button-bg: #aa1c3a;
--button-text: #ffffff;
--button-hover: #8b1531;
--input-bg: rgba(255,255,255,0.1);
--input-border: rgba(255,255,255,0.3);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
height: 100%;
overflow: hidden;
}
.dialog-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg);
}
.title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: var(--text);
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.close-btn:hover {
background: var(--hover-bg);
}
.content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text);
}
.form-input {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text);
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: var(--button-bg);
background: rgba(255,255,255,0.15);
}
.form-input::placeholder {
color: rgba(255,255,255,0.6);
}
.path-section {
background: rgba(0,0,0,0.1);
padding: 16px;
border-radius: 6px;
border: 1px solid var(--border);
}
.path-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.path-input {
flex: 1;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text);
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.path-input:focus {
outline: none;
border-color: var(--button-bg);
background: rgba(255,255,255,0.15);
}
.browse-btn {
background: var(--button-bg);
color: var(--button-text);
border: 1px solid var(--button-bg);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
white-space: nowrap;
}
.browse-btn:hover {
background: var(--button-hover);
}
.file-type {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
opacity: 0.8;
}
.footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0,0,0,0.1);
}
.file-type-info {
font-size: 14px;
opacity: 0.8;
}
.buttons {
display: flex;
gap: 12px;
}
.btn {
padding: 8px 20px;
border-radius: 4px;
border: 1px solid;
cursor: pointer;
font-weight: 600;
min-width: 80px;
}
.btn-primary {
background: var(--button-bg);
color: var(--button-text);
border-color: var(--button-bg);
}
.btn-primary:hover {
background: var(--button-hover);
}
.btn-secondary {
background: transparent;
color: var(--text);
border-color: var(--border);
}
.btn-secondary:hover {
background: var(--hover-bg);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.files-section {
background: rgba(0,0,0,0.1);
padding: 16px;
border-radius: 6px;
border: 1px solid var(--border);
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.files-label {
font-weight: 500;
margin-bottom: 12px;
color: var(--text);
}
.files-list {
flex: 1;
overflow-y: auto;
background: rgba(0,0,0,0.2);
border-radius: 4px;
border: 1px solid var(--border);
min-height: 150px;
}
.file-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid rgba(255,255,255,0.05);
font-size: 13px;
}
.file-item:last-child {
border-bottom: none;
}
.file-item:hover {
background: var(--hover-bg);
}
.file-icon {
width: 16px;
height: 16px;
margin-right: 8px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.file-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-name {
font-weight: 500;
}
.file-size {
font-size: 11px;
opacity: 0.7;
margin-left: 8px;
}
</style>
</head>
<body>
<div class="dialog-container">
<div class="header">
<h3 class="title" id="dialogTitle">Save File</h3>
<button class="close-btn" id="closeBtn" title="Close">×</button>
</div>
<div class="content">
<div class="form-group">
<label class="form-label" for="fileName">File Name</label>
<input type="text" class="form-input" id="fileName" placeholder="Enter file name..." />
</div>
<div class="path-section">
<div class="path-row">
<input type="text" class="path-input" id="pathInput" placeholder="Enter directory path..." readonly />
<button class="browse-btn" id="browseBtn">Browse</button>
</div>
<div class="file-type" id="fileType">All Files (*.*)</div>
</div>
<div class="files-section">
<div class="files-label">Files in current directory</div>
<div class="files-list" id="filesList">
<!-- Files will be populated here -->
</div>
</div>
</div>
<div class="footer">
<div class="file-type-info" id="fileTypeInfo">All Files (*.*)</div>
<div class="buttons">
<button class="btn btn-secondary" id="cancelBtn">Cancel</button>
<button class="btn btn-primary" id="saveBtn" disabled>Save</button>
</div>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
const fs = require('fs');
const path = require('path');
const os = require('os');
let currentPath = '';
let config = {};
let content = '';
const elements = {
dialogTitle: document.getElementById('dialogTitle'),
closeBtn: document.getElementById('closeBtn'),
fileName: document.getElementById('fileName'),
pathInput: document.getElementById('pathInput'),
browseBtn: document.getElementById('browseBtn'),
fileType: document.getElementById('fileType'),
fileTypeInfo: document.getElementById('fileTypeInfo'),
cancelBtn: document.getElementById('cancelBtn'),
saveBtn: document.getElementById('saveBtn'),
filesList: document.getElementById('filesList')
};
// Apply custom colors if provided
function applyColors(colors) {
if (!colors) return;
const root = document.documentElement;
if (colors.background) root.style.setProperty('--bg', colors.background);
if (colors.text) root.style.setProperty('--text', colors.text);
if (colors.buttonBg) root.style.setProperty('--button-bg', colors.buttonBg);
if (colors.buttonText) root.style.setProperty('--button-text', colors.buttonText);
if (colors.border) root.style.setProperty('--border', colors.border);
if (colors.inputBg) root.style.setProperty('--input-bg', colors.inputBg);
if (colors.inputBorder) root.style.setProperty('--input-border', colors.inputBorder);
}
// Update save button state
function updateSaveButton() {
const fileName = elements.fileName.value.trim();
const hasPath = currentPath && fs.existsSync(currentPath);
elements.saveBtn.disabled = !fileName || !hasPath;
}
// Update file path display
function updatePathDisplay() {
elements.pathInput.value = currentPath;
updateSaveButton();
}
// Get file extension from filename
function getFileExtension(filename) {
const ext = path.extname(filename).toLowerCase();
return ext || '';
}
// Format file size
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Validate filename
function isValidFilename(filename) {
if (!filename || filename.trim() === '') return false;
// Check for invalid characters
const invalidChars = /[<>:"/\\|?*]/;
if (invalidChars.test(filename)) return false;
// Check for reserved names (Windows)
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
const nameWithoutExt = path.parse(filename).name.toUpperCase();
if (reservedNames.includes(nameWithoutExt)) return false;
return true;
}
// Show files in current directory
function showFilesInDirectory() {
console.log('showFilesInDirectory called with currentPath:', currentPath);
if (!currentPath) {
elements.filesList.innerHTML = '<div class="file-item">No directory selected</div>';
return;
}
if (!fs.existsSync(currentPath)) {
console.error('Directory does not exist:', currentPath);
elements.filesList.innerHTML = '<div class="file-item">Directory does not exist: ' + currentPath + '</div>';
return;
}
try {
const stats = fs.statSync(currentPath);
if (!stats.isDirectory()) {
console.error('Path is not a directory:', currentPath);
elements.filesList.innerHTML = '<div class="file-item">Path is not a directory: ' + currentPath + '</div>';
return;
}
const files = fs.readdirSync(currentPath);
console.log('Files found:', files);
const fileStats = files.map(file => {
try {
const filePath = path.join(currentPath, file);
const stats = fs.statSync(filePath);
return {
name: file,
path: filePath,
isDirectory: stats.isDirectory(),
size: stats.size
};
} catch (error) {
console.error('Error getting stats for file:', file, error);
return {
name: file,
path: path.join(currentPath, file),
isDirectory: false,
size: 0,
error: error.message
};
}
}).filter(file => file); // Remove any null/undefined entries
// Show all files without filtering
const filteredFiles = fileStats;
// Sort: directories first, then files
filteredFiles.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
});
elements.filesList.innerHTML = '';
console.log ('filteredFiles', filteredFiles);
if (filteredFiles.length === 0) {
elements.filesList.innerHTML = '<div class="file-item">No files found</div>';
return;
}
filteredFiles.forEach(file => {
const div = document.createElement('div');
div.className = 'file-item';
div.innerHTML = `
<div class="file-icon">${file.isDirectory ? '📁' : '📄'}</div>
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${file.isDirectory ? 'DIR' : formatFileSize(file.size)}</span>
</div>
`;
div.addEventListener('click', () => {
if (file.isDirectory) {
currentPath = file.path;
elements.pathInput.value = currentPath;
showFilesInDirectory();
} else {
// Select file and update filename input
elements.fileName.value = file.name;
updateSaveButton();
}
});
elements.filesList.appendChild(div);
});
} catch (error) {
console.error('Error reading directory:', error);
elements.filesList.innerHTML = '<div class="file-item">Error reading directory: ' + error.message + '</div>';
}
}
// Event listeners
elements.closeBtn.addEventListener('click', () => {
ipcRenderer.send('save-dialog:result', { canceled: true });
});
elements.cancelBtn.addEventListener('click', () => {
ipcRenderer.send('save-dialog:result', { canceled: true });
});
elements.saveBtn.addEventListener('click', () => {
const fileName = elements.fileName.value.trim();
if (!isValidFilename(fileName)) {
alert('Invalid filename. Please use valid characters and avoid reserved names.');
return;
}
const filePath = path.join(currentPath, fileName);
try {
fs.writeFileSync(filePath, content, 'utf-8');
ipcRenderer.send('save-dialog:result', {
canceled: false,
filePath: filePath
});
} catch (error) {
alert(`Error saving file: ${error.message}`);
}
});
elements.fileName.addEventListener('input', updateSaveButton);
elements.fileName.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !elements.saveBtn.disabled) {
elements.saveBtn.click();
}
});
elements.browseBtn.addEventListener('click', async () => {
try {
// Request main process to show directory picker
const result = await ipcRenderer.invoke('dialog:showDirectoryPicker', {
title: 'Select Directory',
defaultPath: currentPath
});
if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
currentPath = result.filePaths[0];
updatePathDisplay();
showFilesInDirectory();
}
} catch (error) {
console.error('Error opening directory picker:', error);
// Fallback to prompt
const newPath = prompt('Enter directory path:', currentPath);
if (newPath && fs.existsSync(newPath) && fs.statSync(newPath).isDirectory()) {
currentPath = newPath;
updatePathDisplay();
showFilesInDirectory();
}
}
});
// Initialize
ipcRenderer.on('save-dialog:config', (event, dialogConfig) => {
config = dialogConfig || {};
content = config.content || '';
// Apply custom colors
applyColors(config.colors);
// Set title
elements.dialogTitle.textContent = config.title || 'Save File';
// Set default path
currentPath = config.defaultPath || os.homedir();
updatePathDisplay();
// Set default filename
if (config.suggestedName) {
elements.fileName.value = config.suggestedName;
updateSaveButton();
}
// Set file type filter text
if (config.filters && config.filters.length > 0) {
const filterText = config.filters.map(f => `${f.name} (${f.extensions.join(', ')})`).join('; ');
elements.fileType.textContent = filterText;
elements.fileTypeInfo.textContent = filterText;
}
// Show files in directory
showFilesInDirectory();
// Focus filename input
elements.fileName.focus();
elements.fileName.select();
});
</script>
</body>
</html>