Separated frontend code into separate html and css templates, numerous fixes and error handling.
This commit is contained in:
parent
0d941e82d2
commit
2c87a4847e
9 changed files with 1189 additions and 102 deletions
232
assets/app.js
Normal file
232
assets/app.js
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
// QuickSearch Application JavaScript
|
||||||
|
|
||||||
|
// Enhanced UI interactions
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('QuickSearch UI loaded');
|
||||||
|
|
||||||
|
// Add loading states to buttons
|
||||||
|
function addLoadingState(button, originalText) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<span class="loading"></span>' + originalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoadingState(button, originalText) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced form interactions
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
if (submitButton) {
|
||||||
|
addLoadingState(submitButton, submitButton.textContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
// Ctrl+F to focus search
|
||||||
|
if (e.ctrlKey && e.key === 'f') {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchInput = document.querySelector('input[type="text"]');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.focus();
|
||||||
|
searchInput.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape to clear search
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const searchInput = document.querySelector('input[type="text"]');
|
||||||
|
if (searchInput && searchInput === document.activeElement) {
|
||||||
|
searchInput.value = '';
|
||||||
|
searchInput.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced table interactions
|
||||||
|
function enhanceTable(table) {
|
||||||
|
// Add click-to-copy functionality for table cells
|
||||||
|
const cells = table.querySelectorAll('td');
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
const text = cell.textContent.trim();
|
||||||
|
if (text && navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Visual feedback
|
||||||
|
cell.style.backgroundColor = '#4CAF50';
|
||||||
|
cell.style.color = 'white';
|
||||||
|
setTimeout(() => {
|
||||||
|
cell.style.backgroundColor = '';
|
||||||
|
cell.style.color = '';
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add sortable columns (basic implementation)
|
||||||
|
const headers = table.querySelectorAll('th');
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
header.style.cursor = 'pointer';
|
||||||
|
header.addEventListener('click', () => sortTable(table, index));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple table sorting
|
||||||
|
function sortTable(table, columnIndex) {
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aVal = a.cells[columnIndex]?.textContent.trim() || '';
|
||||||
|
const bVal = b.cells[columnIndex]?.textContent.trim() || '';
|
||||||
|
|
||||||
|
// Try numeric sort first
|
||||||
|
const aNum = parseFloat(aVal);
|
||||||
|
const bNum = parseFloat(bVal);
|
||||||
|
|
||||||
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||||
|
return aNum - bNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to string sort
|
||||||
|
return aVal.localeCompare(bVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear tbody and re-append sorted rows
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-enhance any tables that appear
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
mutation.addedNodes.forEach(function(node) {
|
||||||
|
if (node.nodeType === 1) { // Element node
|
||||||
|
const tables = node.querySelectorAll ? node.querySelectorAll('table') : [];
|
||||||
|
tables.forEach(enhanceTable);
|
||||||
|
|
||||||
|
if (node.tagName === 'TABLE') {
|
||||||
|
enhanceTable(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// Enhance existing tables
|
||||||
|
document.querySelectorAll('table').forEach(enhanceTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility functions for Rust integration
|
||||||
|
window.QuickSearch = {
|
||||||
|
// Function to show toast notifications
|
||||||
|
showToast: function(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 2000;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Set background based on type
|
||||||
|
const colors = {
|
||||||
|
info: '#2196F3',
|
||||||
|
success: '#4CAF50',
|
||||||
|
warning: '#FF9800',
|
||||||
|
error: '#f44336'
|
||||||
|
};
|
||||||
|
toast.style.backgroundColor = colors[type] || colors.info;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(toast);
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to update status display
|
||||||
|
updateStatus: function(status) {
|
||||||
|
const statusDisplay = document.querySelector('.status-display');
|
||||||
|
if (statusDisplay) {
|
||||||
|
statusDisplay.textContent = status;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to highlight search terms in results
|
||||||
|
highlightSearchTerms: function(searchTerm, container) {
|
||||||
|
if (!searchTerm || !container) return;
|
||||||
|
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
container,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const textNodes = [];
|
||||||
|
let node;
|
||||||
|
while (node = walker.nextNode()) {
|
||||||
|
textNodes.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
textNodes.forEach(textNode => {
|
||||||
|
const parent = textNode.parentNode;
|
||||||
|
if (parent.tagName === 'B') return; // Skip already highlighted
|
||||||
|
|
||||||
|
const text = textNode.textContent;
|
||||||
|
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
||||||
|
|
||||||
|
if (regex.test(text)) {
|
||||||
|
const highlightedHTML = text.replace(regex, '<mark>$1</mark>');
|
||||||
|
const wrapper = document.createElement('span');
|
||||||
|
wrapper.innerHTML = highlightedHTML;
|
||||||
|
parent.replaceChild(wrapper, textNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add custom CSS for toasts and animations
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background: #ffeb3b;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
21
assets/index.html
Normal file
21
assets/index.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>QuickSearch - File Indexer & Search</title>
|
||||||
|
|
||||||
|
<!-- External CSS -->
|
||||||
|
<link rel="stylesheet" href="assets://styles.css">
|
||||||
|
|
||||||
|
<!-- Favicon (optional) -->
|
||||||
|
<link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%234CAF50' d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Main app container - Dioxus will render into this -->
|
||||||
|
<div id="main"></div>
|
||||||
|
|
||||||
|
<!-- External JavaScript -->
|
||||||
|
<script src="assets://app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
299
assets/styles.css
Normal file
299
assets/styles.css
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
/* QuickSearch Application Styles */
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4CAF50;
|
||||||
|
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(90deg, #f44336 0%, #d32f2f 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: linear-gradient(90deg, #2196F3 0%, #1976D2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #00ff00;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table th {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 2px solid #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table tbody tr:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table tbody tr:nth-child(even) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table tbody tr:nth-child(even):hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clickable path cells */
|
||||||
|
.path-cell.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1976d2;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-cell.clickable:hover {
|
||||||
|
background: #e3f2fd !important;
|
||||||
|
color: #0d47a1;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-cell.clickable:active {
|
||||||
|
background: #bbdefb !important;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
animation: modalSlideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight matched text in search results */
|
||||||
|
.results-table b {
|
||||||
|
background: #ffeb3b;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #4CAF50;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
font-size: 16px; /* Prevents zoom on iOS */
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
index.html
Normal file
170
index.html
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>QuickSearch</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #d32f2f;
|
||||||
|
background-color: #ffebee;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
text-align: left;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>QuickSearch File Indexer</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -85,7 +85,15 @@ pub fn analyze_files_for_batch_update(
|
||||||
};
|
};
|
||||||
|
|
||||||
let fpath = match entry.path().canonicalize() {
|
let fpath = match entry.path().canonicalize() {
|
||||||
Ok(fp) => fp.to_string_lossy().to_string(),
|
Ok(fp) => {
|
||||||
|
let path_str = fp.to_string_lossy().to_string();
|
||||||
|
// Remove Windows UNC prefix \\?\
|
||||||
|
if path_str.starts_with("\\\\?\\") {
|
||||||
|
path_str[4..].to_string()
|
||||||
|
} else {
|
||||||
|
path_str
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -205,7 +213,15 @@ pub fn process_batch_updates_files_only(
|
||||||
}
|
}
|
||||||
|
|
||||||
let fpath = match entry.path().canonicalize() {
|
let fpath = match entry.path().canonicalize() {
|
||||||
Ok(fp) => fp.into_os_string(),
|
Ok(fp) => {
|
||||||
|
let path_str = fp.to_string_lossy().to_string();
|
||||||
|
// Remove Windows UNC prefix \\?\
|
||||||
|
if path_str.starts_with("\\\\?\\") {
|
||||||
|
std::ffi::OsString::from(&path_str[4..])
|
||||||
|
} else {
|
||||||
|
fp.into_os_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -299,7 +315,15 @@ pub fn process_batch_inserts_files_only(
|
||||||
}
|
}
|
||||||
|
|
||||||
let fpath = match entry.path().canonicalize() {
|
let fpath = match entry.path().canonicalize() {
|
||||||
Ok(fp) => fp.into_os_string(),
|
Ok(fp) => {
|
||||||
|
let path_str = fp.to_string_lossy().to_string();
|
||||||
|
// Remove Windows UNC prefix \\?\
|
||||||
|
if path_str.starts_with("\\\\?\\") {
|
||||||
|
std::ffi::OsString::from(&path_str[4..])
|
||||||
|
} else {
|
||||||
|
fp.into_os_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -377,7 +401,8 @@ pub fn process_text_indexing(
|
||||||
|
|
||||||
// Check stop flag
|
// Check stop flag
|
||||||
if *stop_flag.lock().unwrap() {
|
if *stop_flag.lock().unwrap() {
|
||||||
drop(tx);
|
// Commit current transaction before stopping
|
||||||
|
let _ = tx.commit();
|
||||||
drop(conn);
|
drop(conn);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +430,7 @@ pub fn process_text_indexing(
|
||||||
let trimmed_file_string = safe_truncate_string(&file_string, config.processing.maximum_text_size);
|
let trimmed_file_string = safe_truncate_string(&file_string, config.processing.maximum_text_size);
|
||||||
Some(trimmed_file_string)
|
Some(trimmed_file_string)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(_e) => {
|
||||||
// eprintln!("Warning: Failed to read plaintext file {}: {}", fpath, e);
|
// eprintln!("Warning: Failed to read plaintext file {}: {}", fpath, e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,22 +201,25 @@ pub fn App(props: AppProps) -> Element {
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
style: "padding: 20px; font-family: Arial, sans-serif;",
|
class: "app-container",
|
||||||
|
|
||||||
h1 { "QuickSearch File Indexer" }
|
|
||||||
|
|
||||||
div {
|
div {
|
||||||
style: "margin-bottom: 20px;",
|
class: "app-header",
|
||||||
|
h1 { "QuickSearch File Indexer" }
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: "app-content",
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: "section",
|
||||||
h2 { "Indexing Controls" }
|
h2 { "Indexing Controls" }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
style: "margin-bottom: 10px;",
|
class: "form-group",
|
||||||
label {
|
label { "Path to index:" }
|
||||||
style: "display: block; margin-bottom: 5px;",
|
|
||||||
"Path to index:"
|
|
||||||
}
|
|
||||||
input {
|
input {
|
||||||
style: "width: 400px; padding: 5px;",
|
class: "form-control",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
value: "{indexing_path}",
|
value: "{indexing_path}",
|
||||||
oninput: move |evt| indexing_path.set(evt.value())
|
oninput: move |evt| indexing_path.set(evt.value())
|
||||||
|
|
@ -224,13 +227,10 @@ pub fn App(props: AppProps) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
style: "margin-bottom: 10px;",
|
class: "form-group",
|
||||||
label {
|
label { "Database path:" }
|
||||||
style: "display: block; margin-bottom: 5px;",
|
|
||||||
"Database path:"
|
|
||||||
}
|
|
||||||
input {
|
input {
|
||||||
style: "width: 400px; padding: 5px;",
|
class: "form-control",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
value: "{db_path}",
|
value: "{db_path}",
|
||||||
oninput: move |evt| db_path.set(evt.value())
|
oninput: move |evt| db_path.set(evt.value())
|
||||||
|
|
@ -238,9 +238,9 @@ pub fn App(props: AppProps) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
style: "margin-bottom: 20px;",
|
class: "form-group",
|
||||||
button {
|
button {
|
||||||
style: "margin-right: 10px; padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer;",
|
class: "btn btn-primary",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let service = indexing_service_for_start.clone();
|
let service = indexing_service_for_start.clone();
|
||||||
let config = config_for_start.clone();
|
let config = config_for_start.clone();
|
||||||
|
|
@ -266,7 +266,7 @@ pub fn App(props: AppProps) -> Element {
|
||||||
"Start Indexing"
|
"Start Indexing"
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
style: "padding: 10px 20px; background-color: #f44336; color: white; border: none; cursor: pointer;",
|
class: "btn btn-danger",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let _ = indexing_service_for_stop.stop_indexing();
|
let _ = indexing_service_for_stop.stop_indexing();
|
||||||
},
|
},
|
||||||
|
|
@ -276,9 +276,10 @@ pub fn App(props: AppProps) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
class: "section",
|
||||||
h2 { "Status" }
|
h2 { "Status" }
|
||||||
pre {
|
pre {
|
||||||
style: "background-color: #f5f5f5; padding: 10px; border-radius: 5px; font-family: monospace;",
|
class: "status-display",
|
||||||
"{status_text}"
|
"{status_text}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,14 +288,16 @@ pub fn App(props: AppProps) -> Element {
|
||||||
indexing_service: props.indexing_service.clone(),
|
indexing_service: props.indexing_service.clone(),
|
||||||
db_path: db_path().clone()
|
db_path: db_path().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // Close app-content
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration validation dialog
|
// Configuration validation dialog
|
||||||
if show_config_dialog() {
|
if show_config_dialog() {
|
||||||
div {
|
div {
|
||||||
style: "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;",
|
class: "modal-backdrop",
|
||||||
div {
|
div {
|
||||||
style: "background-color: white; padding: 30px; border-radius: 10px; max-width: 600px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);",
|
class: "modal-dialog",
|
||||||
h3 {
|
h3 {
|
||||||
style: "margin-top: 0; color: #d32f2f;",
|
style: "margin-top: 0; color: #d32f2f;",
|
||||||
"⚠️ Configuration Changes Detected"
|
"⚠️ Configuration Changes Detected"
|
||||||
|
|
|
||||||
170
src/indexing.rs
170
src/indexing.rs
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::{Arc, Mutex, mpsc};
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use std::process::Command;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
|
|
||||||
|
|
@ -163,10 +164,25 @@ impl IndexingService {
|
||||||
/// Execute a search query against the database
|
/// Execute a search query against the database
|
||||||
pub fn execute_search(&self, db_path: &str, query: &str) -> Result<Vec<SearchResult>, String> {
|
pub fn execute_search(&self, db_path: &str, query: &str) -> Result<Vec<SearchResult>, String> {
|
||||||
let conn = Connection::open(db_path)
|
let conn = Connection::open(db_path)
|
||||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
.map_err(|e| {
|
||||||
|
if e.to_string().contains("corrupt") || e.to_string().contains("malformed") {
|
||||||
|
format!("DATABASE_CORRUPTED: {}", e)
|
||||||
|
} else {
|
||||||
|
format!("Failed to open database: {}", e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut stmt = conn.prepare(query)
|
let mut stmt = conn.prepare(query)
|
||||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
.map_err(|e| {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
if error_msg.contains("malformed") || error_msg.contains("corrupt") || error_msg.contains("database disk image is malformed") {
|
||||||
|
format!("DATABASE_CORRUPTED: {}", error_msg)
|
||||||
|
} else if error_msg.contains("fts5: syntax error") {
|
||||||
|
format!("Search syntax error: The search term contains characters that cannot be processed. Please try a simpler search term.")
|
||||||
|
} else {
|
||||||
|
format!("Failed to prepare query: {}", error_msg)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let column_count = stmt.column_count();
|
let column_count = stmt.column_count();
|
||||||
let column_names: Vec<String> = (0..column_count)
|
let column_names: Vec<String> = (0..column_count)
|
||||||
|
|
@ -187,13 +203,31 @@ impl IndexingService {
|
||||||
}
|
}
|
||||||
Ok(SearchResultRow { values })
|
Ok(SearchResultRow { values })
|
||||||
})
|
})
|
||||||
.map_err(|e| format!("Failed to execute query: {}", e))?;
|
.map_err(|e| {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
if error_msg.contains("malformed") || error_msg.contains("corrupt") || error_msg.contains("database disk image is malformed") {
|
||||||
|
format!("DATABASE_CORRUPTED: {}", error_msg)
|
||||||
|
} else if error_msg.contains("fts5: syntax error") {
|
||||||
|
format!("Search syntax error: The search term contains characters that cannot be processed. Please try a simpler search term.")
|
||||||
|
} else {
|
||||||
|
format!("Failed to execute query: {}", error_msg)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
match row {
|
match row {
|
||||||
Ok(search_row) => results.push(search_row),
|
Ok(search_row) => results.push(search_row),
|
||||||
Err(e) => return Err(format!("Error reading row: {}", e)),
|
Err(e) => {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
if error_msg.contains("malformed") || error_msg.contains("corrupt") || error_msg.contains("database disk image is malformed") {
|
||||||
|
return Err(format!("DATABASE_CORRUPTED: {}", error_msg));
|
||||||
|
} else if error_msg.contains("fts5: syntax error") {
|
||||||
|
return Err(format!("Search syntax error: The search term contains characters that cannot be processed. Please try a simpler search term."));
|
||||||
|
} else {
|
||||||
|
return Err(format!("Error reading row: {}", error_msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +237,98 @@ impl IndexingService {
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open file explorer to the directory containing the specified file path
|
||||||
|
pub fn open_file_explorer(&self, file_path: &str) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
Command::new("explorer")
|
||||||
|
.arg("/select,")
|
||||||
|
.arg(file_path)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to open file explorer: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Command::new("open")
|
||||||
|
.arg("-R")
|
||||||
|
.arg(file_path)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to open file explorer: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let path = std::path::Path::new(file_path);
|
||||||
|
let dir_path = if path.is_file() {
|
||||||
|
path.parent().unwrap_or(path)
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try different file managers
|
||||||
|
let managers = ["xdg-open", "nautilus", "dolphin", "thunar", "pcmanfm"];
|
||||||
|
let mut success = false;
|
||||||
|
|
||||||
|
for manager in &managers {
|
||||||
|
if let Ok(_) = Command::new(manager)
|
||||||
|
.arg(dir_path)
|
||||||
|
.spawn() {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Err("No suitable file manager found".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up UNC prefixes from existing database entries
|
||||||
|
pub fn clean_unc_prefixes(&self, db_path: &str) -> Result<(), String> {
|
||||||
|
let conn = Connection::open(db_path)
|
||||||
|
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||||
|
|
||||||
|
// Clean UNC prefixes from files table
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE files SET path = SUBSTR(path, 5) WHERE path LIKE '\\\\?\\%'",
|
||||||
|
(),
|
||||||
|
).map_err(|e| format!("Failed to update files table: {}", e))?;
|
||||||
|
|
||||||
|
// Clean UNC prefixes from searchabletext table
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE searchabletext SET path = SUBSTR(path, 5) WHERE path LIKE '\\\\?\\%'",
|
||||||
|
(),
|
||||||
|
).map_err(|e| format!("Failed to update searchabletext table: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the database is corrupted or malformed
|
||||||
|
pub fn check_database_health(&self, db_path: &str) -> Result<bool, String> {
|
||||||
|
match Connection::open(db_path) {
|
||||||
|
Ok(conn) => {
|
||||||
|
// Try to run integrity check
|
||||||
|
match conn.prepare("PRAGMA integrity_check") {
|
||||||
|
Ok(mut stmt) => {
|
||||||
|
match stmt.query_row([], |row| {
|
||||||
|
let result: String = row.get(0)?;
|
||||||
|
Ok(result == "ok")
|
||||||
|
}) {
|
||||||
|
Ok(is_ok) => Ok(is_ok),
|
||||||
|
Err(_) => Ok(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Ok(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if configuration changes require index recreation
|
/// Check if configuration changes require index recreation
|
||||||
pub fn check_config_validation(&self, db_path: &str, config: &Config, indexing_path: &str) -> Result<Option<Vec<String>>, String> {
|
pub fn check_config_validation(&self, db_path: &str, config: &Config, indexing_path: &str) -> Result<Option<Vec<String>>, String> {
|
||||||
let conn = Connection::open(db_path)
|
let conn = Connection::open(db_path)
|
||||||
|
|
@ -571,11 +697,19 @@ impl IndexingService {
|
||||||
// Critical configuration values that require index recreation
|
// Critical configuration values that require index recreation
|
||||||
let hash_length = config.processing.hash_length.to_string();
|
let hash_length = config.processing.hash_length.to_string();
|
||||||
let tokenize = config.processing.tokenize.clone();
|
let tokenize = config.processing.tokenize.clone();
|
||||||
let normalized_path = std::path::Path::new(indexing_path)
|
let normalized_path = {
|
||||||
.canonicalize()
|
let path = std::path::Path::new(indexing_path)
|
||||||
.unwrap_or_else(|_| std::path::PathBuf::from(indexing_path))
|
.canonicalize()
|
||||||
.to_string_lossy()
|
.unwrap_or_else(|_| std::path::PathBuf::from(indexing_path))
|
||||||
.to_string();
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
// Remove Windows UNC prefix \\?\
|
||||||
|
if path.starts_with("\\\\?\\") {
|
||||||
|
path[4..].to_string()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check stored configuration values
|
// Check stored configuration values
|
||||||
let mut stored_hash_length: Option<String> = None;
|
let mut stored_hash_length: Option<String> = None;
|
||||||
|
|
@ -631,11 +765,19 @@ impl IndexingService {
|
||||||
fn update_config(conn: &Connection, config: &Config, indexing_path: &str) -> Result<(), String> {
|
fn update_config(conn: &Connection, config: &Config, indexing_path: &str) -> Result<(), String> {
|
||||||
let hash_length = config.processing.hash_length.to_string();
|
let hash_length = config.processing.hash_length.to_string();
|
||||||
let tokenize = config.processing.tokenize.clone();
|
let tokenize = config.processing.tokenize.clone();
|
||||||
let normalized_path = std::path::Path::new(indexing_path)
|
let normalized_path = {
|
||||||
.canonicalize()
|
let path = std::path::Path::new(indexing_path)
|
||||||
.unwrap_or_else(|_| std::path::PathBuf::from(indexing_path))
|
.canonicalize()
|
||||||
.to_string_lossy()
|
.unwrap_or_else(|_| std::path::PathBuf::from(indexing_path))
|
||||||
.to_string();
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
// Remove Windows UNC prefix \\?\
|
||||||
|
if path.starts_with("\\\\?\\") {
|
||||||
|
path[4..].to_string()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update stored configuration values
|
// Update stored configuration values
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
|
||||||
14
src/main.rs
14
src/main.rs
|
|
@ -1,6 +1,5 @@
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
mod frontend;
|
mod frontend;
|
||||||
mod file_handling;
|
mod file_handling;
|
||||||
mod document_extraction;
|
mod document_extraction;
|
||||||
|
|
@ -27,9 +26,20 @@ fn main() {
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}).expect("Error setting Ctrl-C handler");
|
}).expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
launch(app);
|
LaunchBuilder::desktop()
|
||||||
|
.with_cfg(
|
||||||
|
dioxus_desktop::Config::new()
|
||||||
|
.with_custom_head(format!("<style>{}</style>", include_str!("../assets/styles.css")))
|
||||||
|
.with_window(dioxus_desktop::WindowBuilder::new()
|
||||||
|
.with_title("QuickSearch - File Indexer & Search")
|
||||||
|
.with_resizable(true)
|
||||||
|
.with_inner_size(dioxus_desktop::LogicalSize::new(1000.0, 700.0))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
let config = match config::Config::load() {
|
let config = match config::Config::load() {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
|
|
|
||||||
305
src/search.rs
305
src/search.rs
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use crate::indexing::{IndexingService, SearchResult};
|
use crate::indexing::{IndexingService, SearchResult};
|
||||||
|
|
||||||
|
|
@ -19,21 +20,139 @@ impl PartialEq for SearchProps {
|
||||||
pub fn Search(props: SearchProps) -> Element {
|
pub fn Search(props: SearchProps) -> Element {
|
||||||
let mut search_type = use_signal(|| "fulltext".to_string());
|
let mut search_type = use_signal(|| "fulltext".to_string());
|
||||||
let mut search_term = use_signal(|| String::new());
|
let mut search_term = use_signal(|| String::new());
|
||||||
let mut search_results = use_signal(|| Vec::<SearchResult>::new());
|
let search_results = use_signal(|| Vec::<SearchResult>::new());
|
||||||
let mut search_error = use_signal(|| None::<String>);
|
let mut search_error = use_signal(|| None::<String>);
|
||||||
|
let is_searching = use_signal(|| false);
|
||||||
|
let last_search_time = use_signal(|| None::<f64>);
|
||||||
|
let mut show_corruption_dialog = use_signal(|| false);
|
||||||
|
|
||||||
let service = props.indexing_service.clone();
|
let service = props.indexing_service.clone();
|
||||||
let db_path = props.db_path.clone();
|
let db_path = props.db_path.clone();
|
||||||
|
|
||||||
|
// Create a callback to perform search
|
||||||
|
let perform_search = {
|
||||||
|
let service = service.clone();
|
||||||
|
let db_path = db_path.clone();
|
||||||
|
let search_type = search_type.clone();
|
||||||
|
let search_term = search_term.clone();
|
||||||
|
let search_results = search_results.clone();
|
||||||
|
let search_error = search_error.clone();
|
||||||
|
let is_searching = is_searching.clone();
|
||||||
|
let last_search_time = last_search_time.clone();
|
||||||
|
let show_corruption_dialog = show_corruption_dialog.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
let service_clone = service.clone();
|
||||||
|
let db_clone = db_path.clone();
|
||||||
|
let search_type_val = search_type().clone();
|
||||||
|
let search_term_val = search_term().clone();
|
||||||
|
|
||||||
|
let mut search_results_clone = search_results.clone();
|
||||||
|
let mut search_error_clone = search_error.clone();
|
||||||
|
let mut is_searching_clone = is_searching.clone();
|
||||||
|
let mut last_search_time_clone = last_search_time.clone();
|
||||||
|
let mut show_corruption_dialog_clone = show_corruption_dialog.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
is_searching_clone.set(true);
|
||||||
|
search_error_clone.set(None);
|
||||||
|
last_search_time_clone.set(None);
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
let query = match search_type_val.as_str() {
|
||||||
|
"fulltext" => {
|
||||||
|
if search_term_val.trim().is_empty() {
|
||||||
|
search_error_clone.set(Some("Please enter a search term".to_string()));
|
||||||
|
is_searching_clone.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize search term for FTS5 by removing problematic characters
|
||||||
|
let sanitized_term = search_term_val
|
||||||
|
.replace("'", "''") // Escape single quotes for SQL
|
||||||
|
.replace(":", " ") // Replace colons with spaces (common in file paths, times, etc.)
|
||||||
|
.replace(";", " ") // Replace semicolons with spaces
|
||||||
|
.replace("(", " ") // Replace parentheses with spaces
|
||||||
|
.replace(")", " ")
|
||||||
|
.replace("[", " ") // Replace brackets with spaces
|
||||||
|
.replace("]", " ")
|
||||||
|
.replace("{", " ") // Replace braces with spaces
|
||||||
|
.replace("}", " ")
|
||||||
|
.replace("^", " ") // Replace carets with spaces
|
||||||
|
.replace("~", " ") // Replace tildes with spaces
|
||||||
|
.replace("\"", " "); // Replace quotes with spaces to avoid nesting issues
|
||||||
|
|
||||||
|
// Split into words and rejoin to handle multiple spaces and create a proper FTS5 query
|
||||||
|
let words: Vec<&str> = sanitized_term.split_whitespace().collect();
|
||||||
|
if words.is_empty() {
|
||||||
|
search_error_clone.set(Some("Please enter a valid search term".to_string()));
|
||||||
|
is_searching_clone.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join words with AND for better matching
|
||||||
|
let fts_query = words.join(" AND ");
|
||||||
|
format!("SELECT name, path, snippet(searchabletext, 2, '<b>', '</b>', '<b>...</b>', 64) as snippet FROM searchabletext WHERE text MATCH '{}'", fts_query)
|
||||||
|
},
|
||||||
|
"filename" => {
|
||||||
|
if search_term_val.trim().is_empty() {
|
||||||
|
search_error_clone.set(Some("Please enter a filename pattern".to_string()));
|
||||||
|
is_searching_clone.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
format!("SELECT name, path FROM files WHERE name LIKE '%{}%'", search_term_val.replace("'", "''"))
|
||||||
|
},
|
||||||
|
"duplicates" => "SELECT name, count(*) as cnt, path FROM files GROUP BY hash HAVING cnt > 1 ORDER BY cnt DESC".to_string(),
|
||||||
|
_ => {
|
||||||
|
is_searching_clone.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the search in a blocking task to prevent UI freezing
|
||||||
|
let search_result = tokio::task::spawn_blocking(move || {
|
||||||
|
service_clone.execute_search(&db_clone, &query)
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let elapsed = start_time.elapsed().as_secs_f64();
|
||||||
|
match search_result {
|
||||||
|
Ok(db_result) => {
|
||||||
|
match db_result {
|
||||||
|
Ok(results) => {
|
||||||
|
search_results_clone.set(results);
|
||||||
|
last_search_time_clone.set(Some(elapsed));
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
if e.starts_with("DATABASE_CORRUPTED:") {
|
||||||
|
search_error_clone.set(Some("Database appears to be corrupted".to_string()));
|
||||||
|
show_corruption_dialog_clone.set(true);
|
||||||
|
} else {
|
||||||
|
search_error_clone.set(Some(e));
|
||||||
|
}
|
||||||
|
last_search_time_clone.set(Some(elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
search_error_clone.set(Some(format!("Task execution error: {}", e)));
|
||||||
|
last_search_time_clone.set(Some(elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_searching_clone.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
style: "margin-top: 30px;",
|
class: "section",
|
||||||
h2 { "Search Database" }
|
h2 { "Search Database" }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
style: "margin-bottom: 10px;",
|
class: "form-group",
|
||||||
label { "Search Type: " }
|
label { "Search Type: " }
|
||||||
select {
|
select {
|
||||||
|
class: "form-control",
|
||||||
value: "{search_type}",
|
value: "{search_type}",
|
||||||
onchange: move |evt| search_type.set(evt.value()),
|
onchange: move |evt| search_type.set(evt.value()),
|
||||||
option { value: "fulltext", "Full Text Search" }
|
option { value: "fulltext", "Full Text Search" }
|
||||||
|
|
@ -44,92 +163,104 @@ pub fn Search(props: SearchProps) -> Element {
|
||||||
|
|
||||||
if search_type() != "duplicates" {
|
if search_type() != "duplicates" {
|
||||||
div {
|
div {
|
||||||
style: "margin-bottom: 10px;",
|
class: "form-group",
|
||||||
label { "Search Term: " }
|
label { "Search Term: " }
|
||||||
input {
|
input {
|
||||||
|
class: "form-control",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
value: "{search_term}",
|
value: "{search_term}",
|
||||||
oninput: move |evt| search_term.set(evt.value())
|
oninput: move |evt| search_term.set(evt.value()),
|
||||||
|
onkeydown: {
|
||||||
|
let perform_search = perform_search.clone();
|
||||||
|
move |evt| {
|
||||||
|
if evt.code() == dioxus::events::Code::Enter {
|
||||||
|
perform_search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
div {
|
||||||
style: "padding: 10px 20px; background-color: #2196F3; color: white; border: none; cursor: pointer;",
|
style: "display: flex; align-items: center; gap: 10px;",
|
||||||
onclick: move |_| {
|
button {
|
||||||
let service_clone = service.clone();
|
class: "btn btn-info",
|
||||||
let db_clone = db_path.clone();
|
disabled: is_searching(),
|
||||||
let search_type_val = search_type().clone();
|
onclick: {
|
||||||
let search_term_val = search_term().clone();
|
let perform_search = perform_search.clone();
|
||||||
|
move |_| {
|
||||||
let mut search_results_clone = search_results.clone();
|
perform_search();
|
||||||
let mut search_error_clone = search_error.clone();
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
search_error_clone.set(None);
|
|
||||||
|
|
||||||
let query = match search_type_val.as_str() {
|
|
||||||
"fulltext" => {
|
|
||||||
if search_term_val.trim().is_empty() {
|
|
||||||
search_error_clone.set(Some("Please enter a search term".to_string()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
format!("SELECT name, path, snippet(searchabletext, 2, '<b>', '</b>', '<b>...</b>', 64) as snippet FROM searchabletext WHERE text MATCH '{}'", search_term_val.replace("'", "''"))
|
|
||||||
},
|
|
||||||
"filename" => {
|
|
||||||
if search_term_val.trim().is_empty() {
|
|
||||||
search_error_clone.set(Some("Please enter a filename pattern".to_string()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
format!("SELECT name, path FROM files WHERE name LIKE '%{}%'", search_term_val.replace("'", "''"))
|
|
||||||
},
|
|
||||||
"duplicates" => "SELECT name, count(*) as cnt, path FROM files GROUP BY hash HAVING cnt > 1 ORDER BY cnt DESC".to_string(),
|
|
||||||
_ => return
|
|
||||||
};
|
|
||||||
|
|
||||||
match service_clone.execute_search(&db_clone, &query) {
|
|
||||||
Ok(results) => search_results_clone.set(results),
|
|
||||||
Err(e) => search_error_clone.set(Some(e))
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
},
|
"Search"
|
||||||
"Search"
|
}
|
||||||
|
|
||||||
|
if is_searching() {
|
||||||
|
div {
|
||||||
|
class: "loading",
|
||||||
|
title: "Searching..."
|
||||||
|
}
|
||||||
|
} else if let Some(elapsed) = last_search_time() {
|
||||||
|
span {
|
||||||
|
style: "color: #666; font-size: 0.9em;",
|
||||||
|
"Search completed in {elapsed:.3}s"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = search_error() {
|
if let Some(error) = search_error() {
|
||||||
div {
|
div {
|
||||||
style: "color: red; margin-top: 10px;",
|
class: "error-message",
|
||||||
"Error: {error}"
|
"Error: {error}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !search_results().is_empty() {
|
if !search_results().is_empty() {
|
||||||
div {
|
div {
|
||||||
style: "margin-top: 20px;",
|
class: "search-results",
|
||||||
h3 { "Search Results ({search_results()[0].rows.len()} rows)" }
|
h3 { "Search Results ({search_results()[0].rows.len()} results)" }
|
||||||
div {
|
div {
|
||||||
style: "max-height: 400px; overflow: auto; border: 1px solid #ddd;",
|
class: "results-table",
|
||||||
table {
|
table {
|
||||||
style: "width: 100%; border-collapse: collapse; font-size: 12px;",
|
|
||||||
thead {
|
thead {
|
||||||
style: "background-color: #f5f5f5; position: sticky; top: 0;",
|
|
||||||
tr {
|
tr {
|
||||||
for column in search_results()[0].columns.iter() {
|
for column in search_results()[0].columns.iter() {
|
||||||
th {
|
th { "{column}" }
|
||||||
style: "padding: 8px; border: 1px solid #ddd; text-align: left;",
|
|
||||||
"{column}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for (i, row) in search_results()[0].rows.iter().enumerate() {
|
for (_i, row) in search_results()[0].rows.iter().enumerate() {
|
||||||
tr {
|
tr {
|
||||||
style: if i % 2 == 0 { "background-color: #f9f9f9;" } else { "" },
|
for (col_index, value) in row.values.iter().enumerate() {
|
||||||
for value in row.values.iter() {
|
// Check if this column is a path column
|
||||||
td {
|
if search_results()[0].columns.get(col_index).map(|s| s.as_str()) == Some("path") {
|
||||||
style: "padding: 8px; border: 1px solid #ddd; word-break: break-all;",
|
{
|
||||||
dangerous_inner_html: "{value}"
|
let value_owned = value.clone();
|
||||||
|
let service_owned = props.indexing_service.clone();
|
||||||
|
rsx! {
|
||||||
|
td {
|
||||||
|
class: "path-cell clickable",
|
||||||
|
onclick: move |_| {
|
||||||
|
let path = value_owned.clone();
|
||||||
|
let service_clone = service_owned.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
if let Err(e) = service_clone.open_file_explorer(&path) {
|
||||||
|
eprintln!("Failed to open file explorer: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: "Click to open in file explorer",
|
||||||
|
dangerous_inner_html: "{value}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
td {
|
||||||
|
dangerous_inner_html: "{value}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +270,60 @@ pub fn Search(props: SearchProps) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Database corruption recovery dialog
|
||||||
|
if show_corruption_dialog() {
|
||||||
|
div {
|
||||||
|
class: "modal-backdrop",
|
||||||
|
div {
|
||||||
|
class: "modal-dialog",
|
||||||
|
h3 {
|
||||||
|
style: "margin-top: 0; color: #d32f2f;",
|
||||||
|
"⚠️ Database Corruption Detected"
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
style: "margin: 15px 0;",
|
||||||
|
"The database appears to be corrupted or malformed. This can happen due to unexpected shutdowns or disk issues."
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
style: "margin: 15px 0; font-weight: bold;",
|
||||||
|
"Would you like to delete the corrupted database and create a new one? This will require re-indexing your files."
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
style: "display: flex; gap: 10px; margin-top: 20px;",
|
||||||
|
button {
|
||||||
|
style: "padding: 10px 20px; background-color: #d32f2f; color: white; border: none; border-radius: 5px; cursor: pointer;",
|
||||||
|
onclick: move |_| {
|
||||||
|
let service = props.indexing_service.clone();
|
||||||
|
let db = props.db_path.clone();
|
||||||
|
|
||||||
|
show_corruption_dialog.set(false);
|
||||||
|
search_error.set(Some("Deleting corrupted database...".to_string()));
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
match service.delete_index_for_rebuild(&db) {
|
||||||
|
Ok(()) => {
|
||||||
|
search_error.set(Some("Database deleted. You can now start indexing again.".to_string()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
search_error.set(Some(format!("Error deleting database: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"Yes, Delete & Rebuild"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
style: "padding: 10px 20px; background-color: #666; color: white; border: none; border-radius: 5px; cursor: pointer;",
|
||||||
|
onclick: move |_| {
|
||||||
|
show_corruption_dialog.set(false);
|
||||||
|
},
|
||||||
|
"Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue