Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(CropImageTool): Crop profile picture tool #1253

Merged
merged 36 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2fe00e0
feat(CropImage): Start create UI to crop image (WIP)
lgmarchi Sep 22, 2023
e6ec236
Merge remote-tracking branch 'origin/dev' into #992-crop-image
lgmarchi Sep 22, 2023
af4f7b4
feat(CropImage): (WIP)
lgmarchi Sep 25, 2023
88645f6
feat(CropImage): Crop circle size working good now
lgmarchi Sep 25, 2023
35ba7b3
feat(CropImage): Working zoom in and zoom out on image
lgmarchi Sep 26, 2023
66af07a
feat(CropImage): Remove unnecessary file
lgmarchi Sep 26, 2023
8bacb81
feat(CropImage): Adjust circle crop box if resize screen
lgmarchi Sep 26, 2023
305ca43
Merge remote-tracking branch 'origin/dev' into crop-profile-picture-tool
lgmarchi Sep 27, 2023
3efd18c
feat(CropImage): Crop image tool working not perfect (WIP)
lgmarchi Sep 27, 2023
dc4860f
feat(CropImage): Almost working nice (WIP)
lgmarchi Sep 28, 2023
79d04da
feat(CropImage): Crop image working now
lgmarchi Sep 28, 2023
646efff
feat(CropImage): Fix some small issues
lgmarchi Sep 28, 2023
5bfc115
feat(CropImage): Clean some code
lgmarchi Sep 28, 2023
4c047b1
feat(CropImage): fix clippy
lgmarchi Sep 28, 2023
65d6c45
feat(CropImage): fix fmt
lgmarchi Sep 28, 2023
3de3f76
feat(CropImage): Remove unnecessary clone
lgmarchi Sep 28, 2023
3009e14
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Sep 28, 2023
e9ab879
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Sep 28, 2023
15dd77d
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Oct 2, 2023
6e6d4ae
Merge branch 'dev' into crop-profile-picture-tool
dariusc93 Oct 2, 2023
315ebe3
feat(CropImage): Improve code
lgmarchi Oct 2, 2023
85c26da
Merge remote-tracking branch 'origin/crop-profile-picture-tool' into …
lgmarchi Oct 2, 2023
cfe6ac1
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Oct 3, 2023
1a5d7e1
feat(CropImage): Do some adjusts in the code
lgmarchi Oct 3, 2023
4069c75
Merge branch 'dev' into crop-profile-picture-tool
stavares843 Oct 3, 2023
df6bf38
Merge commit '387928146c82f9d70fc8a1660b6bc87e49f8465f' into crop-pro…
lgmarchi Oct 4, 2023
de75993
feat(CropImage): Improve code to start circle with correct size
lgmarchi Oct 4, 2023
b7ed50a
feat(CropImage): Adjust UI and avoid add event listener every time wh…
lgmarchi Oct 4, 2023
c279ccf
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Oct 4, 2023
79fbc52
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Oct 6, 2023
bf75433
Merge remote-tracking branch 'origin/dev' into crop-profile-picture-tool
lgmarchi Oct 9, 2023
542854f
feat(CropImage): Improve code
lgmarchi Oct 10, 2023
d74e9a1
Merge branch 'dev' into crop-profile-picture-tool
lgmarchi Oct 10, 2023
593593d
Merge branch 'dev' into crop-profile-picture-tool
dariusc93 Oct 10, 2023
852ae31
Merge branch 'dev' into crop-profile-picture-tool
stavares843 Oct 10, 2023
c0fce92
Merge branch 'dev' into crop-profile-picture-tool
stavares843 Oct 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/locales/en-US/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ settings = Settings
.about = About
.licenses = Licenses
.search-placeholder = Search Settings...
.please-select-area-you-want-to-crop = Please select the area you want to crop

