diff --git a/apple/Sources/Logseq/LGPlatformViews.swift b/apple/Sources/Logseq/LGPlatformViews.swift index cd929b65..4991e127 100644 --- a/apple/Sources/Logseq/LGPlatformViews.swift +++ b/apple/Sources/Logseq/LGPlatformViews.swift @@ -16,7 +16,7 @@ enum AssetPresentationPolicy { static func kind(assetType: String?, localPath: String?) -> AssetPresentationKind { let normalizedType = (assetType ?? pathExtension(localPath)).lowercased() if normalizedType.hasPrefix("image/") - || ["jpg", "jpeg", "png", "gif", "heic", "webp"].contains(normalizedType) { + || ["jpg", "jpeg", "png", "gif", "heic", "heif", "webp", "avif", "bmp", "wbmp"].contains(normalizedType) { return .image } if normalizedType.hasPrefix("audio/") diff --git a/shared/src/logseq/core/result_view.ml b/shared/src/logseq/core/result_view.ml index c88e133d..19b11719 100644 --- a/shared/src/logseq/core/result_view.ml +++ b/shared/src/logseq/core/result_view.ml @@ -258,12 +258,20 @@ let column_json ?(include_candidates = true) ?(include_values = true) db rows p "filterValues", `List filter_values; "valuesDeferred", `Bool (not include_values); "choices", choices_json db p.choices] -let row_json ~compact_block db encode properties = function +let row_json ~compact_block ~include_asset db encode properties = function | Query_block.Cells cells -> `Assoc ["cells", `List (List.map (fun cell -> `String cell) cells)] | Query_block.Block block -> let id = eid db block.uuid in - let encoded = if compact_block then `Assoc [ - "uuid", `String block.uuid; "title", `String block.title; "pageId", `String block.page_id; - "displayTitle", `String (Markup.plain_text (Markup.parse block.references block.tags block.title))] + let encoded = if compact_block then + let asset_fields = + if include_asset && block.is_asset then + let field key = Option.fold ~none:[] ~some:(fun value -> [key, `String value]) in + ("isAsset", `Bool true) :: ( + field "assetType" block.asset_type @ field "assetChecksum" block.asset_checksum + @ field "localPath" block.local_path) + else [] in + `Assoc (["uuid", `String block.uuid; "title", `String block.title; "pageId", `String block.page_id; + "displayTitle", `String (Markup.plain_text (Markup.parse block.references block.tags block.title))] + @ asset_fields) else encode block in `Assoc ["block", encoded; "properties", `List (List.map (fun p -> let editor = if p.closed then @@ -375,7 +383,8 @@ let render_rows_cached ?cache ~reuse_prepared ?(input = "") ?(include_values = t "rowsDeferred", `Bool (window <> None); "input", `String input; "columns", prepared.columns; "properties", prepared.catalog; - "rows", `List (List.map (row_json ~compact_block:(typ <> "list") db encode_block prepared.properties) rows)]) + "rows", `List (List.map (row_json ~compact_block:(typ <> "list") ~include_asset:(typ = "gallery") + db encode_block prepared.properties) rows)]) with error -> `Assoc (fields @ ["error", `String (match error with Invalid_argument message -> message | _ -> Printexc.to_string error); "rows", `List []; "columns", `List []]) in `Assoc ["ownerUuid", `String owner; "featureType", `String feature; "views", `List (List.map view_json views)] diff --git a/shared/src/logseq/view_query.ml b/shared/src/logseq/view_query.ml index 5b397268..9528294d 100644 --- a/shared/src/logseq/view_query.ml +++ b/shared/src/logseq/view_query.ml @@ -681,6 +681,28 @@ let value_sheet owner feature view model_source send item property close = payload owner feature view.id "editCell" (("blockUuid", `String (Option.value ~default:"" item.uuid)) :: fields))))) model_source property close +let gallery_image (row : Model.outline_row) = + if not row.is_asset then None else + let asset_type = View_base.outliner_row_asset_type row in + let path = View_base.outliner_row_local_path row in + let format = + if asset_type <> "" then asset_type else + let extension = Filename.extension (if path = "" then row.row_title else path) in + if extension = "" then "" else String.sub extension 1 (String.length extension - 1) in + let normalized = String.lowercase_ascii format in + if String.starts_with ~prefix:"image/" normalized + || List.mem normalized ["jpg"; "jpeg"; "png"; "gif"; "heic"; "heif"; "webp"; "avif"; "bmp"; "wbmp"] + then Some (fun context parent -> + let node = Lui_ui.extension context "outliner-block-content" in + attach context parent node; + List.iter (fun (key, value) -> Lui_ui.extension_property context node key (Lui_protocol.StringValue value)) + ["block-id", row.row_uuid; "title", row.row_title; "asset-type", format; "local-path", path; + "markup-json", "[]"; "youtube-target-url", ""]; + Lui_ui.extension_property context node "is-asset" (Lui_protocol.BoolValue true); + Lui_ui.extension_property context node "is-completed" (Lui_protocol.BoolValue false); + node) + else None + let table_column_width column = if jfield "attr" column = `String "block/title" then 260 else 170 let table_column_window columns (offset, width) = @@ -932,14 +954,17 @@ let collection_json ?(fill_height = false) ~render_block model_source parsed sen (Signal.map2 (fun view item -> view, item) view_source item_source) column_window))) row_context parent in let gallery_card item_source : t = fun card_context parent -> - (dyn (fun (view, item) -> column ~gap:10 ~padding:16 ~background:"secondary" ~border_color:"border" ~border_width:1 ~corner_radius:12 - ~accessibility_identifier:("view.card." ^ view.id ^ "." ^ Option.value ~default:"" item.uuid) [ - button ~text:item.title ~text_alignment:`start ~variant:`ghost ~min_height:44 - ~on_press:(fun _ -> Option.iter (fun uuid -> ignore (send (View_base.request_node_action (Signal.sample model_source) uuid))) item.uuid) []; - column ~gap:8 (choices view |> List.filter (fun col -> jfield "attr" col <> `String "block/title") - |> List.map (fun col -> let property = cell view item (jtext (jfield "attr" col)) in - row ~gap:12 ~main:`space_between [text ~value:(property_title (Signal.sample model_source) col) ~foreground:"muted-foreground" ~style_class:"caption" []; - text ~value:(jtext (jfield "text" property)) ~style_class:"caption" []]))]) + (dyn (fun (view, item) -> + let image = Option.bind item.block (fun block -> gallery_image block.row) in + column ~gap:10 ~padding:16 ~background:"secondary" ~border_color:"border" ~border_width:1 ~corner_radius:12 + ~accessibility_identifier:("view.card." ^ view.id ^ "." ^ Option.value ~default:"" item.uuid) + (Option.to_list image @ [ + button ~text:item.title ~text_alignment:`start ~variant:`ghost ~min_height:44 + ~on_press:(fun _ -> Option.iter (fun uuid -> ignore (send (View_base.request_node_action (Signal.sample model_source) uuid))) item.uuid) []; + column ~gap:8 (choices view |> List.filter (fun col -> jfield "attr" col <> `String "block/title") + |> List.map (fun col -> let property = cell view item (jtext (jfield "attr" col)) in + row ~gap:12 ~main:`space_between [text ~value:(property_title (Signal.sample model_source) col) ~foreground:"muted-foreground" ~style_class:"caption" []; + text ~value:(jtext (jfield "text" property)) ~style_class:"caption" []]))])) (Signal.own_signal card_context.Lui_ui.ui_scope (Signal.map2 (fun view item -> view, item) view_source item_source))) card_context parent in let item_key item = match item.uuid with | Some uuid -> "block:" ^ uuid | None -> "tuple:" ^ string_of_int item.index in diff --git a/shared/test/logseq/result_view_test.ml b/shared/test/logseq/result_view_test.ml index d2f9d711..f0b9be90 100644 --- a/shared/test/logseq/result_view_test.ml +++ b/shared/test/logseq/result_view_test.ml @@ -500,6 +500,46 @@ let native_view_tabs_search_table_and_gallery () = check (find ("outliner.block." ^ alpha) <> -1)))) [App_test.ios_backend; App_test.kotlin_backend] +let gallery_cards_render_image_assets () = + let db = Ds.db_with [ + put alpha "block/title" (Ds.String "Photo.jpg"); + put alpha "block/tags" (Ds.Ref_to (Ds.Ident "logseq.class/Asset")); + put alpha "logseq.property.asset/type" (Ds.String "jpeg"); + put alpha "logseq.property.asset/checksum" (Ds.String "photo-checksum"); + put beta "block/title" (Ds.String "Document.pdf"); + put beta "block/tags" (Ds.Ref_to (ref_uuid Q.project)); + put beta "block/tags" (Ds.Ref_to (Ds.Ident "logseq.class/Asset")); + put beta "logseq.property.asset/type" (Ds.String "pdf"); + put gallery_view "logseq.property/view-for" (Ds.Ref_to (ref_uuid Q.project)); + put gallery_view "logseq.property.view/feature-type" (Ds.Keyword "class-objects"); + ] (fixture ()) in + let session, _ = make_session ~configured:true db in + let path = "Assets/Photo.jpg" in + Cache_model.cache_local_asset (Session.state session).model alpha "Photo.jpg" "jpeg" 123 + "photo-checksum" path 1 None; + let response = Rpc_test.dispatch_json session "openNode" (Json.to_string (`Assoc ["uuid", `String Q.project])) in + check_eq (member "ok" response) (`Bool true); + List.iter (fun backend -> + Q.with_ui backend (Json.to_string response) (fun app -> + App_test.send app (Model.RequestAppNode Q.project); + let projection = match Response_snapshot.decode_json_response response with + | Ok value -> value | Error message -> fail message in + App_test.send app (Model.ApplyCoreSnapshot projection); App_test.flush app; + let find = find_visible session app in + let runtime = Lui_app.runtime app in + let card = find ("view.card." ^ gallery_view ^ "." ^ alpha) in + check ~msg:"the tagged photo is already a Gallery result" (card <> -1); + let preview = App_test.descendant_with_extension runtime app card "outliner-block-content" in + check ~msg:"the Gallery card mounts the native photo preview" (preview <> -1); + check_eq (App_test.extension_property app preview "asset-type") + (Some (Lui_protocol.StringValue "jpeg")); + check_eq (App_test.extension_property app preview "local-path") + (Some (Lui_protocol.StringValue path)); + let pdf = find ("view.card." ^ gallery_view ^ "." ^ beta) in + check ~msg:"the PDF remains a result" (pdf <> -1); + check_eq (App_test.descendant_with_extension runtime app pdf "outliner-block-content") (-1))) + [App_test.ios_backend; App_test.kotlin_backend] + let native_view_settings_and_local_errors () = List.iter (fun backend -> with_runtime (fun session _ _ -> Q.with_ui backend (Session.snapshot_visible session) (fun app -> @@ -1571,6 +1611,7 @@ let cases = [ case "saved filters preserve nested UUID sets and nullable bounds" settings_preserve_nested_match_values; case "many numeric and scalar checkbox and text edits preserve types" many_and_scalar_cells_preserve_types; case "table edits assign owner values with validation and conflict checks" editable_cells_use_owner_values_and_conflicts; + case "Gallery cards show tagged photos" gallery_cards_render_image_assets; case "both native hosts expose tabs search table editing and gallery" native_view_tabs_search_table_and_gallery; case "native settings expose filters sorting and local action errors" native_view_settings_and_local_errors; case "native sorting survives Done and subsequent core refreshes" native_sort_settings_survive_save_and_refresh;