file manager
git clone https://git.lucas.co/cce-files.git
Implement image downscaling and integrate image previews into file browser
Cargo.toml | 1 +
src/main.rs | 2 +-
src/pages/preview.rs | 1 +
src/services/fs.rs | 91 +++++++++++++++++++++++++++++++++++++---------------
4 files changed, 69 insertions(+), 26 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index c2e12d6..972e947 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ calloop = "0.13.0"
chrono = "0.4.44"
tokio = { version = "1", features = ["full"] }
notify = "8.2.0"
+image = { version = "0.25.10", features = ["png", "jpeg"] }
[lib]
name = "cce_filesystem_interface"
diff --git a/src/main.rs b/src/main.rs
index e8fc427..3900578 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -500,7 +500,7 @@ impl Application for FilesystemApp {
let _current_dir = browse.current_dir.clone();
let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
- let paginator = cce_ui::widget::Paginator::new(56.0, pages_names);
+ let paginator = cce_ui::widget::Paginator::new(pages_names);
let fs_service = services::fs::FsService::new(sender.clone());
let initial_w = if select_mode { 900 } else { 1200 };
diff --git a/src/pages/preview.rs b/src/pages/preview.rs
index 86b111c..8298e1f 100644
--- a/src/pages/preview.rs
+++ b/src/pages/preview.rs
@@ -34,6 +34,7 @@ pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
file_type: data.file_type,
target: data.target,
content_preview: data.content_preview,
+ image_preview: data.image_preview,
scroll_line: 0,
..PreviewState::default()
};
diff --git a/src/services/fs.rs b/src/services/fs.rs
index 0e54179..62189b3 100644
--- a/src/services/fs.rs
+++ b/src/services/fs.rs
@@ -3,6 +3,8 @@ use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
use crate::pages::browse::DirEntry;
+use image::GenericImageView;
+use cce_ui::widget::ImagePreviewData;
#[derive(Debug, Clone, Default)]
pub struct PreviewData {
@@ -14,6 +16,7 @@ pub struct PreviewData {
pub file_type: String,
pub target: String,
pub content_preview: Option<String>,
+ pub image_preview: Option<ImagePreviewData>,
}
#[derive(Debug, Clone)]
@@ -150,6 +153,36 @@ pub fn read_directory_internal(path: &Path) -> Vec<DirEntry> {
entries
}
+fn load_image_preview(path: &Path) -> Option<ImagePreviewData> {
+ let img = image::open(path).ok()?;
+ let (orig_w, orig_h) = img.dimensions();
+ if orig_w == 0 || orig_h == 0 {
+ return None;
+ }
+ let max_dim = 96.0;
+ let ratio = (max_dim / orig_w as f32).min(max_dim / orig_h as f32).min(1.0);
+ let target_w = (orig_w as f32 * ratio).round() as u32;
+ let target_h = (orig_h as f32 * ratio).round() as u32;
+ if target_w == 0 || target_h == 0 {
+ return None;
+ }
+
+ let resized = if target_w == orig_w && target_h == orig_h {
+ img
+ } else {
+ img.resize(target_w, target_h, image::imageops::FilterType::Triangle)
+ };
+
+ let rgba = resized.to_rgba8();
+ let pixels = rgba.chunks_exact(4).map(|p| [p[0], p[1], p[2], p[3]]).collect();
+
+ Some(ImagePreviewData {
+ width: target_w,
+ height: target_h,
+ pixels,
+ })
+}
+
fn load_preview_data_internal(path: &Path) -> PreviewData {
let meta = fs::symlink_metadata(path).ok();
let name = path
@@ -184,8 +217,10 @@ fn load_preview_data_internal(path: &Path) -> PreviewData {
String::new()
};
- // Load content preview
- let content_preview = if is_dir {
+ let mut content_preview = None;
+ let mut image_preview = None;
+
+ if is_dir {
if let Ok(entries) = fs::read_dir(path) {
let mut names = Vec::new();
for entry in entries.flatten().take(100) {
@@ -195,34 +230,39 @@ fn load_preview_data_internal(path: &Path) -> PreviewData {
names.push(format!("{} {}", icon, name));
}
if names.is_empty() {
- Some("[Empty directory]".to_string())
+ content_preview = Some("[Empty directory]".to_string())
} else {
- Some(names.join("\n"))
+ content_preview = Some(names.join("\n"))
}
- } else {
- None
}
- } else if let Ok(mut file) = fs::File::open(path) {
- use std::io::Read;
- let mut buf = vec![0u8; 65536];
- if let Ok(n) = file.read(&mut buf) {
- buf.truncate(n);
- let is_text = match std::str::from_utf8(&buf) {
- Ok(_) => true,
- Err(err) => err.error_len().is_none() && err.valid_up_to() > 0,
- };
- if is_text {
- let utf8_str = String::from_utf8_lossy(&buf).into_owned();
- Some(utf8_str)
- } else {
- Some("[Binary file content]".to_string())
+ } else {
+ let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
+ let is_img_ext = matches!(ext.as_str(), "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "ico");
+
+ if is_img_ext {
+ image_preview = load_image_preview(path);
+ }
+
+ if image_preview.is_none() {
+ if let Ok(mut file) = fs::File::open(path) {
+ use std::io::Read;
+ let mut buf = vec![0u8; 65536];
+ if let Ok(n) = file.read(&mut buf) {
+ buf.truncate(n);
+ let is_text = match std::str::from_utf8(&buf) {
+ Ok(_) => true,
+ Err(err) => err.error_len().is_none() && err.valid_up_to() > 0,
+ };
+ if is_text {
+ let utf8_str = String::from_utf8_lossy(&buf).into_owned();
+ content_preview = Some(utf8_str);
+ } else {
+ content_preview = Some("[Binary file content]".to_string());
+ }
+ }
}
- } else {
- None
}
- } else {
- None
- };
+ }
PreviewData {
name,
@@ -233,6 +273,7 @@ fn load_preview_data_internal(path: &Path) -> PreviewData {
file_type,
target,
content_preview,
+ image_preview,
}
}