Implement Properties

Implement a properties sidebar. In order to prevent
the UI from freezing, we use futures to load and cancel
loading file information. We also make use of
`gdk_pixbuf` and `svg_metadata` in order to get the file's
metadata before the file fully loads, meaning there's
little to no delay between changing files and seeing
the metadata for large images.

The properties widget uses expressions and
properties to avoid glue code for setting each label,
allowing us to have a clean separation between UI
and data.
This commit is contained in:
Christopher Davis 2022-07-26 11:31:11 -04:00
parent 1e95dd4cbb
commit ba4661031f
12 changed files with 664 additions and 45 deletions

72
Cargo.lock generated
View file

@ -232,6 +232,12 @@ dependencies = [
"syn",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "easy-parallel"
version = "3.1.0"
@ -299,9 +305,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
@ -314,9 +320,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
"futures-core",
"futures-sink",
@ -324,15 +330,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
@ -341,9 +347,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]]
name = "futures-lite"
@ -362,9 +368,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
@ -373,21 +379,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
[[package]]
name = "futures-task"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
[[package]]
name = "futures-util"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-channel",
"futures-core",
@ -863,14 +869,17 @@ version = "0.1.0"
dependencies = [
"anyhow",
"ashpd",
"futures",
"futures-channel",
"gettext-rs",
"gio",
"gtk-macros",
"gtk4",
"libadwaita",
"log",
"once_cell",
"pretty_env_logger",
"svg_metadata",
]
[[package]]
@ -1157,6 +1166,15 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "roxmltree"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671"
dependencies = [
"xmlparser",
]
[[package]]
name = "rustc_version"
version = "0.3.3"
@ -1249,6 +1267,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "svg_metadata"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39f36262efe61096a17aa42140b0e44b9189806c3fa71ab3cff123429938eb0"
dependencies = [
"doc-comment",
"lazy_static",
"regex",
"roxmltree",
]
[[package]]
name = "syn"
version = "1.0.83"
@ -1399,6 +1429,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xmlparser"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
[[package]]
name = "zbus"
version = "2.1.1"

View file

@ -10,6 +10,8 @@ log = "0.4.11"
once_cell = "1.9.0"
pretty_env_logger = "0.4.0"
futures-channel = "0.3.19"
futures = "0.3.21"
svg_metadata = "0.4.2"
[dependencies.ashpd]
version = "0.2.2"
@ -20,6 +22,11 @@ package = "gtk4"
version = "0.4.4"
features = ["v4_6"]
[dependencies.gio]
package = "gio"
version = "0.15.2"
features = ["v2_70"]
[dependencies.adw]
package = "libadwaita"
version = "0.1.0"

110
data/gtk/properties_view.ui Normal file
View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="LpPropertiesView" parent="AdwBin">
<property name="width-request">255</property>
<property name="child">
<object class="AdwPreferencesPage">
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Folder</property>
<property name="activatable-widget">folder_button</property>
<binding name="subtitle">
<closure type="gchararray" function="folder_name">
<lookup name="file">LpPropertiesView</lookup>
</closure>
</binding>
<child type="suffix">
<object class="GtkButton" id="folder_button">
<property name="valign">center</property>
<property name="action-name">properties.open-folder</property>
<property name="icon-name">folder-open-symbolic</property>
<property name="tooltip-text" translatable="yes">Open Containing Folder</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
<style>
<class name="property"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Image Size</property>
<binding name="subtitle">
<lookup name="dimensions">LpPropertiesView</lookup>
</binding>
<style>
<class name="property"/>
</style>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">File Type</property>
<binding name="subtitle">
<closure type="gchararray" function="file_type">
<lookup name="file-info">LpPropertiesView</lookup>
</closure>
</binding>
<style>
<class name="property"/>
</style>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">File Size</property>
<binding name="subtitle">
<closure type="gchararray" function="file_size">
<lookup name="file-info">LpPropertiesView</lookup>
</closure>
</binding>
<style>
<class name="property"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Created</property>
<binding name="subtitle">
<closure type="gchararray" function="created_date">
<lookup name="file-info">LpPropertiesView</lookup>
</closure>
</binding>
<style>
<class name="property"/>
</style>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Modified</property>
<binding name="subtitle">
<closure type="gchararray" function="modified_date">
<lookup name="file-info">LpPropertiesView</lookup>
</closure>
</binding>
<style>
<class name="property"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View file

