Merge image uploading from kernel
kernelkind (8):
upload media button
get file binary
import base64
notedeck_columns: use sha2 & base64
use rfd for desktop file selection
add utils for uploading media
draft fields for media upload feat
ui: user can upload images
This commit is contained in:
476
Cargo.lock
generated
476
Cargo.lock
generated
@@ -207,6 +207,171 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3"
|
||||
dependencies = [
|
||||
"async-fs",
|
||||
"async-net",
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand",
|
||||
"raw-window-handle 0.6.2",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-net"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.83"
|
||||
@@ -493,6 +658,19 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.7.5"
|
||||
@@ -1010,7 +1188,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"pollster",
|
||||
"pollster 0.3.0",
|
||||
"puffin 0.19.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"raw-window-handle 0.6.2",
|
||||
"static_assertions",
|
||||
@@ -1187,6 +1365,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enostr"
|
||||
version = "0.1.0"
|
||||
@@ -1227,6 +1411,27 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumn"
|
||||
version = "0.1.14"
|
||||
@@ -1285,6 +1490,27 @@ version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ewebsock"
|
||||
version = "0.8.0"
|
||||
@@ -1446,6 +1672,19 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
@@ -2425,6 +2664,19 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
@@ -2546,6 +2798,7 @@ dependencies = [
|
||||
name = "notedeck_columns"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.6.0",
|
||||
"dirs",
|
||||
"eframe",
|
||||
@@ -2565,10 +2818,12 @@ dependencies = [
|
||||
"poll-promise",
|
||||
"puffin 0.19.1 (git+https://github.com/jb55/puffin?rev=70ff86d5503815219b01a009afd3669b7903a057)",
|
||||
"puffin_egui",
|
||||
"rfd",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
@@ -2681,7 +2936,7 @@ version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -2693,7 +2948,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
@@ -2705,7 +2960,7 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
@@ -2970,6 +3225,16 @@ dependencies = [
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -2985,6 +3250,12 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
@@ -3085,6 +3356,17 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.31"
|
||||
@@ -3136,6 +3418,12 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
@@ -3188,6 +3476,15 @@ dependencies = [
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit 0.22.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
@@ -3507,6 +3804,30 @@ dependencies = [
|
||||
"usvg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2",
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"pollster 0.4.0",
|
||||
"raw-window-handle 0.6.2",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.50"
|
||||
@@ -3753,6 +4074,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
@@ -3799,6 +4131,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
@@ -4379,6 +4720,17 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.0"
|
||||
@@ -5334,6 +5686,16 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon-dl"
|
||||
version = "0.4.2"
|
||||
@@ -5389,6 +5751,69 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "192a0d989036cd60a1e91a54c9851fb9ad5bd96125d41803eed79d2e2ef74bd7"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"static_assertions",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.59.0",
|
||||
"winnow 0.6.20",
|
||||
"xdg-home",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3685b5c81fce630efc3e143a4ded235b107f1b1cdf186c3f115529e5e5ae4265"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "519629a3f80976d89c575895b05677cbc45eaf9f70d62a364d819ba646409cc8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"winnow 0.6.20",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
@@ -5482,3 +5907,46 @@ checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"url",
|
||||
"winnow 0.6.20",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"syn 2.0.90",
|
||||
"winnow 0.6.20",
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ members = [
|
||||
|
||||
[workspace.dependencies]
|
||||
base32 = "0.4.0"
|
||||
base64 = "0.22.1"
|
||||
bech32 = { version = "0.11", default-features = false }
|
||||
bitflags = "2.5.0"
|
||||
dirs = "5.0.1"
|
||||
|
||||
BIN
assets/icons/media_upload_dark_4x.png
Normal file
BIN
assets/icons/media_upload_dark_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -43,6 +43,11 @@ tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
rfd = "0.15"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use crate::ui::note::PostType;
|
||||
use poll_promise::Promise;
|
||||
|
||||
use crate::{media_upload::Nip94Event, ui::note::PostType, Error};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Draft {
|
||||
pub buffer: String,
|
||||
pub uploaded_media: Vec<Nip94Event>, // media uploads to include
|
||||
pub uploading_media: Vec<Promise<Result<Nip94Event, Error>>>, // promises that aren't ready yet
|
||||
pub upload_errors: Vec<String>, // media upload errors to show the user
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -42,5 +47,8 @@ impl Draft {
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.buffer = "".to_string();
|
||||
self.upload_errors = Vec::new();
|
||||
self.uploaded_media = Vec::new();
|
||||
self.uploading_media = Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use notedeck::ImageCache;
|
||||
use notedeck::Result;
|
||||
use poll_promise::Promise;
|
||||
use std::path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
//pub type ImageCacheKey = String;
|
||||
@@ -198,6 +199,10 @@ fn fetch_img_from_disk(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>> {
|
||||
std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string()))
|
||||
}
|
||||
|
||||
/// Controls type-specific handling
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ImageType {
|
||||
|
||||
@@ -18,6 +18,7 @@ mod frame_history;
|
||||
mod images;
|
||||
mod key_parsing;
|
||||
pub mod login_manager;
|
||||
mod media_upload;
|
||||
mod multi_subscriber;
|
||||
mod nav;
|
||||
mod post;
|
||||
|
||||
447
crates/notedeck_columns/src/media_upload.rs
Normal file
447
crates/notedeck_columns/src/media_upload.rs
Normal file
@@ -0,0 +1,447 @@
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||
use ehttp::Request;
|
||||
use nostrdb::{Note, NoteBuilder};
|
||||
use poll_promise::Promise;
|
||||
use sha2::{Digest, Sha256};
|
||||
use url::Url;
|
||||
|
||||
use crate::{images::fetch_binary_from_disk, Error};
|
||||
|
||||
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
||||
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
||||
|
||||
fn get_upload_url(nip96_url: Url) -> Promise<Result<String, Error>> {
|
||||
let request = Request::get(nip96_url);
|
||||
let (sender, promise) = Promise::new();
|
||||
|
||||
ehttp::fetch(request, move |response| {
|
||||
let result = match response {
|
||||
Ok(resp) => {
|
||||
if resp.status == 200 {
|
||||
if let Some(text) = resp.text() {
|
||||
get_api_url_from_json(text)
|
||||
} else {
|
||||
Err(Error::Generic(
|
||||
"ehttp::Response payload is not text".to_owned(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"ehttp::Response status: {}",
|
||||
resp.status
|
||||
)))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Error::Generic(e)),
|
||||
};
|
||||
|
||||
sender.send(result);
|
||||
});
|
||||
|
||||
promise
|
||||
}
|
||||
|
||||
fn get_api_url_from_json(json: &str) -> Result<String, Error> {
|
||||
match serde_json::from_str::<serde_json::Value>(json) {
|
||||
Ok(json) => {
|
||||
if let Some(url) = json
|
||||
.get("api_url")
|
||||
.and_then(|url| url.as_str())
|
||||
.map(|url| url.to_string())
|
||||
{
|
||||
Ok(url)
|
||||
} else {
|
||||
Err(Error::Generic(
|
||||
"api_url key not found in ehttp::Response".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Error::Generic(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_upload_url_from_provider(mut provider_url: Url) -> Promise<Result<String, Error>> {
|
||||
provider_url.set_path(NIP96_WELL_KNOWN);
|
||||
get_upload_url(provider_url)
|
||||
}
|
||||
|
||||
pub fn get_nostr_build_upload_url() -> Promise<Result<String, Error>> {
|
||||
get_upload_url_from_provider(NOSTR_BUILD_URL())
|
||||
}
|
||||
|
||||
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note {
|
||||
NoteBuilder::new()
|
||||
.kind(27235)
|
||||
.start_tag()
|
||||
.tag_str("u")
|
||||
.tag_str(&upload_url)
|
||||
.start_tag()
|
||||
.tag_str("method")
|
||||
.tag_str("POST")
|
||||
.start_tag()
|
||||
.tag_str("payload")
|
||||
.tag_str(&payload_hash)
|
||||
.sign(seckey)
|
||||
.build()
|
||||
.expect("build note")
|
||||
}
|
||||
|
||||
fn create_nip96_request(
|
||||
upload_url: &str,
|
||||
media_path: MediaPath,
|
||||
file_contents: Vec<u8>,
|
||||
nip98_base64: &str,
|
||||
) -> ehttp::Request {
|
||||
let boundary = "----boundary";
|
||||
|
||||
let mut body = format!(
|
||||
"--{}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
|
||||
boundary, media_path.file_name, media_path.media_type.to_mime()
|
||||
)
|
||||
.into_bytes();
|
||||
body.extend(file_contents);
|
||||
body.extend(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
||||
|
||||
let headers = {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
"Content-Type".to_owned(),
|
||||
format!("multipart/form-data; boundary={boundary}"),
|
||||
);
|
||||
map.insert("Authorization".to_owned(), format!("Nostr {nip98_base64}"));
|
||||
map
|
||||
};
|
||||
|
||||
Request {
|
||||
method: "POST".to_string(),
|
||||
url: upload_url.to_string(),
|
||||
headers,
|
||||
body: body.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sha256_hex(contents: &Vec<u8>) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(contents);
|
||||
let hash = hasher.finalize();
|
||||
hex::encode(hash)
|
||||
}
|
||||
|
||||
pub fn nip96_upload(
|
||||
seckey: [u8; 32],
|
||||
upload_url: String,
|
||||
media_path: MediaPath,
|
||||
) -> Promise<Result<Nip94Event, Error>> {
|
||||
let bytes_res = fetch_binary_from_disk(media_path.full_path.clone());
|
||||
|
||||
let file_bytes = match bytes_res {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return Promise::from_ready(Err(Error::Generic(format!(
|
||||
"could not read contents of file to upload: {e}"
|
||||
))))
|
||||
}
|
||||
};
|
||||
|
||||
internal_nip96_upload(seckey, upload_url, media_path, file_bytes)
|
||||
}
|
||||
|
||||
pub fn nostrbuild_nip96_upload(
|
||||
seckey: [u8; 32],
|
||||
media_path: MediaPath,
|
||||
) -> Promise<Result<Nip94Event, Error>> {
|
||||
let (sender, promise) = Promise::new();
|
||||
std::thread::spawn(move || {
|
||||
let upload_url = match get_nostr_build_upload_url().block_and_take() {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
sender.send(Err(Error::Generic(format!(
|
||||
"could not get nostrbuild upload url: {e}"
|
||||
))));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let res = nip96_upload(seckey, upload_url, media_path).block_and_take();
|
||||
sender.send(res);
|
||||
});
|
||||
promise
|
||||
}
|
||||
|
||||
fn internal_nip96_upload(
|
||||
seckey: [u8; 32],
|
||||
upload_url: String,
|
||||
media_path: MediaPath,
|
||||
file_contents: Vec<u8>,
|
||||
) -> Promise<Result<Nip94Event, Error>> {
|
||||
let file_hash = sha256_hex(&file_contents);
|
||||
let nip98_note = create_nip98_note(&seckey, upload_url.to_owned(), file_hash);
|
||||
|
||||
let nip98_base64 = match nip98_note.json() {
|
||||
Ok(json) => BASE64_URL_SAFE.encode(json),
|
||||
Err(e) => return Promise::from_ready(Err(Error::Generic(e.to_string()))),
|
||||
};
|
||||
|
||||
let request = create_nip96_request(&upload_url, media_path, file_contents, &nip98_base64);
|
||||
|
||||
let (sender, promise) = Promise::new();
|
||||
|
||||
ehttp::fetch(request, move |response| {
|
||||
let maybe_uploaded_media = match response {
|
||||
Ok(response) => {
|
||||
if response.ok {
|
||||
match String::from_utf8(response.bytes.clone()) {
|
||||
Ok(str_response) => find_nip94_ev_in_json(str_response),
|
||||
Err(e) => Err(Error::Generic(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"ehttp Response was unsuccessful. Code {} with message: {}",
|
||||
response.status, response.status_text
|
||||
)))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Error::Generic(e)),
|
||||
};
|
||||
|
||||
sender.send(maybe_uploaded_media);
|
||||
});
|
||||
|
||||
promise
|
||||
}
|
||||
|
||||
fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
|
||||
match serde_json::from_str::<serde_json::Value>(&json) {
|
||||
Ok(v) => {
|
||||
let tags = v["nip94_event"]["tags"].clone();
|
||||
let content = v["nip94_event"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
match serde_json::from_value::<Vec<Vec<String>>>(tags) {
|
||||
Ok(tags) => Nip94Event::from_tags_and_content(tags, content)
|
||||
.map_err(|e| Error::Generic(e.to_owned())),
|
||||
Err(e) => Err(Error::Generic(e.to_string())),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Error::Generic(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MediaPath {
|
||||
full_path: PathBuf,
|
||||
file_name: String,
|
||||
media_type: SupportedMediaType,
|
||||
}
|
||||
|
||||
impl MediaPath {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
||||
let media_type = SupportedMediaType::from_extension(ex)?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(&format!("file.{}", ex))
|
||||
.to_owned();
|
||||
|
||||
Ok(MediaPath {
|
||||
full_path: path,
|
||||
file_name,
|
||||
media_type,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"{:?} does not have an extension",
|
||||
path
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SupportedMediaType {
|
||||
Png,
|
||||
Jpeg,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl SupportedMediaType {
|
||||
pub fn mime_extension(&self) -> &str {
|
||||
match &self {
|
||||
SupportedMediaType::Png => "png",
|
||||
SupportedMediaType::Jpeg => "jpeg",
|
||||
SupportedMediaType::Webp => "webp",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mime(&self) -> String {
|
||||
format!("{}/{}", self.mime_type(), self.mime_extension())
|
||||
}
|
||||
|
||||
fn mime_type(&self) -> String {
|
||||
match &self {
|
||||
SupportedMediaType::Png | SupportedMediaType::Jpeg | SupportedMediaType::Webp => {
|
||||
"image"
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn from_extension(ext: &str) -> Result<Self, Error> {
|
||||
match ext.to_lowercase().as_str() {
|
||||
"jpeg" | "jpg" => Ok(SupportedMediaType::Jpeg),
|
||||
"png" => Ok(SupportedMediaType::Png),
|
||||
"webp" => Ok(SupportedMediaType::Webp),
|
||||
unsupported_type => Err(Error::Generic(format!(
|
||||
"{unsupported_type} is not a valid file type to upload."
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
pub struct Nip94Event {
|
||||
pub url: String,
|
||||
pub ox: Option<String>,
|
||||
pub x: Option<String>,
|
||||
pub media_type: Option<String>,
|
||||
pub dimensions: Option<(u32, u32)>,
|
||||
pub blurhash: Option<String>,
|
||||
pub thumb: Option<String>,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl Nip94Event {
|
||||
pub fn new(url: String, width: u32, height: u32) -> Self {
|
||||
Self {
|
||||
url,
|
||||
ox: None,
|
||||
x: None,
|
||||
media_type: None,
|
||||
dimensions: Some((width, height)),
|
||||
blurhash: None,
|
||||
thumb: None,
|
||||
content: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const URL: &str = "url";
|
||||
const OX: &str = "ox";
|
||||
const X: &str = "x";
|
||||
const M: &str = "m";
|
||||
const DIM: &str = "dim";
|
||||
const BLURHASH: &str = "blurhash";
|
||||
const THUMB: &str = "thumb";
|
||||
|
||||
impl Nip94Event {
|
||||
fn from_tags_and_content(
|
||||
tags: Vec<Vec<String>>,
|
||||
content: String,
|
||||
) -> Result<Self, &'static str> {
|
||||
let mut url = None;
|
||||
let mut ox = None;
|
||||
let mut x = None;
|
||||
let mut media_type = None;
|
||||
let mut dimensions = None;
|
||||
let mut blurhash = None;
|
||||
let mut thumb = None;
|
||||
|
||||
for tag in tags {
|
||||
match tag.as_slice() {
|
||||
[key, value] if key == URL => url = Some(value.to_string()),
|
||||
[key, value] if key == OX => ox = Some(value.to_string()),
|
||||
[key, value] if key == X => x = Some(value.to_string()),
|
||||
[key, value] if key == M => media_type = Some(value.to_string()),
|
||||
[key, value] if key == DIM => {
|
||||
if let Some((w, h)) = value.split_once('x') {
|
||||
if let (Ok(w), Ok(h)) = (w.parse::<u32>(), h.parse::<u32>()) {
|
||||
dimensions = Some((w, h));
|
||||
}
|
||||
}
|
||||
}
|
||||
[key, value] if key == BLURHASH => blurhash = Some(value.to_string()),
|
||||
[key, value] if key == THUMB => thumb = Some(value.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
url: url.ok_or("Missing url")?,
|
||||
ox,
|
||||
x,
|
||||
media_type,
|
||||
dimensions,
|
||||
blurhash,
|
||||
thumb,
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
|
||||
use enostr::FullKeypair;
|
||||
|
||||
use crate::media_upload::{
|
||||
get_upload_url_from_provider, nostrbuild_nip96_upload, MediaPath, NOSTR_BUILD_URL,
|
||||
};
|
||||
|
||||
use super::internal_nip96_upload;
|
||||
|
||||
#[test]
|
||||
fn test_nostrbuild_upload_url() {
|
||||
let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
|
||||
|
||||
let url = promise.block_until_ready();
|
||||
|
||||
assert!(url.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // this test should not run automatically since it sends data to a real server
|
||||
fn test_internal_nip96() {
|
||||
// just a random image to test image upload
|
||||
let file_path = PathBuf::from_str("../../../assets/damus_rounded_80.png").unwrap();
|
||||
let media_path = MediaPath::new(file_path).unwrap();
|
||||
let img_bytes = include_bytes!("../../../assets/damus_rounded_80.png");
|
||||
let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
|
||||
let kp = FullKeypair::generate();
|
||||
println!("Using pubkey: {:?}", kp.pubkey);
|
||||
|
||||
if let Ok(upload_url) = promise.block_until_ready() {
|
||||
let promise = internal_nip96_upload(
|
||||
kp.secret_key.secret_bytes(),
|
||||
upload_url.to_string(),
|
||||
media_path,
|
||||
img_bytes.to_vec(),
|
||||
);
|
||||
let res = promise.block_until_ready();
|
||||
assert!(res.is_ok())
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // this test should not run automatically since it sends data to a real server
|
||||
async fn test_nostrbuild_nip96() {
|
||||
// just a random image to test image upload
|
||||
let file_path =
|
||||
fs::canonicalize(PathBuf::from_str("../../assets/damus_rounded_80.png").unwrap())
|
||||
.unwrap();
|
||||
let media_path = MediaPath::new(file_path).unwrap();
|
||||
let kp = FullKeypair::generate();
|
||||
println!("Using pubkey: {:?}", kp.pubkey);
|
||||
|
||||
let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), media_path);
|
||||
|
||||
let out = promise.block_and_take();
|
||||
assert!(out.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,12 @@ use enostr::FullKeypair;
|
||||
use nostrdb::{Note, NoteBuilder, NoteReply};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::media_upload::Nip94Event;
|
||||
|
||||
pub struct NewPost {
|
||||
pub content: String,
|
||||
pub account: FullKeypair,
|
||||
pub media: Vec<Nip94Event>,
|
||||
}
|
||||
|
||||
fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
||||
@@ -15,26 +18,36 @@ fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
||||
}
|
||||
|
||||
impl NewPost {
|
||||
pub fn new(content: String, account: FullKeypair) -> Self {
|
||||
NewPost { content, account }
|
||||
pub fn new(content: String, account: FullKeypair, media: Vec<Nip94Event>) -> Self {
|
||||
NewPost {
|
||||
content,
|
||||
account,
|
||||
media,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_note(&self, seckey: &[u8; 32]) -> Note {
|
||||
let mut builder = add_client_tag(NoteBuilder::new())
|
||||
.kind(1)
|
||||
.content(&self.content);
|
||||
let mut content = self.content.clone();
|
||||
append_urls(&mut content, &self.media);
|
||||
|
||||
let mut builder = add_client_tag(NoteBuilder::new()).kind(1).content(&content);
|
||||
|
||||
for hashtag in Self::extract_hashtags(&self.content) {
|
||||
builder = builder.start_tag().tag_str("t").tag_str(&hashtag);
|
||||
}
|
||||
|
||||
if !self.media.is_empty() {
|
||||
builder = add_imeta_tags(builder, &self.media);
|
||||
}
|
||||
|
||||
builder.sign(seckey).build().expect("note should be ok")
|
||||
}
|
||||
|
||||
pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note {
|
||||
let builder = add_client_tag(NoteBuilder::new())
|
||||
.kind(1)
|
||||
.content(&self.content);
|
||||
let mut content = self.content.clone();
|
||||
append_urls(&mut content, &self.media);
|
||||
|
||||
let builder = add_client_tag(NoteBuilder::new()).kind(1).content(&content);
|
||||
|
||||
let nip10 = NoteReply::new(replying_to.tags());
|
||||
|
||||
@@ -96,6 +109,10 @@ impl NewPost {
|
||||
builder = builder.start_tag().tag_str("p").tag_str(&hex::encode(id));
|
||||
}
|
||||
|
||||
if !self.media.is_empty() {
|
||||
builder = add_imeta_tags(builder, &self.media);
|
||||
}
|
||||
|
||||
builder
|
||||
.sign(seckey)
|
||||
.build()
|
||||
@@ -103,18 +120,24 @@ impl NewPost {
|
||||
}
|
||||
|
||||
pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note {
|
||||
let new_content = format!(
|
||||
let mut new_content = format!(
|
||||
"{}\nnostr:{}",
|
||||
self.content,
|
||||
enostr::NoteId::new(*quoting.id()).to_bech().unwrap()
|
||||
);
|
||||
|
||||
append_urls(&mut new_content, &self.media);
|
||||
|
||||
let mut builder = NoteBuilder::new().kind(1).content(&new_content);
|
||||
|
||||
for hashtag in Self::extract_hashtags(&self.content) {
|
||||
builder = builder.start_tag().tag_str("t").tag_str(&hashtag);
|
||||
}
|
||||
|
||||
if !self.media.is_empty() {
|
||||
builder = add_imeta_tags(builder, &self.media);
|
||||
}
|
||||
|
||||
builder
|
||||
.start_tag()
|
||||
.tag_str("q")
|
||||
@@ -143,6 +166,43 @@ impl NewPost {
|
||||
}
|
||||
}
|
||||
|
||||
fn append_urls(content: &mut String, media: &Vec<Nip94Event>) {
|
||||
for ev in media {
|
||||
content.push(' ');
|
||||
content.push_str(&ev.url);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_imeta_tags<'a>(builder: NoteBuilder<'a>, media: &Vec<Nip94Event>) -> NoteBuilder<'a> {
|
||||
let mut builder = builder;
|
||||
for item in media {
|
||||
builder = builder
|
||||
.start_tag()
|
||||
.tag_str("imeta")
|
||||
.tag_str(&format!("url {}", item.url));
|
||||
|
||||
if let Some(ox) = &item.ox {
|
||||
builder = builder.tag_str(&format!("ox {ox}"));
|
||||
};
|
||||
if let Some(x) = &item.x {
|
||||
builder = builder.tag_str(&format!("x {x}"));
|
||||
}
|
||||
if let Some(media_type) = &item.media_type {
|
||||
builder = builder.tag_str(&format!("m {media_type}"));
|
||||
}
|
||||
if let Some(dims) = &item.dimensions {
|
||||
builder = builder.tag_str(&format!("dim {}x{}", dims.0, dims.1));
|
||||
}
|
||||
if let Some(bh) = &item.blurhash {
|
||||
builder = builder.tag_str(&format!("blurhash {bh}"));
|
||||
}
|
||||
if let Some(thumb) = &item.thumb {
|
||||
builder = builder.tag_str(&format!("thumb {thumb}"));
|
||||
}
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::draft::{Draft, Drafts};
|
||||
use crate::images::fetch_img;
|
||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
||||
use crate::post::NewPost;
|
||||
use crate::ui::{self, Preview, PreviewConfig};
|
||||
use crate::Result;
|
||||
use egui::widgets::text_edit::TextEdit;
|
||||
use egui::{Frame, Layout};
|
||||
use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense};
|
||||
use enostr::{FilledKeypair, FullKeypair, NoteId, RelayPool};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
|
||||
use notedeck::{ImageCache, NoteCache};
|
||||
use tracing::error;
|
||||
|
||||
use super::contents::render_note_preview;
|
||||
|
||||
@@ -156,7 +159,6 @@ impl<'a> PostView<'a> {
|
||||
let stroke = if focused {
|
||||
ui.visuals().selection.stroke
|
||||
} else {
|
||||
//ui.visuals().selection.stroke
|
||||
ui.visuals().noninteractive().bg_stroke
|
||||
};
|
||||
|
||||
@@ -181,27 +183,48 @@ impl<'a> PostView<'a> {
|
||||
ui.vertical(|ui| {
|
||||
let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner;
|
||||
|
||||
if let PostType::Quote(id) = self.post_type {
|
||||
let avail_size = ui.available_size_before_wrap();
|
||||
ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| {
|
||||
Frame::none().show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_max_width(avail_size.x * 0.8);
|
||||
render_note_preview(
|
||||
ui,
|
||||
self.ndb,
|
||||
self.note_cache,
|
||||
self.img_cache,
|
||||
txn,
|
||||
id.bytes(),
|
||||
nostrdb::NoteKey::new(0),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(0.0, 8.0))
|
||||
.show(ui, |ui| {
|
||||
ScrollArea::horizontal().show(ui, |ui| {
|
||||
ui.with_layout(Layout::left_to_right(egui::Align::Min), |ui| {
|
||||
ui.add_space(4.0);
|
||||
self.show_media(ui);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.transfer_uploads(ui);
|
||||
self.show_upload_errors(ui);
|
||||
|
||||
let action = ui
|
||||
.horizontal(|ui| {
|
||||
if let PostType::Quote(id) = self.post_type {
|
||||
let avail_size = ui.available_size_before_wrap();
|
||||
ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| {
|
||||
Frame::none().show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_max_width(avail_size.x * 0.8);
|
||||
render_note_preview(
|
||||
ui,
|
||||
self.ndb,
|
||||
self.note_cache,
|
||||
self.img_cache,
|
||||
txn,
|
||||
id.bytes(),
|
||||
nostrdb::NoteKey::new(0),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.with_layout(
|
||||
egui::Layout::left_to_right(egui::Align::BOTTOM),
|
||||
|ui| {
|
||||
self.show_upload_media_button(ui);
|
||||
},
|
||||
);
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
|
||||
if ui
|
||||
@@ -214,6 +237,7 @@ impl<'a> PostView<'a> {
|
||||
let new_post = NewPost::new(
|
||||
self.draft.buffer.clone(),
|
||||
self.poster.to_full(),
|
||||
self.draft.uploaded_media.clone(),
|
||||
);
|
||||
Some(PostAction::new(self.post_type.clone(), new_post))
|
||||
} else {
|
||||
@@ -233,6 +257,134 @@ impl<'a> PostView<'a> {
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
fn show_media(&mut self, ui: &mut egui::Ui) {
|
||||
let mut to_remove = Vec::new();
|
||||
for (i, media) in self.draft.uploaded_media.iter().enumerate() {
|
||||
let (width, height) = if let Some(dims) = media.dimensions {
|
||||
(dims.0, dims.1)
|
||||
} else {
|
||||
(300, 300)
|
||||
};
|
||||
let m_cached_promise = self.img_cache.map().get(&media.url);
|
||||
if m_cached_promise.is_none() {
|
||||
let promise = fetch_img(
|
||||
&self.img_cache,
|
||||
ui.ctx(),
|
||||
&media.url,
|
||||
crate::images::ImageType::Content(width, height),
|
||||
);
|
||||
self.img_cache
|
||||
.map_mut()
|
||||
.insert(media.url.to_owned(), promise);
|
||||
}
|
||||
|
||||
match self.img_cache.map()[&media.url].ready() {
|
||||
Some(Ok(texture)) => {
|
||||
let media_size = vec2(width as f32, height as f32);
|
||||
let max_size = vec2(300.0, 300.0);
|
||||
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
|
||||
max_size
|
||||
} else {
|
||||
media_size
|
||||
};
|
||||
|
||||
let img_resp = ui.add(egui::Image::new(texture).max_size(size).rounding(12.0));
|
||||
|
||||
let remove_button_rect = {
|
||||
let top_left = img_resp.rect.left_top();
|
||||
let spacing = 13.0;
|
||||
let center = Pos2::new(top_left.x + spacing, top_left.y + spacing);
|
||||
egui::Rect::from_center_size(center, egui::vec2(26.0, 26.0))
|
||||
};
|
||||
if show_remove_upload_button(ui, remove_button_rect).clicked() {
|
||||
to_remove.push(i);
|
||||
}
|
||||
ui.advance_cursor_after_rect(img_resp.rect);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
self.draft.upload_errors.push(e.to_string());
|
||||
error!("{e}");
|
||||
}
|
||||
None => {
|
||||
ui.spinner();
|
||||
}
|
||||
}
|
||||
}
|
||||
to_remove.reverse();
|
||||
for i in to_remove {
|
||||
self.draft.uploaded_media.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_upload_media_button(&mut self, ui: &mut egui::Ui) {
|
||||
if ui.add(media_upload_button()).clicked() {
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
if let Some(file) = rfd::FileDialog::new().pick_file() {
|
||||
match MediaPath::new(file) {
|
||||
Ok(media_path) => {
|
||||
let promise = nostrbuild_nip96_upload(
|
||||
self.poster.secret_key.secret_bytes(),
|
||||
media_path,
|
||||
);
|
||||
self.draft.uploading_media.push(promise);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
self.draft.upload_errors.push(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_uploads(&mut self, ui: &mut egui::Ui) {
|
||||
let mut indexes_to_remove = Vec::new();
|
||||
for (i, promise) in self.draft.uploading_media.iter().enumerate() {
|
||||
match promise.ready() {
|
||||
Some(Ok(media)) => {
|
||||
self.draft.uploaded_media.push(media.clone());
|
||||
indexes_to_remove.push(i);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
self.draft.upload_errors.push(e.to_string());
|
||||
error!("{e}");
|
||||
}
|
||||
None => {
|
||||
ui.spinner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indexes_to_remove.reverse();
|
||||
for i in indexes_to_remove {
|
||||
let _ = self.draft.uploading_media.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_upload_errors(&mut self, ui: &mut egui::Ui) {
|
||||
let mut to_remove = Vec::new();
|
||||
for (i, error) in self.draft.upload_errors.iter().enumerate() {
|
||||
if ui
|
||||
.add(
|
||||
egui::Label::new(egui::RichText::new(error).color(ui.visuals().warn_fg_color))
|
||||
.sense(Sense::click())
|
||||
.selectable(false),
|
||||
)
|
||||
.on_hover_text_at_pointer("Dismiss")
|
||||
.clicked()
|
||||
{
|
||||
to_remove.push(i);
|
||||
}
|
||||
}
|
||||
to_remove.reverse();
|
||||
|
||||
for i in to_remove {
|
||||
self.draft.upload_errors.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn post_button(interactive: bool) -> impl egui::Widget {
|
||||
@@ -252,7 +404,86 @@ fn post_button(interactive: bool) -> impl egui::Widget {
|
||||
}
|
||||
}
|
||||
|
||||
fn media_upload_button() -> impl egui::Widget {
|
||||
|ui: &mut egui::Ui| -> egui::Response {
|
||||
let resp = ui.allocate_response(egui::vec2(32.0, 32.0), egui::Sense::click());
|
||||
let painter = ui.painter();
|
||||
let (fill_color, stroke) = if resp.hovered() {
|
||||
(
|
||||
ui.visuals().widgets.hovered.bg_fill,
|
||||
ui.visuals().widgets.hovered.bg_stroke,
|
||||
)
|
||||
} else if resp.clicked() {
|
||||
(
|
||||
ui.visuals().widgets.active.bg_fill,
|
||||
ui.visuals().widgets.active.bg_stroke,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
ui.visuals().widgets.inactive.bg_fill,
|
||||
ui.visuals().widgets.inactive.bg_stroke,
|
||||
)
|
||||
};
|
||||
|
||||
painter.rect_filled(resp.rect, 8.0, fill_color);
|
||||
painter.rect_stroke(resp.rect, 8.0, stroke);
|
||||
egui::Image::new(egui::include_image!(
|
||||
"../../../../../assets/icons/media_upload_dark_4x.png"
|
||||
))
|
||||
.max_size(egui::vec2(16.0, 16.0))
|
||||
.paint_at(ui, resp.rect.shrink(8.0));
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn show_remove_upload_button(ui: &mut egui::Ui, desired_rect: egui::Rect) -> egui::Response {
|
||||
let resp = ui.allocate_rect(desired_rect, egui::Sense::click());
|
||||
let size = 24.0;
|
||||
let (fill_color, stroke) = if resp.hovered() {
|
||||
(
|
||||
ui.visuals().widgets.hovered.bg_fill,
|
||||
ui.visuals().widgets.hovered.bg_stroke,
|
||||
)
|
||||
} else if resp.clicked() {
|
||||
(
|
||||
ui.visuals().widgets.active.bg_fill,
|
||||
ui.visuals().widgets.active.bg_stroke,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
ui.visuals().widgets.inactive.bg_fill,
|
||||
ui.visuals().widgets.inactive.bg_stroke,
|
||||
)
|
||||
};
|
||||
let center = desired_rect.center();
|
||||
let painter = ui.painter_at(desired_rect);
|
||||
let radius = size / 2.0;
|
||||
|
||||
painter.circle_filled(center, radius, fill_color);
|
||||
painter.circle_stroke(center, radius, stroke);
|
||||
|
||||
painter.line_segment(
|
||||
[
|
||||
Pos2::new(center.x - 4.0, center.y - 4.0),
|
||||
Pos2::new(center.x + 4.0, center.y + 4.0),
|
||||
],
|
||||
egui::Stroke::new(1.33, ui.visuals().text_color()),
|
||||
);
|
||||
|
||||
painter.line_segment(
|
||||
[
|
||||
Pos2::new(center.x + 4.0, center.y - 4.0),
|
||||
Pos2::new(center.x - 4.0, center.y + 4.0),
|
||||
],
|
||||
egui::Stroke::new(1.33, ui.visuals().text_color()),
|
||||
);
|
||||
resp
|
||||
}
|
||||
|
||||
mod preview {
|
||||
|
||||
use crate::media_upload::Nip94Event;
|
||||
|
||||
use super::*;
|
||||
use notedeck::{App, AppContext};
|
||||
|
||||
@@ -263,8 +494,30 @@ mod preview {
|
||||
|
||||
impl PostPreview {
|
||||
fn new() -> Self {
|
||||
let mut draft = Draft::new();
|
||||
// can use any url here
|
||||
draft.uploaded_media.push(Nip94Event::new(
|
||||
"https://image.nostr.build/41b40657dd6abf7c275dffc86b29bd863e9337a74870d4ee1c33a72a91c9d733.jpg".to_owned(),
|
||||
612,
|
||||
407,
|
||||
));
|
||||
draft.uploaded_media.push(Nip94Event::new(
|
||||
"https://image.nostr.build/thumb/fdb46182b039d29af0f5eac084d4d30cd4ad2580ea04fe6c7e79acfe095f9852.png".to_owned(),
|
||||
80,
|
||||
80,
|
||||
));
|
||||
draft.uploaded_media.push(Nip94Event::new(
|
||||
"https://i.nostr.build/7EznpHsnBZ36Akju.png".to_owned(),
|
||||
2438,
|
||||
1476,
|
||||
));
|
||||
draft.uploaded_media.push(Nip94Event::new(
|
||||
"https://i.nostr.build/qCCw8szrjTydTiMV.png".to_owned(),
|
||||
2002,
|
||||
2272,
|
||||
));
|
||||
PostPreview {
|
||||
draft: Draft::new(),
|
||||
draft,
|
||||
poster: FullKeypair::generate(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user