add fav + opti
This commit is contained in:
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
41
src/app.rs
41
src/app.rs
@ -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
121
src/ui.rs
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user