@ -18,6 +18,12 @@
<property name="swipe-to-close">False</property>
<property name="locked">true</property>
<property name="modal">false</property>
<binding name="fold-policy">
<closure type="AdwFlapFoldPolicy" function="fold_policy">
<lookup name="fullscreened">LpWindow</lookup>
<lookup type="AdwFlap" name="reveal-flap">properties_flap</lookup>
</closure>
</binding>
<property name="flap">
<object class="GtkHeaderBar" id="headerbar">
<property name="show_title_buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean"/>
@ -35,11 +41,22 @@
<property name="tooltip-text" translatable="yes">Primary Menu</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="properties_button">
<property name="icon-name">help-about-symbolic</property>
</object>
</child>
</object>
</property>
<property name="content">
<object class="AdwToastOverlay" id="toast_overlay">
<property name="child">
<object class="AdwFlap" id="properties_flap">
<property name="flap-position">end</property>
<property name="fold-policy">never</property>
<property name="locked">True</property>
<property name="reveal-flap" bind-source="properties_button" bind-property="active" bind-flags="bidirectional|sync-create"/>
<property name="content">
<object class="GtkStack" id="stack">
<property name="visible-child">status_page</property>
<property name="vexpand">True</property>
@ -70,6 +87,16 @@
</child>
</object>
</property>
<property name="separator">
<object class="GtkSeparator"/>
</property>
<property name="flap">
<object class="LpPropertiesView" id="properties_view">
<property name="file" bind-source="image_view" bind-property="active-file" bind-flags="sync-create"/>
</object>
</property>
</object>
</property>
</object>
</property>
<style>

View file

@ -5,6 +5,8 @@
<file compressed="true" preprocess="xml-stripblanks">gtk/window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/image_page.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/image_view.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/properties_view.ui</file>
<file>style.css</file>
<file>style-hc.css</file>
</gresource>
</gresources>

3
data/style-hc.css Normal file
View file

@ -0,0 +1,3 @@
row.property label.title {
opacity: 0.9;
}

View file

