add image-overlay

This commit is contained in:
Swrup 2025-04-15 08:39:45 +02:00
parent cd4fc18585
commit 26b17f57cd
10 changed files with 132 additions and 77 deletions

View file

@ -10,6 +10,7 @@
--text: #333333; --text: #333333;
--light-text: #5a5a5a; --light-text: #5a5a5a;
--quote: #FFB300; --quote: #FFB300;
--bg-image-overlay: rgba(0, 0, 0, 0.75);
--bg-post: #C5E1A5; --bg-post: #C5E1A5;
--border-post: #9dd162; --border-post: #9dd162;
--bg-post-highlight: #9dd162; --bg-post-highlight: #9dd162;
@ -110,6 +111,7 @@ nav a:hover, nav button:hover,
width: 100%; width: 100%;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1;
} }
.home-left-navigation-div { .home-left-navigation-div {
@ -127,7 +129,7 @@ nav a:hover, nav button:hover,
justify-content: flex-end; justify-content: flex-end;
right: 0; right: 0;
margin-right: 7vw; margin-right: 7vw;
z-index: 999; z-index: 2;
} }
.new-thread-view { .new-thread-view {
@ -149,7 +151,7 @@ nav a:hover, nav button:hover,
} }
.dropdown-content { .dropdown-content {
transition: 0.2s ease-out; transition: 0.2s ease-out;
z-index: 100000; z-index: 2;
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
@ -189,7 +191,7 @@ nav a:hover, nav button:hover,
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
z-index: 99; z-index: 2;
} }
&:focus-within .dropdown-close-btn:not(:focus) { &:focus-within .dropdown-close-btn:not(:focus) {
display: inline-block; display: inline-block;
@ -214,6 +216,31 @@ nav a:hover, nav button:hover,
margin-top: auto; margin-top: auto;
} }
/* don't use [img] selector because it interfere with leaflet */
.image {
object-fit: contain;
width: 100%;
height: auto;
}
.image-small {
max-width: 30vw;
max-height: 30vh;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: var(--bg-image-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.thread { .thread {
margin-inline: 1vw; margin-inline: 1vw;
margin-block: 3vw; margin-block: 3vw;
@ -314,20 +341,9 @@ nav a:hover, nav button:hover,
gap: 10px; gap: 10px;
} }
/* TODO use image dim? better max-size? */
.post-image-div { .post-image-div {
} }
.post-image {
max-width: 90vw;
height: auto;
}
.post-image-small {
max-width: 30vw;
max-height: 30vh;
}
.post-comment { .post-comment {
color: var(--text); color: var(--text);
padding-top: 10px; padding-top: 10px;
@ -353,7 +369,7 @@ nav a:hover, nav button:hover,
background-color: var(--bg-form); background-color: var(--bg-form);
border: 2px solid var(--border-form); border: 2px solid var(--border-form);
padding: 5px; padding: 5px;
z-index: 999990; z-index: 3;
} }
.reply-popup-dragzone { .reply-popup-dragzone {
display: flex; display: flex;
@ -387,7 +403,7 @@ nav a:hover, nav button:hover,
right: 1vw; right: 1vw;
bottom: 1vh; bottom: 1vh;
padding: 5px; padding: 5px;
z-index: 999999; z-index: 4;
background-color: var(--bg-error-popup); background-color: var(--bg-error-popup);
border: 2px solid var(--border-error-popup); border: 2px solid var(--border-error-popup);
border-radius: 12px; border-radius: 12px;

View file

@ -109,13 +109,19 @@ type form_action =
needed to compute quickview position *) needed to compute quickview position *)
type rect = float * float * float * float type rect = float * float * float * float
module Img_info = struct
type t =
| Avatar of string * img_info
| Post of int * img_info
end
type action = type action =
| Navigation_event of (Page.t option * Fragment.t) | Navigation_event of (Page.t option * Fragment.t)
| Post_form_change of form_action | Post_form_change of form_action
| Map_input of map_action | Map_input of map_action
| Submit_event of (Form_kind.wrapped * Brr.El.t) | Submit_event of (Form_kind.wrapped * Brr.El.t)
| Quickview_change of (rect * post_id) option | Quickview_change of (rect * post_id) option
| Image_click of post_id | Image_change of Img_info.t option
| Clear_error | Clear_error
type data_update = type data_update =
@ -181,6 +187,13 @@ let pp_form_action fmt a =
| Some (lat, lng) -> Fmt.pf fmt "latlng `(%f, %f)`" lat lng ) | Some (lat, lng) -> Fmt.pf fmt "latlng `(%f, %f)`" lat lng )
| Form_reset -> Fmt.pf fmt "reset" | Form_reset -> Fmt.pf fmt "reset"
let pp_image_change fmt = function
| None -> ()
| Some v -> (
match v with
| Img_info.Avatar (id, _img_info) -> Fmt.pf fmt "image click `%s`" id
| Post (id, _img_info) -> Fmt.pf fmt "image click `%d`" id )
let pp_action fmt = function let pp_action fmt = function
| Navigation_event (opt, frag) -> | Navigation_event (opt, frag) ->
let s = let s =
@ -194,7 +207,7 @@ let pp_action fmt = function
| Submit_event (W kind, _el) -> | Submit_event (W kind, _el) ->
Fmt.pf fmt "submit event `%s`" (Form_kind.name kind) Fmt.pf fmt "submit event `%s`" (Form_kind.name kind)
| Quickview_change _opt -> Fmt.pf fmt "quickview change" | Quickview_change _opt -> Fmt.pf fmt "quickview change"
| Image_click post_id -> Fmt.pf fmt "image click `%d`" post_id | Image_change opt -> pp_image_change fmt opt
| Clear_error -> Fmt.pf fmt "clear error" | Clear_error -> Fmt.pf fmt "clear error"
let pp_data_update fmt a = let pp_data_update fmt a =

