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