Fixed Cropping
This commit is contained in:
parent
e2f92fbce8
commit
6517773cf3
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
|
|
@ -15,3 +15,5 @@ rusttype = "0.9"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
# rust_decimal = "1.20"
|
||||||
|
# rust_decimal_macros = "1.20"
|
||||||
|
|
|
||||||
BIN
out.png
BIN
out.png
Binary file not shown.
|
Before Width: | Height: | Size: 987 KiB After Width: | Height: | Size: 210 KiB |
|
|
@ -1,41 +1,48 @@
|
||||||
use crate::utils::{self, ImageContainer};
|
use crate::utils::{self, Coord, ImageContainer, ImageProperties};
|
||||||
use fltk::{
|
use fltk::{
|
||||||
app, button::Button, draw, enums::Event, frame::Frame, group::Flex, prelude::*, window::Window,
|
app, button::Button, draw, enums::Event, frame::Frame, group::Flex, prelude::*, window::Window,
|
||||||
};
|
};
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
static mut PATH: String = String::new();
|
||||||
|
|
||||||
|
/// Window to crop the existing image
|
||||||
pub(crate) struct CropWindow {
|
pub(crate) struct CropWindow {
|
||||||
pub win: Window,
|
pub win: Window,
|
||||||
apply_btn: Button,
|
apply_btn: Button,
|
||||||
img_view: Frame,
|
container: Rc<RefCell<Option<ImageContainer>>>,
|
||||||
img: Rc<RefCell<ImageContainer>>,
|
page: Page,
|
||||||
bound: Rc<RefCell<(u32, u32, u32, u32)>>,
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Page {
|
||||||
|
pub(crate) image_view: Frame,
|
||||||
|
pub(crate) row_flex: Flex,
|
||||||
|
pub(crate) col_flex: Flex,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CropWindow {
|
impl CropWindow {
|
||||||
pub(crate) fn new(container: Rc<RefCell<ImageContainer>>) -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
// let image = &container.borrow().image;
|
let mut win = Window::new(0, 0, 500, 600, "Crop").center_screen();
|
||||||
let (image_width, image_height) = container.borrow().image.dimensions();
|
|
||||||
let mut win = Window::default()
|
|
||||||
.with_size(image_width as i32, 600)
|
|
||||||
.with_label("Crop");
|
|
||||||
|
|
||||||
let mut main_flex = Flex::default().size_of_parent().column();
|
let mut main_flex = Flex::default().size_of_parent().column();
|
||||||
|
|
||||||
// Work area
|
// Work area
|
||||||
let mut center_row_flex = Flex::default().row();
|
let center_row_flex = Flex::default().row();
|
||||||
Frame::default();
|
Frame::default();
|
||||||
|
|
||||||
let mut center_col_flex = Flex::default().column();
|
let center_col_flex = Flex::default().column();
|
||||||
Frame::default();
|
Frame::default();
|
||||||
let img_view = Frame::default();
|
let img_view = Frame::default();
|
||||||
Frame::default();
|
Frame::default();
|
||||||
center_col_flex.set_size(&img_view, image_height as i32);
|
|
||||||
center_col_flex.end();
|
center_col_flex.end();
|
||||||
|
|
||||||
Frame::default();
|
Frame::default();
|
||||||
center_row_flex.set_size(¢er_col_flex, image_width as i32);
|
|
||||||
center_row_flex.end();
|
center_row_flex.end();
|
||||||
|
|
||||||
// Panel
|
// Panel
|
||||||
|
|
@ -56,16 +63,15 @@ impl CropWindow {
|
||||||
win.end();
|
win.end();
|
||||||
win.make_resizable(true);
|
win.make_resizable(true);
|
||||||
|
|
||||||
let (bound_width, bound_height) = utils::get_4_5(image_width, image_height);
|
|
||||||
let bound_x = image_width / 2 - bound_width / 2;
|
|
||||||
let bound_y = image_height / 2 - bound_height / 2;
|
|
||||||
|
|
||||||
let mut crop_win = Self {
|
let mut crop_win = Self {
|
||||||
win,
|
win,
|
||||||
apply_btn,
|
apply_btn,
|
||||||
img_view,
|
container: Rc::new(RefCell::new(None)),
|
||||||
img: Rc::clone(&container),
|
page: Page {
|
||||||
bound: Rc::new(RefCell::new((bound_x, bound_y, bound_width, bound_height))),
|
image_view: img_view,
|
||||||
|
row_flex: center_row_flex,
|
||||||
|
col_flex: center_col_flex,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
crop_win.draw();
|
crop_win.draw();
|
||||||
|
|
@ -73,12 +79,64 @@ impl CropWindow {
|
||||||
crop_win
|
crop_win
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call it to show window to crop image
|
||||||
|
pub(crate) fn load_to_crop(
|
||||||
|
&mut self,
|
||||||
|
path: &str,
|
||||||
|
crop_pos: Option<(f64, f64)>,
|
||||||
|
) -> Option<(f64, f64)> {
|
||||||
|
unsafe {
|
||||||
|
PATH = path.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut container =
|
||||||
|
ImageContainer::new(path, Arc::new(RwLock::new(ImageProperties::new())));
|
||||||
|
{
|
||||||
|
let prop = &mut container.properties.write().unwrap();
|
||||||
|
prop.dimension = prop.original_dimension;
|
||||||
|
prop.crop_position = match crop_pos {
|
||||||
|
Some(a) => Some(a),
|
||||||
|
None => Some((0.0, 0.0)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
container.apply_scale();
|
||||||
|
let (image_width, image_height): (f64, f64) =
|
||||||
|
Coord::from(container.image.dimensions()).into();
|
||||||
|
self.win.set_size(image_width as i32, 600);
|
||||||
|
|
||||||
|
self.page
|
||||||
|
.row_flex
|
||||||
|
.set_size(&self.page.col_flex, image_width as i32);
|
||||||
|
self.page.row_flex.recalc();
|
||||||
|
|
||||||
|
self.page
|
||||||
|
.col_flex
|
||||||
|
.set_size(&self.page.image_view, image_height as i32);
|
||||||
|
self.page.col_flex.recalc();
|
||||||
|
|
||||||
|
*self.container.borrow_mut() = Some(container);
|
||||||
|
|
||||||
|
self.page.image_view.redraw();
|
||||||
|
self.win.show();
|
||||||
|
self.win.make_modal(true);
|
||||||
|
while self.win.shown() {
|
||||||
|
app::wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cont) = &*self.container.borrow() {
|
||||||
|
cont.properties.read().unwrap().crop_position
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self) {
|
fn draw(&mut self) {
|
||||||
let cont = Rc::clone(&self.img);
|
let container = Rc::clone(&self.container);
|
||||||
let bound = Rc::clone(&self.bound);
|
self.page.image_view.draw(move |f| {
|
||||||
self.img_view.draw(move |f| {
|
if let Some(cont) = &*container.borrow() {
|
||||||
let image = &cont.borrow().image;
|
let image = &cont.buffer;
|
||||||
let (bound_x, bound_y, bound_width, bound_height) = *bound.borrow();
|
|
||||||
draw::draw_image(
|
draw::draw_image(
|
||||||
image.as_rgb8().unwrap().as_raw(),
|
image.as_rgb8().unwrap().as_raw(),
|
||||||
f.x(),
|
f.x(),
|
||||||
|
|
@ -88,6 +146,18 @@ impl CropWindow {
|
||||||
fltk::enums::ColorDepth::Rgb8,
|
fltk::enums::ColorDepth::Rgb8,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let prop = cont.properties.read().unwrap();
|
||||||
|
let (original_width, original_height) = prop.original_dimension;
|
||||||
|
let (original_x, original_y) = prop.crop_position.unwrap();
|
||||||
|
let (resized_width, resized_height) = (image.width() as f64, image.height() as f64);
|
||||||
|
let (bound_width, bound_height) = utils::get_4_5(resized_width, resized_height);
|
||||||
|
|
||||||
|
let (bound_x, bound_y) = (
|
||||||
|
(original_x * resized_width as f64) / original_width,
|
||||||
|
(original_y * resized_height as f64) / original_height,
|
||||||
|
);
|
||||||
|
|
||||||
draw::set_color_rgb(255, 0, 0);
|
draw::set_color_rgb(255, 0, 0);
|
||||||
draw::draw_rect(
|
draw::draw_rect(
|
||||||
f.x() + bound_x as i32,
|
f.x() + bound_x as i32,
|
||||||
|
|
@ -95,75 +165,72 @@ impl CropWindow {
|
||||||
bound_width as i32,
|
bound_width as i32,
|
||||||
bound_height as i32,
|
bound_height as i32,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self) {
|
fn event(&mut self) {
|
||||||
let mut last: Option<(i32, i32)> = None;
|
let mut last: Option<(f64, f64)> = None;
|
||||||
let cont = Rc::clone(&self.img);
|
let container = Rc::clone(&self.container);
|
||||||
let bound = Rc::clone(&self.bound);
|
self.page.image_view.handle(move |f, ev| {
|
||||||
self.img_view.handle(move |f, ev| {
|
if let Some(cont) = &*container.borrow_mut() {
|
||||||
let image = &cont.borrow().image;
|
let image = &cont.buffer;
|
||||||
let (bound_x, bound_y, bound_width, bound_height) = *bound.borrow();
|
|
||||||
|
let mut prop = cont.properties.write().unwrap();
|
||||||
|
|
||||||
|
let (original_x, original_y) = prop.crop_position.unwrap();
|
||||||
|
let (original_width, original_heigth) = prop.original_dimension;
|
||||||
|
let (original_bound_width, original_bound_height) =
|
||||||
|
utils::get_4_5(original_width, original_heigth);
|
||||||
|
let point = original_width / image.width() as f64;
|
||||||
|
let (event_x, event_y) = (
|
||||||
|
(app::event_x() - f.x()) as f64 * point,
|
||||||
|
(app::event_y() - f.y()) as f64 * point,
|
||||||
|
);
|
||||||
if ev == Event::Push {
|
if ev == Event::Push {
|
||||||
last = Some((app::event_x(), app::event_y()));
|
last = Some((event_x, event_y));
|
||||||
} else if ev == Event::Drag {
|
} else if ev == Event::Drag {
|
||||||
if let Some((lx, ly)) = last {
|
if let Some((lx, ly)) = last {
|
||||||
let dx = app::event_x() - lx;
|
let dx = event_x - lx;
|
||||||
if (dx > 0 && bound_x + bound_width < image.width()) || (dx < 0 && bound_x > 0)
|
if (dx > 0.0 && original_x + original_bound_width < original_width)
|
||||||
|
|| (dx < 0.0 && original_x > 0.0)
|
||||||
{
|
{
|
||||||
let mut new_x = bound_x as i32 + dx;
|
let mut new_x = original_x + dx;
|
||||||
if new_x + bound_width as i32 > image.width() as i32 {
|
if new_x + original_bound_width > original_width {
|
||||||
new_x = (image.width() - bound_width) as i32
|
new_x = original_width - original_bound_width;
|
||||||
} else if new_x < 0 {
|
} else if new_x < 0.0 {
|
||||||
new_x = 0
|
new_x = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bound.borrow_mut().0 = new_x as u32;
|
prop.crop_position = prop.crop_position.map(|(_, y)| (new_x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
let dy = app::event_y() - ly;
|
let dy = event_y - ly;
|
||||||
if (dy > 0 && bound_y + bound_height < image.height())
|
if (dy > 0.0 && original_y + original_bound_height < original_heigth)
|
||||||
|| (dy < 0 && bound_y > 0)
|
|| (dy < 0.0 && original_y > 0.0)
|
||||||
{
|
{
|
||||||
let mut new_y = bound_y as i32 + dy;
|
let mut new_y = original_y + dy;
|
||||||
if new_y + bound_height as i32 > image.height() as i32 {
|
if new_y + original_bound_height > original_heigth {
|
||||||
new_y = (image.height() - bound_height) as i32
|
new_y = original_heigth - original_bound_height;
|
||||||
} else if new_y < 0 {
|
} else if new_y < 0.0 {
|
||||||
new_y = 0
|
new_y = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bound.borrow_mut().1 = new_y as u32;
|
prop.crop_position = prop.crop_position.map(|(x, _)| (x, new_y));
|
||||||
}
|
}
|
||||||
|
|
||||||
f.redraw();
|
f.redraw();
|
||||||
last = Some((app::event_x(), app::event_y()));
|
last = Some((event_x, event_y));
|
||||||
}
|
}
|
||||||
} else if ev == Event::Released {
|
} else if ev == Event::Released {
|
||||||
last = None;
|
last = None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut wind = self.win.clone();
|
let mut wind = self.win.clone();
|
||||||
let cont = Rc::clone(&self.img);
|
|
||||||
let bound = Rc::clone(&self.bound);
|
|
||||||
self.apply_btn.set_callback(move |_| {
|
self.apply_btn.set_callback(move |_| {
|
||||||
let (bound_x, bound_y, bound_width, bound_height) = *bound.borrow();
|
|
||||||
|
|
||||||
let image = cont
|
|
||||||
.borrow_mut()
|
|
||||||
.image
|
|
||||||
.crop(bound_x, bound_y, bound_width, bound_height);
|
|
||||||
|
|
||||||
cont.borrow_mut().image = image;
|
|
||||||
|
|
||||||
let (width, height) = cont.borrow().original_dimension;
|
|
||||||
cont.borrow_mut().crop_position = Some((
|
|
||||||
(bound_x * width) / bound_width,
|
|
||||||
(bound_y * height) / bound_height,
|
|
||||||
));
|
|
||||||
|
|
||||||
wind.do_callback();
|
wind.do_callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Thread to manage drawing in background
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
main_window::{MainWindow, Page},
|
main_window::{MainWindow, Page},
|
||||||
AppMessage,
|
AppMessage,
|
||||||
|
|
@ -20,7 +22,8 @@ use crate::utils::{ImageContainer, ImageProperties};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) enum DrawMessage {
|
pub(crate) enum DrawMessage {
|
||||||
Open,
|
Open, // Open file or cropped file
|
||||||
|
ChangeCrop((f64, f64)),
|
||||||
Recalc,
|
Recalc,
|
||||||
Flush,
|
Flush,
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +54,27 @@ pub(crate) fn spawn_image_thread(
|
||||||
status.set_label("Loading...");
|
status.set_label("Loading...");
|
||||||
load_image(
|
load_image(
|
||||||
&mut file_choice,
|
&mut file_choice,
|
||||||
|
None,
|
||||||
|
&mut quote,
|
||||||
|
&mut tag,
|
||||||
|
&mut layer_red,
|
||||||
|
&mut layer_green,
|
||||||
|
&mut layer_blue,
|
||||||
|
&mut layer_alpha,
|
||||||
|
&mut quote_position,
|
||||||
|
&mut tag_position,
|
||||||
|
&mut page,
|
||||||
|
&app_sender,
|
||||||
|
&properties,
|
||||||
|
&mut _container,
|
||||||
|
);
|
||||||
|
status.set_label("");
|
||||||
|
}
|
||||||
|
DrawMessage::ChangeCrop((x, y)) => {
|
||||||
|
status.set_label("Loading...");
|
||||||
|
load_image(
|
||||||
|
&mut file_choice,
|
||||||
|
Some((x, y)),
|
||||||
&mut quote,
|
&mut quote,
|
||||||
&mut tag,
|
&mut tag,
|
||||||
&mut layer_red,
|
&mut layer_red,
|
||||||
|
|
@ -81,6 +105,7 @@ pub(crate) fn spawn_image_thread(
|
||||||
|
|
||||||
fn load_image(
|
fn load_image(
|
||||||
file_choice: &mut menu::Choice,
|
file_choice: &mut menu::Choice,
|
||||||
|
crop: Option<(f64, f64)>,
|
||||||
quote: &mut MultilineInput,
|
quote: &mut MultilineInput,
|
||||||
tag: &mut Input,
|
tag: &mut Input,
|
||||||
layer_red: &mut Spinner,
|
layer_red: &mut Spinner,
|
||||||
|
|
@ -103,7 +128,6 @@ fn load_image(
|
||||||
|
|
||||||
if let Some(cont) = container {
|
if let Some(cont) = container {
|
||||||
quote.set_value("");
|
quote.set_value("");
|
||||||
tag.set_value("");
|
|
||||||
|
|
||||||
let file = Path::new(&file);
|
let file = Path::new(&file);
|
||||||
let conf = file.with_extension("conf");
|
let conf = file.with_extension("conf");
|
||||||
|
|
@ -111,19 +135,19 @@ fn load_image(
|
||||||
let properties = Arc::clone(&cont.properties);
|
let properties = Arc::clone(&cont.properties);
|
||||||
let mut use_defaults = true;
|
let mut use_defaults = true;
|
||||||
if conf.exists() {
|
if conf.exists() {
|
||||||
|
let mut prop = properties.write().unwrap();
|
||||||
let read = fs::read_to_string(&conf).unwrap();
|
let read = fs::read_to_string(&conf).unwrap();
|
||||||
if let Ok(saved_prop) = serde_json::from_str::<ImageProperties>(&read) {
|
if let Ok(saved_prop) = serde_json::from_str::<ImageProperties>(&read) {
|
||||||
let mut prop = properties.write().unwrap();
|
|
||||||
layer_red.set_value(saved_prop.rgba[0] as f64);
|
layer_red.set_value(saved_prop.rgba[0] as f64);
|
||||||
layer_green.set_value(saved_prop.rgba[1] as f64);
|
layer_green.set_value(saved_prop.rgba[1] as f64);
|
||||||
layer_blue.set_value(saved_prop.rgba[2] as f64);
|
layer_blue.set_value(saved_prop.rgba[2] as f64);
|
||||||
layer_alpha.set_value(saved_prop.rgba[3] as f64);
|
layer_alpha.set_value(saved_prop.rgba[3] as f64);
|
||||||
quote.set_value(&saved_prop.quote);
|
quote.set_value(&saved_prop.quote);
|
||||||
tag.set_value(&saved_prop.tag);
|
tag.set_value(&saved_prop.tag);
|
||||||
quote_position.set_range(0.0, prop.original_dimension.1 as f64);
|
quote_position.set_range(0.0, prop.original_dimension.1);
|
||||||
quote_position.set_value(saved_prop.quote_position as f64);
|
quote_position.set_value(saved_prop.quote_position);
|
||||||
tag_position.set_range(0.0, prop.original_dimension.1 as f64);
|
tag_position.set_range(0.0, prop.original_dimension.1);
|
||||||
tag_position.set_value(saved_prop.tag_position as f64);
|
tag_position.set_value(saved_prop.tag_position);
|
||||||
|
|
||||||
prop.quote = saved_prop.quote;
|
prop.quote = saved_prop.quote;
|
||||||
prop.tag = saved_prop.tag;
|
prop.tag = saved_prop.tag;
|
||||||
|
|
@ -131,23 +155,29 @@ fn load_image(
|
||||||
prop.tag_position = saved_prop.quote_position;
|
prop.tag_position = saved_prop.quote_position;
|
||||||
prop.rgba = saved_prop.rgba;
|
prop.rgba = saved_prop.rgba;
|
||||||
use_defaults = false;
|
use_defaults = false;
|
||||||
|
let saved = prop.is_saved;
|
||||||
drop(prop);
|
drop(prop);
|
||||||
|
if saved {
|
||||||
if let Some((x, y)) = saved_prop.crop_position {
|
if let Some((x, y)) = saved_prop.crop_position {
|
||||||
cont.apply_crop_pos(x, y);
|
cont.apply_crop_pos(x, y);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
match crop {
|
||||||
|
Some((x, y)) => cont.apply_crop_pos(x, y),
|
||||||
|
None => cont.apply_crop(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if use_defaults {
|
if use_defaults {
|
||||||
let mut prop = properties.write().unwrap();
|
let mut prop = properties.write().unwrap();
|
||||||
prop.quote = "".to_owned();
|
prop.quote = "".to_owned();
|
||||||
prop.tag = "".to_owned();
|
|
||||||
|
|
||||||
quote_position.set_range(0.0, prop.original_dimension.1 as f64);
|
quote_position.set_range(0.0, prop.original_dimension.1);
|
||||||
quote_position.set_value(prop.quote_position as f64);
|
quote_position.set_value(prop.quote_position);
|
||||||
tag_position.set_range(0.0, prop.original_dimension.1 as f64);
|
tag_position.set_range(0.0, prop.original_dimension.1);
|
||||||
tag_position.set_value(prop.tag_position as f64);
|
tag_position.set_value(prop.tag_position);
|
||||||
|
|
||||||
prop.rgba = [
|
prop.rgba = [
|
||||||
layer_red.value() as u8,
|
layer_red.value() as u8,
|
||||||
|
|
@ -156,7 +186,11 @@ fn load_image(
|
||||||
layer_alpha.value() as u8,
|
layer_alpha.value() as u8,
|
||||||
];
|
];
|
||||||
drop(prop);
|
drop(prop);
|
||||||
cont.apply_crop();
|
|
||||||
|
match crop {
|
||||||
|
Some((x, y)) => cont.apply_crop_pos(x, y),
|
||||||
|
None => cont.apply_crop(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cont.apply_scale();
|
cont.apply_scale();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
mod config;
|
mod config;
|
||||||
// mod crop_window;
|
mod crop_window;
|
||||||
mod draw_thread;
|
mod draw_thread;
|
||||||
mod main_window;
|
mod main_window;
|
||||||
mod properties;
|
mod properties;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
use crate::crop_window::CropWindow;
|
||||||
|
use crate::draw_thread::*;
|
||||||
use crate::utils::ImageProperties;
|
use crate::utils::ImageProperties;
|
||||||
use crate::{draw_thread::*, properties};
|
|
||||||
use fltk::{
|
use fltk::{
|
||||||
app,
|
app,
|
||||||
button::Button,
|
button::Button,
|
||||||
|
|
@ -15,7 +16,7 @@ use fltk::{
|
||||||
window::Window,
|
window::Window,
|
||||||
};
|
};
|
||||||
use std::sync::{mpsc, RwLock};
|
use std::sync::{mpsc, RwLock};
|
||||||
use std::{ffi::OsStr, fs, path::Path, sync::Arc};
|
use std::{ffi::OsStr, fs, sync::Arc};
|
||||||
|
|
||||||
pub(crate) struct MainWindow {
|
pub(crate) struct MainWindow {
|
||||||
pub(crate) win: Window,
|
pub(crate) win: Window,
|
||||||
|
|
@ -33,7 +34,6 @@ pub(crate) struct MainWindow {
|
||||||
pub(crate) quote_position: Spinner,
|
pub(crate) quote_position: Spinner,
|
||||||
pub(crate) tag_position: Spinner,
|
pub(crate) tag_position: Spinner,
|
||||||
pub(crate) crop_btn: Button,
|
pub(crate) crop_btn: Button,
|
||||||
pub(crate) reset_btn: Button,
|
|
||||||
pub(crate) status: Frame,
|
pub(crate) status: Frame,
|
||||||
pub(crate) page: Page,
|
pub(crate) page: Page,
|
||||||
pub(crate) draw_buff: Arc<RwLock<Vec<u8>>>,
|
pub(crate) draw_buff: Arc<RwLock<Vec<u8>>>,
|
||||||
|
|
@ -55,9 +55,7 @@ impl MainWindow {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let color = [25, 29, 34, 190];
|
let color = [25, 29, 34, 190];
|
||||||
|
|
||||||
let mut win = Window::default()
|
let mut win = Window::new(0, 0, 1000, 600, "Post Maker").center_screen();
|
||||||
.with_size(1000, 600)
|
|
||||||
.with_label("Post Maker");
|
|
||||||
|
|
||||||
let mut main_flex = Flex::default().size_of_parent().column();
|
let mut main_flex = Flex::default().size_of_parent().column();
|
||||||
let menubar = menu::SysMenuBar::default();
|
let menubar = menu::SysMenuBar::default();
|
||||||
|
|
@ -144,8 +142,6 @@ impl MainWindow {
|
||||||
Frame::default();
|
Frame::default();
|
||||||
let crop_btn = Button::default().with_label("Crop");
|
let crop_btn = Button::default().with_label("Crop");
|
||||||
actions_flex.set_size(&crop_btn, 100);
|
actions_flex.set_size(&crop_btn, 100);
|
||||||
let reset_btn = Button::default().with_label("Reset Values");
|
|
||||||
actions_flex.set_size(&reset_btn, 100);
|
|
||||||
Frame::default();
|
Frame::default();
|
||||||
actions_flex.end();
|
actions_flex.end();
|
||||||
controls_flex.set_size(&actions_flex, 30);
|
controls_flex.set_size(&actions_flex, 30);
|
||||||
|
|
@ -196,7 +192,6 @@ impl MainWindow {
|
||||||
quote_position,
|
quote_position,
|
||||||
tag_position,
|
tag_position,
|
||||||
crop_btn,
|
crop_btn,
|
||||||
reset_btn,
|
|
||||||
status,
|
status,
|
||||||
draw_buff,
|
draw_buff,
|
||||||
properties: Arc::clone(&properties),
|
properties: Arc::clone(&properties),
|
||||||
|
|
@ -216,17 +211,7 @@ impl MainWindow {
|
||||||
|
|
||||||
fn menu(&mut self) {
|
fn menu(&mut self) {
|
||||||
let mut file_choice = self.file_choice.clone();
|
let mut file_choice = self.file_choice.clone();
|
||||||
// let mut quote = self.quote.clone();
|
|
||||||
// let mut tag = self.tag.clone();
|
|
||||||
// let mut layer_red = self.layer_red.clone();
|
|
||||||
// let mut layer_green = self.layer_green.clone();
|
|
||||||
// let mut layer_blue = self.layer_blue.clone();
|
|
||||||
// let mut layer_alpha = self.layer_alpha.clone();
|
|
||||||
// let mut quote_position = self.quote_position.clone();
|
|
||||||
// let mut tag_position = self.tag_position.clone();
|
|
||||||
// let mut page = self.page.clone();
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
// let properties = Arc::clone(&self.properties);
|
|
||||||
let mut win = self.win.clone();
|
let mut win = self.win.clone();
|
||||||
self.menubar.add(
|
self.menubar.add(
|
||||||
"&File/Open Folder...\t",
|
"&File/Open Folder...\t",
|
||||||
|
|
@ -242,7 +227,14 @@ impl MainWindow {
|
||||||
win.activate();
|
win.activate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let files = fs::read_dir(path).unwrap();
|
let expost_dir = path.join("export");
|
||||||
|
if !expost_dir.exists() {
|
||||||
|
if let Err(_) = fs::create_dir(expost_dir) {
|
||||||
|
fltk::dialog::message_default("Failed: Readonly folder!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let files = fs::read_dir(&path).unwrap();
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
for file in files {
|
for file in files {
|
||||||
let file = file.unwrap();
|
let file = file.unwrap();
|
||||||
|
|
@ -298,6 +290,18 @@ impl MainWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn events(&mut self) {
|
fn events(&mut self) {
|
||||||
|
let properties = Arc::clone(&self.properties);
|
||||||
|
let mut crop_win = CropWindow::new();
|
||||||
|
let sender = self.sender.clone();
|
||||||
|
self.crop_btn.set_callback(move |_| {
|
||||||
|
let prop = properties.read().unwrap();
|
||||||
|
if let Some(path) = &prop.path {
|
||||||
|
if let Some((x, y)) = crop_win.load_to_crop(path, prop.crop_position) {
|
||||||
|
sender.send(DrawMessage::ChangeCrop((x, y))).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut file_choice = self.file_choice.clone();
|
let mut file_choice = self.file_choice.clone();
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
self.next_btn.set_callback(move |_| {
|
self.next_btn.set_callback(move |_| {
|
||||||
|
|
@ -358,7 +362,7 @@ impl MainWindow {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
self.quote_position.set_callback(move |f| {
|
self.quote_position.set_callback(move |f| {
|
||||||
let mut prop = properties.write().unwrap();
|
let mut prop = properties.write().unwrap();
|
||||||
prop.quote_position = f.value() as u32;
|
prop.quote_position = f.value();
|
||||||
sender.send(DrawMessage::Recalc).unwrap();
|
sender.send(DrawMessage::Recalc).unwrap();
|
||||||
sender.send(DrawMessage::Flush).unwrap();
|
sender.send(DrawMessage::Flush).unwrap();
|
||||||
image.redraw();
|
image.redraw();
|
||||||
|
|
@ -369,7 +373,7 @@ impl MainWindow {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
self.tag_position.set_callback(move |f| {
|
self.tag_position.set_callback(move |f| {
|
||||||
let mut prop = properties.write().unwrap();
|
let mut prop = properties.write().unwrap();
|
||||||
prop.tag_position = f.value() as u32;
|
prop.tag_position = f.value();
|
||||||
sender.send(DrawMessage::Recalc).unwrap();
|
sender.send(DrawMessage::Recalc).unwrap();
|
||||||
sender.send(DrawMessage::Flush).unwrap();
|
sender.send(DrawMessage::Flush).unwrap();
|
||||||
image.redraw();
|
image.redraw();
|
||||||
|
|
|
||||||
142
src/utils.rs
142
src/utils.rs
|
|
@ -5,7 +5,45 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::properties;
|
use crate::properties;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub(crate) struct Coord(pub(crate) f64, pub(crate) f64);
|
||||||
|
|
||||||
|
impl From<(i32, i32)> for Coord {
|
||||||
|
fn from((x, y): (i32, i32)) -> Self {
|
||||||
|
Coord(x as f64, y as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(u32, u32)> for Coord {
|
||||||
|
fn from((x, y): (u32, u32)) -> Self {
|
||||||
|
Coord(x as f64, y as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(f32, f32)> for Coord {
|
||||||
|
fn from((x, y): (f32, f32)) -> Self {
|
||||||
|
Coord(x as f64, y as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<(f64, f64)> for Coord {
|
||||||
|
fn into(self) -> (f64, f64) {
|
||||||
|
(self.0, self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<(u32, u32)> for Coord {
|
||||||
|
fn into(self) -> (u32, u32) {
|
||||||
|
(self.0 as u32, self.1 as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<(i32, i32)> for Coord {
|
||||||
|
fn into(self) -> (i32, i32) {
|
||||||
|
(self.0 as i32, self.1 as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ImageContainer {
|
pub(crate) struct ImageContainer {
|
||||||
pub(crate) image: DynamicImage, //plain
|
pub(crate) image: DynamicImage, //plain
|
||||||
pub(crate) buffer: DynamicImage, //buffer to show
|
pub(crate) buffer: DynamicImage, //buffer to show
|
||||||
|
|
@ -15,13 +53,14 @@ pub(crate) struct ImageContainer {
|
||||||
impl ImageContainer {
|
impl ImageContainer {
|
||||||
pub(crate) fn new(path: &str, properties: Arc<RwLock<ImageProperties>>) -> Self {
|
pub(crate) fn new(path: &str, properties: Arc<RwLock<ImageProperties>>) -> Self {
|
||||||
let img = image::open(path).unwrap();
|
let img = image::open(path).unwrap();
|
||||||
let (width, height) = img.dimensions();
|
let (width, height): (f64, f64) = Coord::from(img.dimensions()).into();
|
||||||
|
let (width, height) = (width, height);
|
||||||
|
|
||||||
let mut prop = properties.write().unwrap();
|
let mut prop = properties.write().unwrap();
|
||||||
prop.path = path.to_owned();
|
prop.path = Some(path.to_owned());
|
||||||
prop.original_dimension = (width, height);
|
prop.original_dimension = (width, height);
|
||||||
prop.quote_position = height / 2;
|
prop.quote_position = (height * 2.0) / 3.0;
|
||||||
prop.tag_position = (height * 2) / 3;
|
prop.tag_position = height / 2.0;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
image: img.clone(),
|
image: img.clone(),
|
||||||
|
|
@ -33,13 +72,11 @@ impl ImageContainer {
|
||||||
pub(crate) fn apply_scale(&mut self) {
|
pub(crate) fn apply_scale(&mut self) {
|
||||||
let mut prop = self.properties.write().unwrap();
|
let mut prop = self.properties.write().unwrap();
|
||||||
let (width, height) = prop.dimension;
|
let (width, height) = prop.dimension;
|
||||||
let (s_width, s_height) = ((width * 500) / height, 500);
|
let (s_width, s_height) = ((width * 500.0) / height, 500.0);
|
||||||
self.image =
|
|
||||||
self.image
|
self.image = self.image.thumbnail_exact(s_width as u32, s_height as u32);
|
||||||
.resize_exact(s_width, s_height, image::imageops::FilterType::Nearest);
|
|
||||||
|
|
||||||
self.buffer = self.image.clone();
|
self.buffer = self.image.clone();
|
||||||
|
|
||||||
prop.dimension = (s_width, s_height);
|
prop.dimension = (s_width, s_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,26 +85,28 @@ impl ImageContainer {
|
||||||
let (original_width, original_height) = prop.original_dimension;
|
let (original_width, original_height) = prop.original_dimension;
|
||||||
let (origina_crop_width, origina_crop_height) = get_4_5(original_width, original_height);
|
let (origina_crop_width, origina_crop_height) = get_4_5(original_width, original_height);
|
||||||
prop.crop_position = Some((
|
prop.crop_position = Some((
|
||||||
original_width / 2 - origina_crop_width / 2,
|
original_width / 2.0 - origina_crop_width / 2.0,
|
||||||
original_height / 2 - origina_crop_height / 2,
|
original_height / 2.0 - origina_crop_height / 2.0,
|
||||||
));
|
));
|
||||||
|
|
||||||
let (s_width, s_height) = self.image.dimensions();
|
let (s_width, s_height): (f64, f64) = Coord::from(self.image.dimensions()).into();
|
||||||
let (c_width, c_height) = get_4_5(s_width, s_height);
|
let (c_width, c_height) = get_4_5(s_width, s_height);
|
||||||
let (cx, cy) = (s_width / 2 - c_width / 2, s_height / 2 - c_height / 2);
|
let (cx, cy) = ((s_width - c_width) / 2.0, (s_height - c_height) / 2.0);
|
||||||
|
|
||||||
prop.dimension = (c_width, c_height);
|
prop.dimension = (c_width, c_height);
|
||||||
|
|
||||||
self.image = self.image.crop(cx, cy, c_width, c_height);
|
self.image = self
|
||||||
|
.image
|
||||||
|
.crop(cx as u32, cy as u32, c_width as u32, c_height as u32);
|
||||||
self.buffer = self.image.clone();
|
self.buffer = self.image.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_crop_pos(&mut self, original_x: u32, original_y: u32) {
|
pub(crate) fn apply_crop_pos(&mut self, original_x: f64, original_y: f64) {
|
||||||
let mut prop = self.properties.write().unwrap();
|
let mut prop = self.properties.write().unwrap();
|
||||||
let (original_width, original_height) = prop.original_dimension;
|
let (original_width, original_height) = prop.original_dimension;
|
||||||
prop.crop_position = Some((original_x, original_y));
|
prop.crop_position = Some((original_x, original_y));
|
||||||
|
|
||||||
let (s_width, s_height) = self.image.dimensions();
|
let (s_width, s_height): (f64, f64) = Coord::from(self.image.dimensions()).into();
|
||||||
let (c_width, c_height) = get_4_5(s_width, s_height);
|
let (c_width, c_height) = get_4_5(s_width, s_height);
|
||||||
let (cx, cy) = (
|
let (cx, cy) = (
|
||||||
(original_x * s_width) / original_width,
|
(original_x * s_width) / original_width,
|
||||||
|
|
@ -76,16 +115,19 @@ impl ImageContainer {
|
||||||
|
|
||||||
prop.dimension = (c_width, c_height);
|
prop.dimension = (c_width, c_height);
|
||||||
|
|
||||||
self.image = self.image.crop(cx, cy, c_width, c_height);
|
self.image = self
|
||||||
|
.image
|
||||||
|
.crop(cx as u32, cy as u32, c_width as u32, c_height as u32);
|
||||||
self.buffer = self.image.clone();
|
self.buffer = self.image.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn recalc(&mut self) {
|
pub(crate) fn recalc(&mut self) {
|
||||||
let prop = self.properties.read().unwrap();
|
let prop = self.properties.read().unwrap();
|
||||||
let mut tmp = self.image.clone();
|
let mut tmp = self.image.clone();
|
||||||
let (width, height) = tmp.dimensions();
|
let (width, height): (f64, f64) = Coord::from(tmp.dimensions()).into();
|
||||||
|
|
||||||
let layer = DynamicImage::ImageRgba8(ImageBuffer::from_fn(width, height, |_, _| {
|
let layer =
|
||||||
|
DynamicImage::ImageRgba8(ImageBuffer::from_fn(width as u32, height as u32, |_, _| {
|
||||||
image::Rgba(prop.rgba)
|
image::Rgba(prop.rgba)
|
||||||
}));
|
}));
|
||||||
image::imageops::overlay(&mut tmp, &layer, 0, 0);
|
image::imageops::overlay(&mut tmp, &layer, 0, 0);
|
||||||
|
|
@ -101,10 +143,10 @@ impl ImageContainer {
|
||||||
imageproc::drawing::draw_text_mut(
|
imageproc::drawing::draw_text_mut(
|
||||||
&mut tmp,
|
&mut tmp,
|
||||||
image::Rgba([255, 255, 255, 255]),
|
image::Rgba([255, 255, 255, 255]),
|
||||||
((width as f32 - text_width) / 2.0) as u32,
|
((width - text_width) / 2.0) as u32,
|
||||||
(prop.quote_position * height) / prop.original_dimension.1
|
((prop.quote_position * height) / prop.original_dimension.1
|
||||||
+ (text_height / 2.0) as u32
|
+ (text_height / 2.0)
|
||||||
+ index as u32 * (text_height * 1.2) as u32,
|
+ index as f64 * (text_height * 1.2)) as u32,
|
||||||
rusttype::Scale::uniform(size as f32),
|
rusttype::Scale::uniform(size as f32),
|
||||||
&properties::FONT_QUOTE,
|
&properties::FONT_QUOTE,
|
||||||
line,
|
line,
|
||||||
|
|
@ -122,10 +164,10 @@ impl ImageContainer {
|
||||||
imageproc::drawing::draw_text_mut(
|
imageproc::drawing::draw_text_mut(
|
||||||
&mut tmp,
|
&mut tmp,
|
||||||
image::Rgba([255, 255, 255, 255]),
|
image::Rgba([255, 255, 255, 255]),
|
||||||
(width as f32 * 0.99 - text_width) as u32,
|
(width * 0.99 - text_width) as u32,
|
||||||
(prop.tag_position * height) / prop.original_dimension.1
|
((prop.tag_position * height) / prop.original_dimension.1
|
||||||
+ (text_height / 2.0) as u32
|
+ (text_height / 2.0)
|
||||||
+ index as u32 * (text_height * 1.2) as u32,
|
+ index as f64 * (text_height * 1.2)) as u32,
|
||||||
rusttype::Scale::uniform(size as f32),
|
rusttype::Scale::uniform(size as f32),
|
||||||
&properties::FONT_TAG,
|
&properties::FONT_TAG,
|
||||||
line,
|
line,
|
||||||
|
|
@ -138,14 +180,14 @@ impl ImageContainer {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub(crate) struct ImageProperties {
|
pub(crate) struct ImageProperties {
|
||||||
pub(crate) path: String,
|
pub(crate) path: Option<String>,
|
||||||
pub(crate) dimension: (u32, u32),
|
pub(crate) dimension: (f64, f64),
|
||||||
pub(crate) original_dimension: (u32, u32),
|
pub(crate) original_dimension: (f64, f64),
|
||||||
pub(crate) crop_position: Option<(u32, u32)>,
|
pub(crate) crop_position: Option<(f64, f64)>,
|
||||||
pub(crate) quote: String,
|
pub(crate) quote: String,
|
||||||
pub(crate) tag: String,
|
pub(crate) tag: String,
|
||||||
pub(crate) quote_position: u32,
|
pub(crate) quote_position: f64, // as per original
|
||||||
pub(crate) tag_position: u32,
|
pub(crate) tag_position: f64, // as per original
|
||||||
pub(crate) rgba: [u8; 4],
|
pub(crate) rgba: [u8; 4],
|
||||||
pub(crate) is_saved: bool,
|
pub(crate) is_saved: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -153,21 +195,21 @@ pub(crate) struct ImageProperties {
|
||||||
impl ImageProperties {
|
impl ImageProperties {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: "".to_owned(),
|
path: None,
|
||||||
dimension: (0, 0),
|
dimension: (0.0, 0.0),
|
||||||
original_dimension: (0, 0),
|
original_dimension: (0.0, 0.0),
|
||||||
crop_position: None,
|
crop_position: None,
|
||||||
quote: "".to_owned(),
|
quote: "".to_owned(),
|
||||||
tag: "".to_owned(),
|
tag: "".to_owned(),
|
||||||
quote_position: 0,
|
quote_position: 0.0,
|
||||||
tag_position: 0,
|
tag_position: 0.0,
|
||||||
rgba: [0; 4],
|
rgba: [0; 4],
|
||||||
is_saved: true,
|
is_saved: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_4_5(width: u32, height: u32) -> (u32, u32) {
|
pub(crate) fn get_4_5(width: f64, height: f64) -> (f64, f64) {
|
||||||
if width > width_from_height(height) {
|
if width > width_from_height(height) {
|
||||||
(width_from_height(height), height)
|
(width_from_height(height), height)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -175,27 +217,27 @@ pub(crate) fn get_4_5(width: u32, height: u32) -> (u32, u32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn width_from_height(height: u32) -> u32 {
|
pub(crate) fn width_from_height(height: f64) -> f64 {
|
||||||
(4 * height) / 5
|
(4.0 * height) / 5.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn height_from_width(width: u32) -> u32 {
|
pub(crate) fn height_from_width(width: f64) -> f64 {
|
||||||
(5 * width) / 4
|
(5.0 * width) / 4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn quote_from_height(height: u32) -> u32 {
|
pub(crate) fn quote_from_height(height: f64) -> f64 {
|
||||||
(height * 70) / 1350
|
(height * 60.0) / 1350.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn tag_from_height(height: u32) -> u32 {
|
pub(crate) fn tag_from_height(height: f64) -> f64 {
|
||||||
(height * 50) / 1350
|
(height * 50.0) / 1350.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn measure_line(
|
pub(crate) fn measure_line(
|
||||||
font: &rusttype::Font,
|
font: &rusttype::Font,
|
||||||
text: &str,
|
text: &str,
|
||||||
scale: rusttype::Scale,
|
scale: rusttype::Scale,
|
||||||
) -> (f32, f32) {
|
) -> (f64, f64) {
|
||||||
let width = font
|
let width = font
|
||||||
.layout(text, scale, rusttype::point(0.0, 0.0))
|
.layout(text, scale, rusttype::point(0.0, 0.0))
|
||||||
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
|
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
|
||||||
|
|
@ -205,5 +247,5 @@ pub(crate) fn measure_line(
|
||||||
let v_metrics = font.v_metrics(scale);
|
let v_metrics = font.v_metrics(scale);
|
||||||
let height = v_metrics.ascent - v_metrics.descent + v_metrics.line_gap;
|
let height = v_metrics.ascent - v_metrics.descent + v_metrics.line_gap;
|
||||||
|
|
||||||
(width, height)
|
Coord::from((width, height)).into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue