Add text previews

This commit is contained in:
Jeremy Soller 2024-10-10 10:05:48 -06:00
parent 3ea075b2c6
commit 081156670e
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
3 changed files with 87 additions and 30 deletions

View file

@ -1109,12 +1109,12 @@ impl App {
widget::settings::view_column(children).into()
}
fn preview(
&self,
fn preview<'a>(
&'a self,
entity_opt: &Option<Entity>,
kind: &PreviewKind,
kind: &'a PreviewKind,
context_drawer: bool,
) -> Element<Message> {
) -> Element<'a, Message> {
let mut children = Vec::with_capacity(1);
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
match kind {

View file

@ -450,7 +450,7 @@ impl App {
col.into()
}
fn preview(&self, kind: &PreviewKind) -> Element<AppMessage> {
fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, AppMessage> {
let mut children = Vec::with_capacity(1);
match kind {
PreviewKind::Custom(PreviewItem(item)) => {

View file

@ -77,6 +77,8 @@ pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
//TODO: best limit for search items
const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20);
const MAX_SEARCH_RESULTS: usize = 200;
//TODO: configurable thumbnail size?
const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32);
//TODO: adjust for locales?
const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p";
@ -1049,18 +1051,49 @@ impl ItemMetadata {
}
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum ItemThumbnail {
NotImage,
Image(widget::image::Handle, Option<(u32, u32)>),
Svg(widget::svg::Handle),
Text(widget::text_editor::Content),
}
impl Clone for ItemThumbnail {
fn clone(&self) -> Self {
match self {
Self::NotImage => Self::NotImage,
Self::Image(handle, size_opt) => Self::Image(handle.clone(), *size_opt),
Self::Svg(handle) => Self::Svg(handle.clone()),
// Content cannot be cloned
Self::Text(content) => Self::NotImage,
}
}
}
impl ItemThumbnail {
pub fn new(path: &Path, mime: mime::Mime, thumbnail_size: u32) -> Self {
if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
pub fn new(path: &Path, metadata: fs::Metadata, mime: mime::Mime, thumbnail_size: u32) -> Self {
let size = metadata.len();
let check_size = |thumbnailer: &str, max_size| {
if size <= max_size {
true
} else {
log::warn!(
"skipping internal {} thumbnailer for {:?}: file size {} is larger than {}",
thumbnailer,
path,
format_size(size),
format_size(max_size)
);
false
}
};
//TODO: adjust limits for internal thumbnailers as desired
if mime.type_() == mime::IMAGE
&& mime.subtype() == mime::SVG
&& check_size("svg", 8 * 1000 * 1000)
{
// Try built-in svg thumbnailer
//TODO: have a reasonable limit on SVG size?
match fs::read(&path) {
Ok(data) => {
//TODO: validate SVG data
@ -1070,7 +1103,7 @@ impl ItemThumbnail {
log::warn!("failed to read {:?}: {}", path, err);
}
}
} else if mime.type_() == mime::IMAGE {
} else if mime.type_() == mime::IMAGE && check_size("image", 64 * 1000 * 1000) {
// Try built-in image thumbnailer
match image::io::Reader::open(&path).and_then(|img| img.with_guessed_format()) {
Ok(reader) => match reader.decode() {
@ -1094,6 +1127,15 @@ impl ItemThumbnail {
log::warn!("failed to read {:?}: {}", path, err);
}
}
} else if mime.type_() == mime::TEXT && check_size("text", 8 * 1000 * 1000) {
match fs::read_to_string(&path) {
Ok(data) => {
return ItemThumbnail::Text(widget::text_editor::Content::with_text(&data));
}
Err(err) => {
log::warn!("failed to read {:?}: {}", path, err);
}
}
}
// Try external thumbnailers
@ -1188,11 +1230,11 @@ impl Item {
self.location_opt.as_ref()?.path_opt()
}
pub fn is_image(&self) -> bool {
self.mime.type_() == mime::IMAGE
pub fn can_gallery(&self) -> bool {
self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT
}
fn preview(&self, sizes: IconSizes) -> Element<'static, app::Message> {
fn preview<'a>(&'a self, sizes: IconSizes) -> Element<'a, app::Message> {
// This loads the image only if thumbnailing worked
let icon = widget::icon::icon(self.icon_handle_grid.clone())
.content_fit(ContentFit::Contain)
@ -1206,17 +1248,25 @@ impl Item {
ItemThumbnail::NotImage => icon,
ItemThumbnail::Image(handle, _) => {
if let Some(path) = self.path_opt() {
if self.is_image() {
if self.mime.type_() == mime::IMAGE {
return widget::image(widget::image::Handle::from_path(path)).into();
}
}
widget::image(handle.clone()).into()
}
ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(),
ItemThumbnail::Text(content) => widget::container(widget::text_editor(content))
.width(Length::Fixed(THUMBNAIL_SIZE as f32))
.height(Length::Fixed(THUMBNAIL_SIZE as f32))
.into(),
}
}
pub fn preview_view(&self, sizes: IconSizes, nav_row: bool) -> Element<'static, app::Message> {
pub fn preview_view<'a>(
&'a self,
sizes: IconSizes,
nav_row: bool,
) -> Element<'a, app::Message> {
let cosmic_theme::Spacing {
space_xxxs,
space_xxs,
@ -1237,7 +1287,7 @@ impl Item {
.on_press(app::Message::TabMessage(None, Message::ItemRight)),
);
if self.is_image() {
if self.can_gallery() {
if let Some(_path) = self.path_opt() {
row = row.push(
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
@ -1349,11 +1399,11 @@ impl Item {
column.into()
}
pub fn replace_view(
&self,
pub fn replace_view<'a>(
&'a self,
heading: String,
sizes: IconSizes,
) -> Element<'static, app::Message> {
) -> Element<'a, app::Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut row = widget::row().spacing(space_xxxs);
@ -2188,7 +2238,7 @@ impl Tab {
continue;
}
if found {
if item.is_image() {
if item.can_gallery() {
pos_opt = item.pos_opt.get();
if pos_opt.is_some() {
break;
@ -2214,7 +2264,7 @@ impl Tab {
Message::GalleryToggle => {
if let Some(indices) = self.column_sort() {
for (_, item) in indices.iter() {
if item.selected && item.is_image() {
if item.selected && item.can_gallery() {
self.gallery = !self.gallery;
break;
}
@ -2572,6 +2622,8 @@ impl Tab {
symbolic: false,
data: widget::icon::Data::Svg(handle.clone()),
}),
//TODO: text thumbnails?
ItemThumbnail::Text(_text) => None,
};
if let Some(handle) = handle_opt {
item.icon_handle_grid = handle.clone();
@ -2886,7 +2938,7 @@ impl Tab {
//TODO: display error messages when image not found?
let mut name_opt = None;
let mut image_opt: Option<Element<Message>> = None;
let mut element_opt: Option<Element<Message>> = None;
if let Some(index) = self.select_focus {
if let Some(items) = &self.items_opt {
if let Some(item) = items.get(index) {
@ -2899,7 +2951,7 @@ impl Tab {
ItemThumbnail::NotImage => {}
ItemThumbnail::Image(handle, _) => {
if let Some(path) = item.path_opt() {
image_opt = Some(
element_opt = Some(
//TODO: use widget::image::viewer, when its zoom can be reset
widget::image(widget::image::Handle::from_path(path))
.width(Length::Fill)
@ -2907,7 +2959,7 @@ impl Tab {
.into(),
);
} else {
image_opt = Some(
element_opt = Some(
//TODO: use widget::image::viewer, when its zoom can be reset
widget::image(handle.clone())
.width(Length::Fill)
@ -2917,13 +2969,16 @@ impl Tab {
}
}
ItemThumbnail::Svg(handle) => {
image_opt = Some(
element_opt = Some(
widget::svg(handle.clone())
.width(Length::Fill)
.height(Length::Fill)
.into(),
);
}
ItemThumbnail::Text(text) => {
element_opt = Some(widget::text_editor(text).into())
}
}
}
}
@ -2960,8 +3015,8 @@ impl Tab {
.on_press(Message::GalleryPrevious),
);
row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into())));
if let Some(image) = image_opt {
row = row.push(image);
if let Some(element) = element_opt {
row = row.push(element);
} else {
//TODO: what to do when no image?
row = row.push(widget::Space::new(Length::Fill, Length::Fill));
@ -4167,6 +4222,9 @@ impl Tab {
let Some(path) = item.path_opt().map(|path| path.to_path_buf()) else {
continue;
};
let ItemMetadata::Path { metadata, .. } = item.metadata.clone() else {
continue;
};
let mime = item.mime.clone();
subscriptions.push(subscription::channel(
path.clone(),
@ -4176,9 +4234,8 @@ impl Tab {
let path = path.clone();
tokio::task::spawn_blocking(move || {
let start = Instant::now();
//TODO: configurable thumbnail size?
let thumbnail_size = (ICON_SIZE_GRID * ICON_SCALE_MAX) as u32;
let thumbnail = ItemThumbnail::new(&path, mime, thumbnail_size);
let thumbnail =
ItemThumbnail::new(&path, metadata, mime, THUMBNAIL_SIZE);
log::debug!("thumbnailed {:?} in {:?}", path, start.elapsed());
Message::Thumbnail(path.clone(), thumbnail)
})