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;", 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( conn.execute(
"CREATE TABLE IF NOT EXISTS history ( "CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -296,12 +304,14 @@ impl Database {
} }
pub fn clear_history(&self) -> Result<(), Box<dyn Error>> { 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"); let images_dir = Path::new(&self.dir_path).join("images");
if images_dir.exists() { if images_dir.exists() {
fs::remove_dir_all(&images_dir)?; fs::remove_dir_all(&images_dir)?;
} }
fs::create_dir_all(&images_dir)?; fs::create_dir_all(&images_dir)?;
self.conn.execute("DELETE FROM history", [])?;
Ok(()) Ok(())
} }
} }

View File

@ -9,6 +9,7 @@ use ratatui::widgets::ListState;
use ratatui_image::{picker::Picker, protocol}; use ratatui_image::{picker::Picker, protocol};
use regex::Regex; use regex::Regex;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
@ -22,6 +23,7 @@ const IMAGE_CACHE_MAX: usize = 8;
const PAGE_SIZE: usize = 50; const PAGE_SIZE: usize = 50;
const MAX_HIGHLIGHT_LINES: usize = 500; const MAX_HIGHLIGHT_LINES: usize = 500;
const SYNC_INTERVAL_MS: u64 = 1000; const SYNC_INTERVAL_MS: u64 = 1000;
const UNDO_STACK_MAX: usize = 50;
#[inline] #[inline]
pub fn is_image(s: &str) -> bool { pub fn is_image(s: &str) -> bool {
@ -41,10 +43,9 @@ pub fn extract_url(content: &str) -> Option<String> {
let re = RE let re = RE
.get_or_init(|| Regex::new(r#"https?://[^\s<>"'()\[\]{}]+"#).expect("URL regex invalide")); .get_or_init(|| Regex::new(r#"https?://[^\s<>"'()\[\]{}]+"#).expect("URL regex invalide"));
re.find(content).map(|m| { re.find(content).map(|m| {
let url = m m.as_str()
.as_str() .trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '!' | '?'))
.trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '!' | '?')); .to_string()
url.to_string()
}) })
} }
@ -113,9 +114,11 @@ pub struct App {
pub has_more: bool, pub has_more: bool,
pub preview_highlighted: Option<Vec<Line<'static>>>, pub preview_highlighted: Option<Vec<Line<'static>>>,
pub preview_lang: Option<String>, pub preview_lang: Option<String>,
pub data_dir: Option<PathBuf>,
last_sync: Instant, last_sync: Instant,
image_cache: HashMap<String, Arc<DynamicImage>>, image_cache: HashMap<String, Arc<DynamicImage>>,
image_cache_order: VecDeque<String>, image_cache_order: VecDeque<String>,
matcher: SkimMatcherV2,
} }
fn syn_color(c: syntect::highlighting::Color) -> Color { fn syn_color(c: syntect::highlighting::Color) -> Color {
@ -165,7 +168,6 @@ pub fn highlight_code(
let syntax = detect_syntax(content, syntax_set); let syntax = detect_syntax(content, syntax_set);
let mut h = HighlightLines::new(syntax, theme); let mut h = HighlightLines::new(syntax, theme);
let mut lines = Vec::new(); let mut lines = Vec::new();
let total_lines = content.lines().count(); let total_lines = content.lines().count();
for (no, line) in LinesWithEndings::from(content) for (no, line) in LinesWithEndings::from(content)
@ -205,6 +207,9 @@ pub fn highlight_code(
impl App { impl App {
pub fn new() -> Self { 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 items = ipc::fetch_history(PAGE_SIZE).unwrap_or_default();
let has_more = items.len() == PAGE_SIZE; let has_more = items.len() == PAGE_SIZE;
let mut list_state = ListState::default(); let mut list_state = ListState::default();
@ -212,8 +217,8 @@ impl App {
let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks()); let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
let salt = match directories::ProjectDirs::from("com", "zefad", "rklipd") { let salt = match &data_dir {
Some(dirs) => match Crypto::load_or_create_salt(dirs.data_dir()) { Some(dir) => match Crypto::load_or_create_salt(dir) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
eprintln!("Erreur sel cryptographique : {e}"); eprintln!("Erreur sel cryptographique : {e}");
@ -251,8 +256,10 @@ impl App {
last_sync: Instant::now() - Duration::from_secs(10), last_sync: Instant::now() - Duration::from_secs(10),
preview_highlighted: None, preview_highlighted: None,
preview_lang: None, preview_lang: None,
data_dir,
image_cache: HashMap::new(), image_cache: HashMap::new(),
image_cache_order: VecDeque::new(), image_cache_order: VecDeque::new(),
matcher: SkimMatcherV2::default(),
}; };
app.update_preview(); app.update_preview();
app app
@ -409,11 +416,10 @@ impl App {
} }
} }
} else { } else {
let matcher = SkimMatcherV2::default();
let mut matched: Vec<(i64, HistoryItem)> = base let mut matched: Vec<(i64, HistoryItem)> = base
.into_iter() .into_iter()
.filter_map(|item| { .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 }; let adjusted = score + if item.pinned { 1000 } else { 0 };
Some((adjusted, item)) Some((adjusted, item))
}) })
@ -472,7 +478,13 @@ impl App {
if let Some(i) = self.list_state.selected() { if let Some(i) = self.list_state.selected() {
if i < self.filtered_items.len() { if i < self.filtered_items.len() {
let item = self.filtered_items.remove(i); 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.undo_stack.push(item.clone());
self.all_items.retain(|x| x.content != item.content); self.all_items.retain(|x| x.content != item.content);
if is_image(&item.content) { if is_image(&item.content) {
self.image_cache.remove(&item.content); self.image_cache.remove(&item.content);
@ -564,10 +576,11 @@ impl App {
match extract_url(&content) { match extract_url(&content) {
Some(url) => match std::process::Command::new("xdg-open").arg(&url).spawn() { Some(url) => match std::process::Command::new("xdg-open").arg(&url).spawn() {
Ok(_) => { Ok(_) => {
let preview = if url.len() > 48 { let preview: String = url.chars().take(48).collect();
format!("{}", &url[..48]) let preview = if url.chars().count() > 48 {
format!("{preview}")
} else { } else {
url.clone() preview
}; };
self.set_status(format!("Ouverture : {preview}")); self.set_status(format!("Ouverture : {preview}"));
} }
@ -756,8 +769,8 @@ impl App {
}; };
if is_image(&content) { if is_image(&content) {
if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") { if let Some(dir) = self.data_dir.clone() {
if let Some(arc_img) = self.get_cached_image(&content, dirs.data_dir()) { if let Some(arc_img) = self.get_cached_image(&content, &dir) {
let img = (*arc_img).clone(); let img = (*arc_img).clone();
self.current_image = Some(self.picker.new_resize_protocol(img)); 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 { fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + (area.width.saturating_sub(width)) / 2; let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 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>> { fn help_lines() -> Vec<Line<'static>> {
let k = |s: &'static str| { let key = |s: &'static str| {
Span::styled( Span::styled(
s, s,
Style::default() Style::default()
.fg(Color::Yellow) .fg(Color::Rgb(255, 215, 100))
.add_modifier(Modifier::BOLD), .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 sep = || Span::raw(" ");
let h = |s: &'static str| { let header = |s: &'static str| {
Span::styled( Span::styled(
s, s,
Style::default() Style::default()
.fg(Color::Cyan) .fg(Color::Rgb(130, 190, 255))
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED), .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![ vec![
Line::from(vec![h("Navigation")]), Line::from(vec![header(" Navigation")]),
divider(),
Line::from(vec![ Line::from(vec![
k(" j / ↓"), key(" j / ↓"),
sep(), sep(),
d("Bas"), desc("Bas"),
sep(), sep(),
sep(), sep(),
k("k / ↑"), key("k / ↑"),
sep(), sep(),
d("Haut"), desc("Haut"),
]), ]),
Line::from(vec![ Line::from(vec![
k(" g g"), key(" g g"),
sep(), sep(),
d("Premier"), desc("Premier"),
sep(), sep(),
sep(), sep(),
k("G"), key("G"),
sep(), sep(),
d("Dernier"), desc("Dernier"),
]), ]),
Line::from(vec![ Line::from(vec![
k(" Ctrl+j"), key(" Ctrl+j"),
sep(), sep(),
d("Scroll prévisualisation ↓"), desc("Scroll prévisualisation ↓"),
sep(), sep(),
k("Ctrl+k"), key("Ctrl+k"),
sep(), sep(),
d(""), desc(""),
]), ]),
Line::from(""), Line::from(""),
Line::from(vec![h("Actions")]), Line::from(vec![header(" Actions")]),
Line::from(vec![k(" Entrée"), sep(), d("Coller & quitter")]), 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![ Line::from(vec![
k(" d d"), key(" o"),
sep(), 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![ Line::from(vec![
k(" t"), key(" t"),
sep(), sep(),
d("Filtrer par type (Tous → Texte → Image)"), desc("Filtrer : Tous → Texte → Image"),
]), ]),
Line::from(""), Line::from(""),
Line::from(vec![h("Recherche (mode /)")]), Line::from(vec![header(" Recherche ( / )")]),
divider(),
Line::from(vec![ Line::from(vec![
k(" /texte"), key(" /texte"),
sep(), sep(),
d("Fuzzy search"), desc("Fuzzy"),
sep(), sep(),
k("//regex"), key("//regex"),
sep(), sep(),
d("Regex (préfixe /)"), desc("Regex"),
]), ]),
Line::from(vec![ Line::from(vec![
k(" after:YYYY-MM-DD"), key(" after:YYYY-MM-DD"),
sep(), sep(),
d("Après date"), desc("Après date"),
sep(), sep(),
k("before:..."), key("before:"),
sep(), sep(),
d("Avant date"), desc("Avant date"),
]), ]),
Line::from(""), Line::from(""),
Line::from(vec![h("Commandes (:)")]), Line::from(vec![header(" Commandes ( : )")]),
divider(),
Line::from(vec![ Line::from(vec![
k(" :clear"), key(" :clear"),
sep(), sep(),
d("Effacer tout l'historique"), desc("Tout effacer"),
sep(), sep(),
k(":password"), key(":password"),
sep(), sep(),
d("Mot de passe session"), desc("Mot de passe session"),
sep(), sep(),
k(":q"), key(":q"),
sep(), sep(),
d("Quitter"), desc("Quitter"),
]), ]),
Line::from(""), 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) { 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); f.render_widget(Clear, popup);
let bg = Color::Rgb(22, 26, 50);
let border_color = Color::Rgb(80, 130, 220);
let block = Block::default() let block = Block::default()
.title(Span::styled( .title(Span::styled(
" Aide — Raccourcis clavier ", " Raccourcis clavier ",
Style::default() Style::default()
.fg(Color::White) .fg(Color::Rgb(200, 220, 255))
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
)) ))
.title_alignment(Alignment::Center) .title_alignment(Alignment::Center)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Cyan)) .border_style(Style::default().fg(border_color))
.style(Style::default().bg(Color::Rgb(18, 18, 28))); .style(Style::default().bg(bg));
let inner = block.inner(popup); let inner = block.inner(popup);
f.render_widget(block, popup); f.render_widget(block, popup);
f.render_widget( 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, inner,
); );
} }