Skip to content

Commit

Permalink
feat(Files): Preview videos files in Storage and Chats (#1699)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgmarchi authored Jan 23, 2024
1 parent 4d9702b commit 03cd078
Show file tree
Hide file tree
Showing 28 changed files with 699 additions and 445 deletions.
312 changes: 152 additions & 160 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ pub struct StaticArgs {
/// ~/.uplink/.user
/// contains the following: warp (folder), state.json, debug.log
pub uplink_path: PathBuf,
/// Directory for temporary files and deleted everytime app is closed or opened
pub temp_files: PathBuf,
/// custom themes for the user
pub themes_path: PathBuf,
/// custom fonts for the user
Expand Down Expand Up @@ -148,6 +150,7 @@ pub static STATIC_ARGS: Lazy<StaticArgs> = Lazy::new(|| {
StaticArgs {
dot_uplink: uplink_container.clone(),
uplink_path: uplink_path.clone(), // TODO: Should this be "User path" instead?
temp_files: uplink_container.join("temp_files"),
themes_path: uplink_container.join("themes"),
fonts_path: uplink_container.join("fonts"),
cache_path: uplink_path.join("state.json"),
Expand Down Expand Up @@ -194,6 +197,12 @@ pub const VIDEO_FILE_EXTENSIONS: &[&str] = &[

pub const DOC_EXTENSIONS: &[&str] = &[".doc", ".docx", ".pdf", ".txt"];

pub fn is_video(file_name: &str) -> bool {
VIDEO_FILE_EXTENSIONS
.iter()
.any(|x| file_name.to_lowercase().ends_with(x))
}

pub fn get_images_dir() -> anyhow::Result<PathBuf> {
if !cfg!(feature = "production_mode") {
return Ok(Path::new("ui").join("extra").join("images"));
Expand Down
19 changes: 19 additions & 0 deletions common/src/utils/clear_temp_files_dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::{fs, io};
use warp::logging::tracing::log;

use crate::STATIC_ARGS;

pub fn clear_temp_files_directory() -> io::Result<()> {
let temp_files_dir = fs::read_dir(STATIC_ARGS.temp_files.clone())?;
for entry in temp_files_dir {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(&path)?;
} else {
fs::remove_file(&path)?;
}
}
log::debug!("Temporary files directory cleared");
Ok(())
}
3 changes: 3 additions & 0 deletions common/src/utils/img_dimensions_preview.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub const IMAGE_MAX_WIDTH: &str = "80vw";

pub const IMAGE_MAX_HEIGHT: &str = "80vh";
3 changes: 3 additions & 0 deletions common/src/utils/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ pub struct LifeCycle<D: FnOnce()> {
ondestroy: Option<D>,
}

/// It works like a useEffect hook, but it will be called only once
/// when the component is mounted
/// and when the component is unmounted
pub fn use_component_lifecycle<C: FnOnce() + 'static, D: FnOnce() + 'static>(
cx: &ScopeState,
create: C,
Expand Down
18 changes: 18 additions & 0 deletions common/src/utils/local_file_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::path::PathBuf;

/// It will work to load local files in img or video tags, but will ignore drive
const PREFIX_TO_WORK_ON_WINDOWS_OS: &str = "http://dioxus.";

/// This function is used to treat local file path if it needs
/// to be loaded in img or video tags for example
pub fn get_fixed_path_to_load_local_file(path: PathBuf) -> String {
if !cfg!(target_os = "windows") {
path.to_string_lossy().to_string()
} else {
format!(
"{}{}",
PREFIX_TO_WORK_ON_WINDOWS_OS,
path.to_string_lossy().to_string().replace('\\', "/")
)
}
}
3 changes: 3 additions & 0 deletions common/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pub mod clear_temp_files_dir;
pub mod img_dimensions_preview;
pub mod lifecycle;
pub mod local_file_path;
109 changes: 48 additions & 61 deletions kit/src/components/embeds/file_embed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use std::path::PathBuf;

use crate::elements::button::Button;
use crate::elements::Appearance;
use crate::layout::modal::Modal;
use common::icons::outline::Shape as Icon;
use common::icons::Icon as IconElement;
use common::is_video;
use common::utils::local_file_path::get_fixed_path_to_load_local_file;
use common::STATIC_ARGS;
use dioxus_html::input_data::keyboard_types::Modifiers;

use dioxus::prelude::*;
Expand Down Expand Up @@ -54,15 +56,14 @@ pub struct Props<'a> {
download_pending: Option<bool>,

// called shen the icon is clicked
on_press: EventHandler<'a, ()>,
on_press: EventHandler<'a, Option<PathBuf>>,

progress: Option<&'a Progression>,
}

#[allow(non_snake_case)]
pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
//log::trace!("rendering file embed: {}", cx.props.filename);
let fullscreen_preview = use_state(cx, || false);
let file_extension = std::path::Path::new(&cx.props.filename)
.extension()
.and_then(OsStr::to_str)
Expand All @@ -86,8 +87,10 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {

let with_download_button = if let Some(with_download_button) = cx.props.with_download_button {
with_download_button
} else if let Some(is_from_attachments) = cx.props.is_from_attachments {
is_from_attachments
} else {
true
false
};

let is_pending = cx.props.progress.is_some();
Expand Down Expand Up @@ -154,8 +157,12 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
};
let remote = cx.props.remote.unwrap_or_default();
let thumbnail = cx.props.thumbnail.clone().unwrap_or_default();
let large_thumbnail = thumbnail.clone(); // TODO: This should be the source of the image
let has_thumbnail = !thumbnail.is_empty();
let file_name_with_extension = cx.props.filename.to_string();
let temp_dir = STATIC_ARGS
.temp_files
.join(file_name_with_extension.clone());
let is_video = is_video(&file_name_with_extension);

cx.render(rsx! (
div {
Expand Down Expand Up @@ -184,31 +191,14 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
aria_label: "file-icon",
if has_thumbnail {
rsx!(
fullscreen_preview.then(|| rsx!(
Modal {
open: *fullscreen_preview.clone(),
onclose: move |_| fullscreen_preview.set(false),
transparent: false,
close_on_click_inside_modal: true,
dont_pad: true,
img {
id: "image-preview-modal-file-embed",
aria_label: "image-preview-modal-file-embed",
src: "{large_thumbnail}",
max_height: "80vh",
max_width: "80vw",
onclick: move |e| e.stop_propagation(),
},
}
)),
div {
class: "image-container",
aria_label: "message-image-container",
img {
aria_label: "message-image",
onclick: move |mouse_event_data: Event<MouseData>|
if mouse_event_data.modifiers() != Modifiers::CONTROL {
fullscreen_preview.set(true)
if mouse_event_data.modifiers() != Modifiers::CONTROL && !is_from_attachments {
cx.props.on_press.call(Some(temp_dir.clone()));
},
class: format_args!(
"image {} expandable-image",
Expand All @@ -218,19 +208,27 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
),
src: "{thumbnail}",
},
show_download_button_if_enabled(cx, with_download_button, btn_icon),
show_download_or_minus_button_if_enabled(cx, with_download_button, btn_icon),
}
)
} else if let Some(filepath) = cx.props.filepath.clone() {
let thubmnail = get_file_thumbnail_if_is_image(filepath, filename.clone());
if thubmnail.is_empty() {
let is_image_or_video = is_image(filename.clone()) || is_video;
if is_image_or_video && filepath.exists() {
let fixed_path = get_fixed_path_to_load_local_file(filepath.clone());
rsx!(img {
class: "image-preview-modal",
aria_label: "image-preview-modal",
src: "{fixed_path}",
onclick: move |e| e.stop_propagation(),
})
} else {
rsx!(
div {
height: "60px",
width: "60px",
margin: "30px 0",
IconElement {
icon: cx.props.attachment_icon.unwrap_or(Icon::Document)
icon: cx.props.attachment_icon.unwrap_or(if is_video {Icon::DocumentMedia} else {Icon::Document})
}
if !file_extension_is_empty {
rsx!( label {
Expand All @@ -240,32 +238,37 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
}
}
)
} else {
rsx!(img {
aria_label: "image-preview-modal",
src: "{thubmnail}",
onclick: move |e| e.stop_propagation(),
})
}
} else {
rsx!(
div {
class: "document-container",
height: "60px",
onclick: move |mouse_event_data: Event<MouseData>| {
if mouse_event_data.modifiers() != Modifiers::CONTROL && is_video && !is_from_attachments {
cx.props.on_press.call(Some(temp_dir.clone()));
}
},
IconElement {
icon: cx.props.attachment_icon.unwrap_or(Icon::Document)
icon: cx.props.attachment_icon.unwrap_or(if is_video {Icon::DocumentMedia} else {Icon::Document})
}
if !file_extension_is_empty {
rsx!( label {
class: "file-embed-type",
"{file_extension}"
})
}
if !is_from_attachments {
rsx!( div {
class: "button-position",
show_download_or_minus_button_if_enabled(cx, with_download_button, btn_icon),
})
}
}
)
}
}
if !has_thumbnail || is_from_attachments {
rsx!( div {
div {
class: "file-info",
width: "100%",
aria_label: "file-info",
Expand All @@ -281,9 +284,9 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
"{file_description}"
}
},
show_download_button_if_enabled(cx, with_download_button, btn_icon),
)
}
if !has_thumbnail && is_from_attachments {
rsx!(show_download_or_minus_button_if_enabled(cx, with_download_button, btn_icon))
}
if is_pending {
rsx!(div {
class: "upload-bar",
Expand All @@ -298,14 +301,7 @@ pub fn FileEmbed<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
))
}

fn get_file_thumbnail_if_is_image(filepath: PathBuf, filename: String) -> String {
let file = match std::fs::read(filepath) {
Ok(file) => file,
Err(_) => {
return String::new();
}
};

fn is_image(filename: String) -> bool {
let parts_of_filename: Vec<&str> = filename.split('.').collect();
let mime = match parts_of_filename.last() {
Some(m) => match *m {
Expand All @@ -319,22 +315,13 @@ fn get_file_thumbnail_if_is_image(filepath: PathBuf, filename: String) -> String
};

if mime.is_empty() {
return String::new();
return false;
}

let image = match &file.len() {
0 => "".to_string(),
_ => {
let prefix = format!("data:{mime};base64,");
let base64_image = base64::encode(&file);
let img = prefix + base64_image.as_str();
img
}
};
image
true
}

fn show_download_button_if_enabled<'a>(
fn show_download_or_minus_button_if_enabled<'a>(
cx: Scope<'a, Props<'a>>,
with_download_button: bool,
btn_icon: common::icons::outline::Shape,
Expand All @@ -347,7 +334,7 @@ fn show_download_button_if_enabled<'a>(
icon: btn_icon,
appearance: Appearance::Primary,
aria_label: "attachment-button".into(),
onpress: move |_| cx.props.on_press.call(()),
onpress: move |_| cx.props.on_press.call(None),
}
}
))
Expand Down
18 changes: 18 additions & 0 deletions kit/src/components/embeds/file_embed/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@
height: fit-content;
}

.document-container {
position: relative;

.button-position {
position: absolute;
top: -15%;
left: -23%;

svg {
margin-top: 0;
stroke: var(--text-color-dark);
fill: transparent;
height: 1.1rem;
width: 1.1rem;
}
}
}

.image {
width: 100%;
max-width: 350px;
Expand Down
9 changes: 7 additions & 2 deletions kit/src/components/message/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::{collections::HashSet, str::FromStr};

use common::language::{get_local_text, get_local_text_with_args};
Expand Down Expand Up @@ -91,7 +92,7 @@ pub struct Props<'a> {
attachments_pending_download: Option<HashSet<File>>,

/// called when an attachment is downloaded
on_download: EventHandler<'a, File>,
on_download: EventHandler<'a, (File, Option<PathBuf>)>,

/// called when editing is completed
on_edit: EventHandler<'a, String>,
Expand Down Expand Up @@ -163,13 +164,17 @@ pub fn Message<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
thumbnail: thumbnail_to_base64(file),
big: true,
remote: is_remote,
with_download_button: true,
download_pending: cx
.props
.attachments_pending_download
.as_ref()
.map(|x| x.contains(file))
.unwrap_or(false),
on_press: move |_| cx.props.on_download.call(file.clone()),
on_press: move |temp_dir_option| cx
.props
.on_download
.call((file.clone(), temp_dir_option)),
})
})
});
Expand Down
Loading

0 comments on commit 03cd078

Please sign in to comment.