Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
fileset = pkgs.lib.fileset.unions [
(craneLib.fileset.commonCargoSources unfilteredRoot)
(pkgs.lib.fileset.fileFilter (
file: pkgs.lib.any file.hasExt [ "html" "scss" "css" "js" "json" "txt" ]
file: pkgs.lib.any file.hasExt [ "html" "scss" "css" "js" "json" "txt" "png" ]
) unfilteredRoot)
(pkgs.lib.fileset.maybeMissing ./assets)
];
Expand Down
12 changes: 12 additions & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
<link data-trunk rel="css" href="/style/output.css" />
<title>Bible App</title>
<meta name="description" content="A modern Bible reading app built with Rust and WebAssembly. Read, search, and study the Bible with an intuitive interface." />

<!-- Favicons -->
<link data-trunk rel="copy-dir" href="/src/icons" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/32.png">
<link rel="icon" type="image/png" sizes="64x64" href="/icons/64.png">
<link rel="icon" type="image/png" sizes="128x128" href="/icons/128.png">
<link rel="icon" type="image/png" sizes="256x256" href="/icons/256.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icons/512.png">
<link rel="apple-touch-icon" sizes="128x128" href="/icons/128.png">
<link rel="apple-touch-icon" sizes="256x256" href="/icons/256.png">
<link rel="apple-touch-icon" sizes="512x512" href="/icons/512.png">
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
Expand Down
Binary file modified site/src/.DS_Store
Binary file not shown.
110 changes: 83 additions & 27 deletions site/src/components/command_palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ use leptos_router::NavigateOptions;
use wasm_bindgen_futures::spawn_local;
use leptos::web_sys::KeyboardEvent;
use std::collections::HashMap;
use std::sync::OnceLock;

// Cache for normalized verse text to improve search performance
static NORMALIZED_VERSE_CACHE: OnceLock<HashMap<String, String>> = OnceLock::new();

// Initialize the verse cache
fn get_normalized_verse_cache() -> &'static HashMap<String, String> {
NORMALIZED_VERSE_CACHE.get_or_init(|| {
let mut cache = HashMap::new();
let bible = get_bible();

for book in &bible.books {
for chapter in &book.chapters {
for verse in &chapter.verses {
let verse_key = format!("{}:{}:{}", book.name, chapter.chapter, verse.verse);
let normalized_text = normalize_text_for_search(&verse.text);
cache.insert(verse_key, normalized_text);
}
}
}

cache
})
}

