Add text previews
This commit is contained in:
parent
3ea075b2c6
commit
081156670e
3 changed files with 87 additions and 30 deletions
|
|
@ -1109,12 +1109,12 @@ impl App {
|
||||||
widget::settings::view_column(children).into()
|
widget::settings::view_column(children).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(
|
fn preview<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
entity_opt: &Option<Entity>,
|
entity_opt: &Option<Entity>,
|
||||||
kind: &PreviewKind,
|
kind: &'a PreviewKind,
|
||||||
context_drawer: bool,
|
context_drawer: bool,
|
||||||
) -> Element<Message> {
|
) -> Element<'a, Message> {
|
||||||
let mut children = Vec::with_capacity(1);
|
let mut children = Vec::with_capacity(1);
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
match kind {
|
match kind {
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@ impl App {
|
||||||
col.into()
|
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);
|
let mut children = Vec::with_capacity(1);
|
||||||
match kind {
|
match kind {
|
||||||
PreviewKind::Custom(PreviewItem(item)) => {
|
PreviewKind::Custom(PreviewItem(item)) => {
|
||||||
|
|
|
||||||
107
src/tab.rs
107
src/tab.rs
|
|
@ -77,6 +77,8 @@ pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
||||||
//TODO: best limit for search items
|
//TODO: best limit for search items
|
||||||
const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20);
|
const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20);
|
||||||
const MAX_SEARCH_RESULTS: usize = 200;
|
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?
|
//TODO: adjust for locales?
|
||||||
const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p";
|
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 {
|
pub enum ItemThumbnail {
|
||||||
NotImage,
|
NotImage,
|
||||||
Image(widget::image::Handle, Option<(u32, u32)>),
|
Image(widget::image::Handle, Option<(u32, u32)>),
|
||||||
Svg(widget::svg::Handle),
|
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 {
|
impl ItemThumbnail {
|
||||||
pub fn new(path: &Path, mime: mime::Mime, thumbnail_size: u32) -> Self {
|
pub fn new(path: &Path, metadata: fs::Metadata, mime: mime::Mime, thumbnail_size: u32) -> Self {
|
||||||
if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
|
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
|
// Try built-in svg thumbnailer
|
||||||
//TODO: have a reasonable limit on SVG size?
|
|
||||||
match fs::read(&path) {
|
match fs::read(&path) {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
//TODO: validate SVG data
|
//TODO: validate SVG data
|
||||||
|
|
@ -1070,7 +1103,7 @@ impl ItemThumbnail {
|
||||||
log::warn!("failed to read {:?}: {}", path, err);
|
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
|
// Try built-in image thumbnailer
|
||||||
match image::io::Reader::open(&path).and_then(|img| img.with_guessed_format()) {
|
match image::io::Reader::open(&path).and_then(|img| img.with_guessed_format()) {
|
||||||
Ok(reader) => match reader.decode() {
|
Ok(reader) => match reader.decode() {
|
||||||
|
|
@ -1094,6 +1127,15 @@ impl ItemThumbnail {
|
||||||
log::warn!("failed to read {:?}: {}", path, err);
|
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
|
// Try external thumbnailers
|
||||||
|
|
@ -1188,11 +1230,11 @@ impl Item {
|
||||||
self.location_opt.as_ref()?.path_opt()
|
self.location_opt.as_ref()?.path_opt()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_image(&self) -> bool {
|
pub fn can_gallery(&self) -> bool {
|
||||||
self.mime.type_() == mime::IMAGE
|
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
|
// This loads the image only if thumbnailing worked
|
||||||
let icon = widget::icon::icon(self.icon_handle_grid.clone())
|
let icon = widget::icon::icon(self.icon_handle_grid.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
|
|
@ -1206,17 +1248,25 @@ impl Item {
|
||||||
ItemThumbnail::NotImage => icon,
|
ItemThumbnail::NotImage => icon,
|
||||||
ItemThumbnail::Image(handle, _) => {
|
ItemThumbnail::Image(handle, _) => {
|
||||||
if let Some(path) = self.path_opt() {
|
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();
|
return widget::image(widget::image::Handle::from_path(path)).into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
widget::image(handle.clone()).into()
|
widget::image(handle.clone()).into()
|
||||||
}
|
}
|
||||||
ItemThumbnail::Svg(handle) => widget::svg(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 {
|
let cosmic_theme::Spacing {
|
||||||
space_xxxs,
|
space_xxxs,
|
||||||
space_xxs,
|
space_xxs,
|
||||||
|
|
@ -1237,7 +1287,7 @@ impl Item {
|
||||||
.on_press(app::Message::TabMessage(None, Message::ItemRight)),
|
.on_press(app::Message::TabMessage(None, Message::ItemRight)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.is_image() {
|
if self.can_gallery() {
|
||||||
if let Some(_path) = self.path_opt() {
|
if let Some(_path) = self.path_opt() {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
|
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
|
||||||
|
|
@ -1349,11 +1399,11 @@ impl Item {
|
||||||
column.into()
|
column.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_view(
|
pub fn replace_view<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
heading: String,
|
heading: String,
|
||||||
sizes: IconSizes,
|
sizes: IconSizes,
|
||||||
) -> Element<'static, app::Message> {
|
) -> Element<'a, app::Message> {
|
||||||
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let mut row = widget::row().spacing(space_xxxs);
|
let mut row = widget::row().spacing(space_xxxs);
|
||||||
|
|
@ -2188,7 +2238,7 @@ impl Tab {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if found {
|
if found {
|
||||||
if item.is_image() {
|
if item.can_gallery() {
|
||||||
pos_opt = item.pos_opt.get();
|
pos_opt = item.pos_opt.get();
|
||||||
if pos_opt.is_some() {
|
if pos_opt.is_some() {
|
||||||
break;
|
break;
|
||||||
|
|
@ -2214,7 +2264,7 @@ impl Tab {
|
||||||
Message::GalleryToggle => {
|
Message::GalleryToggle => {
|
||||||
if let Some(indices) = self.column_sort() {
|
if let Some(indices) = self.column_sort() {
|
||||||
for (_, item) in indices.iter() {
|
for (_, item) in indices.iter() {
|
||||||
if item.selected && item.is_image() {
|
if item.selected && item.can_gallery() {
|
||||||
self.gallery = !self.gallery;
|
self.gallery = !self.gallery;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -2572,6 +2622,8 @@ impl Tab {
|
||||||
symbolic: false,
|
symbolic: false,
|
||||||
data: widget::icon::Data::Svg(handle.clone()),
|
data: widget::icon::Data::Svg(handle.clone()),
|
||||||
}),
|
}),
|
||||||
|
//TODO: text thumbnails?
|
||||||
|
ItemThumbnail::Text(_text) => None,
|
||||||
};
|
};
|
||||||
if let Some(handle) = handle_opt {
|
if let Some(handle) = handle_opt {
|
||||||
item.icon_handle_grid = handle.clone();
|
item.icon_handle_grid = handle.clone();
|
||||||
|
|
@ -2886,7 +2938,7 @@ impl Tab {
|
||||||
|
|
||||||
//TODO: display error messages when image not found?
|
//TODO: display error messages when image not found?
|
||||||
let mut name_opt = None;
|
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(index) = self.select_focus {
|
||||||
if let Some(items) = &self.items_opt {
|
if let Some(items) = &self.items_opt {
|
||||||
if let Some(item) = items.get(index) {
|
if let Some(item) = items.get(index) {
|
||||||
|
|
@ -2899,7 +2951,7 @@ impl Tab {
|
||||||
ItemThumbnail::NotImage => {}
|
ItemThumbnail::NotImage => {}
|
||||||
ItemThumbnail::Image(handle, _) => {
|
ItemThumbnail::Image(handle, _) => {
|
||||||
if let Some(path) = item.path_opt() {
|
if let Some(path) = item.path_opt() {
|
||||||
image_opt = Some(
|
element_opt = Some(
|
||||||
//TODO: use widget::image::viewer, when its zoom can be reset
|
//TODO: use widget::image::viewer, when its zoom can be reset
|
||||||
widget::image(widget::image::Handle::from_path(path))
|
widget::image(widget::image::Handle::from_path(path))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|
@ -2907,7 +2959,7 @@ impl Tab {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
image_opt = Some(
|
element_opt = Some(
|
||||||
//TODO: use widget::image::viewer, when its zoom can be reset
|
//TODO: use widget::image::viewer, when its zoom can be reset
|
||||||
widget::image(handle.clone())
|
widget::image(handle.clone())
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|
@ -2917,13 +2969,16 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemThumbnail::Svg(handle) => {
|
ItemThumbnail::Svg(handle) => {
|
||||||
image_opt = Some(
|
element_opt = Some(
|
||||||
widget::svg(handle.clone())
|
widget::svg(handle.clone())
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ItemThumbnail::Text(text) => {
|
||||||
|
element_opt = Some(widget::text_editor(text).into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2960,8 +3015,8 @@ impl Tab {
|
||||||
.on_press(Message::GalleryPrevious),
|
.on_press(Message::GalleryPrevious),
|
||||||
);
|
);
|
||||||
row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into())));
|
row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into())));
|
||||||
if let Some(image) = image_opt {
|
if let Some(element) = element_opt {
|
||||||
row = row.push(image);
|
row = row.push(element);
|
||||||
} else {
|
} else {
|
||||||
//TODO: what to do when no image?
|
//TODO: what to do when no image?
|
||||||
row = row.push(widget::Space::new(Length::Fill, Length::Fill));
|
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 {
|
let Some(path) = item.path_opt().map(|path| path.to_path_buf()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let ItemMetadata::Path { metadata, .. } = item.metadata.clone() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let mime = item.mime.clone();
|
let mime = item.mime.clone();
|
||||||
subscriptions.push(subscription::channel(
|
subscriptions.push(subscription::channel(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
|
|
@ -4176,9 +4234,8 @@ impl Tab {
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
//TODO: configurable thumbnail size?
|
let thumbnail =
|
||||||
let thumbnail_size = (ICON_SIZE_GRID * ICON_SCALE_MAX) as u32;
|
ItemThumbnail::new(&path, metadata, mime, THUMBNAIL_SIZE);
|
||||||
let thumbnail = ItemThumbnail::new(&path, mime, thumbnail_size);
|
|
||||||
log::debug!("thumbnailed {:?} in {:?}", path, start.elapsed());
|
log::debug!("thumbnailed {:?} in {:?}", path, start.elapsed());
|
||||||
Message::Thumbnail(path.clone(), thumbnail)
|
Message::Thumbnail(path.clone(), thumbnail)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue