Separated frontend code into separate html and css templates, numerous fixes and error handling.

This commit is contained in:
Jeremy Karst 2025-09-12 00:31:52 -04:00
parent 0d941e82d2
commit 2c87a4847e
9 changed files with 1189 additions and 102 deletions

232
assets/app.js Normal file
View 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
View 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
View 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
View 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>

View file

@ -85,7 +85,15 @@ pub fn analyze_files_for_batch_update(
};
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,
};
@ -205,7 +213,15 @@ pub fn process_batch_updates_files_only(
}
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,
};
@ -299,7 +315,15 @@ pub fn process_batch_inserts_files_only(
}
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,
};
@ -377,7 +401,8 @@ pub fn process_text_indexing(
// Check stop flag
if *stop_flag.lock().unwrap() {
drop(tx);
// Commit current transaction before stopping
let _ = tx.commit();
drop(conn);
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);
Some(trimmed_file_string)
}
Err(e) => {
Err(_e) => {
// eprintln!("Warning: Failed to read plaintext file {}: {}", fpath, e);
None
}

View file

@ -201,22 +201,25 @@ pub fn App(props: AppProps) -> Element {
rsx! {
div {
style: "padding: 20px; font-family: Arial, sans-serif;",
h1 { "QuickSearch File Indexer" }
class: "app-container",
div {
style: "margin-bottom: 20px;",
class: "app-header",
h1 { "QuickSearch File Indexer" }
}
div {
class: "app-content",
div {
class: "section",
h2 { "Indexing Controls" }
div {
style: "margin-bottom: 10px;",
label {
style: "display: block; margin-bottom: 5px;",
"Path to index:"
}
class: "form-group",
label { "Path to index:" }
input {
style: "width: 400px; padding: 5px;",
class: "form-control",
r#type: "text",
value: "{indexing_path}",
oninput: move |evt| indexing_path.set(evt.value())
@ -224,13 +227,10 @@ pub fn App(props: AppProps) -> Element {
}
div {
style: "margin-bottom: 10px;",
label {
style: "display: block; margin-bottom: 5px;",
"Database path:"
}
class: "form-group",
label { "Database path:" }
input {
style: "width: 400px; padding: 5px;",
class: "form-control",
r#type: "text",
value: "{db_path}",
oninput: move |evt| db_path.set(evt.value())
@ -238,9 +238,9 @@ pub fn App(props: AppProps) -> Element {
}
div {
style: "margin-bottom: 20px;",
class: "form-group",
button {
style: "margin-right: 10px; padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer;",
class: "btn btn-primary",
onclick: move |_| {
let service = indexing_service_for_start.clone();
let config = config_for_start.clone();
@ -266,7 +266,7 @@ pub fn App(props: AppProps) -> Element {
"Start Indexing"
}
button {
style: "padding: 10px 20px; background-color: #f44336; color: white; border: none; cursor: pointer;",
class: "btn btn-danger",
onclick: move |_| {
let _ = indexing_service_for_stop.stop_indexing();
},
@ -276,9 +276,10 @@ pub fn App(props: AppProps) -> Element {
}
div {
class: "section",
h2 { "Status" }
pre {
style: "background-color: #f5f5f5; padding: 10px; border-radius: 5px; font-family: monospace;",
class: "status-display",
"{status_text}"
}
}
@ -287,14 +288,16 @@ pub fn App(props: AppProps) -> Element {
indexing_service: props.indexing_service.clone(),
db_path: db_path().clone()
}
} // Close app-content
}
// Configuration validation dialog
if show_config_dialog() {
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 {
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 {
style: "margin-top: 0; color: #d32f2f;",
"⚠️ Configuration Changes Detected"

View file

@ -1,6 +1,7 @@
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::Instant;
use std::process::Command;
use walkdir::WalkDir;
use rusqlite::{Connection, params};
@ -163,10 +164,25 @@ impl IndexingService {
/// Execute a search query against the database
pub fn execute_search(&self, db_path: &str, query: &str) -> Result<Vec<SearchResult>, String> {
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)
.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_names: Vec<String> = (0..column_count)
@ -187,13 +203,31 @@ impl IndexingService {
}
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();
for row in rows {
match 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
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)
@ -571,11 +697,19 @@ impl IndexingService {
// Critical configuration values that require index recreation
let hash_length = config.processing.hash_length.to_string();
let tokenize = config.processing.tokenize.clone();
let normalized_path = std::path::Path::new(indexing_path)
let normalized_path = {
let path = std::path::Path::new(indexing_path)
.canonicalize()
.unwrap_or_else(|_| std::path::PathBuf::from(indexing_path))
.to_string_lossy()
.to_string();
// Remove Windows UNC prefix \\?\
if path.starts_with("\\\\?\\") {
path[4..].to_string()
} else {
path
}
};
// Check stored configuration values
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> {
let hash_length = config.processing.hash_length.to_string();
let tokenize = config.processing.tokenize.clone();
let normalized_path = std::path::Path::new(indexing_path)
let normalized_path = {
let path = std::path::Path::new(indexing_path)
.canonicalize()
.unwrap_or_else(|_| std::path::PathBuf::from(indexing_path))
.to_string_lossy()
.to_string();
// Remove Windows UNC prefix \\?\
if path.starts_with("\\\\?\\") {
path[4..].to_string()
} else {
path
}
};
// Update stored configuration values
conn.execute(

View file

@ -1,6 +1,5 @@
use std::sync::{Arc, OnceLock};
use dioxus::prelude::*;
mod frontend;
mod file_handling;
mod document_extraction;
@ -27,9 +26,20 @@ fn main() {
std::process::exit(0);
}).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 {
let config = match config::Config::load() {
Ok(config) => config,

View file

@ -1,6 +1,7 @@
#![allow(non_snake_case)]
use std::sync::Arc;
use std::time::Instant;
use dioxus::prelude::*;
use crate::indexing::{IndexingService, SearchResult};
@ -19,21 +20,139 @@ impl PartialEq for SearchProps {
pub fn Search(props: SearchProps) -> Element {
let mut search_type = use_signal(|| "fulltext".to_string());
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 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 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! {
div {
style: "margin-top: 30px;",
class: "section",
h2 { "Search Database" }
div {
style: "margin-bottom: 10px;",
class: "form-group",
label { "Search Type: " }
select {
class: "form-control",
value: "{search_type}",
onchange: move |evt| search_type.set(evt.value()),
option { value: "fulltext", "Full Text Search" }
@ -44,91 +163,102 @@ pub fn Search(props: SearchProps) -> Element {
if search_type() != "duplicates" {
div {
style: "margin-bottom: 10px;",
class: "form-group",
label { "Search Term: " }
input {
class: "form-control",
r#type: "text",
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();
}
}
}
}
}
}
div {
style: "display: flex; align-items: center; gap: 10px;",
button {
style: "padding: 10px 20px; background-color: #2196F3; color: white; border: none; cursor: pointer;",
onclick: 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();
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;
class: "btn btn-info",
disabled: is_searching(),
onclick: {
let perform_search = perform_search.clone();
move |_| {
perform_search();
}
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"
}
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() {
div {
style: "color: red; margin-top: 10px;",
class: "error-message",
"Error: {error}"
}
}
if !search_results().is_empty() {
div {
style: "margin-top: 20px;",
h3 { "Search Results ({search_results()[0].rows.len()} rows)" }
class: "search-results",
h3 { "Search Results ({search_results()[0].rows.len()} results)" }
div {
style: "max-height: 400px; overflow: auto; border: 1px solid #ddd;",
class: "results-table",
table {
style: "width: 100%; border-collapse: collapse; font-size: 12px;",
thead {
style: "background-color: #f5f5f5; position: sticky; top: 0;",
tr {
for column in search_results()[0].columns.iter() {
th {
style: "padding: 8px; border: 1px solid #ddd; text-align: left;",
"{column}"
}
th { "{column}" }
}
}
}
tbody {
for (i, row) in search_results()[0].rows.iter().enumerate() {
for (_i, row) in search_results()[0].rows.iter().enumerate() {
tr {
style: if i % 2 == 0 { "background-color: #f9f9f9;" } else { "" },
for value in row.values.iter() {
for (col_index, value) in row.values.iter().enumerate() {
// Check if this column is a path column
if search_results()[0].columns.get(col_index).map(|s| s.as_str()) == Some("path") {
{
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 {
style: "padding: 8px; border: 1px solid #ddd; word-break: break-all;",
dangerous_inner_html: "{value}"
}
}
@ -140,5 +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"
}
}
}
}
}
}
}
}