View file

@ -310,6 +310,23 @@ module Error_popup = struct
el el
end end
module Image_overlay = struct
let mk opt =
match opt with
| None -> []
| Some img ->
let el = mk_image ~is_small:false img in
[ el ]
let f t_s =
let el = El.div ~at:[ class' "image-overlay" ] [] in
def_visibility `On (S.map (fun t -> Option.is_some t.opened_image) t_s) el;
Elr.def_children el (S.map (fun t -> mk t.opened_image) t_s);
(* on overlay click (that should cover whole screen) send action to close overlay *)
hold_on el Ev.click (fun _ev -> Events.send_action (Image_change None));
el
end
module Main = struct module Main = struct
let f t_s = let f t_s =
let l = let l =
@ -326,6 +343,7 @@ module Main = struct
; Delete.f ; Delete.f
; Report.f ; Report.f
; Error_popup.f ; Error_popup.f
; Image_overlay.f
] ]
in in
let main = El.v (str "main") l in let main = El.v (str "main") l in

View file

@ -154,54 +154,20 @@ let backlinks t_s post =
let l = List.map (post_id_quote t_s) post.backlinks in let l = List.map (post_id_quote t_s) post.backlinks in
El.div ~at:[ class' "post-replies" ] l El.div ~at:[ class' "post-replies" ] l
let image t_s ?(is_vignette = false) post = let image _t_s ?(is_vignette = false) post =
match post.image_info with match post.image_info with
| None -> None | None -> None
| Some image -> ( | Some image -> (
(* TODO show image dimension/name *) let img_small =
let mk is_small = Html_util.mk_image ~is_small:true (Img_info.Post (post.id, image))
let class_small =
if is_small then [ class' "post-image-small" ] else []
in in
let sizes =
[ mk_at "width"
(string_of_int (if is_small then image.thumb_w else image.w))
; mk_at "height"
(string_of_int (if is_small then image.thumb_h else image.h))
]
in
let url =
src
@@
if is_small then Fmt.str "/img/s/%d" post.id
else Fmt.str "/img/%d" post.id
in
let at =
class_small @ sizes
@ url
:: [ class' "post-image"
; alt image.alt
; title image.alt
; mk_at "data-id" (string_of_int post.id)
; mk_at "loading" "lazy"
]
in
El.img ~at ()
in
let img_small, img_big = (mk true, mk false) in
let el = El.div ~at:[ class' "post-image-div" ] [ img_small ] in let el = El.div ~at:[ class' "post-image-div" ] [ img_small ] in
match is_vignette with match is_vignette with
| true -> Some el | true -> Some el
| false -> | false ->
(* swap img_(small/big) on click *) hold_on el Ev.click (fun _ev ->
hold_on el Ev.click (fun _ev -> Events.send_action (Image_click post.id)); let v = Img_info.Post (post.id, image) in
let img_s = Events.send_action (Image_change (Some v)) );
S.map (fun t -> t.Model.opened_image) t_s
|> S.map (function
| Some id when Int.equal id post.id -> [ img_big ]
| Some _ | None -> [ img_small ] )
in
Elr.def_children el img_s;
Some el ) Some el )
let comment = let comment =

View file

@ -99,3 +99,38 @@ let new_thread_link_el t_s =
let children = S.map get_user t_s |> S.map (fun u -> [ mk u ]) in let children = S.map get_user t_s |> S.map (fun u -> [ mk u ]) in
Elr.def_children el children; Elr.def_children el children;
el el
let mk_image ~is_small img_info =
let id, image =
match img_info with
| Client_types.Img_info.Avatar (id, image) -> (id, image)
| Post (id, image) -> (string_of_int id, image)
in
let url =
match img_info with
| Client_types.Img_info.Avatar _ when is_small ->
Fmt.str "/img/avatar/s/%s" id
| Avatar _ -> Fmt.str "/img/avatar/%s" id
| Post _ when is_small -> Fmt.str "/img/s/%s" id
| Post _ -> Fmt.str "/img/%s" id
in
let class_small = if is_small then [ class' "image-small" ] else [] in
let sizes =
[ mk_at "width"
(string_of_int (if is_small then image.thumb_w else image.w))
; mk_at "height"
(string_of_int (if is_small then image.thumb_h else image.h))
]
in
let url = src url in
let at =
class_small @ sizes
@ url
:: [ class' "image"
; alt image.alt
; title image.alt
; mk_at "data-id" id
; mk_at "loading" "lazy"
]
in
El.img ~at ()

View file

@ -112,7 +112,7 @@ let toggle_latlng_popup latlng_opt =
module Markers = struct module Markers = struct
let icon mode = let icon mode =
(* TODO define in App *) (* TODO config *)
let default_url = "/assets/img/marker-icon.png" in let default_url = "/assets/img/marker-icon.png" in
let default_icon = Icon.create default_url [||] in let default_icon = Icon.create default_url [||] in
let selected_icon = Icon.create default_url [||] in let selected_icon = Icon.create default_url [||] in

View file

@ -13,7 +13,7 @@ type t =
; map_view : float * float * int ; map_view : float * float * int
; (* todo: just remove rect from here *) ; (* todo: just remove rect from here *)
quickview : (rect * (post_id, post) wrap) option quickview : (rect * (post_id, post) wrap) option
; opened_image : post_id option ; opened_image : Img_info.t option
; error : Client_types.error option ; error : Client_types.error option
} }
@ -264,11 +264,7 @@ let do_action : Client_types.action -> t -> t =
| _ -> () | _ -> ()
end; end;
{ t with quickview } { t with quickview }
| Image_click id -> ( | Image_change opened_image -> { t with opened_image }
match t.opened_image with
| Some current_image_id when Int.equal current_image_id id ->
{ t with opened_image = None }
| Some _ | None -> { t with opened_image = Some id } )
| Clear_error -> { t with error = None } | Clear_error -> { t with error = None }
let do_data_update : Client_types.data_update -> t -> t = let do_data_update : Client_types.data_update -> t -> t =

View file

@ -71,12 +71,17 @@ let get_post_image ~thumbnail request =
in in
render_result_img request ~headers res render_result_img request ~headers res
let get_avatar_image request = let get_avatar_image ~thumbnail request =
(* TODO cache *)
let headers =
[ ("Cache-Control", Fmt.str "max-age=%d, immutable" cache_max_age) ]
in
let f = if thumbnail then User.get_thumbnail_data else User.get_image_data in
let res = let res =
let* user_id = Api.url_param request User_id in let* user_id = Api.url_param request User_id in
User.get_image user_id f user_id
in in
render_result_img request ~headers:[] res render_result_img request ~headers res
(* -- ~~ -- *) (* -- ~~ -- *)
@ -123,7 +128,8 @@ let routes =
:: [ Dream.get "/assets/**" (Dream.static ~loader:asset_loader "") :: [ Dream.get "/assets/**" (Dream.static ~loader:asset_loader "")
; Dream.get "/img/:image_id" (get_post_image ~thumbnail:false) ; Dream.get "/img/:image_id" (get_post_image ~thumbnail:false)
; Dream.get "/img/s/:image_id" (get_post_image ~thumbnail:true) ; Dream.get "/img/s/:image_id" (get_post_image ~thumbnail:true)
; Dream.get "/user/:user_id/avatar" get_avatar_image ; Dream.get "/img/avatar/:user_id" (get_avatar_image ~thumbnail:false)
; Dream.get "/img/avatar/s/:user_id" (get_avatar_image ~thumbnail:true)
; Dream.get "/**" app_start_page ; Dream.get "/**" app_start_page
] ]

View file

@ -95,15 +95,20 @@ let update_password user_id password =
in in
Db_user.update_password_hash user_id password_hash Db_user.update_password_hash user_id password_hash
let get_image user_id = let default_avatar =
(* TODO config *)
let default_avatar_path = "/img/default_avatar.png" in let default_avatar_path = "/img/default_avatar.png" in
let* opt = Db_image.U.data user_id in
match opt with
| Some data -> Ok data
| None -> (
match Assets.read default_avatar_path with match Assets.read default_avatar_path with
| None -> Error (Internal (Db_not_found "can not find default avatar file")) | None -> Error (Internal (Db_not_found "can not find default avatar file"))
| Some avatar -> Ok avatar ) | Some avatar -> Ok avatar
let get_thumbnail_data user_id =
let* opt = Db_image.U.data user_id in
match opt with Some data -> Ok data | None -> default_avatar
let get_image_data user_id =
let* opt = Db_image.U.data user_id in
match opt with Some data -> Ok data | None -> default_avatar
(* TODO sql : rm image db functor, handle avatar image and transaction in db_user *) (* TODO sql : rm image db functor, handle avatar image and transaction in db_user *)
let upload_avatar user_id (name_opt, alt, content) = let upload_avatar user_id (name_opt, alt, content) =

View file

@ -96,7 +96,7 @@ module Test_user = struct
let get_image () = let get_image () =
(check unit_result) "is ok" (Ok ()) (check unit_result) "is ok" (Ok ())
(let* id = get_id () in (let* id = get_id () in
let* _image_data : string = User.get_image id in let* _image_data : string = User.get_image_data id in
Ok () ); Ok () );
() ()