#[derive(Debug, Clone, PartialEq)]
pub enum SearchResult {
Expand Down Expand Up @@ -345,12 +369,31 @@ pub fn CommandPalette(
) -> impl IntoView {
let navigate = use_navigate();
let location = use_location();

// Separate signals for input display vs actual search query (for debouncing)
let (input_value, set_input_value) = signal(String::new());
let (search_query, set_search_query) = signal(String::new());
let (selected_index, set_selected_index) = signal(0usize);
let (navigate_to, set_navigate_to) = signal::<Option<String>>(None);
let (is_mounted, set_is_mounted) = signal(false);
let (execute_instruction, set_execute_instruction) = signal::<Option<Instruction>>(None);

// Debouncing effect: update search_query 150ms after input_value stops changing
Effect::new(move |_| {
let input_val = input_value.get();
let set_search_query_clone = set_search_query.clone();

spawn_local(async move {
// Wait 150ms before updating search query
gloo_timers::future::TimeoutFuture::new(150).await;

// Only update if the input value hasn't changed in the meantime
if input_val == input_value.get_untracked() {
set_search_query_clone.set(input_val);
}
});
});

// Create a node ref for the input element
let input_ref = NodeRef::<leptos::html::Input>::new();

Expand Down Expand Up @@ -399,7 +442,7 @@ pub fn CommandPalette(
// Handle initial search query when palette opens
Effect::new(move |_| {
if let Some(query) = initial_search_query.get() {
set_search_query.set(query);
set_input_value.set(query); // Set input_value to show in field, debouncing will handle search_query
// Note: We can't clear the signal here because this is a ReadSignal
// The signal will be cleared by the parent component
}
Expand Down Expand Up @@ -637,6 +680,10 @@ pub fn CommandPalette(
let mut verse_matches: Vec<(SearchResult, usize)> = Vec::new();
let mut search_count = 0;

// Normalize query once outside the loop for performance
let query_normalized = normalize_text_for_search(&query);
let verse_cache = get_normalized_verse_cache();

'global_search: for book in &get_bible().books {
for chapter in &book.chapters {
for verse in &chapter.verses {
Expand All @@ -645,29 +692,32 @@ pub fn CommandPalette(
break 'global_search;
}

let verse_text_lower = verse.text.to_lowercase();
if verse_text_lower.contains(&query) {
// Score based on how early the match appears in the verse
let match_position = verse_text_lower.find(&query).unwrap_or(verse.text.len());
let score = if verse_text_lower.starts_with(&query) {
1000 // Starts with query
} else if match_position < 10 {
800 // Match near beginning
} else if match_position < 30 {
600 // Match in first part
} else {
400 // Match later in verse
};

verse_matches.push((
SearchResult::Verse {
chapter: chapter.clone(),
verse_number: verse.verse,
verse_text: verse.text.clone(),
},
score
));
search_count += 1;
// Use cached normalized text for performance
let verse_key = format!("{}:{}:{}", book.name, chapter.chapter, verse.verse);
if let Some(verse_text_normalized) = verse_cache.get(&verse_key) {
if verse_text_normalized.contains(&query_normalized) {
// Score based on how early the match appears in the verse
let match_position = verse_text_normalized.find(&query_normalized).unwrap_or(verse.text.len());
let score = if verse_text_normalized.starts_with(&query_normalized) {
1000 // Starts with query
} else if match_position < 10 {
800 // Match near beginning
} else if match_position < 30 {
600 // Match in first part
} else {
400 // Match later in verse
};

verse_matches.push((
SearchResult::Verse {
chapter: chapter.clone(),
verse_number: verse.verse,
verse_text: verse.text.clone(),
},
score
));
search_count += 1;
}
}
}
}
Expand Down Expand Up @@ -876,8 +926,8 @@ pub fn CommandPalette(
placeholder="Search chapters, verses, or text... (e.g., 'Genesis 1', 'john 3:16', 'love', '>' for shortcuts)"
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2"
style="background-color: var(--theme-palette-background); color: var(--theme-palette-text); border-color: var(--theme-palette-border); --tw-ring-color: var(--theme-palette-highlight)"
prop:value=search_query
on:input=move |e| set_search_query.set(event_target_value(&e))
prop:value=input_value
on:input=move |e| set_input_value.set(event_target_value(&e))
role="combobox"
aria-expanded="true"
aria-autocomplete="list"
Expand Down Expand Up @@ -1073,7 +1123,7 @@ pub fn CommandPalette(
/// - "gen 3" matches "genesis 3" (partial word + number)
/// - "john 3:16" matches "johannes 3:16" (partial word + full number)
fn normalize_text_for_search(text: &str) -> String {
// Normalize Dutch characters and other diacritics for better search matching
// Normalize Dutch characters, remove punctuation, and clean up spacing for better search matching
text.chars()
.map(|c| match c {
// Dutch characters
Expand All @@ -1094,11 +1144,17 @@ fn normalize_text_for_search(text: &str) -> String {
'Ý' | 'Ỳ' | 'Ŷ' | 'Ÿ' => 'Y',
'Ç' => 'C',
'Ñ' => 'N',
// Remove punctuation characters - replace with space to maintain word boundaries
',' | '.' | ';' | ':' | '!' | '?' | '"' | '\'' | '(' | ')' | '[' | ']' | '-' | '—' | '–' | '/' | '\\' | '«' | '»' => ' ',
// Keep other characters as-is
_ => c,
})
.collect::<String>()
.to_lowercase()
// Clean up multiple spaces and trim
.split_whitespace()
.collect::<Vec<&str>>()
.join(" ")
}

fn convert_arabic_to_roman(text: &str) -> String {
Expand Down
6 changes: 6 additions & 0 deletions site/src/components/theme_sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub fn ThemeSidebar(
current_theme: ReadSignal<Theme>,
set_current_theme: WriteSignal<Theme>,
set_sidebar_open: WriteSignal<bool>,
palette_open: ReadSignal<bool>,
) -> impl IntoView {
let themes = get_themes();
let themes_len = themes.len();
Expand All @@ -34,6 +35,11 @@ pub fn ThemeSidebar(
return;
}

// Don't handle navigation when command palette is open (let palette handle it)
if palette_open.get() {
return;
}

match (e.key().as_str(), e.ctrl_key()) {
("j", true) => {
// Ctrl+J: Next theme and apply it instantly
Expand Down
Binary file added site/src/icons/128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/src/icons/16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/src/icons/256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/src/icons/32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/src/icons/512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/src/icons/64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions site/src/instructions/keyboard_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"k": "PreviousVerse",
"<Down>": "NextVerse",
"<Up>": "PreviousVerse",
"<S-J>": "ExtendSelectionNextVerse",
"<S-K>": "ExtendSelectionPreviousVerse",

"l": "NextChapter",
"h": "PreviousChapter",
Expand Down
8 changes: 6 additions & 2 deletions site/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ pub use types::*;
pub use processor::*;
pub use vim_keys::*;

use leptos::prelude::{ReadSignal, WriteSignal, create_signal, Set};
use leptos::prelude::{ReadSignal, WriteSignal, signal, Set};
use std::sync::OnceLock;

// Global signal for dispatching instructions from command palette to main app
#[allow(dead_code)]
static GLOBAL_INSTRUCTION_SIGNAL: OnceLock<(ReadSignal<Option<Instruction>>, WriteSignal<Option<Instruction>>)> = OnceLock::new();

/// Initialize the global instruction signal (call this from main app)
#[allow(dead_code)]
pub fn init_global_instruction_signal() -> (ReadSignal<Option<Instruction>>, WriteSignal<Option<Instruction>>) {
let signals = create_signal::<Option<Instruction>>(None);
let signals = signal::<Option<Instruction>>(None);
GLOBAL_INSTRUCTION_SIGNAL.set(signals).expect("Global instruction signal can only be initialized once");
signals
}

/// Get the global instruction signal writer (for command palette)
#[allow(dead_code)]
pub fn get_global_instruction_writer() -> Option<WriteSignal<Option<Instruction>>> {
GLOBAL_INSTRUCTION_SIGNAL.get().map(|(_, writer)| *writer)
}

/// Dispatch an instruction globally
#[allow(dead_code)]
pub fn dispatch_instruction(instruction: Instruction) {
if let Some(writer) = get_global_instruction_writer() {
writer.set(Some(instruction));
Expand Down
Loading