@ -37,3 +37,13 @@ flap.fullscreen-widget dimming, shadow, border, outline {
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
row.property label.title {
opacity: 0.55;
font-size: smaller;
}
row.property label.subtitle {
opacity: 1.0;
font-size: 11pt;
}

View file

@ -17,6 +17,7 @@ rust_sources = files(
'widgets/image_page.rs',
'widgets/image_view.rs',
'widgets/mod.rs',
'widgets/properties_view.rs',
'application.rs',
'config.rs',
'file_model.rs',

View file

@ -37,6 +37,25 @@ pub fn query_attributes(file: &gio::File, attributes: Vec<&str>) -> anyhow::Resu
.context("Failed to query string")
}
pub async fn query_attributes_future(
file: &gio::File,
attributes: Vec<&str>,
) -> anyhow::Result<gio::FileInfo> {
let mut attr_str = String::from(*attributes.get(0).context("No attributes")?);
for attr in &attributes[1..] {
write!(attr_str, ",{}", attr)?;
}
file.query_info_future(
&attr_str,
gio::FileQueryInfoFlags::empty(),
glib::Priority::default(),
)
.await
.context("Failed to query attributes")
}
pub fn compare_by_name(file_a: &gio::File, file_b: &gio::File) -> std::cmp::Ordering {
let name_a = get_file_display_name(file_a).unwrap_or_default();
let name_b = get_file_display_name(file_b).unwrap_or_default();

View file

@ -20,7 +20,9 @@
mod image;
mod image_page;
mod image_view;
mod properties_view;
pub use image::LpImage;
pub use image_page::LpImagePage;
pub use image_view::LpImageView;
pub use properties_view::LpPropertiesView;

View file

@ -0,0 +1,388 @@
// properties_view.rs
//
// Copyright $year Christopher Davis <christopherdavis@gnome.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::deps::*;
use crate::util;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::clone;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use anyhow::Context;
use ashpd::desktop::open_uri;
use ashpd::WindowIdentifier;
use futures::future::{AbortHandle, Abortable};
use once_cell::sync::Lazy;
use std::cell::RefCell;
const MB: f64 = 1_000_000.0;
const KB: f64 = 1_000.0;
const FALLBACK: &str = "-";
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Loupe/gtk/properties_view.ui")]
pub struct LpPropertiesView {
pub file: RefCell<Option<glib::WeakRef<gio::File>>>,
pub file_info: RefCell<Option<gio::FileInfo>>,
pub dimensions: RefCell<Option<String>>,
pub info_handle: RefCell<Option<AbortHandle>>,
pub dimensions_handle: RefCell<Option<AbortHandle>>,
}
#[glib::object_subclass]
impl ObjectSubclass for LpPropertiesView {
const NAME: &'static str = "LpPropertiesView";
type Type = super::LpPropertiesView;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.install_action("properties.open-folder", None, move |properties, _, _| {
properties.open_directory();
});
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for LpPropertiesView {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::new(
"file",
"",
"",
gio::File::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecObject::new(
"file-info",
"",
"",
gio::FileInfo::static_type(),
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecString::new(
"dimensions",
"",
"",
Some(FALLBACK),
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
PROPERTIES.as_ref()
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let prop_name = pspec.name();
match prop_name {
"file" => obj.file().to_value(),
"file-info" => obj.file_info().to_value(),
"dimensions" => self.dimensions.borrow().to_value(),
_ => unimplemented!("Failed to get property \"{prop_name}\""),
}
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
let prop_name = pspec.name();
match prop_name {
"file" => obj.set_file(&value.get().ok()),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
obj.action_set_enabled("properties.open-folder", false);
}
}
impl WidgetImpl for LpPropertiesView {}
impl BinImpl for LpPropertiesView {}
}
glib::wrapper! {
pub struct LpPropertiesView(ObjectSubclass<imp::LpPropertiesView>)
@extends gtk::Widget, adw::Bin,
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
#[gtk::template_callbacks]
impl LpPropertiesView {
fn set_file(&self, file: &Option<gio::File>) {
let imp = self.imp();
self.action_set_enabled("properties.open-folder", file.is_some());
if let Some(file) = file {
if let Some(current_file) = self.file() {
if current_file.equal(file) {
return;
}
}
// Cancel the file info future. See `build_file_info()` for details.
if let Some(handle) = imp.info_handle.take() {
handle.abort();
}
if let Some(handle) = imp.dimensions_handle.take() {
handle.abort();
}
self.build_file_info(file);
let weak = file.downgrade();
imp.file.replace(Some(weak));
self.notify("file");
}
}
fn file(&self) -> Option<gio::File> {
let imp = self.imp();
imp.file.borrow().as_ref().and_then(|w| w.upgrade())
}
fn file_info(&self) -> Option<gio::FileInfo> {
let imp = self.imp();
imp.file_info.borrow().clone()
}
fn build_file_info(&self, file: &gio::File) {
let imp = self.imp();
let ctx = glib::MainContext::default();
// We need to be able to cancel this future so that
// changing files before the metadata loads does not cause
// old metadata to be loaded. In order to make a cancellable
// future, we use `AbortHandle` and `Abortable` from the `futures`
// crate.
//
// `handle` is the `AbortHandle`, which we need to store in order
// to abort the future. `reg` is the `AbortRegistration` that we
// give to `Abortable::new()` to tie our future to the `AbortHandle`.
//
// Here we build the future:
let (handle, reg) = AbortHandle::new_pair();
let fut = Abortable::new(
clone!(@weak self as view, @weak file => async move {
if let Ok(info) = util::query_attributes_future(
&file,
vec![
&gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
&gio::FILE_ATTRIBUTE_STANDARD_SIZE,
&gio::FILE_ATTRIBUTE_TIME_CREATED,
&gio::FILE_ATTRIBUTE_TIME_MODIFIED,
],
)
.await {
view.on_info_loaded(info, &file);
}
}),
reg,
);
// ...store the handle:
imp.info_handle.replace(Some(handle));
// ...then spawn the future.
ctx.spawn_local(async {
let _ = fut.await;
});
}
// This is where we handle the results of the future.
// This could technically be part of the future, but we keep
// them separate for a bit of cleanliness.
fn on_info_loaded(&self, info: gio::FileInfo, file: &gio::File) {
let imp = self.imp();
self.build_dimensions(&info, file);
imp.file_info.replace(Some(info));
self.notify("file-info");
}
fn build_dimensions(&self, info: &gio::FileInfo, file: &gio::File) {
let imp = self.imp();
let ctx = glib::MainContext::default();
// Here we do the same thing we do for the rest of the file info.
let (handle, reg) = AbortHandle::new_pair();
let fut = Abortable::new(
clone!(@weak self as view, @weak file, @weak info => async move {
let is_svg = info.content_type()
.map(|t| t.to_string())
.map(|t| t == "image/svg+xml")
.unwrap_or_default();
let path = file.peek_path();
if path.is_none() {
return;
}
let dimensions = if is_svg {
parse_svg_file_future(path.unwrap())
.await
.ok()
.and_then(|m| {
match (m.width(), m.height()) {
(Some(w), Some(h)) => Some((w, h)),
_ => None::<(f64, f64)>,
}
})
.map(|(w, h)| format!("{w:.1} \u{D7} {h:.1}"))
} else {
gdk_pixbuf::Pixbuf::file_info_future(path.unwrap())
.await
.ok()
.flatten()
.map(|(_, w, h)| format!("{w} \u{D7} {h}"))
};
view.on_dimensions_loaded(dimensions.unwrap_or_else(|| FALLBACK.to_string()));
}),
reg,
);
imp.dimensions_handle.replace(Some(handle));
ctx.spawn_local(async {
let _ = fut.await;
});
}
fn on_dimensions_loaded(&self, dimensions: String) {
let imp = self.imp();
imp.dimensions.replace(Some(dimensions));
self.notify("dimensions");
}
fn open_directory(&self) {
if let Some(directory) = self
.file()
.and_then(|f| f.peek_path())
.and_then(|p| std::fs::File::open(p).ok())
{
let ctx = glib::MainContext::default();
ctx.spawn_local(clone!(@weak self as view => async move {
let id = WindowIdentifier::from_native(&view.native().expect("No GtkNative for view")).await;
if let Err(e) = open_uri::open_directory(&id, &directory).await {
log::error!("Could not open parent directory: {e}");
};
}));
}
}
// In the LpPropertiesView UI file we define a few `gtk::Expression`s
// that are closures. These closures take either the current `gio::File`
// or the current file's associated `gio::FileInfo` and process them
// accordingly.
//
// In this function we chain `Option`s with `and_then()` in order
// to handle optional results with a fallback, without needing to
// have multiple `match` or `if let` branches, and without needing
// to unwrap.
#[template_callback]
fn folder_name(&self, file: Option<gio::File>) -> String {
file.and_then(|f| f.parent()) // If the file exists, get the parent
.and_then(|p| util::get_file_display_name(&p)) // if that exists, the display name
.unwrap_or_else(|| FALLBACK.to_owned()) // and if we get nothing, use `FALLBACK`
}
#[template_callback]
fn file_type(&self, info: Option<gio::FileInfo>) -> String {
info.and_then(|i| i.content_type())
.map(|t| t.to_string())
.map(|t| {
t.rsplit('/').collect::<Vec<&str>>()[0]
.to_owned()
.to_uppercase()
})
.unwrap_or_else(|| FALLBACK.to_owned())
}
#[template_callback]
fn file_size(&self, info: Option<gio::FileInfo>) -> String {
info.map(|info| {
let bytes = info.size() as f64;
let (mut size, mut unit) = (bytes, "B");
let sizes = [(MB, "MB"), (KB, "kB")];
for (b, u) in sizes {
if bytes > b {
let res = bytes / b;
size = res;
unit = u;
break;
}
}
String::from(&format!("{size:.1} {unit}"))
})
.unwrap_or_else(|| FALLBACK.to_owned())
}
#[template_callback]
fn created_date(&self, info: Option<gio::FileInfo>) -> String {
info.and_then(|i| i.creation_date_time())
.and_then(|t| t.format("%x, %X").ok())
.map(|t| t.to_string())
.unwrap_or_else(|| FALLBACK.to_owned())
}
#[template_callback]
fn modified_date(&self, info: Option<gio::FileInfo>) -> String {
info.and_then(|i| i.modification_date_time())
.and_then(|t| t.format("%x, %X").ok())
.map(|t| t.to_string())
.unwrap_or_else(|| FALLBACK.to_owned())
}
}
async fn parse_svg_file_future(path: std::path::PathBuf) -> anyhow::Result<svg_metadata::Metadata> {
let (sender, receiver) = futures_channel::oneshot::channel();
std::thread::spawn(move || {
let result = svg_metadata::Metadata::parse_file(path);
sender.send(result).unwrap()
});
receiver.await.unwrap().context("Could not load metadata")
}

View file

@ -30,7 +30,7 @@ use ashpd::WindowIdentifier;
use crate::config;
use crate::util;
use crate::widgets::LpImageView;
use crate::widgets::{LpImageView, LpPropertiesView};
mod imp {
use super::*;
@ -70,6 +70,8 @@ mod imp {
#[template_child]
pub image_view: TemplateChild<LpImageView>,
#[template_child]
pub properties_view: TemplateChild<LpPropertiesView>,
#[template_child]
pub drop_target: TemplateChild<gtk::DropTarget>,
}
@ -209,11 +211,11 @@ impl LpWindow {
let imp = self.imp();
if fullscreen {
imp.flap.set_fold_policy(adw::FlapFoldPolicy::Always);
imp.headerbar.add_css_class("osd");
imp.properties_view.add_css_class("osd")
} else {
imp.flap.set_fold_policy(adw::FlapFoldPolicy::Never);
imp.headerbar.remove_css_class("osd");
imp.properties_view.remove_css_class("osd")
}
self.set_fullscreened(fullscreen);
@ -382,4 +384,16 @@ impl LpWindow {
file.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name
.unwrap_or_else(|| String::from("Image Viewer")) // Return that or the default if there's nothing
}
// We also have a closure that returns `adw::FlapFoldPolicy`.
// if we aren't fullscreened, or if the properties are revealed,
// we unfold the main flap. Otherwise we're always folded.
#[template_callback]
fn fold_policy(&self, fullscreened: bool, properties_revealed: bool) -> adw::FlapFoldPolicy {
if !fullscreened || properties_revealed {
adw::FlapFoldPolicy::Never
} else {
adw::FlapFoldPolicy::Always
}
}
}