diff --git a/Cargo.lock b/Cargo.lock index dc84a1a..7e7c01f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ dependencies = [ "android-properties", "bitflags 2.11.0", "cc", - "jni", + "jni 0.22.4", "libc", "log", "ndk", @@ -567,6 +567,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", + "wry", ] [[package]] @@ -584,7 +585,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -593,6 +603,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.3" @@ -813,6 +829,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-expr" version = "0.15.8" @@ -852,7 +874,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -960,6 +982,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1110,6 +1142,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "ctor-lite" version = "0.1.2" @@ -1191,6 +1246,27 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -1231,6 +1307,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1238,7 +1323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -1250,10 +1335,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1298,6 +1395,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set 0.8.0", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1350,6 +1462,27 @@ dependencies = [ "linux-raw-sys 0.9.4", ] +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -1577,6 +1710,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font-types" version = "0.7.3" @@ -1818,6 +1957,33 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1835,7 +2001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.4", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -2064,7 +2230,7 @@ dependencies = [ "presser", "thiserror 1.0.69", "winapi", - "windows", + "windows 0.52.0", ] [[package]] @@ -2185,7 +2351,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2248,6 +2414,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "http" version = "1.4.0" @@ -2819,6 +2995,45 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -2833,7 +3048,7 @@ dependencies = [ "simd_cesu8", "thiserror 2.0.18", "walkdir", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3045,7 +3260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3225,6 +3440,17 @@ dependencies = [ "libc", ] +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "matchit" version = "0.8.4" @@ -3349,7 +3575,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" dependencies = [ - "bit-set", + "bit-set 0.5.3", "bitflags 2.11.0", "codespan-reporting", "hexf-parse", @@ -3547,6 +3773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -3733,6 +3960,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + [[package]] name = "objc2-foundation" version = "0.2.2" @@ -3850,6 +4086,18 @@ dependencies = [ "objc2-user-notifications", ] +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-uniform-type-identifiers" version = "0.2.2" @@ -3874,6 +4122,20 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -3976,7 +4238,7 @@ dependencies = [ "approx", "fast-srgb8", "palette_derive", - "phf", + "phf 0.11.3", ] [[package]] @@ -4067,7 +4329,7 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4143,8 +4405,29 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", ] [[package]] @@ -4153,18 +4436,41 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared", + "phf_shared 0.11.3", "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.117", @@ -4179,6 +4485,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -4323,6 +4638,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "presser" version = "0.3.1" @@ -4771,6 +5092,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "refinery" version = "0.8.12" @@ -5182,6 +5514,25 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen", + "precomputed-hash", + "rustc-hash 2.1.2", + "servo_arc", + "smallvec", +] + [[package]] name = "self_cell" version = "1.2.2" @@ -5293,6 +5644,15 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5522,6 +5882,32 @@ dependencies = [ "x11rb", ] +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -5558,6 +5944,30 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot 0.12.5", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5676,6 +6086,17 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5695,6 +6116,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -6238,6 +6669,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -6587,6 +7024,62 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -6596,6 +7089,42 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + [[package]] name = "weezl" version = "0.1.12" @@ -6634,7 +7163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" dependencies = [ "arrayvec", - "bit-vec", + "bit-vec 0.6.3", "bitflags 2.11.0", "cfg_aliases 0.1.1", "codespan-reporting", @@ -6662,7 +7191,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.5.3", "bitflags 2.11.0", "block", "cfg_aliases 0.1.1", @@ -6770,6 +7299,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -6779,6 +7330,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -6787,9 +7351,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -6814,19 +7389,53 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -6835,7 +7444,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -6880,7 +7498,22 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -6920,7 +7553,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -6931,6 +7564,30 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6949,6 +7606,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6967,6 +7630,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6997,6 +7666,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7015,6 +7690,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7033,6 +7714,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7051,6 +7738,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -7095,7 +7788,7 @@ dependencies = [ "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", - "objc2-ui-kit", + "objc2-ui-kit 0.2.2", "orbclient", "percent-encoding", "pin-project", @@ -7251,6 +7944,50 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wry" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" +dependencies = [ + "base64", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/crates/bds-ui/Cargo.toml b/crates/bds-ui/Cargo.toml index 5919cf5..080ee09 100644 --- a/crates/bds-ui/Cargo.toml +++ b/crates/bds-ui/Cargo.toml @@ -17,6 +17,7 @@ chrono = { workspace = true } open = { workspace = true } anyhow = { workspace = true } tokio = { workspace = true } +wry = "0.55" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index 19cd427..02b30e0 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use chrono::Datelike; -use iced::{Element, Subscription, Task}; +use iced::{Element, Subscription, Task, window}; use serde_json::json; use bds_core::db::Database; @@ -13,6 +13,7 @@ use bds_core::engine::ai::{self, AiEndpointConfig, AiEndpointKind}; use bds_core::i18n::{detect_os_locale, UiLocale}; use bds_core::model::{Media, Post, PostStatus, Project, PublishingPreferences, Script, SshMode, Template}; +use crate::components::webview::{self, WebViewConfig, WebViewController}; use crate::i18n::{t, tw}; use crate::platform::menu::{self, MenuAction, MenuRegistry}; use crate::state::navigation::{ @@ -81,6 +82,8 @@ pub enum Message { // macOS lifecycle FileOpenRequested(PathBuf), UrlOpenRequested(String), + MainWindowLoaded(Option), + EmbeddedPreviewReady(Result<(), String>), // Panel SetPanelTab(PanelTab), @@ -225,6 +228,11 @@ struct PreviewSession { handle: engine::preview::PreviewServerHandle, } +struct EmbeddedPreviewState { + controller: WebViewController, + current_url: Option, +} + fn persist_media_editor_state_impl( db: &Database, data_dir: &Path, @@ -281,6 +289,35 @@ fn default_post_editor_mode(settings_state: Option<&SettingsViewState>) -> &str .unwrap_or("markdown") } +fn referenced_media_ids(content: &str) -> Vec { + let mut ids = Vec::new(); + let mut rest = content; + + while let Some(start) = rest.find("bds-media://") { + let suffix = &rest[start + "bds-media://".len()..]; + let end = suffix + .find(|ch: char| ch == ')' || ch == ']' || ch.is_whitespace()) + .unwrap_or(suffix.len()); + let media_id = suffix[..end].trim(); + if !media_id.is_empty() && !ids.iter().any(|existing| existing == media_id) { + ids.push(media_id.to_string()); + } + rest = &suffix[end..]; + } + + ids +} + +fn draft_preview_url(post_id: &str, language: &str) -> String { + format!( + "http://{}:{}/__draft/{}?language={}", + engine::preview::PREVIEW_HOST, + engine::preview::PREVIEW_PORT, + post_id, + language + ) +} + fn save_template_editor_state_impl( db: &Database, project_id: &str, @@ -639,6 +676,47 @@ mod tests { assert_eq!(diff.value, "side-by-side"); } + #[test] + fn load_post_media_items_includes_media_referenced_only_in_markdown() { + let (db, project, tmp) = setup(); + let source = tmp.path().join("tiny.png"); + std::fs::write(&source, tiny_png_bytes()).unwrap(); + let imported = media::import_media( + db.conn(), + tmp.path(), + &project.id, + &source, + "tiny.png", + Some("Tiny"), + Some("Alt"), + None, + None, + Some("en"), + vec!["photo".to_string()], + ) + .unwrap(); + let post = post::create_post( + db.conn(), + tmp.path(), + &project.id, + "Referenced", + Some(&format!("![](bds-media://{})", imported.id)), + Vec::new(), + vec!["article".to_string()], + None, + Some("en"), + None, + ) + .unwrap(); + let app = make_app(db, project, &tmp); + + let linked_media = app.load_post_media_items(&post.id, post.content.as_deref()); + + assert_eq!(linked_media.len(), 1); + assert_eq!(linked_media[0].media_id, imported.id); + assert!(linked_media[0].is_image); + } + #[test] fn task_tick_autosaves_dirty_post_editor() { let (db, project, tmp) = setup(); @@ -1178,6 +1256,8 @@ pub struct BdsApp { // Local preview preview_session: Option, + embedded_preview: Option, + main_window_id: Option, // Editor states (keyed by entity id) post_editors: HashMap, @@ -1256,7 +1336,11 @@ impl BdsApp { // Chain menu initialization after project loading // (must happen after the event loop has started for macOS) - let init_task = Task::batch([init_task, Task::done(Message::InitMenuBar)]); + let init_task = Task::batch([ + init_task, + Task::done(Message::InitMenuBar), + window::get_oldest().map(Message::MainWindowLoaded), + ]); registry.set_enabled(MenuAction::Save, false); registry.set_enabled(MenuAction::PublishSelected, false); registry.set_enabled(MenuAction::PreviewPost, false); @@ -1306,6 +1390,8 @@ impl BdsApp { toasts: Vec::new(), active_modal: None, preview_session: None, + embedded_preview: None, + main_window_id: None, post_editors: HashMap::new(), media_editors: HashMap::new(), template_editors: HashMap::new(), @@ -1361,6 +1447,8 @@ impl BdsApp { toasts: Vec::new(), active_modal: None, preview_session: None, + embedded_preview: None, + main_window_id: None, post_editors: HashMap::new(), media_editors: HashMap::new(), template_editors: HashMap::new(), @@ -1457,7 +1545,7 @@ impl BdsApp { } self.enforce_panel_tab_fallback(); self.sync_menu_state(); - Task::none() + self.sync_embedded_preview_for_active_post() } Message::CloseTab(id) => { if self.active_tab.as_deref() == Some(id.as_str()) { @@ -1470,7 +1558,7 @@ impl BdsApp { } self.enforce_panel_tab_fallback(); self.sync_menu_state(); - Task::none() + self.sync_embedded_preview_for_active_post() } Message::SelectTab(id) => { if self.tabs.iter().any(|t| t.id == id) { @@ -1480,7 +1568,7 @@ impl BdsApp { self.active_tab = Some(id); } self.enforce_panel_tab_fallback(); - Task::none() + self.sync_embedded_preview_for_active_post() } Message::PinTab(id) => { tabs::pin_tab(&mut self.tabs, &id); @@ -1490,6 +1578,7 @@ impl BdsApp { self.flush_active_post_editor(); self.tabs.clear(); self.active_tab = None; + self.hide_embedded_preview(); Task::none() } @@ -1532,6 +1621,7 @@ impl BdsApp { Ok(()) => { self.active_project = self.projects.iter().find(|p| p.id == project_id).cloned(); self.preview_session = None; + self.hide_embedded_preview(); self.data_dir = self .active_project .as_ref() @@ -1643,6 +1733,32 @@ impl BdsApp { // Media import will be expanded in later milestones Task::none() } + Message::MainWindowLoaded(window_id) => { + self.main_window_id = window_id; + if self.active_post_uses_embedded_preview() { + self.sync_embedded_preview_for_active_post() + } else { + Task::none() + } + } + Message::EmbeddedPreviewReady(result) => { + match result { + Ok(()) => { + let visible = self.active_post_uses_embedded_preview(); + if let Some(preview) = &mut self.embedded_preview { + preview.controller.take_staged(); + if let Some(url) = preview.current_url.as_deref() { + preview.controller.navigate(url); + } + preview.controller.set_visible(visible); + } + } + Err(error) => { + self.notify(ToastLevel::Error, &error); + } + } + Task::none() + } // ── Tasks ── Message::TaskTick => { @@ -2082,6 +2198,7 @@ impl BdsApp { Message::PostEditor(msg) => { enum DeferredPostAction { None, + SyncEmbeddedPreview, Analyze(String), AnalyzeTaxonomy(String), DetectLanguage(String), @@ -2113,6 +2230,7 @@ impl BdsApp { } let mut deferred = DeferredPostAction::None; + let mut refresh_linked_media: Option<(String, String)> = None; if let Some(tab_id) = self.active_tab.clone() { if let Some(state) = self.post_editors.get_mut(&tab_id) { match msg { @@ -2129,6 +2247,7 @@ impl BdsApp { } PostEditorMsg::SwitchEditorMode(mode) => { state.set_editor_mode(&mode); + deferred = DeferredPostAction::SyncEmbeddedPreview; } PostEditorMsg::DetectLanguage => { state.quick_actions_open = false; @@ -2147,6 +2266,7 @@ impl BdsApp { PostEditorMsg::ContentChanged(new_text) => { state.content = new_text; state.mark_dirty(); + refresh_linked_media = Some((state.post_id.clone(), state.content.clone())); } PostEditorMsg::AuthorChanged(s) => { state.author = s; state.mark_dirty(); } PostEditorMsg::LanguageChanged(s) => { state.language = s; state.mark_dirty(); } @@ -2154,7 +2274,12 @@ impl BdsApp { PostEditorMsg::ToggleDoNotTranslate(b) => { state.do_not_translate = b; state.mark_dirty(); } PostEditorMsg::ToggleMetadata => { state.metadata_expanded = !state.metadata_expanded; } PostEditorMsg::ToggleExcerpt => { state.excerpt_expanded = !state.excerpt_expanded; } - PostEditorMsg::SwitchLanguage(lang) => { state.switch_language(&lang); } + PostEditorMsg::SwitchLanguage(lang) => { + state.switch_language(&lang); + if state.editor_mode == "preview" { + deferred = DeferredPostAction::SyncEmbeddedPreview; + } + } PostEditorMsg::TagsInputChanged(s) => { state.tags_input = s; } PostEditorMsg::TagsInputSubmit => { let tag = state.tags_input.trim().to_string(); @@ -2274,14 +2399,21 @@ impl BdsApp { } if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == state.post_id) { - state.refresh_preview_items(); tab.is_dirty = state.is_dirty; } } } + if let Some((post_id, content)) = refresh_linked_media { + let linked_media = self.load_post_media_items(&post_id, Some(&content)); + if let Some(state) = self.post_editors.get_mut(&post_id) { + state.linked_media = linked_media; + } + } + match deferred { DeferredPostAction::None => Task::none(), + DeferredPostAction::SyncEmbeddedPreview => self.sync_embedded_preview_for_active_post(), DeferredPostAction::Analyze(tab_id) => self.run_post_ai_analysis(&tab_id), DeferredPostAction::AnalyzeTaxonomy(tab_id) => self.run_post_taxonomy_analysis(&tab_id), DeferredPostAction::DetectLanguage(tab_id) => self.detect_post_language(&tab_id), @@ -2642,7 +2774,7 @@ impl BdsApp { } } let (outlinks, backlinks) = self.load_post_links(&post.id); - let linked_media = self.load_post_media_items(&post.id); + let linked_media = self.load_post_media_items(&post.id, post.content.as_deref()); let state = PostEditorState::from_post( &post, default_post_editor_mode(self.settings_state.as_ref()), @@ -2712,6 +2844,13 @@ impl BdsApp { SidebarView::Pages => &self.page_filter, _ => &self.post_filter, }; + let post_preview_widget = if self.active_post_uses_embedded_preview() { + self.embedded_preview + .as_ref() + .map(|preview| webview::webview(&preview.controller).into()) + } else { + None + }; workspace::view( self.sidebar_view, @@ -2745,6 +2884,7 @@ impl BdsApp { &self.toasts, self.active_modal.clone(), self.data_dir.as_deref(), + post_preview_widget, &self.post_editors, &self.media_editors, &self.template_editors, @@ -5356,12 +5496,12 @@ impl BdsApp { (outlinks, backlinks) } - fn load_post_media_items(&self, post_id: &str) -> Vec { + fn load_post_media_items(&self, post_id: &str, content: Option<&str>) -> Vec { let Some(ref db) = self.db else { return Vec::new(); }; - bds_core::db::queries::post_media::list_post_media_by_post(db.conn(), post_id) + let mut media_by_id = bds_core::db::queries::post_media::list_post_media_by_post(db.conn(), post_id) .unwrap_or_default() .into_iter() .filter_map(|link| { @@ -5370,16 +5510,45 @@ impl BdsApp { .map(|media| LinkedMediaItem { media_id: media.id, name: media.title.unwrap_or(media.original_name), + file_path: media.file_path, is_image: media.mime_type.starts_with("image/"), sort_order: link.sort_order, }) }) - .collect() + .map(|media| (media.media_id.clone(), media)) + .collect::>(); + + for media_id in referenced_media_ids(content.unwrap_or_default()) { + if media_by_id.contains_key(&media_id) { + continue; + } + + if let Ok(media) = bds_core::db::queries::media::get_media_by_id(db.conn(), &media_id) { + media_by_id.insert( + media_id, + LinkedMediaItem { + media_id: media.id, + name: media.title.unwrap_or(media.original_name), + file_path: media.file_path, + is_image: media.mime_type.starts_with("image/"), + sort_order: i32::MAX, + }, + ); + } + } + + let mut items = media_by_id.into_values().collect::>(); + items.sort_by_key(|media| media.sort_order); + items } fn refresh_post_relationships(&mut self, post_id: &str) { let (outlinks, backlinks) = self.load_post_links(post_id); - let linked_media = self.load_post_media_items(post_id); + let current_content = self + .post_editors + .get(post_id) + .map(|state| state.content.clone()); + let linked_media = self.load_post_media_items(post_id, current_content.as_deref()); if let Some(state) = self.post_editors.get_mut(post_id) { state.outlinks = outlinks; state.backlinks = backlinks; @@ -5437,7 +5606,7 @@ impl BdsApp { } } let (outlinks, backlinks) = self.load_post_links(&post.id); - let linked_media = self.load_post_media_items(&post.id); + let linked_media = self.load_post_media_items(&post.id, post.content.as_deref()); self.post_editors.insert( post.id.clone(), PostEditorState::from_post( @@ -5677,29 +5846,12 @@ impl BdsApp { }) } - fn preview_active_post(&mut self) -> Task { - let Some(active_id) = self.active_tab.clone() else { - return Task::none(); - }; - if !self - .tabs - .iter() - .any(|tab| tab.id == active_id && tab.tab_type == TabType::Post) - { - return Task::none(); - } - if let Err(error) = self.persist_post_editor_state(&active_id) { - self.notify(ToastLevel::Error, &error); - return Task::none(); - } - + fn ensure_preview_server(&mut self) -> Result<(), String> { let Some(project) = self.active_project.as_ref() else { - self.notify(ToastLevel::Error, &t(self.ui_locale, "engine.generateSiteNoProject")); - return Task::none(); + return Err(t(self.ui_locale, "engine.generateSiteNoProject")); }; let Some(data_dir) = self.data_dir.clone() else { - self.notify(ToastLevel::Error, &t(self.ui_locale, "engine.previewDataDirUnavailable")); - return Task::none(); + return Err(t(self.ui_locale, "engine.previewDataDirUnavailable")); }; let should_restart = self @@ -5721,12 +5873,10 @@ impl BdsApp { }); } Err(engine::EngineError::Conflict(_)) if self.preview_session.is_none() => { - self.notify(ToastLevel::Error, &t(self.ui_locale, "engine.previewPortInUse")); - return Task::none(); + return Err(t(self.ui_locale, "engine.previewPortInUse")); } Err(error) => { - self.notify(ToastLevel::Error, &error.to_string()); - return Task::none(); + return Err(error.to_string()); } } } @@ -5735,19 +5885,111 @@ impl BdsApp { let _ = &session.handle; } + Ok(()) + } + + fn preview_url_for_post(&mut self, post_id: &str) -> Result { + if let Err(error) = self.persist_post_editor_state(post_id) { + return Err(error); + } + self.ensure_preview_server()?; + let language = self .post_editors - .get(&active_id) + .get(post_id) .map(|editor| editor.active_language.clone()) .filter(|language| !language.is_empty()) .unwrap_or_else(|| self.content_language.clone()); - let url = format!( - "http://{}:{}/__draft/{}?language={}", - engine::preview::PREVIEW_HOST, - engine::preview::PREVIEW_PORT, - active_id, - language - ); + + Ok(draft_preview_url(post_id, &language)) + } + + fn active_post_uses_embedded_preview(&self) -> bool { + self.active_tab + .as_ref() + .and_then(|tab_id| self.post_editors.get(tab_id)) + .map(|editor| editor.editor_mode == "preview") + .unwrap_or(false) + } + + fn hide_embedded_preview(&self) { + if let Some(preview) = &self.embedded_preview { + preview.controller.set_visible(false); + } + } + + fn sync_embedded_preview_for_active_post(&mut self) -> Task { + let Some(active_id) = self.active_tab.clone() else { + self.hide_embedded_preview(); + return Task::none(); + }; + let Some(editor) = self.post_editors.get(&active_id) else { + self.hide_embedded_preview(); + return Task::none(); + }; + if editor.editor_mode != "preview" { + self.hide_embedded_preview(); + return Task::none(); + } + + let url = match self.preview_url_for_post(&active_id) { + Ok(url) => url, + Err(error) => { + self.notify(ToastLevel::Error, &error); + return Task::none(); + } + }; + + if let Some(preview) = &mut self.embedded_preview { + preview.current_url = Some(url.clone()); + if preview.controller.is_active() { + preview.controller.navigate(&url); + preview.controller.set_visible(true); + return Task::none(); + } + } else { + self.embedded_preview = Some(EmbeddedPreviewState { + controller: WebViewController::new(WebViewConfig::default().url(url.clone())), + current_url: Some(url.clone()), + }); + } + + let Some(window_id) = self.main_window_id else { + return window::get_oldest().map(Message::MainWindowLoaded); + }; + + if let Some(preview) = &mut self.embedded_preview { + if !preview.controller.is_active() { + preview.controller = WebViewController::new(WebViewConfig::default().url(url)); + return preview + .controller + .create_task(window_id, Message::EmbeddedPreviewReady); + } + } + + Task::none() + } + + fn preview_active_post(&mut self) -> Task { + let Some(active_id) = self.active_tab.clone() else { + return Task::none(); + }; + if !self + .tabs + .iter() + .any(|tab| tab.id == active_id && tab.tab_type == TabType::Post) + { + return Task::none(); + } + + let url = match self.preview_url_for_post(&active_id) { + Ok(url) => url, + Err(error) => { + self.notify(ToastLevel::Error, &error); + return Task::none(); + } + }; + if let Err(error) = open::that(&url) { self.notify(ToastLevel::Error, &error.to_string()); } diff --git a/crates/bds-ui/src/components/mod.rs b/crates/bds-ui/src/components/mod.rs index cefb36d..5e1bd78 100644 --- a/crates/bds-ui/src/components/mod.rs +++ b/crates/bds-ui/src/components/mod.rs @@ -1 +1,2 @@ pub mod inputs; +pub mod webview; diff --git a/crates/bds-ui/src/components/webview.rs b/crates/bds-ui/src/components/webview.rs new file mode 100644 index 0000000..506b1ed --- /dev/null +++ b/crates/bds-ui/src/components/webview.rs @@ -0,0 +1,317 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use iced::event; +use iced::advanced::layout::{self, Node}; +use iced::advanced::renderer; +use iced::advanced::widget::Tree; +use iced::advanced::{Clipboard, Layout, Shell, Widget}; +use iced::mouse; +use iced::{Element, Event, Length, Rectangle, Size, Task, window}; +use wry::dpi::{LogicalPosition, LogicalSize}; +use wry::{Rect, WebView, WebViewBuilder}; + +static NEXT_ID: AtomicU64 = AtomicU64::new(0); + +thread_local! { + static STAGED: RefCell> = RefCell::new(HashMap::new()); +} + +pub enum Content { + Url(String), + Html(String), +} + +impl Default for Content { + fn default() -> Self { + Self::Url(String::new()) + } +} + +#[derive(Default)] +pub struct WebViewConfig { + content: Content, + transparent: bool, + devtools: bool, +} + +impl WebViewConfig { + pub fn url(mut self, url: impl Into) -> Self { + self.content = Content::Url(url.into()); + self + } + + pub fn html(mut self, html: impl Into) -> Self { + self.content = Content::Html(html.into()); + self + } + + pub fn transparent(mut self, transparent: bool) -> Self { + self.transparent = transparent; + self + } + + pub fn devtools(mut self, devtools: bool) -> Self { + self.devtools = devtools; + self + } +} + +struct SharedState { + webview: Option, + last_bounds: Option, +} + +#[derive(Clone)] +pub(crate) struct BoundsSender(Rc>); + +impl BoundsSender { + pub(crate) fn apply(&self, bounds: Rectangle) { + let mut state = self.0.borrow_mut(); + state.last_bounds = Some(bounds); + if let Some(webview) = &state.webview { + let rect = Rect { + position: LogicalPosition::new(bounds.x as f64, bounds.y as f64).into(), + size: LogicalSize::new(bounds.width as f64, bounds.height as f64).into(), + }; + let _ = webview.set_bounds(rect); + } + } + + pub(crate) fn refocus_parent(&self) { + let state = self.0.borrow(); + if let Some(webview) = &state.webview { + let _ = webview.focus_parent(); + } + } +} + +pub struct WebViewController { + id: u64, + shared: Rc>, + config: WebViewConfig, +} + +impl WebViewController { + pub fn new(config: WebViewConfig) -> Self { + Self { + id: NEXT_ID.fetch_add(1, Ordering::Relaxed), + shared: Rc::new(RefCell::new(SharedState { + webview: None, + last_bounds: None, + })), + config, + } + } + + pub(crate) fn bounds_sender(&self) -> BoundsSender { + BoundsSender(Rc::clone(&self.shared)) + } + + pub fn create_task( + &mut self, + window_id: window::Id, + on_result: fn(Result<(), String>) -> M, + ) -> Task { + let id = self.id; + let content = std::mem::take(&mut self.config.content); + let transparent = self.config.transparent; + let devtools = self.config.devtools; + + window::run_with_handle(window_id, move |handle| { + build_webview(id, handle, content, transparent, devtools) + }) + .map(on_result) + } + + pub fn take_staged(&mut self) { + let webview = STAGED.with(|cell| cell.borrow_mut().remove(&self.id)); + let mut state = self.shared.borrow_mut(); + state.webview = webview; + + if let (Some(webview), Some(bounds)) = (&state.webview, state.last_bounds) { + let rect = Rect { + position: LogicalPosition::new(bounds.x as f64, bounds.y as f64).into(), + size: LogicalSize::new(bounds.width as f64, bounds.height as f64).into(), + }; + let _ = webview.set_bounds(rect); + } + } + + pub fn set_visible(&self, visible: bool) { + let state = self.shared.borrow(); + if let Some(webview) = &state.webview { + let _ = webview.set_visible(visible); + } + } + + pub fn navigate(&self, url: &str) { + let state = self.shared.borrow(); + if let Some(webview) = &state.webview { + let _ = webview.load_url(url); + } + } + + pub fn destroy(&mut self) { + self.shared.borrow_mut().webview = None; + } + + pub fn is_active(&self) -> bool { + self.shared.borrow().webview.is_some() + } +} + +fn build_webview( + id: u64, + handle: window::raw_window_handle::WindowHandle<'_>, + content: Content, + transparent: bool, + devtools: bool, +) -> Result<(), String> { + let mut builder = WebViewBuilder::new() + .with_transparent(transparent) + .with_devtools(devtools) + .with_focused(false); + + builder = match content { + Content::Html(html) => builder.with_html(html), + Content::Url(url) => builder.with_url(url), + }; + + let webview = builder + .build_as_child(&handle) + .map_err(|error| error.to_string())?; + + STAGED.with(|cell| { + cell.borrow_mut().insert(id, webview); + }); + + Ok(()) +} + +#[derive(Default)] +struct PlaceholderState { + last_bounds: Option, +} + +pub struct WebViewPlaceholder { + width: Length, + height: Length, + bounds_tx: Option, + _message: std::marker::PhantomData, +} + +impl WebViewPlaceholder { + pub fn new() -> Self { + Self { + width: Length::Fill, + height: Length::Fill, + bounds_tx: None, + _message: std::marker::PhantomData, + } + } + + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + pub(crate) fn bounds_sender(mut self, sender: BoundsSender) -> Self { + self.bounds_tx = Some(sender); + self + } +} + +impl Default for WebViewPlaceholder { + fn default() -> Self { + Self::new() + } +} + +impl Widget for WebViewPlaceholder +where + Renderer: renderer::Renderer, +{ + fn tag(&self) -> iced::advanced::widget::tree::Tag { + iced::advanced::widget::tree::Tag::of::() + } + + fn state(&self) -> iced::advanced::widget::tree::State { + iced::advanced::widget::tree::State::new(PlaceholderState::default()) + } + + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits) -> Node { + Node::new(limits.resolve(self.width, self.height, Size::ZERO)) + } + + fn draw( + &self, + _tree: &Tree, + _renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let Some(tx) = &self.bounds_tx else { + return event::Status::Ignored; + }; + + let state = tree.state.downcast_mut::(); + let bounds = layout.bounds(); + + if state.last_bounds.as_ref() != Some(&bounds) { + state.last_bounds = Some(bounds); + tx.apply(bounds); + } + + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event + && !cursor.is_over(bounds) + { + tx.refocus_parent(); + } + + event::Status::Ignored + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: renderer::Renderer + 'a, +{ + fn from(placeholder: WebViewPlaceholder) -> Self { + Self::new(placeholder) + } +} + +pub fn webview(controller: &WebViewController) -> WebViewPlaceholder { + WebViewPlaceholder::new().bounds_sender(controller.bounds_sender()) +} \ No newline at end of file diff --git a/crates/bds-ui/src/views/post_editor.rs b/crates/bds-ui/src/views/post_editor.rs index 19123b1..af3f8cd 100644 --- a/crates/bds-ui/src/views/post_editor.rs +++ b/crates/bds-ui/src/views/post_editor.rs @@ -1,8 +1,9 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::path::Path; use iced::widget::text::{Shaping, Wrapping}; -use iced::widget::{button, column, container, row, scrollable, text, text_input, Column, Space}; +use iced::widget::{button, column, container, image, row, scrollable, text, text_input, Column, Space}; use iced::{Color, Element, Length, Theme}; use iced_widget::markdown; @@ -20,7 +21,7 @@ use crate::views::status_bar; pub struct TranslationFlag { pub language: String, pub flag_emoji: String, - pub status: String, // "draft" | "published" | "missing" + pub status: String, pub is_active: bool, } @@ -46,10 +47,17 @@ pub struct ResolvedPostLink { pub struct LinkedMediaItem { pub media_id: String, pub name: String, + pub file_path: String, pub is_image: bool, pub sort_order: i32, } +#[derive(Debug, Clone)] +enum PreviewBlock { + Markdown(Vec), + Image { alt: String, target: String }, +} + /// State for an open post editor. pub struct PostEditorState { pub post_id: String, @@ -59,6 +67,7 @@ pub struct PostEditorState { pub content: String, pub editor_buffer: RefCell, pub preview_items: Vec, + preview_blocks: Vec, pub tags: Vec, pub categories: Vec, pub author: String, @@ -77,22 +86,13 @@ pub struct PostEditorState { pub quick_actions_open: bool, pub tags_input: String, pub categories_input: String, - // ── Translation flags ── - /// Currently-displayed language (canonical or translation). pub active_language: String, - /// The post's own language (canonical). pub canonical_language: String, - /// All blog languages from project metadata. pub blog_languages: Vec, - /// Saved canonical title/excerpt/content when viewing a translation. pub saved_canonical: Option, - /// Translation drafts keyed by language code. pub translation_drafts: HashMap, - /// Outgoing links from this post to other posts. pub outlinks: Vec, - /// Incoming links from other posts to this post. pub backlinks: Vec, - /// Media linked to this post. pub linked_media: Vec, } @@ -116,6 +116,7 @@ impl Clone for PostEditorState { content: self.content.clone(), editor_buffer: RefCell::new(EditorBuffer::new(&self.content)), preview_items: self.preview_items.clone(), + preview_blocks: self.preview_blocks.clone(), tags: self.tags.clone(), categories: self.categories.clone(), author: self.author.clone(), @@ -160,8 +161,9 @@ impl PostEditorState { let excerpt = post.excerpt.clone().unwrap_or_default(); let content = post.content.clone().unwrap_or_default(); let canonical_lang = post.language.clone().unwrap_or_else(|| "en".to_string()); - let preview_items = markdown::parse(&preview_markdown_document_parts(&title, &excerpt, &content)) - .collect::>(); + let preview_document = preview_markdown_document_parts(&title, &excerpt, &content); + let preview_items = markdown::parse(&preview_document).collect::>(); + let preview_blocks = build_preview_blocks_from_document(&preview_document, &preview_items); let mut translation_drafts = HashMap::new(); for tr in translations { @@ -184,6 +186,7 @@ impl PostEditorState { content: content.clone(), editor_buffer: RefCell::new(EditorBuffer::new(&content)), preview_items, + preview_blocks, tags: post.tags.clone(), categories: post.categories.clone(), author: post.author.clone().unwrap_or_default(), @@ -235,19 +238,17 @@ impl PostEditorState { } pub fn refresh_preview_items(&mut self) { - self.preview_items = markdown::parse(&preview_markdown_document(self)).collect(); + let preview_document = preview_markdown_document(self); + self.preview_items = markdown::parse(&preview_document).collect(); + self.preview_blocks = build_preview_blocks_from_document(&preview_document, &self.preview_items); } - /// Switch the editor to display a different language. - /// Saves current fields and loads the target language's draft. pub fn switch_language(&mut self, target_lang: &str) { if target_lang == self.active_language { return; } - // Save current fields if self.active_language == self.canonical_language { - // Switching away from canonical — stash canonical fields self.saved_canonical = Some(TranslationDraft { title: self.title.clone(), excerpt: self.excerpt.clone(), @@ -256,7 +257,6 @@ impl PostEditorState { is_dirty: self.is_dirty, }); } else { - // Switching away from a translation — save to drafts self.translation_drafts.insert( self.active_language.clone(), TranslationDraft { @@ -269,9 +269,7 @@ impl PostEditorState { ); } - // Load target fields if target_lang == self.canonical_language { - // Restore canonical if let Some(saved) = self.saved_canonical.take() { self.title = saved.title; self.excerpt = saved.excerpt; @@ -281,14 +279,12 @@ impl PostEditorState { self.is_dirty = saved.is_dirty; } } else if let Some(draft) = self.translation_drafts.get(target_lang) { - // Load existing translation self.title = draft.title.clone(); self.excerpt = draft.excerpt.clone(); self.content = draft.content.clone(); self.editor_buffer = RefCell::new(EditorBuffer::new(&draft.content)); self.is_dirty = draft.is_dirty; } else { - // No translation yet — blank fields self.title = String::new(); self.excerpt = String::new(); self.content = String::new(); @@ -300,7 +296,6 @@ impl PostEditorState { self.refresh_preview_items(); } - /// Build the translation flags list for the view. pub fn translation_flags(&self) -> Vec { let mut flags = Vec::new(); @@ -402,6 +397,7 @@ pub fn view<'a>( state: &'a PostEditorState, locale: UiLocale, word_wrap: bool, + preview_widget: Option>, ) -> Element<'a, Message> { let on_translation = state.active_language != state.canonical_language; @@ -859,19 +855,17 @@ pub fn view<'a>( ], ); let editor_widget: Element<'a, Message> = if state.editor_mode == "preview" { - scrollable( + preview_widget.unwrap_or_else(|| { container( - markdown::view( - &state.preview_items, - markdown::Settings::with_text_size(15), - markdown::Style::from_palette(Theme::TokyoNightStorm.palette()), - ) - .map(|url: markdown::Url| Message::UrlOpenRequested(url.to_string())), + text(t(locale, "tabBar.loading")) + .size(14) + .shaping(Shaping::Advanced), ) - .padding(16), - ) - .height(Length::Fill) - .into() + .padding(16) + .width(Length::Fill) + .height(Length::Fill) + .into() + }) } else { CodeEditor::new(&state.editor_buffer, highlighter(), "md") .word_wrap(word_wrap) @@ -1051,6 +1045,133 @@ fn preview_markdown_document_parts(title: &str, excerpt: &str, content: &str) -> sections.join("\n\n") } +fn build_preview_blocks_from_document( + document: &str, + fallback_items: &[markdown::Item], +) -> Vec { + let mut blocks = Vec::new(); + let mut markdown_buffer = Vec::new(); + + for line in document.lines() { + if let Some((alt, target)) = parse_standalone_markdown_image(line) { + flush_markdown_buffer(&mut blocks, &mut markdown_buffer); + blocks.push(PreviewBlock::Image { alt, target }); + } else { + markdown_buffer.push(line.to_string()); + } + } + flush_markdown_buffer(&mut blocks, &mut markdown_buffer); + + if blocks.is_empty() { + blocks.push(PreviewBlock::Markdown(fallback_items.to_vec())); + } + + blocks +} + +fn flush_markdown_buffer(blocks: &mut Vec, markdown_buffer: &mut Vec) { + let markdown_text = markdown_buffer.join("\n"); + if !markdown_text.trim().is_empty() { + blocks.push(PreviewBlock::Markdown( + markdown::parse(&markdown_text).collect::>(), + )); + } + markdown_buffer.clear(); +} + +fn parse_standalone_markdown_image(line: &str) -> Option<(String, String)> { + let trimmed = line.trim(); + if !trimmed.starts_with("![") { + return None; + } + let alt_end = trimmed.find("](")?; + if !trimmed.ends_with(')') { + return None; + } + let alt = trimmed[2..alt_end].to_string(); + let target = trimmed[alt_end + 2..trimmed.len() - 1].trim().to_string(); + if target.is_empty() { + None + } else { + Some((alt, target)) + } +} + +fn preview_block_view<'a>( + block: &'a PreviewBlock, + state: &'a PostEditorState, + data_dir: Option<&'a Path>, +) -> Element<'a, Message> { + match block { + PreviewBlock::Markdown(items) => markdown::view( + items, + markdown::Settings::with_text_size(15), + markdown::Style::from_palette(Theme::TokyoNightStorm.palette()), + ) + .map(|url: markdown::Url| Message::UrlOpenRequested(url.to_string())) + .into(), + PreviewBlock::Image { alt, target } => preview_image_block(state, data_dir, alt, target), + } +} + +fn preview_image_block<'a>( + state: &'a PostEditorState, + data_dir: Option<&'a Path>, + alt: &str, + target: &str, +) -> Element<'a, Message> { + let Some(path) = resolve_preview_image_path(state, data_dir, target) else { + return text(format!("![{alt}]({target})")) + .size(12) + .color(Color::from_rgb(0.70, 0.50, 0.50)) + .shaping(Shaping::Advanced) + .into(); + }; + + let mut content = column![ + container( + image(path) + .width(Length::Fill) + .height(Length::Shrink) + ) + .width(Length::Fill) + ] + .spacing(6); + if !alt.is_empty() { + content = content.push( + text(alt.to_string()) + .size(12) + .color(Color::from_rgb(0.62, 0.66, 0.74)) + .shaping(Shaping::Advanced), + ); + } + + content.into() +} + +fn resolve_preview_image_path( + state: &PostEditorState, + data_dir: Option<&Path>, + target: &str, +) -> Option { + let data_dir = data_dir?; + if let Some(media_id) = target.strip_prefix("bds-media://") { + return state + .linked_media + .iter() + .find(|media| media.media_id == media_id && media.is_image) + .map(|media| data_dir.join(&media.file_path).to_string_lossy().to_string()); + } + + let relative = target.trim_start_matches('/'); + let candidate = data_dir.join(relative); + if candidate.exists() { + Some(candidate.to_string_lossy().to_string()) + } else { + None + } +} + fn normalize_editor_mode(mode: &str) -> String { match mode { "preview" => "preview".to_string(), @@ -1117,7 +1238,6 @@ fn flag_inactive_style(_theme: &Theme, status: button::Status) -> button::Style #[cfg(test)] mod tests { use super::*; - fn sample_state() -> PostEditorState { let post = Post { id: "post-1".to_string(), @@ -1187,4 +1307,5 @@ mod tests { state.set_editor_mode("preview"); assert_eq!(state.editor_mode, "preview"); } + } diff --git a/crates/bds-ui/src/views/workspace.rs b/crates/bds-ui/src/views/workspace.rs index 9c03edf..7e04b76 100644 --- a/crates/bds-ui/src/views/workspace.rs +++ b/crates/bds-ui/src/views/workspace.rs @@ -101,6 +101,7 @@ pub fn view<'a>( active_modal: Option, // Data directory (for thumbnail paths) data_dir: Option<&'a Path>, + post_preview_widget: Option>, // Editor states post_editors: &'a HashMap, media_editors: &'a HashMap, @@ -122,6 +123,7 @@ pub fn view<'a>( active_tab, locale, data_dir, + post_preview_widget, post_editors, media_editors, template_editors, @@ -343,6 +345,7 @@ fn route_content_area<'a>( active_tab: Option<&'a str>, locale: UiLocale, data_dir: Option<&'a Path>, + post_preview_widget: Option>, post_editors: &'a HashMap, media_editors: &'a HashMap, template_editors: &'a HashMap, @@ -368,7 +371,7 @@ fn route_content_area<'a>( ContentRoute::Post(tab_id) => { if let Some(state) = post_editors.get(tab_id) { let wrap = settings_state.map(|s| s.wrap_long_lines).unwrap_or(true); - post_editor::view(state, locale, wrap) + post_editor::view(state, locale, wrap, post_preview_widget) } else { loading_view(locale) }