Files
knock-gui/desktop-angular/src/main/open-dialog.html
2025-10-03 15:05:48 +06:00

600 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>Open 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;
}
* { 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;
}
.path-bar {
padding: 12px 20px;
background: rgba(0,0,0,0.1);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
}
.path-input {
flex: 1;
background: rgba(255,255,255,0.1);
border: 1px solid var(--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;
}
.browse-btn:hover {
background: var(--button-hover);
}
.file-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.file-item {
display: flex;
align-items: center;
padding: 12px 20px;
cursor: pointer;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.file-item:hover {
background: var(--hover-bg);
}
.file-item.selected {
background: var(--selected-bg);
}
.file-icon {
width: 24px;
height: 24px;
margin-right: 12px;
font-size: 18px;
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: 12px;
opacity: 0.7;
margin-left: 12px;
}
.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);
}
.preview-section {
padding: 16px 20px;
border-top: 1px solid var(--border);
background: rgba(0,0,0,0.1);
max-height: 120px;
overflow: hidden;
}
.preview-label {
font-weight: 500;
margin-bottom: 8px;
color: var(--text);
font-size: 14px;
}
.preview-content {
background: rgba(0,0,0,0.2);
padding: 12px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
max-height: 80px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
color: var(--text);
border: 1px solid var(--border);
}
.file-type {
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;
}
.btn-danger {
background: #dc3545;
color: white;
border: 1px solid #dc3545;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
border-color: #bd2130;
}
</style>
</head>
<body>
<div class="dialog-container">
<div class="header">
<h3 class="title" id="dialogTitle">Open File</h3>
<button class="close-btn" id="closeBtn" title="Close">×</button>
</div>
<div class="content">
<div class="path-bar">
<input type="text" class="path-input" id="pathInput" placeholder="Enter path or browse..." />
<button class="browse-btn" id="browseBtn">Browse</button>
</div>
<div class="file-list" id="fileList">
<!-- Files will be populated here -->
</div>
</div>
<div class="preview-section" id="previewSection" style="display: none;">
<div class="preview-label">File Preview (first 500 characters)</div>
<div class="preview-content" id="previewContent"></div>
</div>
<div class="footer">
<div class="file-type" id="fileType">All Files (*.*)</div>
<div class="buttons">
<button class="btn btn-danger" id="deleteBtn" disabled>Delete</button>
<button class="btn btn-secondary" id="cancelBtn">Cancel</button>
<button class="btn btn-primary" id="openBtn" disabled>Open</button>
</div>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
const fs = require('fs');
const path = require('path');
let currentPath = '';
let selectedFile = null;
let config = {};
const elements = {
dialogTitle: document.getElementById('dialogTitle'),
closeBtn: document.getElementById('closeBtn'),
pathInput: document.getElementById('pathInput'),
browseBtn: document.getElementById('browseBtn'),
fileList: document.getElementById('fileList'),
fileType: document.getElementById('fileType'),
cancelBtn: document.getElementById('cancelBtn'),
openBtn: document.getElementById('openBtn'),
deleteBtn: document.getElementById('deleteBtn'),
previewSection: document.getElementById('previewSection'),
previewContent: document.getElementById('previewContent')
};
// 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);
}
// Load directory contents
function loadDirectory(dirPath) {
try {
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
return;
}
currentPath = dirPath;
elements.pathInput.value = dirPath;
const items = fs.readdirSync(dirPath, { withFileTypes: true });
const files = [];
const directories = [];
items.forEach(item => {
const fullPath = path.join(dirPath, item.name);
const stat = fs.statSync(fullPath);
if (item.isDirectory()) {
directories.push({
name: item.name,
path: fullPath,
isDirectory: true,
size: '-',
icon: '📁'
});
} else if (item.isFile()) {
// Check file extension filter
const ext = path.extname(item.name).toLowerCase();
const shouldShow = config.filters ?
config.filters.some(filter =>
filter.extensions.some(extension =>
extension.toLowerCase() === ext.toLowerCase() ||
extension === '*' ||
extension === '.*'
)
) : true;
if (shouldShow) {
files.push({
name: item.name,
path: fullPath,
isDirectory: false,
size: formatFileSize(stat.size),
icon: getFileIcon(ext)
});
}
}
});
// Sort: directories first, then files
directories.sort((a, b) => a.name.localeCompare(b.name));
files.sort((a, b) => a.name.localeCompare(b.name));
const allItems = [...directories, ...files];
renderFileList(allItems);
} catch (error) {
console.error('Error loading directory:', error);
elements.fileList.innerHTML = '<div style="padding: 20px; text-align: center; opacity: 0.7;">Error loading directory</div>';
}
}
// Render file list
function renderFileList(items) {
elements.fileList.innerHTML = '';
if (items.length === 0) {
elements.fileList.innerHTML = '<div style="padding: 20px; text-align: center; opacity: 0.7;">No files found</div>';
return;
}
items.forEach(item => {
const div = document.createElement('div');
div.className = 'file-item';
div.innerHTML = `
<div class="file-icon">${item.icon}</div>
<div class="file-info">
<span class="file-name">${item.name}</span>
<span class="file-size">${item.size}</span>
</div>
`;
div.addEventListener('click', () => {
// Remove previous selection
document.querySelectorAll('.file-item.selected').forEach(el => {
el.classList.remove('selected');
});
div.classList.add('selected');
selectedFile = item;
// Enable/disable buttons
elements.openBtn.disabled = item.isDirectory;
elements.deleteBtn.disabled = item.isDirectory;
// Show preview for files
if (!item.isDirectory) {
showFilePreview(item.path);
} else {
elements.previewSection.style.display = 'none';
}
// If it's a directory, navigate into it
if (item.isDirectory) {
loadDirectory(item.path);
}
});
elements.fileList.appendChild(div);
});
}
// Format file size
function formatFileSize(bytes) {
if (bytes === '-') return '-';
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
// Get file icon based on extension
function getFileIcon(ext) {
const iconMap = {
'.yaml': '📄',
'.yml': '📄',
'.txt': '📄',
'.json': '📄',
'.js': '📄',
'.ts': '📄',
'.html': '📄',
'.css': '📄',
'.md': '📄',
'.xml': '📄',
'.log': '📄'
};
return iconMap[ext.toLowerCase()] || '📄';
}
// Show file preview
function showFilePreview(filePath) {
if (!filePath || !fs.existsSync(filePath)) {
elements.previewSection.style.display = 'none';
return;
}
try {
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
elements.previewSection.style.display = 'none';
return;
}
// Only show preview for text files (under 1MB)
if (stats.size > 1024 * 1024) {
elements.previewSection.style.display = 'none';
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
const preview = content.length > 500 ? content.substring(0, 500) + '...' : content;
elements.previewContent.textContent = preview;
elements.previewSection.style.display = 'block';
} catch (error) {
elements.previewSection.style.display = 'none';
}
}
// Event listeners
elements.closeBtn.addEventListener('click', () => {
ipcRenderer.send('file-dialog:result', { canceled: true });
});
elements.cancelBtn.addEventListener('click', () => {
ipcRenderer.send('file-dialog:result', { canceled: true });
});
elements.openBtn.addEventListener('click', () => {
if (selectedFile && !selectedFile.isDirectory) {
ipcRenderer.send('file-dialog:result', {
canceled: false,
filePath: selectedFile.path,
content: fs.readFileSync(selectedFile.path, 'utf-8')
});
}
});
elements.deleteBtn.addEventListener('click', async () => {
if (!selectedFile || selectedFile.isDirectory) return;
const confirmDelete = confirm(`Are you sure you want to delete "${selectedFile.name}"?`);
if (!confirmDelete) return;
try {
fs.unlinkSync(selectedFile.path);
// Reload directory to refresh file list
loadDirectory(currentPath);
// Clear selection
selectedFile = null;
elements.openBtn.disabled = true;
elements.deleteBtn.disabled = true;
elements.previewSection.style.display = 'none';
} catch (error) {
alert(`Error deleting file: ${error.message}`);
}
});
elements.pathInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const newPath = elements.pathInput.value.trim();
if (newPath && fs.existsSync(newPath)) {
loadDirectory(newPath);
}
}
});
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];
elements.pathInput.value = currentPath;
loadDirectory(currentPath);
}
} 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;
elements.pathInput.value = currentPath;
loadDirectory(currentPath);
}
}
});
// Initialize
ipcRenderer.on('file-dialog:config', (event, dialogConfig) => {
config = dialogConfig || {};
// Apply custom colors
applyColors(config.colors);
// Set title and message
elements.dialogTitle.textContent = config.title || 'Open File';
// 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;
}
// Load initial directory
const initialPath = config.defaultPath || os.homedir();
loadDirectory(initialPath);
});
// Handle double-click to open
elements.fileList.addEventListener('dblclick', (e) => {
const fileItem = e.target.closest('.file-item');
if (fileItem && selectedFile && !selectedFile.isDirectory) {
elements.openBtn.click();
}
});
</script>
</body>
</html>