settings-profile = Profile Settings
.failed = Failed to update profile
Expand Down
14 changes: 14 additions & 0 deletions kit/src/elements/button/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@
stroke: var(--danger-light);
}
}
&.appearance-danger-alternative {
background-color: var(--danger);
color: var(--danger-light);
&:hover {
background-color: var(--danger-light);
color: var(--text-color-bright);
svg {
stroke: var(--text-color-bright);
}
}
svg {
stroke: var(--text-color-bright);
}
}
&.appearance-transparent {
background-color: transparent;
color: var(--text-color);
Expand Down
3 changes: 3 additions & 0 deletions kit/src/elements/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub enum Appearance {
#[display(fmt = "danger")]
Danger,

#[display(fmt = "danger-alternative")]
DangerAlternative,

#[display(fmt = "disabled")]
Disabled,

Expand Down
6 changes: 5 additions & 1 deletion kit/src/layout/modal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct Props<'a> {
transparent: bool,
#[props(optional)]
dont_pad: Option<bool>,
show_close_button: Option<bool>,
close_on_click_inside_modal: Option<bool>,
children: Element<'a>,
onclose: EventHandler<'a, ()>,
Expand All @@ -30,6 +31,9 @@ pub fn Modal<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
} else {
""
};

let show_close_button = cx.props.show_close_button.unwrap_or(true);

let title = cx.props.with_title.clone().unwrap_or_default();

let close_on_click_inside_modal = cx.props.close_on_click_inside_modal.unwrap_or(true);
Expand All @@ -45,7 +49,7 @@ pub fn Modal<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
},
div {
class: "modal {cx.props.class.unwrap_or_default()}",
(!cx.props.transparent).then(|| rsx!(
(!cx.props.transparent && show_close_button).then(|| rsx!(
div {
class: "close-btn",
z_index: "10",
Expand Down
17 changes: 17 additions & 0 deletions ui/src/components/crop_image_tool/adjust_crop_circle_size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
var imgElement = document.getElementById('image-preview-modal-file-embed');
var cropCircle = document.getElementById('crop-box');
var firstRender = '$FIRST_RENDER';

function adjustCropCircleSize() {
var imageWidth = imgElement.clientWidth;
var imageHeight = imgElement.clientHeight;

var minDimension = Math.min(imageWidth, imageHeight);
cropCircle.style.width = minDimension + 'px';
cropCircle.style.height = minDimension + 'px';
}


window.addEventListener('resize', adjustCropCircleSize);

adjustCropCircleSize();
11 changes: 11 additions & 0 deletions ui/src/components/crop_image_tool/get_image_dimensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var imgElement = document.getElementById('image-preview-modal-file-embed');

var imgStyle = window.getComputedStyle(imgElement);

var maxWidth = imgStyle.getPropertyValue('max-width');
var maxHeight = imgStyle.getPropertyValue('max-height');

var imageWidth = imgElement.width;
var imageHeight = imgElement.height;

return {"width": imageWidth, "height": imageHeight};
203 changes: 203 additions & 0 deletions ui/src/components/crop_image_tool/mod.rs
lgmarchi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use common::{icons::outline::Shape, language::get_local_text, STATIC_ARGS};
use dioxus::prelude::*;
use kit::{
elements::{button::Button, label::Label, range::Range, Appearance},
layout::modal::Modal,
};
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;

const ADJUST_CROP_CIRCLE_SIZE_SCRIPT: &str = include_str!("./adjust_crop_circle_size.js");

const GET_IMAGE_DIMENSIONS_SCRIPT: &str = include_str!("./get_image_dimensions.js");

const SAVE_CROPPED_IMAGE_SCRIPT: &str = include_str!("./save_cropped_image.js");

#[derive(Debug, Clone)]
struct ImageDimensions {
height: i64,
width: i64,
}

#[derive(Props)]
pub struct Props<'a> {
pub large_thumbnail: String,
pub on_cancel: EventHandler<'a, ()>,
pub on_crop: EventHandler<'a, PathBuf>,
}

#[allow(non_snake_case)]
pub fn CropImageModal<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
let large_thumbnail = use_ref(cx, || cx.props.large_thumbnail.clone());

let image_scale: &UseRef<f32> = use_ref(cx, || 1.0);
let crop_image = use_state(cx, || true);
let cropped_image_pathbuf = use_ref(cx, PathBuf::new);
let clicked_button_to_crop = use_state(cx, || false);
let first_render = use_ref(cx, || true);

let image_dimensions = use_ref(cx, || ImageDimensions {
lgmarchi marked this conversation as resolved.
Show resolved Hide resolved
height: 0,
width: 0,
});

if *clicked_button_to_crop.get() {
cx.props.on_crop.call(cropped_image_pathbuf.read().clone());
clicked_button_to_crop.set(false);
crop_image.set(false);
}

let eval = use_eval(cx);

use_future(cx, (), |_| {
to_owned![eval, image_dimensions];
async move {
while image_dimensions.read().width == 0 && image_dimensions.read().height == 0 {
if let Ok(r) = eval(GET_IMAGE_DIMENSIONS_SCRIPT) {
if let Ok(val) = r.join().await {
*image_dimensions.write_silent() = ImageDimensions {
height: val["height"].as_i64().unwrap_or_default(),
width: val["width"].as_i64().unwrap_or_default(),
};
}
};
}
let _ = eval(ADJUST_CROP_CIRCLE_SIZE_SCRIPT);
}
});

return cx.render(rsx!(div {
Modal {
open: *crop_image.clone(),
onclose: move |_| {
// Not close if user clicks outside modal
},
transparent: false,
show_close_button: false,
close_on_click_inside_modal: false,
dont_pad: false,
div {
max_height: "85vh",
max_width: "80vw",
padding: "16px",
onclick: move |_| {},
div {
id: "crop-image-topbar",
background: "var(--secondary)",
height: "70px",
border_radius: "12px",
div {
id: "crop-image-topbar-left",
padding: "16px",
display: "inline-flex",
align_items: "center",
div {
class: "crop-image-topbar-left-title",
Label {
text: get_local_text("settings.please-select-area-you-want-to-crop")
}
},
Button {
appearance: Appearance::DangerAlternative,
icon: Shape::XMark,
onpress: move |_| {
*first_render.write_silent() = true;
cx.props.on_cancel.call(());
crop_image.set(false);
}
},
div {
margin_right: "16px",
}
Button {
appearance: Appearance::Success,
icon: Shape::Check,
onpress: move |_| {
*first_render.write_silent() = false;
cx.spawn({
lgmarchi marked this conversation as resolved.
Show resolved Hide resolved
to_owned![eval, image_scale, cropped_image_pathbuf, clicked_button_to_crop];
async move {
let save_image_cropped_js = SAVE_CROPPED_IMAGE_SCRIPT
.replace("$IMAGE_SCALE", (1.0 / *image_scale.read()).to_string().as_str());
if let Ok(r) = eval(&save_image_cropped_js) {
if let Ok(val) = r.join().await {
let thumbnail = val.as_str().unwrap_or_default();
let base64_string = thumbnail.trim_matches('\"');
let decoded_bytes = match base64::decode(base64_string) {
Ok(bytes) => bytes,
Err(e) => {
log::error!("Error decoding base64 string for cropped image: {}", e);
return;
},
};
let cropped_image_path = STATIC_ARGS.uplink_path.join("cropped_image.png");
let mut file = match tokio::fs::File::create(cropped_image_path.clone()).await {
Ok(file) => file,
Err(e) => {
log::error!("Error creating cropped image file: {}", e);
return;
},
};

if let Err(e) = file.write_all(&decoded_bytes).await {
log::error!("Error writing cropped image file. {}", e);
return;
}
cropped_image_pathbuf.with_mut(|f| *f = cropped_image_path.clone());
clicked_button_to_crop.set(true);
}
}
}
});
}
}
},
}
div {
class: "container",
margin_bottom: "16px",
text_align: "center",
padding: "16px",
div {
id: "image-crop-box-container",
width: "auto",
div {
overflow: "hidden",
border: "3px solid var(--secondary)",
img {
id: "image-preview-modal-file-embed",
aria_label: "image-preview-modal-file-embed",
src: format_args!("{}", large_thumbnail.read()),
transform: format_args!("scale({})", image_scale.read()),
overflow: "hidden",
transition: "transform 0.2s ease",
max_height: "50vh",
max_width: "50vw",
display: "inline-block",
vertical_align: "middle",
onclick: move |e| e.stop_propagation(),

},
}
div {
id: "crop-box",
class: "crop-box",
}
}
}
Range {
initial_value: 1.0,
min: 1.0,
max: 5.0,
step: 0.1,
icon_left: Shape::Minus,
icon_right: Shape::Plus,
onchange: move |size_f32| {
*image_scale.write() = size_f32;
}
}
}

}
},));
}
24 changes: 24 additions & 0 deletions ui/src/components/crop_image_tool/save_cropped_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const image = document.getElementById('image-preview-modal-file-embed');
const cropBox = document.getElementById('crop-box');
const { width, height } = cropBox.getBoundingClientRect();
const { naturalWidth, naturalHeight } = image;

const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');

const scale = $IMAGE_SCALE;

const scaleX = naturalWidth / (image.width / scale);
const scaleY = naturalHeight / (image.height / scale);

const cropX = (naturalWidth - scaleX * width) / 2;
const cropY = (naturalHeight - scaleY * height) / 2;

ctx.drawImage(image, cropX, cropY, scaleX * width, scaleY * height, 0, 0, width, height);

const base64Canvas = canvas.toDataURL("image/png").split(';base64,')[1];

return base64Canvas;

33 changes: 33 additions & 0 deletions ui/src/components/crop_image_tool/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.crop-cursor {
width: 100px;
height: 100px;
border: 2px solid red;
position: absolute;
border-radius: 50%;
pointer-events: none;
}

.container {
position: relative;
text-align: center; /* Center content horizontally */
}

.crop-box {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
border-radius: 50%;
border: 2px dashed red; /* Dotted border style */
}

.crop-image-topbar-left-title {
color: var(--text-color);
margin-right: 32px;
line-height: 1.0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
1 change: 1 addition & 0 deletions ui/src/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod community;
pub mod crop_image_tool;
pub mod debug_logger;
pub mod emoji_group;
pub mod files;
Expand Down
Loading