opti
This commit is contained in:
BIN
profile.json.gz
BIN
profile.json.gz
Binary file not shown.
Binary file not shown.
@ -80,26 +80,30 @@ impl Database {
|
|||||||
ClipboardData::Image(img) => {
|
ClipboardData::Image(img) => {
|
||||||
match &img.raw_pixels {
|
match &img.raw_pixels {
|
||||||
Some(px) => {
|
Some(px) => {
|
||||||
if px.len() > self.max_entry_size_bytes * 4 {
|
|
||||||
eprintln!(
|
|
||||||
"Image rejetée dans DB : {} Mo > limite {} Mo",
|
|
||||||
px.len() / 1_048_576,
|
|
||||||
(self.max_entry_size_bytes * 4) / 1_048_576
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let path = img.file_path(&self.dir_path);
|
|
||||||
let file = fs::File::create(&path)?;
|
|
||||||
let rgb: Vec<u8> = px
|
let rgb: Vec<u8> = px
|
||||||
.chunks_exact(4)
|
.chunks_exact(4)
|
||||||
.flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
|
.flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
|
||||||
.collect();
|
.collect();
|
||||||
JpegEncoder::new_with_quality(file, 70).write_image(
|
|
||||||
|
let mut jpeg_buf = Vec::new();
|
||||||
|
JpegEncoder::new_with_quality(&mut jpeg_buf, 70).write_image(
|
||||||
&rgb,
|
&rgb,
|
||||||
img.width,
|
img.width,
|
||||||
img.height,
|
img.height,
|
||||||
ExtendedColorType::Rgb8,
|
ExtendedColorType::Rgb8,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
if jpeg_buf.len() > self.max_entry_size_bytes {
|
||||||
|
eprintln!(
|
||||||
|
"Image rejetée dans DB : JPEG {} Ko > limite {} Ko",
|
||||||
|
jpeg_buf.len() / 1024,
|
||||||
|
self.max_entry_size_bytes / 1024
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = img.file_path(&self.dir_path);
|
||||||
|
fs::write(&path, &jpeg_buf)?;
|
||||||
}
|
}
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,13 @@ pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<d
|
|||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
for entry in rx {
|
for entry in rx {
|
||||||
let lock = db.lock().unwrap();
|
let lock = match db.lock() {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(poisoned) => {
|
||||||
|
eprintln!("Mutex DB empoisonné, récupération forcée");
|
||||||
|
poisoned.into_inner()
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Err(e) = lock.append(entry) {
|
if let Err(e) = lock.append(entry) {
|
||||||
eprintln!("SQLite write error: {e}");
|
eprintln!("SQLite write error: {e}");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -114,7 +114,8 @@ fn handle_clipboard_event(
|
|||||||
img_data.height,
|
img_data.height,
|
||||||
pixel_count / 1_000_000
|
pixel_count / 1_000_000
|
||||||
);
|
);
|
||||||
*last_image_hash = Some(pixel_count as u64);
|
let sentinel_hash = hash_bytes(&img_data.bytes[..img_data.bytes.len().min(256)]);
|
||||||
|
*last_image_hash = Some(sentinel_hash);
|
||||||
*last_text = None;
|
*last_text = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
147
src/app.rs
147
src/app.rs
@ -20,6 +20,13 @@ const PREVIEW_MAX_WIDTH: u32 = 1280;
|
|||||||
const PREVIEW_MAX_HEIGHT: u32 = 720;
|
const PREVIEW_MAX_HEIGHT: u32 = 720;
|
||||||
const IMAGE_CACHE_MAX: usize = 8;
|
const IMAGE_CACHE_MAX: usize = 8;
|
||||||
const PAGE_SIZE: usize = 50;
|
const PAGE_SIZE: usize = 50;
|
||||||
|
const MAX_HIGHLIGHT_LINES: usize = 500;
|
||||||
|
const SYNC_INTERVAL_MS: u64 = 1000;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_image(s: &str) -> bool {
|
||||||
|
s.ends_with(".jpg") || s.ends_with(".png")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@ -37,34 +44,6 @@ pub enum PendingAction {
|
|||||||
PasteEncrypted,
|
PasteEncrypted,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
|
||||||
pub mode: Mode,
|
|
||||||
pub all_items: Vec<HistoryItem>,
|
|
||||||
pub filtered_items: Vec<HistoryItem>,
|
|
||||||
pub list_state: ListState,
|
|
||||||
pub input_buffer: String,
|
|
||||||
pub should_quit: bool,
|
|
||||||
pub undo_stack: Vec<HistoryItem>,
|
|
||||||
pub current_image: Option<protocol::StatefulProtocol>,
|
|
||||||
pub last_selected_index: Option<usize>,
|
|
||||||
pub picker: Picker,
|
|
||||||
pub preview_scroll: u16,
|
|
||||||
pub crypto: Option<Crypto>,
|
|
||||||
pub salt: Vec<u8>,
|
|
||||||
pub pending_action: Option<PendingAction>,
|
|
||||||
pub error_message: Option<(String, Instant)>,
|
|
||||||
pub status_message: Option<(String, Instant)>,
|
|
||||||
pub syntax_set: SyntaxSet,
|
|
||||||
pub theme_set: ThemeSet,
|
|
||||||
pub type_filter: TypeFilter,
|
|
||||||
pub loaded_count: usize,
|
|
||||||
pub has_more: bool,
|
|
||||||
pub preview_highlighted: Option<Vec<Line<'static>>>,
|
|
||||||
pub preview_lang: Option<String>,
|
|
||||||
image_cache: HashMap<String, Arc<DynamicImage>>,
|
|
||||||
image_cache_order: VecDeque<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TypeFilter {
|
pub enum TypeFilter {
|
||||||
All,
|
All,
|
||||||
@ -90,6 +69,35 @@ impl TypeFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub all_items: Vec<HistoryItem>,
|
||||||
|
pub filtered_items: Vec<HistoryItem>,
|
||||||
|
pub list_state: ListState,
|
||||||
|
pub input_buffer: String,
|
||||||
|
pub should_quit: bool,
|
||||||
|
pub undo_stack: Vec<HistoryItem>,
|
||||||
|
pub current_image: Option<protocol::StatefulProtocol>,
|
||||||
|
pub last_selected_index: Option<usize>,
|
||||||
|
pub picker: Picker,
|
||||||
|
pub preview_scroll: u16,
|
||||||
|
pub crypto: Option<Crypto>,
|
||||||
|
pub salt: Vec<u8>,
|
||||||
|
pub pending_action: Option<PendingAction>,
|
||||||
|
pub error_message: Option<(String, Instant)>,
|
||||||
|
pub status_message: Option<(String, Instant)>,
|
||||||
|
pub syntax_set: SyntaxSet,
|
||||||
|
pub theme_set: ThemeSet,
|
||||||
|
pub type_filter: TypeFilter,
|
||||||
|
pub loaded_count: usize,
|
||||||
|
pub has_more: bool,
|
||||||
|
pub preview_highlighted: Option<Vec<Line<'static>>>,
|
||||||
|
pub preview_lang: Option<String>,
|
||||||
|
last_sync: Instant,
|
||||||
|
image_cache: HashMap<String, Arc<DynamicImage>>,
|
||||||
|
image_cache_order: VecDeque<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn syn_color(c: syntect::highlighting::Color) -> Color {
|
fn syn_color(c: syntect::highlighting::Color) -> Color {
|
||||||
Color::Rgb(c.r, c.g, c.b)
|
Color::Rgb(c.r, c.g, c.b)
|
||||||
}
|
}
|
||||||
@ -100,14 +108,14 @@ pub fn highlight_code(
|
|||||||
theme_set: &ThemeSet,
|
theme_set: &ThemeSet,
|
||||||
) -> Vec<Line<'static>> {
|
) -> Vec<Line<'static>> {
|
||||||
let theme = &theme_set.themes["base16-ocean.dark"];
|
let theme = &theme_set.themes["base16-ocean.dark"];
|
||||||
let syntax = syntax_set
|
let syntax = detect_syntax(content, syntax_set);
|
||||||
.find_syntax_by_first_line(content)
|
|
||||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
|
|
||||||
|
|
||||||
let mut h = HighlightLines::new(syntax, theme);
|
let mut h = HighlightLines::new(syntax, theme);
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
for (no, line) in LinesWithEndings::from(content).enumerate() {
|
for (no, line) in LinesWithEndings::from(content)
|
||||||
|
.enumerate()
|
||||||
|
.take(MAX_HIGHLIGHT_LINES)
|
||||||
|
{
|
||||||
let ranges = h.highlight_line(line, syntax_set).unwrap_or_default();
|
let ranges = h.highlight_line(line, syntax_set).unwrap_or_default();
|
||||||
let mut spans = vec![Span::styled(
|
let mut spans = vec![Span::styled(
|
||||||
format!("{:>4} │ ", no + 1),
|
format!("{:>4} │ ", no + 1),
|
||||||
@ -125,11 +133,51 @@ pub fn highlight_code(
|
|||||||
}
|
}
|
||||||
lines.push(Line::from(spans));
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_lines = content.lines().count();
|
||||||
|
if total_lines > MAX_HIGHLIGHT_LINES {
|
||||||
|
lines.push(Line::from(Span::styled(
|
||||||
|
format!(
|
||||||
|
" … {} lignes supplémentaires non affichées",
|
||||||
|
total_lines - MAX_HIGHLIGHT_LINES
|
||||||
|
),
|
||||||
|
Style::default().fg(Color::Rgb(100, 100, 120)),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn detect_syntax<'a>(
|
||||||
|
content: &str,
|
||||||
|
syntax_set: &'a SyntaxSet,
|
||||||
|
) -> &'a syntect::parsing::SyntaxReference {
|
||||||
|
if let Some(s) = syntax_set.find_syntax_by_first_line(content) {
|
||||||
|
if s.name != "Plain Text" {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in content.lines().take(3) {
|
||||||
|
let trimmed = line.trim_start_matches(|c: char| !c.is_alphanumeric() && c != '.');
|
||||||
|
if let Some(word) = trimmed.split_whitespace().last() {
|
||||||
|
if let Some(ext) = word.rsplit('.').next() {
|
||||||
|
if ext.len() <= 6 && ext.chars().all(|c| c.is_ascii_alphanumeric()) {
|
||||||
|
if let Some(s) = syntax_set.find_syntax_by_extension(ext) {
|
||||||
|
if s.name != "Plain Text" {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syntax_set.find_syntax_plain_text()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn detect_lang(content: &str, syntax_set: &SyntaxSet) -> Option<String> {
|
pub fn detect_lang(content: &str, syntax_set: &SyntaxSet) -> Option<String> {
|
||||||
let s = syntax_set.find_syntax_by_first_line(content)?;
|
let s = detect_syntax(content, syntax_set);
|
||||||
if s.name == "Plain Text" {
|
if s.name == "Plain Text" {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -182,6 +230,7 @@ impl App {
|
|||||||
type_filter: TypeFilter::All,
|
type_filter: TypeFilter::All,
|
||||||
loaded_count: PAGE_SIZE,
|
loaded_count: PAGE_SIZE,
|
||||||
has_more,
|
has_more,
|
||||||
|
last_sync: Instant::now() - Duration::from_secs(10),
|
||||||
preview_highlighted: None,
|
preview_highlighted: None,
|
||||||
preview_lang: None,
|
preview_lang: None,
|
||||||
image_cache: HashMap::new(),
|
image_cache: HashMap::new(),
|
||||||
@ -268,10 +317,13 @@ impl App {
|
|||||||
let nsecs = ((ts_ms % 1000) * 1_000_000) as u32;
|
let nsecs = ((ts_ms % 1000) * 1_000_000) as u32;
|
||||||
match Local.timestamp_opt(secs, nsecs) {
|
match Local.timestamp_opt(secs, nsecs) {
|
||||||
chrono::LocalResult::Single(dt) => {
|
chrono::LocalResult::Single(dt) => {
|
||||||
let diff = Local::now().signed_duration_since(dt);
|
let today = Local::now().date_naive();
|
||||||
if diff.num_days() == 0 {
|
let entry_date = dt.date_naive();
|
||||||
|
let diff_days = (today - entry_date).num_days();
|
||||||
|
|
||||||
|
if diff_days == 0 {
|
||||||
dt.format("%H:%M:%S").to_string()
|
dt.format("%H:%M:%S").to_string()
|
||||||
} else if diff.num_days() < 365 {
|
} else if diff_days < 365 {
|
||||||
dt.format("%d %b %H:%M").to_string()
|
dt.format("%d %b %H:%M").to_string()
|
||||||
} else {
|
} else {
|
||||||
dt.format("%d/%m/%Y").to_string()
|
dt.format("%d/%m/%Y").to_string()
|
||||||
@ -309,12 +361,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
match self.type_filter {
|
match self.type_filter {
|
||||||
TypeFilter::All => true,
|
TypeFilter::All => true,
|
||||||
TypeFilter::Text => {
|
TypeFilter::Text => !is_image(&item.content),
|
||||||
!item.content.ends_with(".jpg") && !item.content.ends_with(".png")
|
TypeFilter::Image => is_image(&item.content),
|
||||||
}
|
|
||||||
TypeFilter::Image => {
|
|
||||||
item.content.ends_with(".jpg") || item.content.ends_with(".png")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
@ -323,7 +371,7 @@ impl App {
|
|||||||
let search_str = |item: &HistoryItem| -> String {
|
let search_str = |item: &HistoryItem| -> String {
|
||||||
if Crypto::is_any_encrypted(&item.content) {
|
if Crypto::is_any_encrypted(&item.content) {
|
||||||
"[chiffré]".to_string()
|
"[chiffré]".to_string()
|
||||||
} else if item.content.ends_with(".jpg") || item.content.ends_with(".png") {
|
} else if is_image(&item.content) {
|
||||||
format!("image {}", item.content)
|
format!("image {}", item.content)
|
||||||
} else {
|
} else {
|
||||||
item.content.clone()
|
item.content.clone()
|
||||||
@ -420,7 +468,7 @@ impl App {
|
|||||||
let item = self.filtered_items.remove(i);
|
let item = self.filtered_items.remove(i);
|
||||||
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 item.content.ends_with(".jpg") || item.content.ends_with(".png") {
|
if is_image(&item.content) {
|
||||||
self.image_cache.remove(&item.content);
|
self.image_cache.remove(&item.content);
|
||||||
self.image_cache_order.retain(|k| k != &item.content);
|
self.image_cache_order.retain(|k| k != &item.content);
|
||||||
}
|
}
|
||||||
@ -638,7 +686,7 @@ impl App {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
if content.ends_with(".jpg") || content.ends_with(".png") {
|
if is_image(&content) {
|
||||||
if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") {
|
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(arc_img) = self.get_cached_image(&content, dirs.data_dir()) {
|
||||||
let img = (*arc_img).clone();
|
let img = (*arc_img).clone();
|
||||||
@ -655,6 +703,7 @@ impl App {
|
|||||||
pub fn scroll_preview_down(&mut self) {
|
pub fn scroll_preview_down(&mut self) {
|
||||||
self.preview_scroll = self.preview_scroll.saturating_add(3);
|
self.preview_scroll = self.preview_scroll.saturating_add(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_preview_up(&mut self) {
|
pub fn scroll_preview_up(&mut self) {
|
||||||
self.preview_scroll = self.preview_scroll.saturating_sub(3);
|
self.preview_scroll = self.preview_scroll.saturating_sub(3);
|
||||||
}
|
}
|
||||||
@ -666,6 +715,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_with_daemon(&mut self) {
|
pub fn sync_with_daemon(&mut self) {
|
||||||
|
if self.last_sync.elapsed() < Duration::from_millis(SYNC_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.last_sync = Instant::now();
|
||||||
|
|
||||||
let Some(new) = ipc::fetch_history(self.loaded_count) else {
|
let Some(new) = ipc::fetch_history(self.loaded_count) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -701,6 +755,7 @@ impl App {
|
|||||||
pub fn set_error(&mut self, msg: String) {
|
pub fn set_error(&mut self, msg: String) {
|
||||||
self.error_message = Some((msg, Instant::now()));
|
self.error_message = Some((msg, Instant::now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_status(&mut self, msg: String) {
|
pub fn set_status(&mut self, msg: String) {
|
||||||
self.status_message = Some((msg, Instant::now()));
|
self.status_message = Some((msg, Instant::now()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user