add fav + opti

This commit is contained in:
2026-05-21 10:47:49 +02:00
parent 72ad88e888
commit 46dfa0fd49
3 changed files with 103 additions and 71 deletions

View File

@ -30,6 +30,14 @@ impl Database {
PRAGMA foreign_keys=ON;",
)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL DEFAULT 1);",
)?;
conn.execute(
"INSERT INTO schema_version (version) SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM schema_version)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -296,12 +304,14 @@ impl Database {
}
pub fn clear_history(&self) -> Result<(), Box<dyn Error>> {
self.conn.execute("DELETE FROM history", [])?;
let images_dir = Path::new(&self.dir_path).join("images");
if images_dir.exists() {
fs::remove_dir_all(&images_dir)?;
}
fs::create_dir_all(&images_dir)?;
self.conn.execute("DELETE FROM history", [])?;
Ok(())
}
}

View File

@ -9,6 +9,7 @@ use ratatui::widgets::ListState;
use ratatui_image::{picker::Picker, protocol};
use regex::Regex;
use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
use syntect::easy::HighlightLines;
@ -22,6 +23,7 @@ const IMAGE_CACHE_MAX: usize = 8;
const PAGE_SIZE: usize = 50;
const MAX_HIGHLIGHT_LINES: usize = 500;
const SYNC_INTERVAL_MS: u64 = 1000;
const UNDO_STACK_MAX: usize = 50;
#[inline]
pub fn is_image(s: &str) -> bool {
@ -41,10 +43,9 @@ pub fn extract_url(content: &str) -> Option<String> {
let re = RE
.get_or_init(|| Regex::new(r#"https?://[^\s<>"'()\[\]{}]+"#).expect("URL regex invalide"));
re.find(content).map(|m| {
let url = m
.as_str()
.trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '!' | '?'));
url.to_string()
m.as_str()
.trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '!' | '?'))
.to_string()
})
}
@ -113,9 +114,11 @@ pub struct App {
pub has_more: bool,
pub preview_highlighted: Option<Vec<Line<'static>>>,
pub preview_lang: Option<String>,
pub data_dir: Option<PathBuf>,
last_sync: Instant,
image_cache: HashMap<String, Arc<DynamicImage>>,
image_cache_order: VecDeque<String>,
matcher: SkimMatcherV2,
}
fn syn_color(c: syntect::highlighting::Color) -> Color {
@ -165,7 +168,6 @@ pub fn highlight_code(
let syntax = detect_syntax(content, syntax_set);
let mut h = HighlightLines::new(syntax, theme);
let mut lines = Vec::new();
let total_lines = content.lines().count();
for (no, line) in LinesWithEndings::from(content)
@ -205,6 +207,9 @@ pub fn highlight_code(
impl App {
pub fn new() -> Self {
let data_dir = directories::ProjectDirs::from("com", "zefad", "rklipd")
.map(|d| d.data_dir().to_path_buf());
let items = ipc::fetch_history(PAGE_SIZE).unwrap_or_default();
let has_more = items.len() == PAGE_SIZE;
let mut list_state = ListState::default();
@ -212,8 +217,8 @@ impl App {
let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
let salt = match directories::ProjectDirs::from("com", "zefad", "rklipd") {
Some(dirs) => match Crypto::load_or_create_salt(dirs.data_dir()) {
let salt = match &data_dir {
Some(dir) => match Crypto::load_or_create_salt(dir) {
Ok(s) => s,
Err(e) => {
eprintln!("Erreur sel cryptographique : {e}");
@ -251,8 +256,10 @@ impl App {
last_sync: Instant::now() - Duration::from_secs(10),
preview_highlighted: None,
preview_lang: None,
data_dir,
image_cache: HashMap::new(),
image_cache_order: VecDeque::new(),
matcher: SkimMatcherV2::default(),
};
app.update_preview();
app
@ -409,11 +416,10 @@ impl App {
}
}
} else {
let matcher = SkimMatcherV2::default();
let mut matched: Vec<(i64, HistoryItem)> = base
.into_iter()
.filter_map(|item| {
let score = matcher.fuzzy_match(&search_str(&item), &text_query)?;
let score = self.matcher.fuzzy_match(&search_str(&item), &text_query)?;
let adjusted = score + if item.pinned { 1000 } else { 0 };
Some((adjusted, item))
})
@ -472,7 +478,13 @@ impl App {
if let Some(i) = self.list_state.selected() {
if i < self.filtered_items.len() {
let item = self.filtered_items.remove(i);
// Borner la pile d'annulation pour éviter une fuite mémoire
if self.undo_stack.len() >= UNDO_STACK_MAX {
self.undo_stack.remove(0);
}
self.undo_stack.push(item.clone());
self.all_items.retain(|x| x.content != item.content);
if is_image(&item.content) {
self.image_cache.remove(&item.content);
@ -564,10 +576,11 @@ impl App {
match extract_url(&content) {
Some(url) => match std::process::Command::new("xdg-open").arg(&url).spawn() {
Ok(_) => {
let preview = if url.len() > 48 {
format!("{}", &url[..48])
let preview: String = url.chars().take(48).collect();
let preview = if url.chars().count() > 48 {
format!("{preview}")
} else {
url.clone()
preview
};
self.set_status(format!("Ouverture : {preview}"));
}
@ -756,8 +769,8 @@ impl App {
};
if is_image(&content) {
if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") {
if let Some(arc_img) = self.get_cached_image(&content, dirs.data_dir()) {
if let Some(dir) = self.data_dir.clone() {
if let Some(arc_img) = self.get_cached_image(&content, &dir) {
let img = (*arc_img).clone();
self.current_image = Some(self.picker.new_resize_protocol(img));
}

121
src/ui.rs
View File

@ -286,10 +286,6 @@ fn render_statusbar(f: &mut Frame, app: &mut App, area: Rect) {
);
}
// ---------------------------------------------------------------------------
// Overlay d'aide
// ---------------------------------------------------------------------------
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
@ -297,135 +293,148 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
}
fn help_lines() -> Vec<Line<'static>> {
let k = |s: &'static str| {
let key = |s: &'static str| {
Span::styled(
s,
Style::default()
.fg(Color::Yellow)
.fg(Color::Rgb(255, 215, 100))
.add_modifier(Modifier::BOLD),
)
};
let d = |s: &'static str| Span::styled(s, Style::default().fg(Color::White));
let desc = |s: &'static str| Span::styled(s, Style::default().fg(Color::Rgb(200, 205, 220)));
let sep = || Span::raw(" ");
let h = |s: &'static str| {
let header = |s: &'static str| {
Span::styled(
s,
Style::default()
.fg(Color::Cyan)
.fg(Color::Rgb(130, 190, 255))
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)
};
let dim = |s: &'static str| Span::styled(s, Style::default().fg(Color::DarkGray));
let dim = |s: &'static str| Span::styled(s, Style::default().fg(Color::Rgb(100, 105, 130)));
let divider = || {
Line::from(Span::styled(
" ─────────────────────────────────────────────────────────────",
Style::default().fg(Color::Rgb(55, 60, 90)),
))
};
vec![
Line::from(vec![h("Navigation")]),
Line::from(vec![header(" Navigation")]),
divider(),
Line::from(vec![
k(" j / ↓"),
key(" j / ↓"),
sep(),
d("Bas"),
desc("Bas"),
sep(),
sep(),
k("k / ↑"),
key("k / ↑"),
sep(),
d("Haut"),
desc("Haut"),
]),
Line::from(vec![
k(" g g"),
key(" g g"),
sep(),
d("Premier"),
desc("Premier"),
sep(),
sep(),
k("G"),
key("G"),
sep(),
d("Dernier"),
desc("Dernier"),
]),
Line::from(vec![
k(" Ctrl+j"),
key(" Ctrl+j"),
sep(),
d("Scroll prévisualisation ↓"),
desc("Scroll prévisualisation ↓"),
sep(),
k("Ctrl+k"),
key("Ctrl+k"),
sep(),
d(""),
desc(""),
]),
Line::from(""),
Line::from(vec![h("Actions")]),
Line::from(vec![k(" Entrée"), sep(), d("Coller & quitter")]),
Line::from(vec![header(" Actions")]),
divider(),
Line::from(vec![key(" Entrée"), sep(), desc("Coller et quitter")]),
Line::from(vec![key(" d d"), sep(), desc("Supprimer (confirmation)")]),
Line::from(vec![key(" u"), sep(), desc("Annuler la suppression")]),
Line::from(vec![key(" p"), sep(), desc("★ Épingler / désépingler")]),
Line::from(vec![key(" e"), sep(), desc("🔒 Chiffrer / déchiffrer")]),
Line::from(vec![
k(" d d"),
key(" o"),
sep(),
d("Supprimer (demande confirmation)"),
desc("Ouvrir l'URL dans le navigateur"),
]),
Line::from(vec![k(" u"), sep(), d("Annuler la suppression")]),
Line::from(vec![k(" p"), sep(), d("★ Épingler / désépingler")]),
Line::from(vec![k(" e"), sep(), d("🔒 Chiffrer / déchiffrer")]),
Line::from(vec![k(" o"), sep(), d("Ouvrir l'URL dans le navigateur")]),
Line::from(vec![
k(" t"),
key(" t"),
sep(),
d("Filtrer par type (Tous → Texte → Image)"),
desc("Filtrer : Tous → Texte → Image"),
]),
Line::from(""),
Line::from(vec![h("Recherche (mode /)")]),
Line::from(vec![header(" Recherche ( / )")]),
divider(),
Line::from(vec![
k(" /texte"),
key(" /texte"),
sep(),
d("Fuzzy search"),
desc("Fuzzy"),
sep(),
k("//regex"),
key("//regex"),
sep(),
d("Regex (préfixe /)"),
desc("Regex"),
]),
Line::from(vec![
k(" after:YYYY-MM-DD"),
key(" after:YYYY-MM-DD"),
sep(),
d("Après date"),
desc("Après date"),
sep(),
k("before:..."),
key("before:"),
sep(),
d("Avant date"),
desc("Avant date"),
]),
Line::from(""),
Line::from(vec![h("Commandes (:)")]),
Line::from(vec![header(" Commandes ( : )")]),
divider(),
Line::from(vec![
k(" :clear"),
key(" :clear"),
sep(),
d("Effacer tout l'historique"),
desc("Tout effacer"),
sep(),
k(":password"),
key(":password"),
sep(),
d("Mot de passe session"),
desc("Mot de passe session"),
sep(),
k(":q"),
key(":q"),
sep(),
d("Quitter"),
desc("Quitter"),
]),
Line::from(""),
Line::from(vec![dim(" ? / Esc pour fermer cette aide")]),
Line::from(vec![dim(" Appuyez sur ? ou Esc pour fermer")]),
]
}
fn render_help_overlay(f: &mut Frame, area: Rect) {
let popup = centered_rect(68, 25, area);
let popup = centered_rect(70, 27, area);
f.render_widget(Clear, popup);
let bg = Color::Rgb(22, 26, 50);
let border_color = Color::Rgb(80, 130, 220);
let block = Block::default()
.title(Span::styled(
" Aide — Raccourcis clavier ",
" Raccourcis clavier ",
Style::default()
.fg(Color::White)
.fg(Color::Rgb(200, 220, 255))
.add_modifier(Modifier::BOLD),
))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Cyan))
.style(Style::default().bg(Color::Rgb(18, 18, 28)));
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(bg));
let inner = block.inner(popup);
f.render_widget(block, popup);
f.render_widget(
Paragraph::new(help_lines()).style(Style::default().bg(Color::Rgb(18, 18, 28))),
Paragraph::new(help_lines()).style(Style::default().bg(bg)),
inner,
);
}