Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c1d3be4c07
|
|||
| 6a08d4b1b2 | |||
| d6d7e4c35e | |||
| c3499729f2 | |||
| dac786e60f | |||
| 41aa2db3c7 | |||
| 10225158e5 | |||
| 557608db9b | |||
| 8697a5cb0a | |||
| 7aca39aae8 | |||
| aa467b9be0 | |||
| 09eeb57bd9 | |||
| b1a5dd6cab | |||
| d12e5b363c | |||
| cc8bafddff | |||
| 3766308ce6 | |||
| 17f72f6127 | |||
| f592015c0c | |||
| 1ab4eeb48c | |||
| a8c6baeacb | |||
| a896a6ecfa | |||
| f282363748 | |||
| ba76b20ad2 | |||
| b04f50a9f6 | |||
| 233be47659 | |||
| 173972f920 | |||
| 31ec21ea02 | |||
| d3d8d7be4b | |||
| 09dc101c1b | |||
| 261477339b | |||
| 9ff5753bca | |||
| b9e2fe5dd1 | |||
| d1a9e0020e | |||
| 1163dd8461 | |||
| 692f4889cf | |||
| f2153f53dc | |||
| 40764d7368 | |||
| be720c0f76 | |||
| 5848f1c355 | |||
| 0dcf70bc15 | |||
| 0fc8e70180 | |||
| 2de6851fbd | |||
| f57d582307 | |||
| 09e608ca75 | |||
| 2bd636ce0a | |||
| 79bf6cf126 | |||
| b8207106d7 | |||
| 5280028a82 | |||
| f4a6e8f9bb | |||
| 83fd6de076 | |||
| b80a0ab0f1 | |||
| e437a0db1c | |||
| 6e81b98d2f | |||
| 217f1e45da | |||
| 96e0366787 | |||
| 2a85ee562c | |||
| 1fabd347ca | |||
| 0087fe7dff | |||
| 51f7744149 | |||
| 6d393c9c37 | |||
| 5c8ab0ce07 | |||
| 590ffa0680 | |||
| 3d18db8fd2 | |||
| 661acb3a12 | |||
| 8306003f6f | |||
| f2e01f0e40 | |||
| 0f00dcf7a7 |
@@ -21,3 +21,5 @@ scripts/macos_build_secrets.sh
|
|||||||
/tags
|
/tags
|
||||||
.zed
|
.zed
|
||||||
.lsp
|
.lsp
|
||||||
|
.idea
|
||||||
|
local.properties
|
||||||
Generated
+268
-68
@@ -765,6 +765,25 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-sys"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
|
||||||
|
dependencies = [
|
||||||
|
"objc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
|
||||||
|
dependencies = [
|
||||||
|
"block-sys",
|
||||||
|
"objc2 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -989,6 +1008,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -1244,6 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1389,20 +1410,26 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eframe"
|
name = "eframe"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1438,24 +1465,25 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui"
|
name = "egui"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"ahash",
|
"ahash",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||||
"epaint",
|
"epaint",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"profiling",
|
"profiling",
|
||||||
"serde",
|
"serde",
|
||||||
|
"similar",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1474,7 +1502,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-winit"
|
name = "egui-winit"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"arboard",
|
"arboard",
|
||||||
@@ -1492,7 +1520,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_extras"
|
name = "egui_extras"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -1509,7 +1537,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_glow"
|
name = "egui_glow"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1588,7 +1616,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "emath"
|
name = "emath"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1606,7 +1634,7 @@ version = "0.3.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bech32",
|
"bech32",
|
||||||
"ewebsock",
|
"ewebsock",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"mio",
|
"mio",
|
||||||
"nostr 0.37.0",
|
"nostr 0.37.0",
|
||||||
@@ -1686,13 +1714,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint"
|
name = "epaint"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"ecolor",
|
"ecolor",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||||
"epaint_default_fonts",
|
"epaint_default_fonts",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -1704,7 +1732,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint_default_fonts"
|
name = "epaint_default_fonts"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equator"
|
name = "equator"
|
||||||
@@ -2280,7 +2308,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"gpu-descriptor-types",
|
"gpu-descriptor-types",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2302,6 +2330,12 @@ dependencies = [
|
|||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.4"
|
version = "0.15.4"
|
||||||
@@ -2346,6 +2380,17 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex_color"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex_lit"
|
name = "hex_lit"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2507,6 +2552,16 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icrate"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
|
||||||
|
dependencies = [
|
||||||
|
"block2 0.4.0",
|
||||||
|
"objc2 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -2665,6 +2720,17 @@ version = "1.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -2672,7 +2738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2744,25 +2810,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is-docker"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is-wsl"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
|
||||||
dependencies = [
|
|
||||||
"is-docker",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
@@ -2879,6 +2926,19 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsoncanvas"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "git+https://github.com/jb55/jsoncanvas?rev=ae60f96e4d022cf037e086b793cacc3225bc14e5#ae60f96e4d022cf037e086b793cacc3225bc14e5"
|
||||||
|
dependencies = [
|
||||||
|
"hex_color",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos-egl"
|
name = "khronos-egl"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -3201,7 +3261,7 @@ dependencies = [
|
|||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"hexf-parse",
|
"hexf-parse",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"log",
|
"log",
|
||||||
"rustc-hash 1.1.0",
|
"rustc-hash 1.1.0",
|
||||||
"spirv",
|
"spirv",
|
||||||
@@ -3418,21 +3478,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck"
|
name = "notedeck"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base32",
|
"base32",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"blurhash",
|
||||||
"dirs",
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
|
"egui_extras",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"fluent",
|
"fluent",
|
||||||
"fluent-langneg",
|
"fluent-langneg",
|
||||||
"fluent-resmgr",
|
"fluent-resmgr",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -3454,6 +3517,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
|
"sys-locale",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokenator",
|
"tokenator",
|
||||||
@@ -3466,7 +3530,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_chrome"
|
name = "notedeck_chrome"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -3477,6 +3541,7 @@ dependencies = [
|
|||||||
"notedeck",
|
"notedeck",
|
||||||
"notedeck_columns",
|
"notedeck_columns",
|
||||||
"notedeck_dave",
|
"notedeck_dave",
|
||||||
|
"notedeck_notebook",
|
||||||
"notedeck_ui",
|
"notedeck_ui",
|
||||||
"profiling",
|
"profiling",
|
||||||
"puffin",
|
"puffin",
|
||||||
@@ -3495,7 +3560,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_columns"
|
name = "notedeck_columns"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
@@ -3510,16 +3575,15 @@ dependencies = [
|
|||||||
"egui_virtual_list",
|
"egui_virtual_list",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"human_format",
|
"human_format",
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
"notedeck_ui",
|
"notedeck_ui",
|
||||||
"oot_bitset",
|
"oot_bitset",
|
||||||
"open",
|
|
||||||
"opener",
|
"opener",
|
||||||
"poll-promise",
|
"poll-promise",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -3528,6 +3592,7 @@ dependencies = [
|
|||||||
"puffin_egui",
|
"puffin_egui",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rmpv",
|
"rmpv",
|
||||||
|
"robius-open",
|
||||||
"security-framework 2.11.1",
|
"security-framework 2.11.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -3549,7 +3614,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_dave"
|
name = "notedeck_dave"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-openai",
|
"async-openai",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -3571,19 +3636,27 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notedeck_notebook"
|
||||||
|
version = "0.5.9"
|
||||||
|
dependencies = [
|
||||||
|
"egui",
|
||||||
|
"jsoncanvas",
|
||||||
|
"notedeck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_ui"
|
name = "notedeck_ui"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"blurhash",
|
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"image",
|
"image",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
@@ -4012,17 +4085,6 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "open"
|
|
||||||
version = "5.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
|
|
||||||
dependencies = [
|
|
||||||
"is-wsl",
|
|
||||||
"libc",
|
|
||||||
"pathdiff",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opener"
|
name = "opener"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -4135,12 +4197,6 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathdiff"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
@@ -4429,7 +4485,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"natord",
|
"natord",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -4757,6 +4813,26 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
|
||||||
|
dependencies = [
|
||||||
|
"ref-cast-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast-impl"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -4948,6 +5024,30 @@ dependencies = [
|
|||||||
"rmp",
|
"rmp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robius-android-env"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef"
|
||||||
|
dependencies = [
|
||||||
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ndk-context",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robius-open"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "243e2abbc8c1ca8ddc283056d4675b67e452fd527c3741c5318642da37840ff3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"icrate",
|
||||||
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"objc2 0.5.2",
|
||||||
|
"robius-android-env",
|
||||||
|
"windows 0.54.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@@ -5094,6 +5194,30 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -5247,7 +5371,7 @@ version = "1.0.140"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -5286,6 +5410,38 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.9.0",
|
||||||
|
"schemars 0.9.0",
|
||||||
|
"schemars 1.0.4",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -5347,6 +5503,12 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "similar"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simplecss"
|
name = "simplecss"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -5560,6 +5722,15 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sys-locale"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.30.13"
|
version = "0.30.13"
|
||||||
@@ -5880,7 +6051,7 @@ version = "0.22.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
@@ -6663,7 +6834,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"document-features",
|
"document-features",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"log",
|
"log",
|
||||||
"naga",
|
"naga",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -6784,6 +6955,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.54.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -6803,6 +6984,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result 0.1.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -6879,6 +7070,15 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
+13
-8
@@ -1,11 +1,12 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
package.version = "0.5.8"
|
package.version = "0.5.9"
|
||||||
members = [
|
members = [
|
||||||
"crates/notedeck",
|
"crates/notedeck",
|
||||||
"crates/notedeck_chrome",
|
"crates/notedeck_chrome",
|
||||||
"crates/notedeck_columns",
|
"crates/notedeck_columns",
|
||||||
"crates/notedeck_dave",
|
"crates/notedeck_dave",
|
||||||
|
"crates/notedeck_notebook",
|
||||||
"crates/notedeck_ui",
|
"crates/notedeck_ui",
|
||||||
|
|
||||||
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
|
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
|
||||||
@@ -48,10 +49,11 @@ notedeck = { path = "crates/notedeck" }
|
|||||||
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
||||||
notedeck_columns = { path = "crates/notedeck_columns" }
|
notedeck_columns = { path = "crates/notedeck_columns" }
|
||||||
notedeck_dave = { path = "crates/notedeck_dave" }
|
notedeck_dave = { path = "crates/notedeck_dave" }
|
||||||
|
notedeck_notebook = { path = "crates/notedeck_notebook" }
|
||||||
notedeck_ui = { path = "crates/notedeck_ui" }
|
notedeck_ui = { path = "crates/notedeck_ui" }
|
||||||
tokenator = { path = "crates/tokenator" }
|
tokenator = { path = "crates/tokenator" }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
open = "5.3.0"
|
robius-open = "0.1"
|
||||||
poll-promise = { version = "0.3.0", features = ["tokio"] }
|
poll-promise = { version = "0.3.0", features = ["tokio"] }
|
||||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
@@ -67,6 +69,7 @@ tracing-appender = "0.2.3"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
unic-langid = { version = "0.9.6", features = ["macros"] }
|
unic-langid = { version = "0.9.6", features = ["macros"] }
|
||||||
|
sys-locale = "0.3"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = { version = "1.10.0", features = ["v4"] }
|
uuid = { version = "1.10.0", features = ["v4"] }
|
||||||
@@ -82,6 +85,8 @@ hashbrown = "0.15.2"
|
|||||||
openai-api-rs = "6.0.3"
|
openai-api-rs = "6.0.3"
|
||||||
re_memory = "0.23.4"
|
re_memory = "0.23.4"
|
||||||
oot_bitset = "0.1.1"
|
oot_bitset = "0.1.1"
|
||||||
|
blurhash = "0.2.3"
|
||||||
|
|
||||||
|
|
||||||
[profile.small]
|
[profile.small]
|
||||||
inherits = 'release'
|
inherits = 'release'
|
||||||
@@ -99,12 +104,12 @@ strip = true # Strip symbols from binary*
|
|||||||
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
||||||
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
||||||
|
|
||||||
egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
|
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ nostrdb = { workspace = true }
|
|||||||
jni = { workspace = true }
|
jni = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
|
blurhash = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
strum_macros = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
enostr = { workspace = true }
|
enostr = { workspace = true }
|
||||||
nostr = { workspace = true }
|
nostr = { workspace = true }
|
||||||
egui = { workspace = true }
|
egui = { workspace = true }
|
||||||
|
egui_extras = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
base32 = { workspace = true }
|
base32 = { workspace = true }
|
||||||
@@ -43,8 +45,10 @@ fluent = { workspace = true }
|
|||||||
fluent-resmgr = { workspace = true }
|
fluent-resmgr = { workspace = true }
|
||||||
fluent-langneg = { workspace = true }
|
fluent-langneg = { workspace = true }
|
||||||
unic-langid = { workspace = true }
|
unic-langid = { workspace = true }
|
||||||
|
sys-locale = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
|
bitflags = { workspace = true }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
+79
-21
@@ -1,13 +1,14 @@
|
|||||||
use crate::account::FALLBACK_PUBKEY;
|
use crate::account::FALLBACK_PUBKEY;
|
||||||
use crate::i18n::Localization;
|
use crate::i18n::Localization;
|
||||||
use crate::persist::{AppSizeHandler, ZoomHandler};
|
use crate::persist::{AppSizeHandler, SettingsHandler};
|
||||||
use crate::wallet::GlobalWallet;
|
use crate::wallet::GlobalWallet;
|
||||||
use crate::zaps::Zaps;
|
use crate::zaps::Zaps;
|
||||||
|
use crate::Error;
|
||||||
use crate::JobPool;
|
use crate::JobPool;
|
||||||
|
use crate::NotedeckOptions;
|
||||||
use crate::{
|
use crate::{
|
||||||
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
||||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
|
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
|
||||||
UnknownIds,
|
|
||||||
};
|
};
|
||||||
use egui::Margin;
|
use egui::Margin;
|
||||||
use egui::ThemePreference;
|
use egui::ThemePreference;
|
||||||
@@ -19,6 +20,7 @@ use std::collections::BTreeSet;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||||
|
|
||||||
pub enum AppAction {
|
pub enum AppAction {
|
||||||
Note(NoteAction),
|
Note(NoteAction),
|
||||||
@@ -40,9 +42,8 @@ pub struct Notedeck {
|
|||||||
global_wallet: GlobalWallet,
|
global_wallet: GlobalWallet,
|
||||||
path: DataPath,
|
path: DataPath,
|
||||||
args: Args,
|
args: Args,
|
||||||
theme: ThemeHandler,
|
settings: SettingsHandler,
|
||||||
app: Option<Rc<RefCell<dyn App>>>,
|
app: Option<Rc<RefCell<dyn App>>>,
|
||||||
zoom: ZoomHandler,
|
|
||||||
app_size: AppSizeHandler,
|
app_size: AppSizeHandler,
|
||||||
unrecognized_args: BTreeSet<String>,
|
unrecognized_args: BTreeSet<String>,
|
||||||
clipboard: Clipboard,
|
clipboard: Clipboard,
|
||||||
@@ -99,10 +100,18 @@ impl eframe::App for Notedeck {
|
|||||||
|
|
||||||
render_notedeck(self, ctx);
|
render_notedeck(self, ctx);
|
||||||
|
|
||||||
self.zoom.try_save_zoom_factor(ctx);
|
self.settings.update_batch(|settings| {
|
||||||
|
settings.zoom_factor = ctx.zoom_factor();
|
||||||
|
settings.locale = self.i18n.get_current_locale().to_string();
|
||||||
|
settings.theme = if ctx.style().visuals.dark_mode {
|
||||||
|
ThemePreference::Dark
|
||||||
|
} else {
|
||||||
|
ThemePreference::Light
|
||||||
|
};
|
||||||
|
});
|
||||||
self.app_size.try_save_app_size(ctx);
|
self.app_size.try_save_app_size(ctx);
|
||||||
|
|
||||||
if self.args.relay_debug {
|
if self.args.options.contains(NotedeckOptions::RelayDebug) {
|
||||||
if self.pool.debug.is_none() {
|
if self.pool.debug.is_none() {
|
||||||
self.pool.use_debug();
|
self.pool.use_debug();
|
||||||
}
|
}
|
||||||
@@ -159,10 +168,11 @@ impl Notedeck {
|
|||||||
1024usize * 1024usize * 1024usize * 1024usize
|
1024usize * 1024usize * 1024usize * 1024usize
|
||||||
};
|
};
|
||||||
|
|
||||||
let theme = ThemeHandler::new(&path);
|
let settings = SettingsHandler::new(&path).load();
|
||||||
|
|
||||||
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
|
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
|
||||||
|
|
||||||
let keystore = if parsed_args.use_keystore {
|
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
|
||||||
let keys_path = path.path(DataPathType::Keys);
|
let keys_path = path.path(DataPathType::Keys);
|
||||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||||
Some(AccountStorage::new(
|
Some(AccountStorage::new(
|
||||||
@@ -213,12 +223,8 @@ impl Notedeck {
|
|||||||
|
|
||||||
let img_cache = Images::new(img_cache_dir);
|
let img_cache = Images::new(img_cache_dir);
|
||||||
let note_cache = NoteCache::default();
|
let note_cache = NoteCache::default();
|
||||||
let zoom = ZoomHandler::new(&path);
|
|
||||||
let app_size = AppSizeHandler::new(&path);
|
|
||||||
|
|
||||||
if let Some(z) = zoom.get_zoom_factor() {
|
let app_size = AppSizeHandler::new(&path);
|
||||||
ctx.set_zoom_factor(z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrate
|
// migrate
|
||||||
if let Err(e) = img_cache.migrate_v0() {
|
if let Err(e) = img_cache.migrate_v0() {
|
||||||
@@ -231,15 +237,22 @@ impl Notedeck {
|
|||||||
|
|
||||||
// Initialize localization
|
// Initialize localization
|
||||||
let mut i18n = Localization::new();
|
let mut i18n = Localization::new();
|
||||||
|
|
||||||
|
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
|
||||||
|
settings.locale().parse();
|
||||||
|
|
||||||
|
if setting_locale.is_ok() {
|
||||||
|
if let Err(err) = i18n.set_locale(setting_locale.unwrap()) {
|
||||||
|
error!("{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(locale) = &parsed_args.locale {
|
if let Some(locale) = &parsed_args.locale {
|
||||||
if let Err(err) = i18n.set_locale(locale.to_owned()) {
|
if let Err(err) = i18n.set_locale(locale.to_owned()) {
|
||||||
error!("{err}");
|
error!("{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize global i18n context
|
|
||||||
//crate::i18n::init_global_i18n(i18n.clone());
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ndb,
|
ndb,
|
||||||
img_cache,
|
img_cache,
|
||||||
@@ -250,9 +263,8 @@ impl Notedeck {
|
|||||||
global_wallet,
|
global_wallet,
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
args: parsed_args,
|
args: parsed_args,
|
||||||
theme,
|
settings,
|
||||||
app: None,
|
app: None,
|
||||||
zoom,
|
|
||||||
app_size,
|
app_size,
|
||||||
unrecognized_args,
|
unrecognized_args,
|
||||||
frame_history: FrameHistory::default(),
|
frame_history: FrameHistory::default(),
|
||||||
@@ -263,6 +275,44 @@ impl Notedeck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Setup egui context
|
||||||
|
pub fn setup(&self, ctx: &egui::Context) {
|
||||||
|
// Initialize global i18n context
|
||||||
|
//crate::i18n::init_global_i18n(i18n.clone());
|
||||||
|
crate::setup::setup_egui_context(
|
||||||
|
ctx,
|
||||||
|
self.args.options,
|
||||||
|
self.theme(),
|
||||||
|
self.note_body_font_size(),
|
||||||
|
self.zoom_factor(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ensure we recognized all the arguments
|
||||||
|
pub fn check_args(&self, other_app_args: &BTreeSet<String>) -> Result<(), Error> {
|
||||||
|
let completely_unrecognized: Vec<String> = self
|
||||||
|
.unrecognized_args()
|
||||||
|
.intersection(other_app_args)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
if !completely_unrecognized.is_empty() {
|
||||||
|
let err = format!("Unrecognized arguments: {:?}", completely_unrecognized);
|
||||||
|
tracing::error!("{}", &err);
|
||||||
|
return Err(Error::Generic(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn options(&self) -> NotedeckOptions {
|
||||||
|
self.args.options
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_option(&self, option: NotedeckOptions) -> bool {
|
||||||
|
self.options().contains(option)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
|
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
|
||||||
self.set_app(app);
|
self.set_app(app);
|
||||||
self
|
self
|
||||||
@@ -279,7 +329,7 @@ impl Notedeck {
|
|||||||
global_wallet: &mut self.global_wallet,
|
global_wallet: &mut self.global_wallet,
|
||||||
path: &self.path,
|
path: &self.path,
|
||||||
args: &self.args,
|
args: &self.args,
|
||||||
theme: &mut self.theme,
|
settings: &mut self.settings,
|
||||||
clipboard: &mut self.clipboard,
|
clipboard: &mut self.clipboard,
|
||||||
zaps: &mut self.zaps,
|
zaps: &mut self.zaps,
|
||||||
frame_history: &mut self.frame_history,
|
frame_history: &mut self.frame_history,
|
||||||
@@ -297,7 +347,15 @@ impl Notedeck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn theme(&self) -> ThemePreference {
|
pub fn theme(&self) -> ThemePreference {
|
||||||
self.theme.load()
|
self.settings.theme()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_body_font_size(&self) -> f32 {
|
||||||
|
self.settings.note_body_font_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_factor(&self) -> f32 {
|
||||||
|
self.settings.zoom_factor()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
||||||
|
|||||||
+14
-26
@@ -1,23 +1,15 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use crate::NotedeckOptions;
|
||||||
use enostr::{Keypair, Pubkey, SecretKey};
|
use enostr::{Keypair, Pubkey, SecretKey};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||||
|
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
pub relays: Vec<String>,
|
pub relays: Vec<String>,
|
||||||
pub is_mobile: Option<bool>,
|
|
||||||
pub locale: Option<LanguageIdentifier>,
|
pub locale: Option<LanguageIdentifier>,
|
||||||
pub show_note_client: bool,
|
|
||||||
pub keys: Vec<Keypair>,
|
pub keys: Vec<Keypair>,
|
||||||
pub light: bool,
|
pub options: NotedeckOptions,
|
||||||
pub debug: bool,
|
|
||||||
pub relay_debug: bool,
|
|
||||||
|
|
||||||
/// Enable when running tests so we don't panic on app startup
|
|
||||||
pub tests: bool,
|
|
||||||
|
|
||||||
pub use_keystore: bool,
|
|
||||||
pub dbpath: Option<String>,
|
pub dbpath: Option<String>,
|
||||||
pub datapath: Option<String>,
|
pub datapath: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -28,14 +20,8 @@ impl Args {
|
|||||||
let mut unrecognized_args = BTreeSet::new();
|
let mut unrecognized_args = BTreeSet::new();
|
||||||
let mut res = Args {
|
let mut res = Args {
|
||||||
relays: vec![],
|
relays: vec![],
|
||||||
is_mobile: None,
|
|
||||||
keys: vec![],
|
keys: vec![],
|
||||||
light: false,
|
options: NotedeckOptions::default(),
|
||||||
show_note_client: false,
|
|
||||||
debug: false,
|
|
||||||
relay_debug: false,
|
|
||||||
tests: false,
|
|
||||||
use_keystore: true,
|
|
||||||
dbpath: None,
|
dbpath: None,
|
||||||
datapath: None,
|
datapath: None,
|
||||||
locale: None,
|
locale: None,
|
||||||
@@ -47,9 +33,9 @@ impl Args {
|
|||||||
let arg = &args[i];
|
let arg = &args[i];
|
||||||
|
|
||||||
if arg == "--mobile" {
|
if arg == "--mobile" {
|
||||||
res.is_mobile = Some(true);
|
res.options.set(NotedeckOptions::Mobile, true);
|
||||||
} else if arg == "--light" {
|
} else if arg == "--light" {
|
||||||
res.light = true;
|
res.options.set(NotedeckOptions::LightTheme, true);
|
||||||
} else if arg == "--locale" {
|
} else if arg == "--locale" {
|
||||||
i += 1;
|
i += 1;
|
||||||
let Some(locale) = args.get(i) else {
|
let Some(locale) = args.get(i) else {
|
||||||
@@ -68,11 +54,11 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if arg == "--dark" {
|
} else if arg == "--dark" {
|
||||||
res.light = false;
|
res.options.set(NotedeckOptions::LightTheme, false);
|
||||||
} else if arg == "--debug" {
|
} else if arg == "--debug" {
|
||||||
res.debug = true;
|
res.options.set(NotedeckOptions::Debug, true);
|
||||||
} else if arg == "--testrunner" {
|
} else if arg == "--testrunner" {
|
||||||
res.tests = true;
|
res.options.set(NotedeckOptions::Tests, true);
|
||||||
} else if arg == "--pub" || arg == "--npub" {
|
} else if arg == "--pub" || arg == "--npub" {
|
||||||
i += 1;
|
i += 1;
|
||||||
let pubstr = if let Some(next_arg) = args.get(i) {
|
let pubstr = if let Some(next_arg) = args.get(i) {
|
||||||
@@ -135,11 +121,13 @@ impl Args {
|
|||||||
};
|
};
|
||||||
res.relays.push(relay.clone());
|
res.relays.push(relay.clone());
|
||||||
} else if arg == "--no-keystore" {
|
} else if arg == "--no-keystore" {
|
||||||
res.use_keystore = false;
|
res.options.set(NotedeckOptions::UseKeystore, true);
|
||||||
} else if arg == "--relay-debug" {
|
} else if arg == "--relay-debug" {
|
||||||
res.relay_debug = true;
|
res.options.set(NotedeckOptions::RelayDebug, true);
|
||||||
} else if arg == "--show-note-client" {
|
} else if arg == "--show-client" {
|
||||||
res.show_note_client = true;
|
res.options.set(NotedeckOptions::ShowClient, true);
|
||||||
|
} else if arg == "--notebook" {
|
||||||
|
res.options.set(NotedeckOptions::FeatureNotebook, true);
|
||||||
} else {
|
} else {
|
||||||
unrecognized_args.insert(arg.clone());
|
unrecognized_args.insert(arg.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
|
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
|
||||||
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
|
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
|
||||||
UnknownIds,
|
UnknownIds,
|
||||||
};
|
};
|
||||||
use egui_winit::clipboard::Clipboard;
|
use egui_winit::clipboard::Clipboard;
|
||||||
@@ -20,7 +20,7 @@ pub struct AppContext<'a> {
|
|||||||
pub global_wallet: &'a mut GlobalWallet,
|
pub global_wallet: &'a mut GlobalWallet,
|
||||||
pub path: &'a DataPath,
|
pub path: &'a DataPath,
|
||||||
pub args: &'a Args,
|
pub args: &'a Args,
|
||||||
pub theme: &'a mut ThemeHandler,
|
pub settings: &'a mut SettingsHandler,
|
||||||
pub clipboard: &'a mut Clipboard,
|
pub clipboard: &'a mut Clipboard,
|
||||||
pub zaps: &'a mut Zaps,
|
pub zaps: &'a mut Zaps,
|
||||||
pub frame_history: &'a mut FrameHistory,
|
pub frame_history: &'a mut FrameHistory,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
use crate::{ui, NotedeckTextStyle};
|
use crate::{ui, NotedeckTextStyle};
|
||||||
|
use egui::FontData;
|
||||||
|
use egui::FontDefinitions;
|
||||||
|
use egui::FontTweak;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub enum NamedFontFamily {
|
pub enum NamedFontFamily {
|
||||||
Medium,
|
Medium,
|
||||||
@@ -31,6 +36,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
|||||||
NotedeckTextStyle::Button => 13.0,
|
NotedeckTextStyle::Button => 13.0,
|
||||||
NotedeckTextStyle::Small => 12.0,
|
NotedeckTextStyle::Small => 12.0,
|
||||||
NotedeckTextStyle::Tiny => 10.0,
|
NotedeckTextStyle::Tiny => 10.0,
|
||||||
|
NotedeckTextStyle::NoteBody => 16.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +52,7 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
|||||||
NotedeckTextStyle::Button => 13.0,
|
NotedeckTextStyle::Button => 13.0,
|
||||||
NotedeckTextStyle::Small => 12.0,
|
NotedeckTextStyle::Small => 12.0,
|
||||||
NotedeckTextStyle::Tiny => 10.0,
|
NotedeckTextStyle::Tiny => 10.0,
|
||||||
|
NotedeckTextStyle::NoteBody => 13.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,3 +63,148 @@ pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32
|
|||||||
desktop_font_size(text_style)
|
desktop_font_size(text_style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use gossip's approach to font loading. This includes japanese fonts
|
||||||
|
// for rending stuff from japanese users.
|
||||||
|
pub fn setup_fonts(ctx: &egui::Context) {
|
||||||
|
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
|
||||||
|
let mut families = BTreeMap::new();
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"Onest".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"OnestMedium".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSans".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"OnestBold".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSansBold".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSans".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||||
|
);
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSansBold".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"Inconsolata".to_owned(),
|
||||||
|
Arc::new(
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/Inconsolata-Regular.ttf"
|
||||||
|
))
|
||||||
|
.tweak(FontTweak {
|
||||||
|
scale: 1.22, // This font is smaller than DejaVuSans
|
||||||
|
y_offset_factor: -0.18, // and too low
|
||||||
|
y_offset: 0.0,
|
||||||
|
baseline_offset_factor: 0.0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"NotoSansCJK".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"NotoSansThai".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/NotoSansThai-Regular.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Some good looking emojis. Use as first priority:
|
||||||
|
font_data.insert(
|
||||||
|
"NotoEmoji".to_owned(),
|
||||||
|
Arc::new(
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/NotoEmoji-Regular.ttf"
|
||||||
|
))
|
||||||
|
.tweak(FontTweak {
|
||||||
|
scale: 1.1, // make them a touch larger
|
||||||
|
y_offset_factor: 0.0,
|
||||||
|
y_offset: 0.0,
|
||||||
|
baseline_offset_factor: 0.0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let base_fonts = vec![
|
||||||
|
"DejaVuSans".to_owned(),
|
||||||
|
"NotoEmoji".to_owned(),
|
||||||
|
"NotoSansCJK".to_owned(),
|
||||||
|
"NotoSansThai".to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut proportional = vec!["Onest".to_owned()];
|
||||||
|
proportional.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut medium = vec!["OnestMedium".to_owned()];
|
||||||
|
medium.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut mono = vec!["Inconsolata".to_owned()];
|
||||||
|
mono.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut bold = vec!["OnestBold".to_owned()];
|
||||||
|
bold.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let emoji = vec!["NotoEmoji".to_owned()];
|
||||||
|
|
||||||
|
families.insert(egui::FontFamily::Proportional, proportional);
|
||||||
|
families.insert(egui::FontFamily::Monospace, mono);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||||
|
medium,
|
||||||
|
);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||||
|
bold,
|
||||||
|
);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::debug!("fonts: {:?}", families);
|
||||||
|
|
||||||
|
let defs = FontDefinitions {
|
||||||
|
font_data,
|
||||||
|
families,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.set_fonts(defs);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use fluent::{FluentArgs, FluentBundle, FluentResource};
|
|||||||
use fluent_langneg::negotiate_languages;
|
use fluent_langneg::negotiate_languages;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use sys_locale;
|
||||||
use unic_langid::{langid, LanguageIdentifier};
|
use unic_langid::{langid, LanguageIdentifier};
|
||||||
|
|
||||||
const EN_US: LanguageIdentifier = langid!("en-US");
|
const EN_US: LanguageIdentifier = langid!("en-US");
|
||||||
@@ -101,10 +102,6 @@ pub struct Localization {
|
|||||||
|
|
||||||
impl Default for Localization {
|
impl Default for Localization {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// Default to English (US)
|
|
||||||
let default_locale = &EN_US;
|
|
||||||
let fallback_locale = default_locale.to_owned();
|
|
||||||
|
|
||||||
// Build available locales list
|
// Build available locales list
|
||||||
let available_locales = vec![
|
let available_locales = vec![
|
||||||
EN_US.clone(),
|
EN_US.clone(),
|
||||||
@@ -132,8 +129,20 @@ impl Default for Localization {
|
|||||||
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
|
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Detect system locale and find best match
|
||||||
|
let current_locale = Self::negotiate_system_locale_with_preferences(&available_locales);
|
||||||
|
|
||||||
|
// Fallback locale is always EN_US
|
||||||
|
let fallback_locale = EN_US.clone();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Localization initialized - Selected locale: {}, Fallback: {}",
|
||||||
|
current_locale,
|
||||||
|
fallback_locale
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_locale: default_locale.to_owned(),
|
current_locale,
|
||||||
available_locales,
|
available_locales,
|
||||||
fallback_locale,
|
fallback_locale,
|
||||||
locale_native_names,
|
locale_native_names,
|
||||||
@@ -159,6 +168,150 @@ impl Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract just the language and region from locale string (e.g., "fr-FR-u-mu-celsius" -> "fr-FR")
|
||||||
|
fn extract_language_region(locale_str: &str) -> String {
|
||||||
|
// Split by '-' and analyze the parts
|
||||||
|
let parts: Vec<&str> = locale_str.split('-').collect();
|
||||||
|
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
// Check if the second part looks like a region
|
||||||
|
let second_part = parts[1];
|
||||||
|
if (second_part.len() >= 2) {
|
||||||
|
format!("{}-{}", parts[0], parts[1])
|
||||||
|
} else {
|
||||||
|
// Second part is not a region, probably an extension (e.g., "u", "t", "x")
|
||||||
|
// Just return the language part
|
||||||
|
parts[0].to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only one part, return as is
|
||||||
|
locale_str.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Negotiate the best locale from all system preferences against available locales
|
||||||
|
fn negotiate_system_locale_with_preferences(
|
||||||
|
available_locales: &[LanguageIdentifier],
|
||||||
|
) -> LanguageIdentifier {
|
||||||
|
// Get all system preferred locales in descending order
|
||||||
|
let mut system_locales: Vec<String> = sys_locale::get_locales().collect();
|
||||||
|
if system_locales.is_empty() {
|
||||||
|
tracing::info!("No system locales detected, using fallback: en-US");
|
||||||
|
return EN_US.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("System preferred locales: {:?}", system_locales);
|
||||||
|
|
||||||
|
// If we only got one locale, it might be that the system only returns the primary locale
|
||||||
|
// In this case, we can try to add common fallbacks based on the detected locale
|
||||||
|
if system_locales.len() == 1 {
|
||||||
|
let primary = &system_locales[0];
|
||||||
|
|
||||||
|
// Try to parse the primary locale, handling extensions
|
||||||
|
let primary_lang = if let Ok(locale) = primary.parse::<LanguageIdentifier>() {
|
||||||
|
locale.language.as_str().to_string()
|
||||||
|
} else {
|
||||||
|
// If parsing fails, try extracting language-region
|
||||||
|
// let stripped = Self::extract_language_region(primary);
|
||||||
|
// if let Ok(locale) = stripped.parse::<LanguageIdentifier>() {
|
||||||
|
// locale.language.as_str().to_string()
|
||||||
|
// } else {
|
||||||
|
tracing::info!("Could not parse primary locale: {}", primary);
|
||||||
|
"unknown".to_string()
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Only one system locale detected: {} (language: {})",
|
||||||
|
primary,
|
||||||
|
primary_lang
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add common fallbacks for the detected language
|
||||||
|
match primary_lang.as_str() {
|
||||||
|
"uk" => {
|
||||||
|
// For Ukrainian, add common fallbacks
|
||||||
|
system_locales.push("es-ES".to_string());
|
||||||
|
system_locales.push("en-US".to_string());
|
||||||
|
tracing::info!("Added fallbacks for Ukrainian: {:?}", system_locales);
|
||||||
|
}
|
||||||
|
"es" => {
|
||||||
|
// For Spanish, add English fallback
|
||||||
|
system_locales.push("en-US".to_string());
|
||||||
|
tracing::info!("Added fallback for Spanish: {:?}", system_locales);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// For other languages, add English fallback
|
||||||
|
system_locales.push("en-US".to_string());
|
||||||
|
tracing::info!("Added fallback for {}: {:?}", primary_lang, system_locales);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert system locale strings to LanguageIdentifiers, handling extensions
|
||||||
|
let mut parsed_system_locales = Vec::new();
|
||||||
|
for locale_str in system_locales {
|
||||||
|
// Try to parse the locale string directly first
|
||||||
|
if let Ok(locale) = locale_str.parse::<LanguageIdentifier>() {
|
||||||
|
parsed_system_locales.push(locale);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parsing fails, try extracting just language-region
|
||||||
|
// let stripped_locale = Self::extract_language_region(&locale_str);
|
||||||
|
// if let Ok(locale) = stripped_locale.parse::<LanguageIdentifier>() {
|
||||||
|
// parsed_system_locales.push(locale);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
tracing::info!("Failed to parse locale string: {}", locale_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed_system_locales.is_empty() {
|
||||||
|
tracing::info!("No valid system locales parsed, using fallback: en-US");
|
||||||
|
return EN_US.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try exact matches with fluent_langneg
|
||||||
|
let fallback = &EN_US;
|
||||||
|
let negotiated = negotiate_languages(
|
||||||
|
&parsed_system_locales,
|
||||||
|
available_locales,
|
||||||
|
Some(fallback),
|
||||||
|
fluent_langneg::NegotiationStrategy::Filtering,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(result) = negotiated.first() {
|
||||||
|
tracing::info!(
|
||||||
|
"Exact match found: {} from preferences: {:?}",
|
||||||
|
result,
|
||||||
|
parsed_system_locales
|
||||||
|
);
|
||||||
|
return (*result).clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact match, try language-only fallbacks
|
||||||
|
tracing::info!("No exact matches found, trying language-only fallbacks");
|
||||||
|
for system_locale in &parsed_system_locales {
|
||||||
|
let system_lang = system_locale.language.as_str();
|
||||||
|
|
||||||
|
// Look for any available locale with the same language
|
||||||
|
for available_locale in available_locales {
|
||||||
|
if available_locale.language.as_str() == system_lang {
|
||||||
|
tracing::debug!(
|
||||||
|
"Language match found: {} (system: {})",
|
||||||
|
available_locale,
|
||||||
|
system_locale
|
||||||
|
);
|
||||||
|
return available_locale.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("No language matches found, using fallback: en-US");
|
||||||
|
EN_US.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets a localized string by its ID
|
/// Gets a localized string by its ID
|
||||||
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
|
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
|
||||||
self.get_cached_string(id, None)
|
self.get_cached_string(id, None)
|
||||||
@@ -458,20 +611,6 @@ impl Localization {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Negotiates the best locale from a list of preferred locales
|
|
||||||
pub fn negotiate_locale(&self, preferred: &[LanguageIdentifier]) -> LanguageIdentifier {
|
|
||||||
let available = self.available_locales.clone();
|
|
||||||
let negotiated = negotiate_languages(
|
|
||||||
preferred,
|
|
||||||
&available,
|
|
||||||
Some(&self.fallback_locale),
|
|
||||||
fluent_langneg::NegotiationStrategy::Filtering,
|
|
||||||
);
|
|
||||||
negotiated
|
|
||||||
.first()
|
|
||||||
.map_or(self.fallback_locale.clone(), |v| (*v).clone())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Statistics about cache usage
|
/// Statistics about cache usage
|
||||||
@@ -484,6 +623,80 @@ pub struct CacheStats {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_language_region() {
|
||||||
|
// Test that we extract just language and region from various locale formats
|
||||||
|
|
||||||
|
// Test locales with extensions
|
||||||
|
let unicode_locale = "fr-FR-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(unicode_locale);
|
||||||
|
assert_eq!(extracted, "fr-FR");
|
||||||
|
|
||||||
|
let transformed_locale = "en-US-t-0-abc123";
|
||||||
|
let extracted = Localization::extract_language_region(transformed_locale);
|
||||||
|
assert_eq!(extracted, "en-US");
|
||||||
|
|
||||||
|
let private_locale = "de-DE-x-phonebk";
|
||||||
|
let extracted = Localization::extract_language_region(private_locale);
|
||||||
|
assert_eq!(extracted, "de-DE");
|
||||||
|
|
||||||
|
// Test simple locale (no extensions)
|
||||||
|
let simple_locale = "en-US";
|
||||||
|
let extracted = Localization::extract_language_region(simple_locale);
|
||||||
|
assert_eq!(extracted, "en-US");
|
||||||
|
|
||||||
|
// Test language-only locale
|
||||||
|
let lang_only = "en";
|
||||||
|
let extracted = Localization::extract_language_region(lang_only);
|
||||||
|
assert_eq!(extracted, "en");
|
||||||
|
|
||||||
|
// Test language with extensions (no region)
|
||||||
|
let lang_with_extensions = "fr-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(lang_with_extensions);
|
||||||
|
assert_eq!(extracted, "fr");
|
||||||
|
|
||||||
|
// Test language with other extension types (no region)
|
||||||
|
let lang_with_t_ext = "en-t-0-abc123";
|
||||||
|
let extracted = Localization::extract_language_region(lang_with_t_ext);
|
||||||
|
assert_eq!(extracted, "en");
|
||||||
|
|
||||||
|
let lang_with_x_ext = "de-x-phonebk";
|
||||||
|
let extracted = Localization::extract_language_region(lang_with_x_ext);
|
||||||
|
assert_eq!(extracted, "de");
|
||||||
|
|
||||||
|
// Test locale with numeric region code
|
||||||
|
let numeric_region = "es-419-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(numeric_region);
|
||||||
|
assert_eq!(extracted, "es-419");
|
||||||
|
|
||||||
|
// Test locale with 3-letter region code
|
||||||
|
let three_letter_region = "en-USA-t-0-abc123";
|
||||||
|
let extracted = Localization::extract_language_region(three_letter_region);
|
||||||
|
assert_eq!(extracted, "en-USA");
|
||||||
|
|
||||||
|
// Test locale with 2-letter region code
|
||||||
|
let two_letter_region = "fr-FR-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(two_letter_region);
|
||||||
|
assert_eq!(extracted, "fr-FR");
|
||||||
|
|
||||||
|
// Test complex locale with multiple parts
|
||||||
|
let complex_locale = "zh-CN-u-ca-chinese-x-private";
|
||||||
|
let extracted = Localization::extract_language_region(complex_locale);
|
||||||
|
assert_eq!(extracted, "zh-CN");
|
||||||
|
|
||||||
|
// Verify that extracted locales can be parsed
|
||||||
|
let test_cases = ["fr-FR", "en-US", "de-DE", "en", "zh-CN"];
|
||||||
|
for extracted in test_cases {
|
||||||
|
if let Ok(locale) = extracted.parse::<LanguageIdentifier>() {
|
||||||
|
tracing::info!("Successfully parsed extracted locale: {}", locale);
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to parse extracted locale: {}", extracted);
|
||||||
|
panic!("Should parse locale after extraction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// TODO(jb55): write tests that work, i broke all these during the refacto
|
// TODO(jb55): write tests that work, i broke all these during the refacto
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
|
use crate::media::gif::ensure_latest_texture_from_cache;
|
||||||
|
use crate::media::images::ImageType;
|
||||||
use crate::urls::{UrlCache, UrlMimes};
|
use crate::urls::{UrlCache, UrlMimes};
|
||||||
|
use crate::ImageMetadata;
|
||||||
|
use crate::ObfuscationType;
|
||||||
|
use crate::RenderableMedia;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
use image::{Delay, Frame};
|
use image::{Delay, Frame};
|
||||||
@@ -21,7 +26,7 @@ use tracing::warn;
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TexturesCache {
|
pub struct TexturesCache {
|
||||||
cache: hashbrown::HashMap<String, TextureStateInternal>,
|
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TexturesCache {
|
impl TexturesCache {
|
||||||
@@ -141,6 +146,12 @@ pub enum TextureState<'a> {
|
|||||||
Loaded(&'a mut TexturedImage),
|
Loaded(&'a mut TexturedImage),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> TextureState<'a> {
|
||||||
|
pub fn is_loaded(&self) -> bool {
|
||||||
|
matches!(self, Self::Loaded(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
|
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
|
||||||
fn from(value: &'a mut TextureStateInternal) -> Self {
|
fn from(value: &'a mut TextureStateInternal) -> Self {
|
||||||
match value {
|
match value {
|
||||||
@@ -402,6 +413,8 @@ pub struct Images {
|
|||||||
pub static_imgs: MediaCache,
|
pub static_imgs: MediaCache,
|
||||||
pub gifs: MediaCache,
|
pub gifs: MediaCache,
|
||||||
pub urls: UrlMimes,
|
pub urls: UrlMimes,
|
||||||
|
/// cached imeta data
|
||||||
|
pub metadata: HashMap<String, ImageMetadata>,
|
||||||
pub gif_states: GifStateMap,
|
pub gif_states: GifStateMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +427,7 @@ impl Images {
|
|||||||
gifs: MediaCache::new(&path, MediaCacheType::Gif),
|
gifs: MediaCache::new(&path, MediaCacheType::Gif),
|
||||||
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
||||||
gif_states: Default::default(),
|
gif_states: Default::default(),
|
||||||
|
metadata: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +436,58 @@ impl Images {
|
|||||||
self.gifs.migrate_v0()
|
self.gifs.migrate_v0()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
|
||||||
|
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_renderable_media(
|
||||||
|
urls: &mut UrlMimes,
|
||||||
|
imeta: &HashMap<String, ImageMetadata>,
|
||||||
|
url: &str,
|
||||||
|
) -> Option<RenderableMedia> {
|
||||||
|
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
|
||||||
|
|
||||||
|
let obfuscation_type = match imeta.get(url) {
|
||||||
|
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
|
||||||
|
None => ObfuscationType::Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(RenderableMedia {
|
||||||
|
url: url.to_string(),
|
||||||
|
media_type,
|
||||||
|
obfuscation_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_texture(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
url: &str,
|
||||||
|
img_type: ImageType,
|
||||||
|
) -> Option<TextureHandle> {
|
||||||
|
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
||||||
|
|
||||||
|
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
|
||||||
|
let is_loaded = self
|
||||||
|
.get_cache_mut(cache_type)
|
||||||
|
.textures_cache
|
||||||
|
.handle_and_get_or_insert(url, || {
|
||||||
|
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
|
||||||
|
})
|
||||||
|
.is_loaded();
|
||||||
|
|
||||||
|
if !is_loaded {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = match cache_type {
|
||||||
|
MediaCacheType::Image => &mut self.static_imgs,
|
||||||
|
MediaCacheType::Gif => &mut self.gifs,
|
||||||
|
};
|
||||||
|
|
||||||
|
ensure_latest_texture_from_cache(ui, url, &mut self.gif_states, &mut cache.textures_cache)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||||
match cache_type {
|
match cache_type {
|
||||||
MediaCacheType::Image => &self.static_imgs,
|
MediaCacheType::Image => &self.static_imgs,
|
||||||
@@ -465,3 +531,35 @@ pub struct GifState {
|
|||||||
pub next_frame_time: Option<SystemTime>,
|
pub next_frame_time: Option<SystemTime>,
|
||||||
pub last_frame_index: usize,
|
pub last_frame_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LatestTexture {
|
||||||
|
pub texture: TextureHandle,
|
||||||
|
pub request_next_repaint: Option<SystemTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_render_state<'a>(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
images: &'a mut Images,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
url: &str,
|
||||||
|
img_type: ImageType,
|
||||||
|
) -> RenderState<'a> {
|
||||||
|
let cache = match cache_type {
|
||||||
|
MediaCacheType::Image => &mut images.static_imgs,
|
||||||
|
MediaCacheType::Gif => &mut images.gifs,
|
||||||
|
};
|
||||||
|
|
||||||
|
let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
|
||||||
|
crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
|
||||||
|
});
|
||||||
|
|
||||||
|
RenderState {
|
||||||
|
texture_state,
|
||||||
|
gifs: &mut images.gif_states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderState<'a> {
|
||||||
|
pub texture_state: TextureState<'a>,
|
||||||
|
pub gifs: &'a mut GifStateMap,
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ impl JobPool {
|
|||||||
pub fn new(num_threads: usize) -> Self {
|
pub fn new(num_threads: usize) -> Self {
|
||||||
let (tx, rx) = mpsc::channel::<Job>();
|
let (tx, rx) = mpsc::channel::<Job>();
|
||||||
|
|
||||||
|
// TODO(jb55) why not mpmc here !???
|
||||||
let arc_rx = Arc::new(Mutex::new(rx));
|
let arc_rx = Arc::new(Mutex::new(rx));
|
||||||
for _ in 0..num_threads {
|
for _ in 0..num_threads {
|
||||||
let arc_rx_clone = arc_rx.clone();
|
let arc_rx_clone = arc_rx.clone();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::JobPool;
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||||
use notedeck::JobPool;
|
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -12,16 +12,20 @@ mod frame_history;
|
|||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
mod imgcache;
|
mod imgcache;
|
||||||
mod job_pool;
|
mod job_pool;
|
||||||
|
mod jobs;
|
||||||
|
pub mod media;
|
||||||
mod muted;
|
mod muted;
|
||||||
pub mod name;
|
pub mod name;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
mod notecache;
|
mod notecache;
|
||||||
|
mod options;
|
||||||
mod persist;
|
mod persist;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay_debug;
|
pub mod relay_debug;
|
||||||
pub mod relayspec;
|
pub mod relayspec;
|
||||||
mod result;
|
mod result;
|
||||||
|
mod setup;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
mod style;
|
mod style;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
@@ -47,10 +51,18 @@ pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
|||||||
pub use fonts::NamedFontFamily;
|
pub use fonts::NamedFontFamily;
|
||||||
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
|
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
|
||||||
pub use imgcache::{
|
pub use imgcache::{
|
||||||
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
|
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
|
||||||
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
|
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
|
||||||
|
TexturedImage, TexturesCache,
|
||||||
};
|
};
|
||||||
pub use job_pool::JobPool;
|
pub use job_pool::JobPool;
|
||||||
|
pub use jobs::{
|
||||||
|
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
|
||||||
|
};
|
||||||
|
pub use media::{
|
||||||
|
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
|
||||||
|
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
|
||||||
|
};
|
||||||
pub use muted::{MuteFun, Muted};
|
pub use muted::{MuteFun, Muted};
|
||||||
pub use name::NostrName;
|
pub use name::NostrName;
|
||||||
pub use note::{
|
pub use note::{
|
||||||
@@ -58,6 +70,7 @@ pub use note::{
|
|||||||
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
||||||
};
|
};
|
||||||
pub use notecache::{CachedNote, NoteCache};
|
pub use notecache::{CachedNote, NoteCache};
|
||||||
|
pub use options::NotedeckOptions;
|
||||||
pub use persist::*;
|
pub use persist::*;
|
||||||
pub use profile::get_profile_url;
|
pub use profile::get_profile_url;
|
||||||
pub use relay_debug::RelayDebugView;
|
pub use relay_debug::RelayDebugView;
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use crate::{Images, MediaCacheType, TexturedImage};
|
||||||
|
use poll_promise::Promise;
|
||||||
|
|
||||||
|
/// Tracks where media was on the screen so that
|
||||||
|
/// we can do fun animations when opening the
|
||||||
|
/// Media Viewer
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MediaInfo {
|
||||||
|
/// The original screen position where it
|
||||||
|
/// was rendered from. This is not where
|
||||||
|
/// it should be rendered in the scene.
|
||||||
|
pub original_position: egui::Rect,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains various information for when a user
|
||||||
|
/// clicks a piece of media. It contains the current
|
||||||
|
/// location on screen for each piece of media.
|
||||||
|
///
|
||||||
|
/// Viewers can use this to smoothly transition from
|
||||||
|
/// the timeline to the viewer
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ViewMediaInfo {
|
||||||
|
pub clicked_index: usize,
|
||||||
|
pub medias: Vec<MediaInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewMediaInfo {
|
||||||
|
pub fn clicked_media(&self) -> &MediaInfo {
|
||||||
|
&self.medias[self.clicked_index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions generated by media ui interactions
|
||||||
|
pub enum MediaAction {
|
||||||
|
/// An image was clicked on in a carousel, we have
|
||||||
|
/// the opportunity to open into a fullscreen media viewer
|
||||||
|
/// with a list of url values
|
||||||
|
ViewMedias(ViewMediaInfo),
|
||||||
|
|
||||||
|
FetchImage {
|
||||||
|
url: String,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
|
||||||
|
},
|
||||||
|
DoneLoading {
|
||||||
|
url: String,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for MediaAction {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ViewMedias(ViewMediaInfo {
|
||||||
|
clicked_index,
|
||||||
|
medias,
|
||||||
|
}) => f
|
||||||
|
.debug_struct("ViewMedias")
|
||||||
|
.field("clicked_index", clicked_index)
|
||||||
|
.field("media", medias)
|
||||||
|
.finish(),
|
||||||
|
Self::FetchImage {
|
||||||
|
url,
|
||||||
|
cache_type,
|
||||||
|
no_pfp_promise,
|
||||||
|
} => f
|
||||||
|
.debug_struct("FetchNoPfpImage")
|
||||||
|
.field("url", url)
|
||||||
|
.field("cache_type", cache_type)
|
||||||
|
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
|
||||||
|
.finish(),
|
||||||
|
Self::DoneLoading { url, cache_type } => f
|
||||||
|
.debug_struct("DoneLoading")
|
||||||
|
.field("url", url)
|
||||||
|
.field("cache_type", cache_type)
|
||||||
|
.finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaAction {
|
||||||
|
/// Handle view media actions
|
||||||
|
pub fn on_view_media(&self, handler: impl FnOnce(&ViewMediaInfo)) {
|
||||||
|
if let MediaAction::ViewMedias(view_medias) = self {
|
||||||
|
handler(view_medias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default processing logic for Media Actions. We don't handle ViewMedias here since
|
||||||
|
/// this may be app specific ?
|
||||||
|
pub fn process_default_media_actions(self, images: &mut Images) {
|
||||||
|
match self {
|
||||||
|
MediaAction::ViewMedias(_urls) => {
|
||||||
|
// NOTE(jb55): don't assume we want to show a fullscreen
|
||||||
|
// media viewer we can use on_view_media for that. We
|
||||||
|
// also don't want to have a notedeck_ui dependency in
|
||||||
|
// the notedeck lib (MediaViewerState)
|
||||||
|
//
|
||||||
|
// In general our notedeck crate should be pretty
|
||||||
|
// agnostic to functionallity in general unless it low
|
||||||
|
// level like image rendering.
|
||||||
|
//
|
||||||
|
//mview_state.set_urls(urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaAction::FetchImage {
|
||||||
|
url,
|
||||||
|
cache_type,
|
||||||
|
no_pfp_promise: promise,
|
||||||
|
} => {
|
||||||
|
images
|
||||||
|
.get_cache_mut(cache_type)
|
||||||
|
.textures_cache
|
||||||
|
.insert_pending(&url, promise);
|
||||||
|
}
|
||||||
|
MediaAction::DoneLoading { url, cache_type } => {
|
||||||
|
let cache = match cache_type {
|
||||||
|
MediaCacheType::Image => &mut images.static_imgs,
|
||||||
|
MediaCacheType::Gif => &mut images.gifs,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.textures_cache.move_to_loaded(&url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ use nostrdb::Note;
|
|||||||
use crate::jobs::{Job, JobError, JobParamsOwned};
|
use crate::jobs::{Job, JobError, JobParamsOwned};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Blur<'a> {
|
pub struct ImageMetadata {
|
||||||
pub blurhash: &'a str,
|
pub blurhash: String,
|
||||||
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ impl PointDimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Blur<'_> {
|
impl ImageMetadata {
|
||||||
pub fn scaled_pixel_dimensions(
|
pub fn scaled_pixel_dimensions(
|
||||||
&self,
|
&self,
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
@@ -75,9 +75,8 @@ impl Blur<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
/// Find blurhashes in image metadata and update our cache
|
||||||
let mut blurs = HashMap::new();
|
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
|
||||||
|
|
||||||
for tag in note.tags() {
|
for tag in note.tags() {
|
||||||
let mut tag_iter = tag.into_iter();
|
let mut tag_iter = tag.into_iter();
|
||||||
if tag_iter
|
if tag_iter
|
||||||
@@ -93,13 +92,11 @@ pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
blurs.insert(url, blur);
|
blurs.insert(url.to_string(), blur);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blurs
|
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
|
||||||
}
|
|
||||||
|
|
||||||
fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
|
||||||
let mut url = None;
|
let mut url = None;
|
||||||
let mut blurhash = None;
|
let mut blurhash = None;
|
||||||
let mut dims = None;
|
let mut dims = None;
|
||||||
@@ -138,21 +135,21 @@ fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Some((
|
Some((
|
||||||
url,
|
url.to_string(),
|
||||||
Blur {
|
ImageMetadata {
|
||||||
blurhash,
|
blurhash: blurhash.to_string(),
|
||||||
dimensions,
|
dimensions,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ObfuscationType<'a> {
|
pub enum ObfuscationType {
|
||||||
Blurhash(Blur<'a>),
|
Blurhash(ImageMetadata),
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn compute_blurhash(
|
pub fn compute_blurhash(
|
||||||
params: Option<JobParamsOwned>,
|
params: Option<JobParamsOwned>,
|
||||||
dims: PixelDimensions,
|
dims: PixelDimensions,
|
||||||
) -> Result<Job, JobError> {
|
) -> Result<Job, JobError> {
|
||||||
@@ -185,9 +182,9 @@ fn generate_blurhash_texturehandle(
|
|||||||
url: &str,
|
url: &str,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
) -> notedeck::Result<egui::TextureHandle> {
|
) -> Result<egui::TextureHandle, crate::Error> {
|
||||||
let bytes = blurhash::decode(blurhash, width, height, 1.0)
|
let bytes = blurhash::decode(blurhash, width, height, 1.0)
|
||||||
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
|
.map_err(|e| crate::Error::Generic(e.to_string()))?;
|
||||||
|
|
||||||
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
|
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
|
||||||
Ok(ctx.load_texture(url, img, Default::default()))
|
Ok(ctx.load_texture(url, img, Default::default()))
|
||||||
@@ -3,37 +3,32 @@ use std::{
|
|||||||
time::{Instant, SystemTime},
|
time::{Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
use notedeck::{GifState, GifStateMap, TexturedImage};
|
|
||||||
|
|
||||||
pub struct LatextTexture<'a> {
|
pub fn ensure_latest_texture_from_cache(
|
||||||
pub texture: &'a TextureHandle,
|
ui: &egui::Ui,
|
||||||
pub request_next_repaint: Option<SystemTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is necessary because other repaint calls can effectively steal our repaint request.
|
|
||||||
/// So we must keep on requesting to repaint at our desired time to ensure our repaint goes through.
|
|
||||||
/// See [`egui::Context::request_repaint_after`]
|
|
||||||
pub fn handle_repaint<'a>(ui: &egui::Ui, latest: LatextTexture<'a>) -> &'a TextureHandle {
|
|
||||||
if let Some(_repaint) = latest.request_next_repaint {
|
|
||||||
// 24fps for gif is fine
|
|
||||||
ui.ctx()
|
|
||||||
.request_repaint_after(std::time::Duration::from_millis(41));
|
|
||||||
}
|
|
||||||
latest.texture
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use = "caller should pass the return value to `gif::handle_repaint`"]
|
|
||||||
pub fn retrieve_latest_texture<'a>(
|
|
||||||
url: &str,
|
url: &str,
|
||||||
gifs: &'a mut GifStateMap,
|
gifs: &mut GifStateMap,
|
||||||
cached_image: &'a mut TexturedImage,
|
textures: &mut TexturesCache,
|
||||||
) -> LatextTexture<'a> {
|
) -> Option<TextureHandle> {
|
||||||
match cached_image {
|
let tstate = textures.cache.get_mut(url)?;
|
||||||
TexturedImage::Static(texture) => LatextTexture {
|
|
||||||
texture,
|
let TextureState::Loaded(img) = tstate.into() else {
|
||||||
request_next_repaint: None,
|
return None;
|
||||||
},
|
};
|
||||||
|
|
||||||
|
Some(ensure_latest_texture(ui, url, gifs, img))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_latest_texture(
|
||||||
|
ui: &egui::Ui,
|
||||||
|
url: &str,
|
||||||
|
gifs: &mut GifStateMap,
|
||||||
|
img: &mut TexturedImage,
|
||||||
|
) -> TextureHandle {
|
||||||
|
match img {
|
||||||
|
TexturedImage::Static(handle) => handle.clone(),
|
||||||
TexturedImage::Animated(animation) => {
|
TexturedImage::Animated(animation) => {
|
||||||
if let Some(receiver) = &animation.receiver {
|
if let Some(receiver) = &animation.receiver {
|
||||||
loop {
|
loop {
|
||||||
@@ -115,12 +110,12 @@ pub fn retrieve_latest_texture<'a>(
|
|||||||
|
|
||||||
if let Some(req) = request_next_repaint {
|
if let Some(req) = request_next_repaint {
|
||||||
tracing::trace!("requesting repaint for {url} after {req:?}");
|
tracing::trace!("requesting repaint for {url} after {req:?}");
|
||||||
|
// 24fps for gif is fine
|
||||||
|
ui.ctx()
|
||||||
|
.request_repaint_after(std::time::Duration::from_millis(41));
|
||||||
}
|
}
|
||||||
|
|
||||||
LatextTexture {
|
texture.clone()
|
||||||
texture,
|
|
||||||
request_next_repaint,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
use crate::{Animation, ImageFrame, MediaCache, MediaCacheType, TextureFrame, TexturedImage};
|
||||||
|
use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
|
||||||
|
use image::codecs::gif::GifDecoder;
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
|
||||||
|
use poll_promise::Promise;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::path::{self, Path};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
// NOTE(jb55): chatgpt wrote this because I was too dumb to
|
||||||
|
pub fn aspect_fill(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
sense: Sense,
|
||||||
|
texture_id: egui::TextureId,
|
||||||
|
aspect_ratio: f32,
|
||||||
|
) -> egui::Response {
|
||||||
|
let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
|
||||||
|
let frame_ratio = frame.width() / frame.height();
|
||||||
|
|
||||||
|
let (width, height) = if frame_ratio > aspect_ratio {
|
||||||
|
// Frame is wider than the content
|
||||||
|
(frame.width(), frame.width() / aspect_ratio)
|
||||||
|
} else {
|
||||||
|
// Frame is taller than the content
|
||||||
|
(frame.height() * aspect_ratio, frame.height())
|
||||||
|
};
|
||||||
|
|
||||||
|
let content_rect = Rect::from_min_size(
|
||||||
|
frame.min
|
||||||
|
+ egui::vec2(
|
||||||
|
(frame.width() - width) / 2.0,
|
||||||
|
(frame.height() - height) / 2.0,
|
||||||
|
),
|
||||||
|
egui::vec2(width, height),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the clipping rectangle to the frame
|
||||||
|
//let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
|
||||||
|
//ui.set_clip_rect(frame);
|
||||||
|
|
||||||
|
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||||
|
|
||||||
|
let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
|
||||||
|
|
||||||
|
// Draw the texture within the calculated rect, potentially clipping it
|
||||||
|
painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
|
||||||
|
painter.image(texture_id, content_rect, uv, Color32::WHITE);
|
||||||
|
|
||||||
|
// Restore the original clipping rectangle
|
||||||
|
//ui.set_clip_rect(clip_rect);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
#[profiling::function]
|
||||||
|
pub fn round_image(image: &mut ColorImage) {
|
||||||
|
// The radius to the edge of of the avatar circle
|
||||||
|
let edge_radius = image.size[0] as f32 / 2.0;
|
||||||
|
let edge_radius_squared = edge_radius * edge_radius;
|
||||||
|
|
||||||
|
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
|
||||||
|
// y coordinate
|
||||||
|
let uy = pixnum / image.size[0];
|
||||||
|
let y = uy as f32;
|
||||||
|
let y_offset = edge_radius - y;
|
||||||
|
|
||||||
|
// x coordinate
|
||||||
|
let ux = pixnum % image.size[0];
|
||||||
|
let x = ux as f32;
|
||||||
|
let x_offset = edge_radius - x;
|
||||||
|
|
||||||
|
// The radius to this pixel (may be inside or outside the circle)
|
||||||
|
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
|
||||||
|
|
||||||
|
// If inside of the avatar circle
|
||||||
|
if pixel_radius_squared <= edge_radius_squared {
|
||||||
|
// squareroot to find how many pixels we are from the edge
|
||||||
|
let pixel_radius: f32 = pixel_radius_squared.sqrt();
|
||||||
|
let distance = edge_radius - pixel_radius;
|
||||||
|
|
||||||
|
// If we are within 1 pixel of the edge, we should fade, to
|
||||||
|
// antialias the edge of the circle. 1 pixel from the edge should
|
||||||
|
// be 100% of the original color, and right on the edge should be
|
||||||
|
// 0% of the original color.
|
||||||
|
if distance <= 1.0 {
|
||||||
|
*pixel = Color32::from_rgba_premultiplied(
|
||||||
|
(pixel.r() as f32 * distance) as u8,
|
||||||
|
(pixel.g() as f32 * distance) as u8,
|
||||||
|
(pixel.b() as f32 * distance) as u8,
|
||||||
|
(pixel.a() as f32 * distance) as u8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Outside of the avatar circle
|
||||||
|
*pixel = Color32::TRANSPARENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the image's longest dimension is greater than max_edge, downscale
|
||||||
|
fn resize_image_if_too_big(
|
||||||
|
image: image::DynamicImage,
|
||||||
|
max_edge: u32,
|
||||||
|
filter: FilterType,
|
||||||
|
) -> image::DynamicImage {
|
||||||
|
// if we have no size hint, resize to something reasonable
|
||||||
|
let w = image.width();
|
||||||
|
let h = image.height();
|
||||||
|
let long = w.max(h);
|
||||||
|
|
||||||
|
if long > max_edge {
|
||||||
|
let scale = max_edge as f32 / long as f32;
|
||||||
|
let new_w = (w as f32 * scale).round() as u32;
|
||||||
|
let new_h = (h as f32 * scale).round() as u32;
|
||||||
|
|
||||||
|
image.resize(new_w, new_h, filter)
|
||||||
|
} else {
|
||||||
|
image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Process an image, resizing so we don't blow up video memory or even crash
|
||||||
|
///
|
||||||
|
/// For profile pictures, make them round and small to fit the size hint
|
||||||
|
/// For everything else, either:
|
||||||
|
///
|
||||||
|
/// - resize to the size hint
|
||||||
|
/// - keep the size if the longest dimension is less than MAX_IMG_LENGTH
|
||||||
|
/// - resize if any larger, using [`resize_image_if_too_big`]
|
||||||
|
///
|
||||||
|
#[profiling::function]
|
||||||
|
fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
|
||||||
|
const MAX_IMG_LENGTH: u32 = 2048;
|
||||||
|
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
|
||||||
|
|
||||||
|
match imgtyp {
|
||||||
|
ImageType::Content(size_hint) => {
|
||||||
|
let image = match size_hint {
|
||||||
|
None => resize_image_if_too_big(image, MAX_IMG_LENGTH, FILTER_TYPE),
|
||||||
|
Some((w, h)) => image.resize(w, h, FILTER_TYPE),
|
||||||
|
};
|
||||||
|
|
||||||
|
let image_buffer = image.into_rgba8();
|
||||||
|
ColorImage::from_rgba_unmultiplied(
|
||||||
|
[
|
||||||
|
image_buffer.width() as usize,
|
||||||
|
image_buffer.height() as usize,
|
||||||
|
],
|
||||||
|
image_buffer.as_flat_samples().as_slice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ImageType::Profile(size) => {
|
||||||
|
// Crop square
|
||||||
|
let smaller = image.width().min(image.height());
|
||||||
|
|
||||||
|
if image.width() > smaller {
|
||||||
|
let excess = image.width() - smaller;
|
||||||
|
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
|
||||||
|
} else if image.height() > smaller {
|
||||||
|
let excess = image.height() - smaller;
|
||||||
|
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
|
||||||
|
}
|
||||||
|
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
|
||||||
|
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
|
||||||
|
let mut color_image = ColorImage::from_rgba_unmultiplied(
|
||||||
|
[
|
||||||
|
image_buffer.width() as usize,
|
||||||
|
image_buffer.height() as usize,
|
||||||
|
],
|
||||||
|
image_buffer.as_flat_samples().as_slice(),
|
||||||
|
);
|
||||||
|
round_image(&mut color_image);
|
||||||
|
color_image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[profiling::function]
|
||||||
|
fn parse_img_response(
|
||||||
|
response: ehttp::Response,
|
||||||
|
imgtyp: ImageType,
|
||||||
|
) -> Result<ColorImage, crate::Error> {
|
||||||
|
let content_type = response.content_type().unwrap_or_default();
|
||||||
|
let size_hint = match imgtyp {
|
||||||
|
ImageType::Profile(size) => SizeHint::Size(size, size),
|
||||||
|
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
|
||||||
|
ImageType::Content(None) => SizeHint::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if content_type.starts_with("image/svg") {
|
||||||
|
profiling::scope!("load_svg");
|
||||||
|
|
||||||
|
let mut color_image =
|
||||||
|
egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
|
||||||
|
round_image(&mut color_image);
|
||||||
|
Ok(color_image)
|
||||||
|
} else if content_type.starts_with("image/") {
|
||||||
|
profiling::scope!("load_from_memory");
|
||||||
|
let dyn_image = image::load_from_memory(&response.bytes)?;
|
||||||
|
Ok(process_image(imgtyp, dyn_image))
|
||||||
|
} else {
|
||||||
|
Err(format!("Expected image, found content-type {content_type:?}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_img_from_disk(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
url: &str,
|
||||||
|
path: &path::Path,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let url = url.to_owned();
|
||||||
|
let path = path.to_owned();
|
||||||
|
|
||||||
|
Promise::spawn_async(async move {
|
||||||
|
Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn async_fetch_img_from_disk(
|
||||||
|
ctx: egui::Context,
|
||||||
|
url: String,
|
||||||
|
path: &path::Path,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
) -> Result<TexturedImage, crate::Error> {
|
||||||
|
match cache_type {
|
||||||
|
MediaCacheType::Image => {
|
||||||
|
let data = fs::read(path).await?;
|
||||||
|
let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image)?;
|
||||||
|
|
||||||
|
let img = buffer_to_color_image(
|
||||||
|
image_buffer.as_flat_samples_u8(),
|
||||||
|
image_buffer.width(),
|
||||||
|
image_buffer.height(),
|
||||||
|
);
|
||||||
|
Ok(TexturedImage::Static(ctx.load_texture(
|
||||||
|
&url,
|
||||||
|
img,
|
||||||
|
Default::default(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
MediaCacheType::Gif => {
|
||||||
|
let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
|
||||||
|
generate_gif(ctx, url, path, gif_bytes, false, |i| {
|
||||||
|
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_gif(
|
||||||
|
ctx: egui::Context,
|
||||||
|
url: String,
|
||||||
|
path: &path::Path,
|
||||||
|
data: Vec<u8>,
|
||||||
|
write_to_disk: bool,
|
||||||
|
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
|
||||||
|
) -> Result<TexturedImage, crate::Error> {
|
||||||
|
let decoder = {
|
||||||
|
let reader = Cursor::new(data.as_slice());
|
||||||
|
GifDecoder::new(reader)?
|
||||||
|
};
|
||||||
|
let (tex_input, tex_output) = mpsc::sync_channel(4);
|
||||||
|
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
|
||||||
|
let (inp, out) = mpsc::sync_channel(4);
|
||||||
|
(Some(inp), Some(out))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut frames: VecDeque<Frame> = decoder
|
||||||
|
.into_frames()
|
||||||
|
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
|
||||||
|
.map_err(|e| crate::Error::Generic(e.to_string()))?;
|
||||||
|
|
||||||
|
let first_frame = frames.pop_front().map(|frame| {
|
||||||
|
generate_animation_frame(
|
||||||
|
&ctx,
|
||||||
|
&url,
|
||||||
|
0,
|
||||||
|
frame,
|
||||||
|
maybe_encoder_input.as_ref(),
|
||||||
|
process_to_egui,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let cur_url = url.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
for (index, frame) in frames.into_iter().enumerate() {
|
||||||
|
let texture_frame = generate_animation_frame(
|
||||||
|
&ctx,
|
||||||
|
&cur_url,
|
||||||
|
index,
|
||||||
|
frame,
|
||||||
|
maybe_encoder_input.as_ref(),
|
||||||
|
process_to_egui,
|
||||||
|
);
|
||||||
|
|
||||||
|
if tex_input.send(texture_frame).is_err() {
|
||||||
|
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(encoder_output) = maybe_encoder_output {
|
||||||
|
let path = path.to_owned();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut imgs = Vec::new();
|
||||||
|
while let Ok(img) = encoder_output.recv() {
|
||||||
|
imgs.push(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
|
||||||
|
tracing::error!("Could not write gif to disk: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
first_frame.map_or_else(
|
||||||
|
|| {
|
||||||
|
Err(crate::Error::Generic(
|
||||||
|
"first frame not found for gif".to_owned(),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
|first_frame| {
|
||||||
|
Ok(TexturedImage::Animated(Animation {
|
||||||
|
other_frames: Default::default(),
|
||||||
|
receiver: Some(tex_output),
|
||||||
|
first_frame,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_animation_frame(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
url: &str,
|
||||||
|
index: usize,
|
||||||
|
frame: image::Frame,
|
||||||
|
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
|
||||||
|
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
|
||||||
|
) -> TextureFrame {
|
||||||
|
let delay = Duration::from(frame.delay());
|
||||||
|
let img = DynamicImage::ImageRgba8(frame.into_buffer());
|
||||||
|
let color_img = process_to_egui(img);
|
||||||
|
|
||||||
|
if let Some(sender) = maybe_encoder_input {
|
||||||
|
if let Err(e) = sender.send(ImageFrame {
|
||||||
|
delay,
|
||||||
|
image: color_img.clone(),
|
||||||
|
}) {
|
||||||
|
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureFrame {
|
||||||
|
delay,
|
||||||
|
texture: ctx.load_texture(format!("{url}{index}"), color_img, Default::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_to_color_image(
|
||||||
|
samples: Option<FlatSamples<&[u8]>>,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> ColorImage {
|
||||||
|
// TODO(jb55): remove unwrap here
|
||||||
|
let flat_samples = samples.unwrap();
|
||||||
|
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>, crate::Error> {
|
||||||
|
std::fs::read(path).map_err(|e| crate::Error::Generic(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls type-specific handling
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ImageType {
|
||||||
|
/// Profile Image (size)
|
||||||
|
Profile(u32),
|
||||||
|
/// Content Image with optional size hint
|
||||||
|
Content(Option<(u32, u32)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_img(
|
||||||
|
img_cache_path: &Path,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
url: &str,
|
||||||
|
imgtyp: ImageType,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||||
|
let key = MediaCache::key(url);
|
||||||
|
let path = img_cache_path.join(key);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
fetch_img_from_disk(ctx, url, &path, cache_type)
|
||||||
|
} else {
|
||||||
|
fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fetch image from local cache
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_img_from_net(
|
||||||
|
cache_path: &path::Path,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
url: &str,
|
||||||
|
imgtyp: ImageType,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||||
|
let (sender, promise) = Promise::new();
|
||||||
|
let request = ehttp::Request::get(url);
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let cloned_url = url.to_owned();
|
||||||
|
let cache_path = cache_path.to_owned();
|
||||||
|
ehttp::fetch(request, move |response| {
|
||||||
|
let handle = response.map_err(crate::Error::Generic).and_then(|resp| {
|
||||||
|
match cache_type {
|
||||||
|
MediaCacheType::Image => {
|
||||||
|
let img = parse_img_response(resp, imgtyp);
|
||||||
|
img.map(|img| {
|
||||||
|
let texture_handle =
|
||||||
|
ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
||||||
|
|
||||||
|
// write to disk
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
MediaCache::write(&cache_path, &cloned_url, img)
|
||||||
|
});
|
||||||
|
|
||||||
|
TexturedImage::Static(texture_handle)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
MediaCacheType::Gif => {
|
||||||
|
let gif_bytes = resp.bytes;
|
||||||
|
generate_gif(
|
||||||
|
ctx.clone(),
|
||||||
|
cloned_url,
|
||||||
|
&cache_path,
|
||||||
|
gif_bytes,
|
||||||
|
true,
|
||||||
|
move |img| process_image(imgtyp, img),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sender.send(Some(handle)); // send the results back to the UI thread.
|
||||||
|
ctx.request_repaint();
|
||||||
|
});
|
||||||
|
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_no_pfp_promise(
|
||||||
|
ctx: &Context,
|
||||||
|
cache: &MediaCache,
|
||||||
|
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||||
|
crate::media::images::fetch_img(
|
||||||
|
&cache.cache_dir,
|
||||||
|
ctx,
|
||||||
|
crate::profile::no_pfp_url(),
|
||||||
|
ImageType::Profile(128),
|
||||||
|
MediaCacheType::Image,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
pub mod action;
|
||||||
|
pub mod blur;
|
||||||
|
pub mod gif;
|
||||||
|
pub mod images;
|
||||||
|
pub mod imeta;
|
||||||
|
pub mod renderable;
|
||||||
|
|
||||||
|
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
|
||||||
|
pub use blur::{
|
||||||
|
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
|
||||||
|
PointDimensions,
|
||||||
|
};
|
||||||
|
pub use images::ImageType;
|
||||||
|
pub use renderable::RenderableMedia;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use super::ObfuscationType;
|
||||||
|
use crate::MediaCacheType;
|
||||||
|
|
||||||
|
/// Media that is prepared for rendering. Use [`Images::get_renderable_media`] to get these
|
||||||
|
pub struct RenderableMedia {
|
||||||
|
pub url: String,
|
||||||
|
pub media_type: MediaCacheType,
|
||||||
|
pub obfuscation_type: ObfuscationType,
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
use super::context::ContextSelection;
|
use super::context::ContextSelection;
|
||||||
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
|
use crate::{zaps::NoteZapTargetOwned, MediaAction};
|
||||||
use egui::Vec2;
|
use egui::Vec2;
|
||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use poll_promise::Promise;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ScrollInfo {
|
pub struct ScrollInfo {
|
||||||
@@ -61,62 +60,3 @@ pub struct ZapTargetAmount {
|
|||||||
pub target: NoteZapTargetOwned,
|
pub target: NoteZapTargetOwned,
|
||||||
pub specified_msats: Option<u64>, // if None use default amount
|
pub specified_msats: Option<u64>, // if None use default amount
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum MediaAction {
|
|
||||||
FetchImage {
|
|
||||||
url: String,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
|
|
||||||
},
|
|
||||||
DoneLoading {
|
|
||||||
url: String,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for MediaAction {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::FetchImage {
|
|
||||||
url,
|
|
||||||
cache_type,
|
|
||||||
no_pfp_promise,
|
|
||||||
} => f
|
|
||||||
.debug_struct("FetchNoPfpImage")
|
|
||||||
.field("url", url)
|
|
||||||
.field("cache_type", cache_type)
|
|
||||||
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
|
|
||||||
.finish(),
|
|
||||||
Self::DoneLoading { url, cache_type } => f
|
|
||||||
.debug_struct("DoneLoading")
|
|
||||||
.field("url", url)
|
|
||||||
.field("cache_type", cache_type)
|
|
||||||
.finish(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MediaAction {
|
|
||||||
pub fn process(self, images: &mut Images) {
|
|
||||||
match self {
|
|
||||||
MediaAction::FetchImage {
|
|
||||||
url,
|
|
||||||
cache_type,
|
|
||||||
no_pfp_promise: promise,
|
|
||||||
} => {
|
|
||||||
images
|
|
||||||
.get_cache_mut(cache_type)
|
|
||||||
.textures_cache
|
|
||||||
.insert_pending(&url, promise);
|
|
||||||
}
|
|
||||||
MediaAction::DoneLoading { url, cache_type } => {
|
|
||||||
let cache = match cache_type {
|
|
||||||
MediaCacheType::Image => &mut images.static_imgs,
|
|
||||||
MediaCacheType::Gif => &mut images.gifs,
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.textures_cache.move_to_loaded(&url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
mod action;
|
mod action;
|
||||||
mod context;
|
mod context;
|
||||||
|
|
||||||
pub use action::{MediaAction, NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||||
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
||||||
|
|
||||||
use crate::Accounts;
|
use crate::Accounts;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use bitflags::bitflags;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct NotedeckOptions: u64 {
|
||||||
|
// ===== Settings ======
|
||||||
|
/// Are we on light theme?
|
||||||
|
const LightTheme = 1 << 0;
|
||||||
|
|
||||||
|
/// Debug controls, fps stats
|
||||||
|
const Debug = 1 << 1;
|
||||||
|
|
||||||
|
/// Show relay debug window?
|
||||||
|
const RelayDebug = 1 << 2;
|
||||||
|
|
||||||
|
/// Are we running as tests?
|
||||||
|
const Tests = 1 << 3;
|
||||||
|
|
||||||
|
/// Use keystore?
|
||||||
|
const UseKeystore = 1 << 4;
|
||||||
|
|
||||||
|
/// Show client on notes?
|
||||||
|
const ShowClient = 1 << 5;
|
||||||
|
|
||||||
|
/// Simulate is_compiled_as_mobile ?
|
||||||
|
const Mobile = 1 << 6;
|
||||||
|
|
||||||
|
// ===== Feature Flags ======
|
||||||
|
/// Is notebook enabled?
|
||||||
|
const FeatureNotebook = 1 << 32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotedeckOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
NotedeckOptions::UseKeystore
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
mod app_size;
|
mod app_size;
|
||||||
mod theme_handler;
|
mod settings_handler;
|
||||||
mod token_handler;
|
mod token_handler;
|
||||||
mod zoom;
|
|
||||||
|
|
||||||
pub use app_size::AppSizeHandler;
|
pub use app_size::AppSizeHandler;
|
||||||
pub use theme_handler::ThemeHandler;
|
pub use settings_handler::Settings;
|
||||||
|
pub use settings_handler::SettingsHandler;
|
||||||
|
pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE;
|
||||||
pub use token_handler::TokenHandler;
|
pub use token_handler::TokenHandler;
|
||||||
pub use zoom::ZoomHandler;
|
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
use crate::{
|
||||||
|
storage::delete_file, timed_serializer::TimedSerializer, DataPath, DataPathType, Directory,
|
||||||
|
};
|
||||||
|
use egui::ThemePreference;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
const THEME_FILE: &str = "theme.txt";
|
||||||
|
const ZOOM_FACTOR_FILE: &str = "zoom_level.json";
|
||||||
|
const SETTINGS_FILE: &str = "settings.json";
|
||||||
|
|
||||||
|
const DEFAULT_THEME: ThemePreference = ThemePreference::Dark;
|
||||||
|
const DEFAULT_LOCALE: &str = "en-US";
|
||||||
|
const DEFAULT_ZOOM_FACTOR: f32 = 1.0;
|
||||||
|
const DEFAULT_SHOW_SOURCE_CLIENT: &str = "hide";
|
||||||
|
const DEFAULT_SHOW_REPLIES_NEWEST_FIRST: bool = false;
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 13.0;
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 16.0;
|
||||||
|
|
||||||
|
fn deserialize_theme(serialized_theme: &str) -> Option<ThemePreference> {
|
||||||
|
match serialized_theme {
|
||||||
|
"dark" => Some(ThemePreference::Dark),
|
||||||
|
"light" => Some(ThemePreference::Light),
|
||||||
|
"system" => Some(ThemePreference::System),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Clone)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub theme: ThemePreference,
|
||||||
|
pub locale: String,
|
||||||
|
pub zoom_factor: f32,
|
||||||
|
pub show_source_client: String,
|
||||||
|
pub show_replies_newest_first: bool,
|
||||||
|
pub note_body_font_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
theme: DEFAULT_THEME,
|
||||||
|
locale: DEFAULT_LOCALE.to_string(),
|
||||||
|
zoom_factor: DEFAULT_ZOOM_FACTOR,
|
||||||
|
show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(),
|
||||||
|
show_replies_newest_first: DEFAULT_SHOW_REPLIES_NEWEST_FIRST,
|
||||||
|
note_body_font_size: DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SettingsHandler {
|
||||||
|
directory: Directory,
|
||||||
|
serializer: TimedSerializer<Settings>,
|
||||||
|
current_settings: Option<Settings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsHandler {
|
||||||
|
fn read_from_theme_file(&self) -> Option<ThemePreference> {
|
||||||
|
match self.directory.get_file(THEME_FILE.to_string()) {
|
||||||
|
Ok(contents) => deserialize_theme(contents.trim()),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_zomfactor_file(&self) -> Option<f32> {
|
||||||
|
match self.directory.get_file(ZOOM_FACTOR_FILE.to_string()) {
|
||||||
|
Ok(contents) => serde_json::from_str::<f32>(&contents).ok(),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_to_settings_file(&mut self) -> bool {
|
||||||
|
let mut settings = Settings::default();
|
||||||
|
let mut migrated = false;
|
||||||
|
// if theme.txt exists migrate
|
||||||
|
if let Some(theme_from_file) = self.read_from_theme_file() {
|
||||||
|
info!("migrating theme preference from theme.txt file");
|
||||||
|
_ = delete_file(&self.directory.file_path, THEME_FILE.to_string());
|
||||||
|
|
||||||
|
settings.theme = theme_from_file;
|
||||||
|
migrated = true;
|
||||||
|
} else {
|
||||||
|
info!("theme.txt file not found, using default theme");
|
||||||
|
};
|
||||||
|
|
||||||
|
// if zoom_factor.txt exists migrate
|
||||||
|
if let Some(zom_factor) = self.read_from_zomfactor_file() {
|
||||||
|
info!("migrating theme preference from zom_factor file");
|
||||||
|
_ = delete_file(&self.directory.file_path, ZOOM_FACTOR_FILE.to_string());
|
||||||
|
|
||||||
|
settings.zoom_factor = zom_factor;
|
||||||
|
migrated = true;
|
||||||
|
} else {
|
||||||
|
info!("zoom_factor.txt exists migrate file not found, using default zoom factor");
|
||||||
|
};
|
||||||
|
|
||||||
|
if migrated {
|
||||||
|
self.current_settings = Some(settings);
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
migrated
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(path: &DataPath) -> Self {
|
||||||
|
let directory = Directory::new(path.path(DataPathType::Setting));
|
||||||
|
let serializer =
|
||||||
|
TimedSerializer::new(path, DataPathType::Setting, "settings.json".to_owned());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
directory,
|
||||||
|
serializer,
|
||||||
|
current_settings: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(mut self) -> Self {
|
||||||
|
if self.migrate_to_settings_file() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.directory.get_file(SETTINGS_FILE.to_string()) {
|
||||||
|
Ok(contents_str) => {
|
||||||
|
// Parse JSON content
|
||||||
|
match serde_json::from_str::<Settings>(&contents_str) {
|
||||||
|
Ok(settings) => {
|
||||||
|
self.current_settings = Some(settings);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
error!("Invalid settings format. Using defaults");
|
||||||
|
self.current_settings = Some(Settings::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
error!("Could not read settings. Using defaults");
|
||||||
|
self.current_settings = Some(Settings::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_save_settings(&mut self) {
|
||||||
|
let settings = self.get_settings_mut().clone();
|
||||||
|
self.serializer.try_save(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_settings_mut(&mut self) -> &mut Settings {
|
||||||
|
if self.current_settings.is_none() {
|
||||||
|
self.current_settings = Some(Settings::default());
|
||||||
|
}
|
||||||
|
self.current_settings.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_theme(&mut self, theme: ThemePreference) {
|
||||||
|
self.get_settings_mut().theme = theme;
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_locale<S>(&mut self, locale: S)
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.get_settings_mut().locale = locale.into();
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_zoom_factor(&mut self, zoom_factor: f32) {
|
||||||
|
self.get_settings_mut().zoom_factor = zoom_factor;
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_source_client<S>(&mut self, option: S)
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.get_settings_mut().show_source_client = option.into();
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_replies_newest_first(&mut self, value: bool) {
|
||||||
|
self.get_settings_mut().show_replies_newest_first = value;
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_note_body_font_size(&mut self, value: f32) {
|
||||||
|
self.get_settings_mut().note_body_font_size = value;
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_batch<F>(&mut self, update_fn: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Settings),
|
||||||
|
{
|
||||||
|
let settings = self.get_settings_mut();
|
||||||
|
update_fn(settings);
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_settings(&mut self, new_settings: Settings) {
|
||||||
|
self.current_settings = Some(new_settings);
|
||||||
|
self.try_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn theme(&self) -> ThemePreference {
|
||||||
|
self.current_settings
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.theme)
|
||||||
|
.unwrap_or(DEFAULT_THEME)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn locale(&self) -> String {
|
||||||
|
self.current_settings
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.locale.clone())
|
||||||
|
.unwrap_or_else(|| DEFAULT_LOCALE.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_factor(&self) -> f32 {
|
||||||
|
self.current_settings
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.zoom_factor)
|
||||||
|
.unwrap_or(DEFAULT_ZOOM_FACTOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_source_client(&self) -> String {
|
||||||
|
self.current_settings
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.show_source_client.to_string())
|
||||||
|
.unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_replies_newest_first(&self) -> bool {
|
||||||
|
self.current_settings
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.show_replies_newest_first)
|
||||||
|
.unwrap_or(DEFAULT_SHOW_REPLIES_NEWEST_FIRST)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_loaded(&self) -> bool {
|
||||||
|
self.current_settings.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_body_font_size(&self) -> f32 {
|
||||||
|
self.current_settings
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.note_body_font_size)
|
||||||
|
.unwrap_or(DEFAULT_NOTE_BODY_FONT_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
use egui::ThemePreference;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::{storage, DataPath, DataPathType, Directory};
|
|
||||||
|
|
||||||
pub struct ThemeHandler {
|
|
||||||
directory: Directory,
|
|
||||||
fallback_theme: ThemePreference,
|
|
||||||
}
|
|
||||||
|
|
||||||
const THEME_FILE: &str = "theme.txt";
|
|
||||||
|
|
||||||
impl ThemeHandler {
|
|
||||||
pub fn new(path: &DataPath) -> Self {
|
|
||||||
let directory = Directory::new(path.path(DataPathType::Setting));
|
|
||||||
let fallback_theme = ThemePreference::Dark;
|
|
||||||
Self {
|
|
||||||
directory,
|
|
||||||
fallback_theme,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(&self) -> ThemePreference {
|
|
||||||
match self.directory.get_file(THEME_FILE.to_owned()) {
|
|
||||||
Ok(contents) => match deserialize_theme(contents) {
|
|
||||||
Some(theme) => theme,
|
|
||||||
None => {
|
|
||||||
error!(
|
|
||||||
"Could not deserialize theme. Using fallback {:?} instead",
|
|
||||||
self.fallback_theme
|
|
||||||
);
|
|
||||||
self.fallback_theme
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Could not read {} file: {:?}\nUsing fallback {:?} instead",
|
|
||||||
THEME_FILE, e, self.fallback_theme
|
|
||||||
);
|
|
||||||
self.fallback_theme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&self, theme: ThemePreference) {
|
|
||||||
match storage::write_file(
|
|
||||||
&self.directory.file_path,
|
|
||||||
THEME_FILE.to_owned(),
|
|
||||||
&theme_to_serialized(&theme),
|
|
||||||
) {
|
|
||||||
Ok(_) => info!(
|
|
||||||
"Successfully saved {:?} theme change to {}",
|
|
||||||
theme, THEME_FILE
|
|
||||||
),
|
|
||||||
Err(_) => error!("Could not save {:?} theme change to {}", theme, THEME_FILE),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn theme_to_serialized(theme: &ThemePreference) -> String {
|
|
||||||
match theme {
|
|
||||||
ThemePreference::Dark => "dark",
|
|
||||||
ThemePreference::Light => "light",
|
|
||||||
ThemePreference::System => "system",
|
|
||||||
}
|
|
||||||
.to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_theme(serialized_theme: String) -> Option<ThemePreference> {
|
|
||||||
match serialized_theme.as_str() {
|
|
||||||
"dark" => Some(ThemePreference::Dark),
|
|
||||||
"light" => Some(ThemePreference::Light),
|
|
||||||
"system" => Some(ThemePreference::System),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
use crate::{DataPath, DataPathType};
|
|
||||||
use egui::Context;
|
|
||||||
|
|
||||||
use crate::timed_serializer::TimedSerializer;
|
|
||||||
|
|
||||||
pub struct ZoomHandler {
|
|
||||||
serializer: TimedSerializer<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ZoomHandler {
|
|
||||||
pub fn new(path: &DataPath) -> Self {
|
|
||||||
let serializer =
|
|
||||||
TimedSerializer::new(path, DataPathType::Setting, "zoom_level.json".to_owned());
|
|
||||||
|
|
||||||
Self { serializer }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_save_zoom_factor(&mut self, ctx: &Context) {
|
|
||||||
let cur_zoom_level = ctx.zoom_factor();
|
|
||||||
self.serializer.try_save(cur_zoom_level);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_zoom_factor(&self) -> Option<f32> {
|
|
||||||
self.serializer.get_item()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
|
|||||||
debug!("updating virtual keyboard height {}", height);
|
debug!("updating virtual keyboard height {}", height);
|
||||||
|
|
||||||
// Convert and store atomically
|
// Convert and store atomically
|
||||||
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
|
KEYBOARD_HEIGHT.store(height, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current Android virtual keyboard height. Useful for transforming
|
/// Gets the current Android virtual keyboard height. Useful for transforming
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::fonts;
|
||||||
|
use crate::theme;
|
||||||
|
use crate::NotedeckOptions;
|
||||||
|
use crate::NotedeckTextStyle;
|
||||||
|
use egui::FontId;
|
||||||
|
use egui::ThemePreference;
|
||||||
|
|
||||||
|
pub fn setup_egui_context(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
options: NotedeckOptions,
|
||||||
|
theme: ThemePreference,
|
||||||
|
note_body_font_size: f32,
|
||||||
|
zoom_factor: f32,
|
||||||
|
) {
|
||||||
|
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
|
||||||
|
|
||||||
|
let is_oled = crate::ui::is_oled();
|
||||||
|
|
||||||
|
ctx.options_mut(|o| {
|
||||||
|
tracing::info!("Loaded theme {:?} from disk", theme);
|
||||||
|
o.theme_preference = theme;
|
||||||
|
});
|
||||||
|
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
|
||||||
|
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
|
||||||
|
|
||||||
|
fonts::setup_fonts(ctx);
|
||||||
|
|
||||||
|
if crate::ui::is_compiled_as_mobile() {
|
||||||
|
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
egui_extras::install_image_loaders(ctx);
|
||||||
|
|
||||||
|
ctx.options_mut(|o| {
|
||||||
|
o.input_options.max_click_duration = 0.4;
|
||||||
|
});
|
||||||
|
ctx.all_styles_mut(|style| crate::theme::add_custom_style(is_mobile, style));
|
||||||
|
|
||||||
|
ctx.set_zoom_factor(zoom_factor);
|
||||||
|
|
||||||
|
let mut style = (*ctx.style()).clone();
|
||||||
|
style.text_styles.insert(
|
||||||
|
NotedeckTextStyle::NoteBody.text_style(),
|
||||||
|
FontId::proportional(note_body_font_size),
|
||||||
|
);
|
||||||
|
ctx.set_style(style);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ pub enum NotedeckTextStyle {
|
|||||||
Button,
|
Button,
|
||||||
Small,
|
Small,
|
||||||
Tiny,
|
Tiny,
|
||||||
|
NoteBody,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NotedeckTextStyle {
|
impl NotedeckTextStyle {
|
||||||
@@ -29,6 +30,7 @@ impl NotedeckTextStyle {
|
|||||||
Self::Button => TextStyle::Button,
|
Self::Button => TextStyle::Button,
|
||||||
Self::Small => TextStyle::Small,
|
Self::Small => TextStyle::Small,
|
||||||
Self::Tiny => TextStyle::Name("Tiny".into()),
|
Self::Tiny => TextStyle::Name("Tiny".into()),
|
||||||
|
Self::NoteBody => TextStyle::Name("NoteBody".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ impl NotedeckTextStyle {
|
|||||||
Self::Button => FontFamily::Proportional,
|
Self::Button => FontFamily::Proportional,
|
||||||
Self::Small => FontFamily::Proportional,
|
Self::Small => FontFamily::Proportional,
|
||||||
Self::Tiny => FontFamily::Proportional,
|
Self::Tiny => FontFamily::Proportional,
|
||||||
|
Self::NoteBody => FontFamily::Proportional,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,35 @@
|
|||||||
use egui::{
|
use crate::{fonts, NotedeckTextStyle};
|
||||||
style::{Selection, WidgetVisuals, Widgets},
|
use egui::style::Interaction;
|
||||||
Color32, CornerRadius, Stroke, Visuals,
|
use egui::style::Selection;
|
||||||
};
|
use egui::style::WidgetVisuals;
|
||||||
|
use egui::style::Widgets;
|
||||||
|
use egui::Color32;
|
||||||
|
use egui::CornerRadius;
|
||||||
|
use egui::FontId;
|
||||||
|
use egui::Stroke;
|
||||||
|
use egui::Style;
|
||||||
|
use egui::Visuals;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||||
|
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
||||||
|
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||||
|
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
||||||
|
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
||||||
|
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
||||||
|
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
||||||
|
|
||||||
|
// BACKGROUNDS
|
||||||
|
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
||||||
|
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
||||||
|
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
||||||
|
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
||||||
|
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
||||||
|
|
||||||
|
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
||||||
|
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
||||||
|
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
||||||
|
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
||||||
|
|
||||||
pub struct ColorTheme {
|
pub struct ColorTheme {
|
||||||
// VISUALS
|
// VISUALS
|
||||||
@@ -86,3 +114,131 @@ pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
|
|||||||
..default
|
..default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn desktop_dark_color_theme() -> ColorTheme {
|
||||||
|
ColorTheme {
|
||||||
|
// VISUALS
|
||||||
|
panel_fill: DARKER_BG,
|
||||||
|
extreme_bg_color: DARK_ISH_BG,
|
||||||
|
text_color: Color32::WHITE,
|
||||||
|
err_fg_color: RED_700,
|
||||||
|
warn_fg_color: ORANGE_700,
|
||||||
|
hyperlink_color: PURPLE,
|
||||||
|
selection_color: PURPLE_ALT,
|
||||||
|
|
||||||
|
// WINDOW
|
||||||
|
window_fill: DARK_ISH_BG,
|
||||||
|
window_stroke_color: DARK_BG,
|
||||||
|
|
||||||
|
// NONINTERACTIVE WIDGET
|
||||||
|
noninteractive_bg_fill: DARK_ISH_BG,
|
||||||
|
noninteractive_weak_bg_fill: DARK_BG,
|
||||||
|
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
||||||
|
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||||
|
|
||||||
|
// INACTIVE WIDGET
|
||||||
|
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
||||||
|
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||||
|
inactive_weak_bg_fill: SEMI_DARK_BG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mobile_dark_color_theme() -> ColorTheme {
|
||||||
|
ColorTheme {
|
||||||
|
panel_fill: Color32::BLACK,
|
||||||
|
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||||
|
..desktop_dark_color_theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light_color_theme() -> ColorTheme {
|
||||||
|
ColorTheme {
|
||||||
|
// VISUALS
|
||||||
|
panel_fill: Color32::WHITE,
|
||||||
|
extreme_bg_color: LIGHTER_GRAY,
|
||||||
|
text_color: BLACK,
|
||||||
|
err_fg_color: RED_700,
|
||||||
|
warn_fg_color: ORANGE_700,
|
||||||
|
hyperlink_color: PURPLE,
|
||||||
|
selection_color: PURPLE_ALT,
|
||||||
|
|
||||||
|
// WINDOW
|
||||||
|
window_fill: Color32::WHITE,
|
||||||
|
window_stroke_color: DARKER_GRAY,
|
||||||
|
|
||||||
|
// NONINTERACTIVE WIDGET
|
||||||
|
noninteractive_bg_fill: Color32::WHITE,
|
||||||
|
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
||||||
|
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
||||||
|
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||||
|
|
||||||
|
// INACTIVE WIDGET
|
||||||
|
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
||||||
|
inactive_bg_fill: LIGHTER_GRAY,
|
||||||
|
inactive_weak_bg_fill: LIGHTER_GRAY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create custom text sizes for any FontSizes
|
||||||
|
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
||||||
|
let font_size = if is_mobile {
|
||||||
|
fonts::mobile_font_size
|
||||||
|
} else {
|
||||||
|
fonts::desktop_font_size
|
||||||
|
};
|
||||||
|
|
||||||
|
style.text_styles = NotedeckTextStyle::iter()
|
||||||
|
.map(|text_style| {
|
||||||
|
(
|
||||||
|
text_style.text_style(),
|
||||||
|
FontId::new(font_size(&text_style), text_style.font_family()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
style.interaction = Interaction {
|
||||||
|
tooltip_delay: 0.1,
|
||||||
|
show_tooltips_only_when_still: false,
|
||||||
|
..Interaction::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// debug: show callstack for the current widget on hover if all
|
||||||
|
// modifier keys are pressed down.
|
||||||
|
/*
|
||||||
|
#[cfg(feature = "debug-widget-callstack")]
|
||||||
|
{
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
compile_error!(
|
||||||
|
"The `debug-widget-callstack` feature requires a debug build, \
|
||||||
|
release builds are unsupported."
|
||||||
|
);
|
||||||
|
style.debug.debug_on_hover_with_all_modifiers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug: show an overlay on all interactive widgets
|
||||||
|
#[cfg(feature = "debug-interactive-widgets")]
|
||||||
|
{
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
compile_error!(
|
||||||
|
"The `debug-interactive-widgets` feature requires a debug build, \
|
||||||
|
release builds are unsupported."
|
||||||
|
);
|
||||||
|
style.debug.show_interactive_widgets = true;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light_mode() -> Visuals {
|
||||||
|
create_themed_visuals(crate::theme::light_color_theme(), Visuals::light())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dark_mode(is_oled: bool) -> Visuals {
|
||||||
|
create_themed_visuals(
|
||||||
|
if is_oled {
|
||||||
|
mobile_dark_color_theme()
|
||||||
|
} else {
|
||||||
|
desktop_dark_color_theme()
|
||||||
|
},
|
||||||
|
Visuals::dark(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ use crate::debouncer::Debouncer;
|
|||||||
use crate::{storage, DataPath, DataPathType, Directory};
|
use crate::{storage, DataPath, DataPathType, Directory};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::info; // Adjust this import path as needed
|
use tracing::info;
|
||||||
|
|
||||||
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
|
pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> {
|
||||||
directory: Directory,
|
directory: Directory,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
debouncer: Debouncer,
|
debouncer: Debouncer,
|
||||||
saved_item: Option<T>,
|
saved_item: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
||||||
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
|
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
|
||||||
let directory = Directory::new(path.path(path_type));
|
let directory = Directory::new(path.path(path_type));
|
||||||
let delay = Duration::from_millis(1000);
|
let delay = Duration::from_millis(1000);
|
||||||
@@ -30,11 +30,11 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns whether successful
|
/// Returns whether it actually wrote the new value
|
||||||
pub fn try_save(&mut self, cur_item: T) -> bool {
|
pub fn try_save(&mut self, cur_item: T) -> bool {
|
||||||
if self.debouncer.should_act() {
|
if self.debouncer.should_act() {
|
||||||
if let Some(saved_item) = self.saved_item {
|
if let Some(ref saved_item) = self.saved_item {
|
||||||
if saved_item != cur_item {
|
if *saved_item != cur_item {
|
||||||
return self.save(cur_item);
|
return self.save(cur_item);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -45,8 +45,8 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_item(&self) -> Option<T> {
|
pub fn get_item(&self) -> Option<T> {
|
||||||
if self.saved_item.is_some() {
|
if let Some(ref item) = self.saved_item {
|
||||||
return self.saved_item;
|
return Some(item.clone());
|
||||||
}
|
}
|
||||||
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
|
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
|
||||||
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
|
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
|
use crate::NotedeckTextStyle;
|
||||||
|
|
||||||
|
pub const NARROW_SCREEN_WIDTH: f32 = 550.0;
|
||||||
|
|
||||||
|
pub fn richtext_small<S>(text: S) -> egui::RichText
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
egui::RichText::new(text).text_style(NotedeckTextStyle::Small.text_style())
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine if the screen is narrow. This is useful for detecting mobile
|
/// Determine if the screen is narrow. This is useful for detecting mobile
|
||||||
/// contexts, but with the nuance that we may also have a wide android tablet.
|
/// contexts, but with the nuance that we may also have a wide android tablet.
|
||||||
pub fn is_narrow(ctx: &egui::Context) -> bool {
|
pub fn is_narrow(ctx: &egui::Context) -> bool {
|
||||||
let screen_size = ctx.input(|c| c.screen_rect().size());
|
let screen_size = ctx.input(|c| c.screen_rect().size());
|
||||||
screen_size.x < 550.0
|
screen_size.x < NARROW_SCREEN_WIDTH
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_oled() -> bool {
|
pub fn is_oled() -> bool {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ egui = { workspace = true }
|
|||||||
notedeck_columns = { workspace = true }
|
notedeck_columns = { workspace = true }
|
||||||
notedeck_ui = { workspace = true }
|
notedeck_ui = { workspace = true }
|
||||||
notedeck_dave = { workspace = true }
|
notedeck_dave = { workspace = true }
|
||||||
|
notedeck_notebook = { workspace = true }
|
||||||
notedeck = { workspace = true }
|
notedeck = { workspace = true }
|
||||||
nostrdb = { workspace = true }
|
nostrdb = { workspace = true }
|
||||||
puffin = { workspace = true, optional = true }
|
puffin = { workspace = true, optional = true }
|
||||||
@@ -63,6 +64,12 @@ short_description = "The nostr browser"
|
|||||||
identifier = "com.damus.notedeck"
|
identifier = "com.damus.notedeck"
|
||||||
icon = ["assets/app_icon.icns"]
|
icon = ["assets/app_icon.icns"]
|
||||||
|
|
||||||
|
[package.metadata.android.manifest.queries]
|
||||||
|
intent = [
|
||||||
|
{ action = ["android.intent.action.MAIN"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
[package.metadata.android]
|
[package.metadata.android]
|
||||||
package = "com.damus.app"
|
package = "com.damus.app"
|
||||||
apk_name = "Notedeck"
|
apk_name = "Notedeck"
|
||||||
|
|||||||
@@ -23,9 +23,16 @@
|
|||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.vulkan.level"
|
<uses-feature android:name="android.hardware.vulkan.level"
|
||||||
android:required="true"
|
android:required="true"
|
||||||
android:version="1" />
|
android:version="1" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|||||||
@@ -2,13 +2,9 @@
|
|||||||
//use egui_android::run_android;
|
//use egui_android::run_android;
|
||||||
|
|
||||||
use egui_winit::winit::platform::android::activity::AndroidApp;
|
use egui_winit::winit::platform::android::activity::AndroidApp;
|
||||||
use notedeck::enostr::Error;
|
|
||||||
use notedeck_columns::Damus;
|
|
||||||
use notedeck_dave::Dave;
|
|
||||||
|
|
||||||
use crate::{app::NotedeckApp, chrome::Chrome, setup::setup_chrome};
|
use crate::chrome::Chrome;
|
||||||
use notedeck::Notedeck;
|
use notedeck::Notedeck;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -69,30 +65,8 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
let ctx = &cc.egui_ctx;
|
let ctx = &cc.egui_ctx;
|
||||||
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
||||||
setup_chrome(ctx, ¬edeck.args(), notedeck.theme());
|
notedeck.setup(ctx);
|
||||||
|
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
||||||
let context = &mut notedeck.app_context();
|
|
||||||
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
|
||||||
let columns = Damus::new(context, &app_args);
|
|
||||||
let mut chrome = Chrome::new();
|
|
||||||
|
|
||||||
// ensure we recognized all the arguments
|
|
||||||
let completely_unrecognized: Vec<String> = notedeck
|
|
||||||
.unrecognized_args()
|
|
||||||
.intersection(columns.unrecognized_args())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
if !completely_unrecognized.is_empty() {
|
|
||||||
error!("Unrecognized arguments: {:?}", completely_unrecognized);
|
|
||||||
return Err(Error::Empty.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.add_app(NotedeckApp::Columns(columns));
|
|
||||||
chrome.add_app(NotedeckApp::Dave(dave));
|
|
||||||
|
|
||||||
// test dav
|
|
||||||
chrome.set_active(0);
|
|
||||||
|
|
||||||
notedeck.set_app(chrome);
|
notedeck.set_app(chrome);
|
||||||
|
|
||||||
Ok(Box::new(notedeck))
|
Ok(Box::new(notedeck))
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use notedeck::{AppAction, AppContext};
|
use notedeck::{AppAction, AppContext};
|
||||||
use notedeck_columns::Damus;
|
use notedeck_columns::Damus;
|
||||||
use notedeck_dave::Dave;
|
use notedeck_dave::Dave;
|
||||||
|
use notedeck_notebook::Notebook;
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum NotedeckApp {
|
pub enum NotedeckApp {
|
||||||
Dave(Dave),
|
Dave(Box<Dave>),
|
||||||
Columns(Damus),
|
Columns(Box<Damus>),
|
||||||
|
Notebook(Box<Notebook>),
|
||||||
Other(Box<dyn notedeck::App>),
|
Other(Box<dyn notedeck::App>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ impl notedeck::App for NotedeckApp {
|
|||||||
match self {
|
match self {
|
||||||
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
||||||
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
||||||
|
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
|
||||||
NotedeckApp::Other(other) => other.update(ctx, ui),
|
NotedeckApp::Other(other) => other.update(ctx, ui),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,22 @@
|
|||||||
//#[cfg(target_arch = "wasm32")]
|
//#[cfg(target_arch = "wasm32")]
|
||||||
//use wasm_bindgen::prelude::*;
|
//use wasm_bindgen::prelude::*;
|
||||||
use crate::app::NotedeckApp;
|
use crate::app::NotedeckApp;
|
||||||
|
use eframe::CreationContext;
|
||||||
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
|
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
use nostrdb::{ProfileRecord, Transaction};
|
use nostrdb::{ProfileRecord, Transaction};
|
||||||
|
use notedeck::Error;
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
|
tr, App, AppAction, AppContext, Localization, Notedeck, NotedeckOptions, NotedeckTextStyle,
|
||||||
|
UserAccount, WalletType,
|
||||||
};
|
};
|
||||||
use notedeck_columns::{
|
use notedeck_columns::{
|
||||||
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
|
column::SelectionResult,
|
||||||
|
timeline::{kind::ListKind, TimelineKind},
|
||||||
|
Damus,
|
||||||
};
|
};
|
||||||
use notedeck_dave::{Dave, DaveAvatar};
|
use notedeck_dave::{Dave, DaveAvatar};
|
||||||
|
use notedeck_notebook::Notebook;
|
||||||
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
|
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
|
||||||
|
|
||||||
static ICON_WIDTH: f32 = 40.0;
|
static ICON_WIDTH: f32 = 40.0;
|
||||||
@@ -112,10 +118,8 @@ impl ChromePanelAction {
|
|||||||
fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
|
fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
|
||||||
match self {
|
match self {
|
||||||
Self::SaveTheme(theme) => {
|
Self::SaveTheme(theme) => {
|
||||||
ui.ctx().options_mut(|o| {
|
ui.ctx().set_theme(*theme);
|
||||||
o.theme_preference = *theme;
|
ctx.settings.set_theme(*theme);
|
||||||
});
|
|
||||||
ctx.theme.save(*theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::Toolbar(toolbar_action) => match toolbar_action {
|
Self::Toolbar(toolbar_action) => match toolbar_action {
|
||||||
@@ -168,9 +172,49 @@ impl ChromePanelAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Some people have been running notedeck in debug, let's catch that!
|
||||||
|
fn stop_debug_mode(options: NotedeckOptions) {
|
||||||
|
if !options.contains(NotedeckOptions::Tests)
|
||||||
|
&& cfg!(debug_assertions)
|
||||||
|
&& !options.contains(NotedeckOptions::Debug)
|
||||||
|
{
|
||||||
|
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
||||||
|
println!(
|
||||||
|
"It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want."
|
||||||
|
);
|
||||||
|
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
||||||
|
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
||||||
|
println!("---------------------------------");
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Chrome {
|
impl Chrome {
|
||||||
pub fn new() -> Self {
|
/// Create a new chrome with the default app setup
|
||||||
Chrome::default()
|
pub fn new_with_apps(
|
||||||
|
cc: &CreationContext,
|
||||||
|
app_args: &[String],
|
||||||
|
notedeck: &mut Notedeck,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
stop_debug_mode(notedeck.options());
|
||||||
|
|
||||||
|
let context = &mut notedeck.app_context();
|
||||||
|
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
||||||
|
let columns = Damus::new(context, app_args);
|
||||||
|
let mut chrome = Chrome::default();
|
||||||
|
|
||||||
|
notedeck.check_args(columns.unrecognized_args())?;
|
||||||
|
|
||||||
|
chrome.add_app(NotedeckApp::Columns(Box::new(columns)));
|
||||||
|
chrome.add_app(NotedeckApp::Dave(Box::new(dave)));
|
||||||
|
|
||||||
|
if notedeck.has_option(NotedeckOptions::FeatureNotebook) {
|
||||||
|
chrome.add_app(NotedeckApp::Notebook(Box::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.set_active(0);
|
||||||
|
|
||||||
|
Ok(chrome)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle(&mut self) {
|
pub fn toggle(&mut self) {
|
||||||
@@ -201,6 +245,16 @@ impl Chrome {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_notebook(&mut self) -> Option<&mut Notebook> {
|
||||||
|
for app in &mut self.apps {
|
||||||
|
if let NotedeckApp::Notebook(notebook) = app {
|
||||||
|
return Some(notebook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn switch_to_dave(&mut self) {
|
fn switch_to_dave(&mut self) {
|
||||||
for (i, app) in self.apps.iter().enumerate() {
|
for (i, app) in self.apps.iter().enumerate() {
|
||||||
if let NotedeckApp::Dave(_) = app {
|
if let NotedeckApp::Dave(_) = app {
|
||||||
@@ -209,6 +263,14 @@ impl Chrome {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn switch_to_notebook(&mut self) {
|
||||||
|
for (i, app) in self.apps.iter().enumerate() {
|
||||||
|
if let NotedeckApp::Notebook(_) = app {
|
||||||
|
self.active = i as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn switch_to_columns(&mut self) {
|
fn switch_to_columns(&mut self) {
|
||||||
for (i, app) in self.apps.iter().enumerate() {
|
for (i, app) in self.apps.iter().enumerate() {
|
||||||
if let NotedeckApp::Columns(_) = app {
|
if let NotedeckApp::Columns(_) = app {
|
||||||
@@ -325,7 +387,12 @@ impl Chrome {
|
|||||||
});
|
});
|
||||||
|
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
if let Some(action) = self.toolbar(ui) {
|
let pk = ctx.accounts.get_selected_account().key.pubkey;
|
||||||
|
|
||||||
|
let unseen_notification =
|
||||||
|
unseen_notification(self.get_columns_app(), ctx.ndb, pk);
|
||||||
|
|
||||||
|
if let Some(action) = self.toolbar(ui, unseen_notification) {
|
||||||
got_action = Some(ChromePanelAction::Toolbar(action))
|
got_action = Some(ChromePanelAction::Toolbar(action))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -334,7 +401,7 @@ impl Chrome {
|
|||||||
got_action
|
got_action
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar(&mut self, ui: &mut egui::Ui) -> Option<ToolbarAction> {
|
fn toolbar(&mut self, ui: &mut egui::Ui, unseen_notification: bool) -> Option<ToolbarAction> {
|
||||||
use egui_tabs::{TabColor, Tabs};
|
use egui_tabs::{TabColor, Tabs};
|
||||||
|
|
||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
@@ -378,7 +445,9 @@ impl Chrome {
|
|||||||
action = Some(ToolbarAction::Dave);
|
action = Some(ToolbarAction::Dave);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if index == 2 && notifications_button(ui, btn_size).clicked() {
|
} else if index == 2
|
||||||
|
&& notifications_button(ui, btn_size, unseen_notification).clicked()
|
||||||
|
{
|
||||||
action = Some(ToolbarAction::Notifications);
|
action = Some(ToolbarAction::Notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,14 +499,12 @@ impl Chrome {
|
|||||||
ui.add(milestone_name(i18n));
|
ui.add(milestone_name(i18n));
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
//let dark_mode = ui.ctx().style().visuals.dark_mode;
|
//let dark_mode = ui.ctx().style().visuals.dark_mode;
|
||||||
{
|
|
||||||
if columns_button(ui)
|
if columns_button(ui)
|
||||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
self.active = 0;
|
self.active = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ui.add_space(32.0);
|
ui.add_space(32.0);
|
||||||
|
|
||||||
if let Some(dave) = self.get_dave() {
|
if let Some(dave) = self.get_dave() {
|
||||||
@@ -448,8 +515,50 @@ impl Chrome {
|
|||||||
self.switch_to_dave();
|
self.switch_to_dave();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//ui.add_space(32.0);
|
||||||
|
|
||||||
|
if let Some(_notebook) = self.get_notebook() {
|
||||||
|
if notebook_button(ui)
|
||||||
|
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.switch_to_notebook();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unseen_notification(
|
||||||
|
columns: Option<&mut Damus>,
|
||||||
|
ndb: &nostrdb::Ndb,
|
||||||
|
current_pk: notedeck::enostr::Pubkey,
|
||||||
|
) -> bool {
|
||||||
|
let Some(columns) = columns else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(tl) = columns
|
||||||
|
.timeline_cache
|
||||||
|
.get_mut(&TimelineKind::Notifications(current_pk))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let freshness = &mut tl.current_view_mut().freshness;
|
||||||
|
freshness.update(|timestamp_last_viewed| {
|
||||||
|
let filter = notedeck_columns::timeline::kind::notifications_filter(¤t_pk)
|
||||||
|
.since_mut(timestamp_last_viewed);
|
||||||
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
|
|
||||||
|
let Some(res) = ndb.query(&txn, &[filter], 1).ok() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
!res.is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
freshness.has_unseen()
|
||||||
|
}
|
||||||
|
|
||||||
impl notedeck::App for Chrome {
|
impl notedeck::App for Chrome {
|
||||||
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
|
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
|
||||||
@@ -504,6 +613,7 @@ fn expanding_button(
|
|||||||
light_img: egui::Image,
|
light_img: egui::Image,
|
||||||
dark_img: egui::Image,
|
dark_img: egui::Image,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
unseen_indicator: bool,
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
||||||
let img = if ui.visuals().dark_mode {
|
let img = if ui.visuals().dark_mode {
|
||||||
@@ -515,16 +625,34 @@ fn expanding_button(
|
|||||||
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
|
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
|
||||||
|
|
||||||
let cur_img_size = helper.scale_1d_pos(img_size);
|
let cur_img_size = helper.scale_1d_pos(img_size);
|
||||||
img.paint_at(
|
|
||||||
ui,
|
let paint_rect = helper
|
||||||
helper
|
|
||||||
.get_animation_rect()
|
.get_animation_rect()
|
||||||
.shrink((max_size - cur_img_size) / 2.0),
|
.shrink((max_size - cur_img_size) / 2.0);
|
||||||
);
|
img.paint_at(ui, paint_rect);
|
||||||
|
|
||||||
|
if unseen_indicator {
|
||||||
|
paint_unseen_indicator(ui, paint_rect, helper.scale_1d_pos(3.0));
|
||||||
|
}
|
||||||
|
|
||||||
helper.take_animation_response()
|
helper.take_animation_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_unseen_indicator(ui: &mut egui::Ui, rect: egui::Rect, radius: f32) {
|
||||||
|
let center = rect.center();
|
||||||
|
let top_right = rect.right_top();
|
||||||
|
let distance = center.distance(top_right);
|
||||||
|
let midpoint = {
|
||||||
|
let mut cur = center;
|
||||||
|
cur.x += distance / 2.0;
|
||||||
|
cur.y -= distance / 2.0;
|
||||||
|
cur
|
||||||
|
};
|
||||||
|
|
||||||
|
let painter = ui.painter_at(rect);
|
||||||
|
painter.circle_filled(midpoint, radius, notedeck_ui::colors::PINK);
|
||||||
|
}
|
||||||
|
|
||||||
fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
||||||
expanding_button(
|
expanding_button(
|
||||||
"help-button",
|
"help-button",
|
||||||
@@ -532,6 +660,7 @@ fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::help_light_image(),
|
app_images::help_light_image(),
|
||||||
app_images::help_dark_image(),
|
app_images::help_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,16 +671,18 @@ fn settings_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::settings_light_image(),
|
app_images::settings_light_image(),
|
||||||
app_images::settings_dark_image(),
|
app_images::settings_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notifications_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
fn notifications_button(ui: &mut egui::Ui, size: f32, unseen_indicator: bool) -> egui::Response {
|
||||||
expanding_button(
|
expanding_button(
|
||||||
"notifications-button",
|
"notifications-button",
|
||||||
size,
|
size,
|
||||||
app_images::notifications_light_image(),
|
app_images::notifications_light_image(),
|
||||||
app_images::notifications_dark_image(),
|
app_images::notifications_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
unseen_indicator,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,6 +693,7 @@ fn home_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
|||||||
app_images::home_light_image(),
|
app_images::home_light_image(),
|
||||||
app_images::home_dark_image(),
|
app_images::home_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,6 +704,7 @@ fn columns_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::columns_image(),
|
app_images::columns_image(),
|
||||||
app_images::columns_image(),
|
app_images::columns_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,6 +715,18 @@ fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::accounts_image().tint(ui.visuals().text_color()),
|
app_images::accounts_image().tint(ui.visuals().text_color()),
|
||||||
app_images::accounts_image(),
|
app_images::accounts_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
|
||||||
|
expanding_button(
|
||||||
|
"notebook-button",
|
||||||
|
40.0,
|
||||||
|
app_images::algo_image(),
|
||||||
|
app_images::algo_image(),
|
||||||
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,6 +840,7 @@ fn chrome_handle_app_action(
|
|||||||
ctx.global_wallet,
|
ctx.global_wallet,
|
||||||
ctx.zaps,
|
ctx.zaps,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
|
&mut columns.view_state,
|
||||||
ui,
|
ui,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -750,6 +896,7 @@ fn columns_route_to_profile(
|
|||||||
ctx.global_wallet,
|
ctx.global_wallet,
|
||||||
ctx.zaps,
|
ctx.zaps,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
|
&mut columns.view_state,
|
||||||
ui,
|
ui,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -861,7 +1008,7 @@ fn bottomup_sidebar(
|
|||||||
.add(wallet_button())
|
.add(wallet_button())
|
||||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
if ctx.args.debug {
|
if ctx.args.options.contains(NotedeckOptions::Debug) {
|
||||||
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
|
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
|
||||||
ui.weak(format!(
|
ui.weak(format!(
|
||||||
"{:10.1}",
|
"{:10.1}",
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
use egui::{FontData, FontDefinitions, FontTweak};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use notedeck::fonts::NamedFontFamily;
|
|
||||||
|
|
||||||
// Use gossip's approach to font loading. This includes japanese fonts
|
|
||||||
// for rending stuff from japanese users.
|
|
||||||
pub fn setup_fonts(ctx: &egui::Context) {
|
|
||||||
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
|
|
||||||
let mut families = BTreeMap::new();
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"Onest".to_owned(),
|
|
||||||
Arc::new(FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"OnestMedium".to_owned(),
|
|
||||||
Arc::new(FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"DejaVuSans".to_owned(),
|
|
||||||
Arc::new(FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"OnestBold".to_owned(),
|
|
||||||
Arc::new(FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
font_data.insert(
|
|
||||||
"DejaVuSansBold".to_owned(),
|
|
||||||
FontData::from_static(include_bytes!(
|
|
||||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"DejaVuSans".to_owned(),
|
|
||||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
|
||||||
);
|
|
||||||
font_data.insert(
|
|
||||||
"DejaVuSansBold".to_owned(),
|
|
||||||
FontData::from_static(include_bytes!(
|
|
||||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"Inconsolata".to_owned(),
|
|
||||||
Arc::new(
|
|
||||||
FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/Inconsolata-Regular.ttf"
|
|
||||||
))
|
|
||||||
.tweak(FontTweak {
|
|
||||||
scale: 1.22, // This font is smaller than DejaVuSans
|
|
||||||
y_offset_factor: -0.18, // and too low
|
|
||||||
y_offset: 0.0,
|
|
||||||
baseline_offset_factor: 0.0,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"NotoSansCJK".to_owned(),
|
|
||||||
Arc::new(FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
|
|
||||||
font_data.insert(
|
|
||||||
"NotoSansThai".to_owned(),
|
|
||||||
Arc::new(FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/NotoSansThai-Regular.ttf"
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Some good looking emojis. Use as first priority:
|
|
||||||
font_data.insert(
|
|
||||||
"NotoEmoji".to_owned(),
|
|
||||||
Arc::new(
|
|
||||||
FontData::from_static(include_bytes!(
|
|
||||||
"../../../assets/fonts/NotoEmoji-Regular.ttf"
|
|
||||||
))
|
|
||||||
.tweak(FontTweak {
|
|
||||||
scale: 1.1, // make them a touch larger
|
|
||||||
y_offset_factor: 0.0,
|
|
||||||
y_offset: 0.0,
|
|
||||||
baseline_offset_factor: 0.0,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let base_fonts = vec![
|
|
||||||
"DejaVuSans".to_owned(),
|
|
||||||
"NotoEmoji".to_owned(),
|
|
||||||
"NotoSansCJK".to_owned(),
|
|
||||||
"NotoSansThai".to_owned(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut proportional = vec!["Onest".to_owned()];
|
|
||||||
proportional.extend(base_fonts.clone());
|
|
||||||
|
|
||||||
let mut medium = vec!["OnestMedium".to_owned()];
|
|
||||||
medium.extend(base_fonts.clone());
|
|
||||||
|
|
||||||
let mut mono = vec!["Inconsolata".to_owned()];
|
|
||||||
mono.extend(base_fonts.clone());
|
|
||||||
|
|
||||||
let mut bold = vec!["OnestBold".to_owned()];
|
|
||||||
bold.extend(base_fonts.clone());
|
|
||||||
|
|
||||||
let emoji = vec!["NotoEmoji".to_owned()];
|
|
||||||
|
|
||||||
families.insert(egui::FontFamily::Proportional, proportional);
|
|
||||||
families.insert(egui::FontFamily::Monospace, mono);
|
|
||||||
families.insert(
|
|
||||||
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
|
||||||
medium,
|
|
||||||
);
|
|
||||||
families.insert(
|
|
||||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
|
||||||
bold,
|
|
||||||
);
|
|
||||||
families.insert(
|
|
||||||
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
|
||||||
emoji,
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!("fonts: {:?}", families);
|
|
||||||
|
|
||||||
let defs = FontDefinitions {
|
|
||||||
font_data,
|
|
||||||
families,
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.set_fonts(defs);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
pub mod fonts;
|
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
pub mod theme;
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
mod android;
|
mod android;
|
||||||
|
|||||||
@@ -9,15 +9,8 @@ use re_memory::AccountingAllocator;
|
|||||||
static GLOBAL: AccountingAllocator<std::alloc::System> =
|
static GLOBAL: AccountingAllocator<std::alloc::System> =
|
||||||
AccountingAllocator::new(std::alloc::System);
|
AccountingAllocator::new(std::alloc::System);
|
||||||
|
|
||||||
use notedeck::enostr::Error;
|
|
||||||
use notedeck::{DataPath, DataPathType, Notedeck};
|
use notedeck::{DataPath, DataPathType, Notedeck};
|
||||||
use notedeck_chrome::{
|
use notedeck_chrome::{setup::generate_native_options, Chrome};
|
||||||
setup::{generate_native_options, setup_chrome},
|
|
||||||
Chrome, NotedeckApp,
|
|
||||||
};
|
|
||||||
use notedeck_columns::Damus;
|
|
||||||
use notedeck_dave::Dave;
|
|
||||||
use tracing::error;
|
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
@@ -93,29 +86,8 @@ async fn main() {
|
|||||||
let ctx = &cc.egui_ctx;
|
let ctx = &cc.egui_ctx;
|
||||||
|
|
||||||
let mut notedeck = Notedeck::new(ctx, base_path, &args);
|
let mut notedeck = Notedeck::new(ctx, base_path, &args);
|
||||||
|
notedeck.setup(ctx);
|
||||||
let mut chrome = Chrome::new();
|
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
|
||||||
let columns = Damus::new(&mut notedeck.app_context(), &args);
|
|
||||||
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
|
||||||
|
|
||||||
setup_chrome(ctx, notedeck.args(), notedeck.theme());
|
|
||||||
|
|
||||||
// ensure we recognized all the arguments
|
|
||||||
let completely_unrecognized: Vec<String> = notedeck
|
|
||||||
.unrecognized_args()
|
|
||||||
.intersection(columns.unrecognized_args())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
if !completely_unrecognized.is_empty() {
|
|
||||||
error!("Unrecognized arguments: {:?}", completely_unrecognized);
|
|
||||||
return Err(Error::Empty.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.add_app(NotedeckApp::Columns(columns));
|
|
||||||
chrome.add_app(NotedeckApp::Dave(dave));
|
|
||||||
|
|
||||||
chrome.set_active(0);
|
|
||||||
|
|
||||||
notedeck.set_app(chrome);
|
notedeck.set_app(chrome);
|
||||||
|
|
||||||
Ok(Box::new(notedeck))
|
Ok(Box::new(notedeck))
|
||||||
@@ -149,7 +121,8 @@ pub fn main() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Damus, Notedeck};
|
use super::Notedeck;
|
||||||
|
use notedeck_columns::Damus;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn create_tmp_dir() -> PathBuf {
|
fn create_tmp_dir() -> PathBuf {
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ impl PreviewRunner {
|
|||||||
"unrecognized args: {:?}",
|
"unrecognized args: {:?}",
|
||||||
notedeck.unrecognized_args()
|
notedeck.unrecognized_args()
|
||||||
);
|
);
|
||||||
setup_chrome(ctx, notedeck.args(), notedeck.theme());
|
setup_chrome(
|
||||||
|
ctx,
|
||||||
|
notedeck.args(),
|
||||||
|
notedeck.theme(),
|
||||||
|
notedeck.note_body_font_size(),
|
||||||
|
notedeck.zoom_factor(),
|
||||||
|
);
|
||||||
|
|
||||||
notedeck.set_app(PreviewApp::new(preview));
|
notedeck.set_app(PreviewApp::new(preview));
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,6 @@
|
|||||||
use crate::{fonts, theme};
|
|
||||||
|
|
||||||
use eframe::NativeOptions;
|
use eframe::NativeOptions;
|
||||||
use egui::ThemePreference;
|
|
||||||
use notedeck::{AppSizeHandler, DataPath};
|
use notedeck::{AppSizeHandler, DataPath};
|
||||||
use notedeck_ui::app_images;
|
use notedeck_ui::app_images;
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
pub fn setup_chrome(ctx: &egui::Context, args: ¬edeck::Args, theme: ThemePreference) {
|
|
||||||
let is_mobile = args
|
|
||||||
.is_mobile
|
|
||||||
.unwrap_or(notedeck::ui::is_compiled_as_mobile());
|
|
||||||
|
|
||||||
let is_oled = notedeck::ui::is_oled();
|
|
||||||
|
|
||||||
// Some people have been running notedeck in debug, let's catch that!
|
|
||||||
if !args.tests && cfg!(debug_assertions) && !args.debug {
|
|
||||||
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
|
||||||
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
|
|
||||||
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
|
||||||
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
|
||||||
println!("---------------------------------");
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.options_mut(|o| {
|
|
||||||
info!("Loaded theme {:?} from disk", theme);
|
|
||||||
o.theme_preference = theme;
|
|
||||||
});
|
|
||||||
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
|
|
||||||
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
|
|
||||||
|
|
||||||
setup_cc(ctx, is_mobile);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) {
|
|
||||||
fonts::setup_fonts(ctx);
|
|
||||||
|
|
||||||
if notedeck::ui::is_compiled_as_mobile() {
|
|
||||||
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
//ctx.set_pixels_per_point(1.0);
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//ctx.tessellation_options_mut(|to| to.feathering = false);
|
|
||||||
|
|
||||||
egui_extras::install_image_loaders(ctx);
|
|
||||||
|
|
||||||
ctx.options_mut(|o| {
|
|
||||||
o.input_options.max_click_duration = 0.4;
|
|
||||||
});
|
|
||||||
ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
||||||
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
use egui::{style::Interaction, Color32, FontId, Style, Visuals};
|
|
||||||
use notedeck::{ColorTheme, NotedeckTextStyle};
|
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
|
|
||||||
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
|
||||||
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
|
||||||
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
|
||||||
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
|
||||||
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
|
||||||
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
|
||||||
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
|
||||||
|
|
||||||
// BACKGROUNDS
|
|
||||||
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
|
||||||
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
|
||||||
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
|
||||||
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
|
||||||
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
|
||||||
|
|
||||||
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
|
||||||
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
|
||||||
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
|
||||||
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
|
||||||
|
|
||||||
pub fn desktop_dark_color_theme() -> ColorTheme {
|
|
||||||
ColorTheme {
|
|
||||||
// VISUALS
|
|
||||||
panel_fill: DARKER_BG,
|
|
||||||
extreme_bg_color: DARK_ISH_BG,
|
|
||||||
text_color: Color32::WHITE,
|
|
||||||
err_fg_color: RED_700,
|
|
||||||
warn_fg_color: ORANGE_700,
|
|
||||||
hyperlink_color: PURPLE,
|
|
||||||
selection_color: PURPLE_ALT,
|
|
||||||
|
|
||||||
// WINDOW
|
|
||||||
window_fill: DARK_ISH_BG,
|
|
||||||
window_stroke_color: DARK_BG,
|
|
||||||
|
|
||||||
// NONINTERACTIVE WIDGET
|
|
||||||
noninteractive_bg_fill: DARK_ISH_BG,
|
|
||||||
noninteractive_weak_bg_fill: DARK_BG,
|
|
||||||
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
|
||||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
|
||||||
|
|
||||||
// INACTIVE WIDGET
|
|
||||||
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
|
||||||
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
|
||||||
inactive_weak_bg_fill: SEMI_DARK_BG,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mobile_dark_color_theme() -> ColorTheme {
|
|
||||||
ColorTheme {
|
|
||||||
panel_fill: Color32::BLACK,
|
|
||||||
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
|
||||||
..desktop_dark_color_theme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light_color_theme() -> ColorTheme {
|
|
||||||
ColorTheme {
|
|
||||||
// VISUALS
|
|
||||||
panel_fill: Color32::WHITE,
|
|
||||||
extreme_bg_color: LIGHTER_GRAY,
|
|
||||||
text_color: BLACK,
|
|
||||||
err_fg_color: RED_700,
|
|
||||||
warn_fg_color: ORANGE_700,
|
|
||||||
hyperlink_color: PURPLE,
|
|
||||||
selection_color: PURPLE_ALT,
|
|
||||||
|
|
||||||
// WINDOW
|
|
||||||
window_fill: Color32::WHITE,
|
|
||||||
window_stroke_color: DARKER_GRAY,
|
|
||||||
|
|
||||||
// NONINTERACTIVE WIDGET
|
|
||||||
noninteractive_bg_fill: Color32::WHITE,
|
|
||||||
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
|
||||||
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
|
||||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
|
||||||
|
|
||||||
// INACTIVE WIDGET
|
|
||||||
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
|
||||||
inactive_bg_fill: LIGHTER_GRAY,
|
|
||||||
inactive_weak_bg_fill: LIGHTER_GRAY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light_mode() -> Visuals {
|
|
||||||
notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dark_mode(is_oled: bool) -> Visuals {
|
|
||||||
notedeck::theme::create_themed_visuals(
|
|
||||||
if is_oled {
|
|
||||||
mobile_dark_color_theme()
|
|
||||||
} else {
|
|
||||||
desktop_dark_color_theme()
|
|
||||||
},
|
|
||||||
Visuals::dark(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create custom text sizes for any FontSizes
|
|
||||||
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
|
||||||
let font_size = if is_mobile {
|
|
||||||
notedeck::fonts::mobile_font_size
|
|
||||||
} else {
|
|
||||||
notedeck::fonts::desktop_font_size
|
|
||||||
};
|
|
||||||
|
|
||||||
style.text_styles = NotedeckTextStyle::iter()
|
|
||||||
.map(|text_style| {
|
|
||||||
(
|
|
||||||
text_style.text_style(),
|
|
||||||
FontId::new(font_size(&text_style), text_style.font_family()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
style.interaction = Interaction {
|
|
||||||
tooltip_delay: 0.1,
|
|
||||||
show_tooltips_only_when_still: false,
|
|
||||||
..Interaction::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// debug: show callstack for the current widget on hover if all
|
|
||||||
// modifier keys are pressed down.
|
|
||||||
#[cfg(feature = "debug-widget-callstack")]
|
|
||||||
{
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
compile_error!(
|
|
||||||
"The `debug-widget-callstack` feature requires a debug build, \
|
|
||||||
release builds are unsupported."
|
|
||||||
);
|
|
||||||
style.debug.debug_on_hover_with_all_modifiers = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// debug: show an overlay on all interactive widgets
|
|
||||||
#[cfg(feature = "debug-interactive-widgets")]
|
|
||||||
{
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
compile_error!(
|
|
||||||
"The `debug-interactive-widgets` feature requires a debug build, \
|
|
||||||
release builds are unsupported."
|
|
||||||
);
|
|
||||||
style.debug.show_interactive_widgets = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@ image = { workspace = true }
|
|||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
nostrdb = { workspace = true }
|
nostrdb = { workspace = true }
|
||||||
notedeck_ui = { workspace = true }
|
notedeck_ui = { workspace = true }
|
||||||
open = { workspace = true }
|
robius-open = { workspace = true }
|
||||||
poll-promise = { workspace = true }
|
poll-promise = { workspace = true }
|
||||||
puffin = { workspace = true, optional = true }
|
puffin = { workspace = true, optional = true }
|
||||||
puffin_egui = { workspace = true, optional = true }
|
puffin_egui = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
ThreadSelection, TimelineCache, TimelineKind,
|
ThreadSelection, TimelineCache, TimelineKind,
|
||||||
},
|
},
|
||||||
|
view_state::ViewState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use enostr::{NoteId, Pubkey, RelayPool};
|
use enostr::{NoteId, Pubkey, RelayPool};
|
||||||
@@ -16,6 +17,7 @@ use notedeck::{
|
|||||||
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
||||||
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
||||||
};
|
};
|
||||||
|
use notedeck_ui::media::MediaViewerFlags;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub struct NewNotes {
|
pub struct NewNotes {
|
||||||
@@ -51,6 +53,7 @@ fn execute_note_action(
|
|||||||
global_wallet: &mut GlobalWallet,
|
global_wallet: &mut GlobalWallet,
|
||||||
zaps: &mut Zaps,
|
zaps: &mut Zaps,
|
||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
|
view_state: &mut ViewState,
|
||||||
router_type: RouterType,
|
router_type: RouterType,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
col: usize,
|
col: usize,
|
||||||
@@ -153,7 +156,16 @@ fn execute_note_action(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
NoteAction::Media(media_action) => {
|
NoteAction::Media(media_action) => {
|
||||||
media_action.process(images);
|
media_action.on_view_media(|medias| {
|
||||||
|
view_state.media_viewer.media_info = medias.clone();
|
||||||
|
tracing::debug!("on_view_media {:?}", &medias);
|
||||||
|
view_state
|
||||||
|
.media_viewer
|
||||||
|
.flags
|
||||||
|
.set(MediaViewerFlags::Open, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
media_action.process_default_media_actions(images)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +192,7 @@ pub fn execute_and_process_note_action(
|
|||||||
global_wallet: &mut GlobalWallet,
|
global_wallet: &mut GlobalWallet,
|
||||||
zaps: &mut Zaps,
|
zaps: &mut Zaps,
|
||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
|
view_state: &mut ViewState,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) -> Option<RouterAction> {
|
) -> Option<RouterAction> {
|
||||||
let router_type = {
|
let router_type = {
|
||||||
@@ -204,6 +217,7 @@ pub fn execute_and_process_note_action(
|
|||||||
global_wallet,
|
global_wallet,
|
||||||
zaps,
|
zaps,
|
||||||
images,
|
images,
|
||||||
|
view_state,
|
||||||
router_type,
|
router_type,
|
||||||
ui,
|
ui,
|
||||||
col,
|
col,
|
||||||
|
|||||||
@@ -10,19 +10,21 @@ use crate::{
|
|||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
support::Support,
|
support::Support,
|
||||||
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
||||||
ui::{self, DesktopSidePanel, SidePanelAction},
|
ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction},
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||||
use nostrdb::Transaction;
|
use nostrdb::Transaction;
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
|
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
|
||||||
Localization, UnknownIds,
|
Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds,
|
||||||
|
};
|
||||||
|
use notedeck_ui::{
|
||||||
|
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
|
||||||
|
NoteOptions,
|
||||||
};
|
};
|
||||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -359,18 +361,54 @@ fn render_damus(
|
|||||||
app_ctx: &mut AppContext<'_>,
|
app_ctx: &mut AppContext<'_>,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) -> Option<AppAction> {
|
) -> Option<AppAction> {
|
||||||
|
damus
|
||||||
|
.note_options
|
||||||
|
.set(NoteOptions::Wide, is_narrow(ui.ctx()));
|
||||||
|
|
||||||
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
|
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
|
||||||
render_damus_mobile(damus, app_ctx, ui)
|
render_damus_mobile(damus, app_ctx, ui)
|
||||||
} else {
|
} else {
|
||||||
render_damus_desktop(damus, app_ctx, ui)
|
render_damus_desktop(damus, app_ctx, ui)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
|
||||||
|
|
||||||
// We use this for keeping timestamps and things up to date
|
// We use this for keeping timestamps and things up to date
|
||||||
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
||||||
|
|
||||||
app_action
|
app_action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Present a fullscreen media viewer if the FullscreenMedia AppOptions flag is set. This is
|
||||||
|
/// typically set by image carousels using a MediaAction's on_view_media callback when
|
||||||
|
/// an image is clicked
|
||||||
|
fn fullscreen_media_viewer_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
state: &mut MediaViewerState,
|
||||||
|
img_cache: &mut Images,
|
||||||
|
) {
|
||||||
|
if !state.should_show(ui) {
|
||||||
|
if state.scene_rect.is_some() {
|
||||||
|
// if we shouldn't show yet we will have a scene
|
||||||
|
// rect, then we should clear it for next time
|
||||||
|
tracing::debug!("fullscreen_media_viewer_ui: resetting scene rect");
|
||||||
|
state.scene_rect = None;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = MediaViewer::new(state).fullscreen(true).ui(img_cache, ui);
|
||||||
|
|
||||||
|
if resp.clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||||
|
fullscreen_media_close(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the fullscreen media player. This also resets the scene_rect state
|
||||||
|
fn fullscreen_media_close(state: &mut MediaViewerState) {
|
||||||
|
state.flags.set(MediaViewerFlags::Open, false);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fn determine_key_storage_type() -> KeyStorageType {
|
fn determine_key_storage_type() -> KeyStorageType {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -404,6 +442,14 @@ impl Damus {
|
|||||||
let mut options = AppOptions::default();
|
let mut options = AppOptions::default();
|
||||||
let tmp_columns = !parsed_args.columns.is_empty();
|
let tmp_columns = !parsed_args.columns.is_empty();
|
||||||
options.set(AppOptions::TmpColumns, tmp_columns);
|
options.set(AppOptions::TmpColumns, tmp_columns);
|
||||||
|
options.set(
|
||||||
|
AppOptions::Debug,
|
||||||
|
app_context.args.options.contains(NotedeckOptions::Debug),
|
||||||
|
);
|
||||||
|
options.set(
|
||||||
|
AppOptions::SinceOptimize,
|
||||||
|
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
|
||||||
|
);
|
||||||
|
|
||||||
let decks_cache = if tmp_columns {
|
let decks_cache = if tmp_columns {
|
||||||
info!("DecksCache: loading from command line arguments");
|
info!("DecksCache: loading from command line arguments");
|
||||||
@@ -448,34 +494,11 @@ impl Damus {
|
|||||||
// cache.add_deck_default(*pk);
|
// cache.add_deck_default(*pk);
|
||||||
//}
|
//}
|
||||||
};
|
};
|
||||||
|
let settings = &app_context.settings;
|
||||||
|
|
||||||
let support = Support::new(app_context.path);
|
let support = Support::new(app_context.path);
|
||||||
let mut note_options = NoteOptions::default();
|
|
||||||
note_options.set(
|
let note_options = get_note_options(parsed_args, settings);
|
||||||
NoteOptions::Textmode,
|
|
||||||
parsed_args.is_flag_set(ColumnsFlag::Textmode),
|
|
||||||
);
|
|
||||||
note_options.set(
|
|
||||||
NoteOptions::ScrambleText,
|
|
||||||
parsed_args.is_flag_set(ColumnsFlag::Scramble),
|
|
||||||
);
|
|
||||||
note_options.set(
|
|
||||||
NoteOptions::HideMedia,
|
|
||||||
parsed_args.is_flag_set(ColumnsFlag::NoMedia),
|
|
||||||
);
|
|
||||||
note_options.set(
|
|
||||||
NoteOptions::ShowNoteClientTop,
|
|
||||||
parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
|
|
||||||
);
|
|
||||||
note_options.set(
|
|
||||||
NoteOptions::ShowNoteClientBottom,
|
|
||||||
parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
|
|
||||||
);
|
|
||||||
options.set(AppOptions::Debug, app_context.args.debug);
|
|
||||||
options.set(
|
|
||||||
AppOptions::SinceOptimize,
|
|
||||||
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
|
|
||||||
);
|
|
||||||
|
|
||||||
let jobs = JobsCache::default();
|
let jobs = JobsCache::default();
|
||||||
|
|
||||||
@@ -557,6 +580,39 @@ impl Damus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions {
|
||||||
|
let mut note_options = NoteOptions::default();
|
||||||
|
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::Textmode,
|
||||||
|
args.is_flag_set(ColumnsFlag::Textmode),
|
||||||
|
);
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::ScrambleText,
|
||||||
|
args.is_flag_set(ColumnsFlag::Scramble),
|
||||||
|
);
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::HideMedia,
|
||||||
|
args.is_flag_set(ColumnsFlag::NoMedia),
|
||||||
|
);
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::ShowNoteClientTop,
|
||||||
|
ShowSourceClientOption::Top == settings_handler.show_source_client().into()
|
||||||
|
|| args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
|
||||||
|
);
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::ShowNoteClientBottom,
|
||||||
|
ShowSourceClientOption::Bottom == settings_handler.show_source_client().into()
|
||||||
|
|| args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::RepliesNewestFirst,
|
||||||
|
settings_handler.show_replies_newest_first(),
|
||||||
|
);
|
||||||
|
note_options
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
||||||
let stroke = ui.style().interact(&response).fg_stroke;
|
let stroke = ui.style().interact(&response).fg_stroke;
|
||||||
@@ -578,6 +634,7 @@ fn render_damus_mobile(
|
|||||||
let mut app_action: Option<AppAction> = None;
|
let mut app_action: Option<AppAction> = None;
|
||||||
|
|
||||||
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
||||||
|
|
||||||
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
||||||
let r = nav::render_nav(
|
let r = nav::render_nav(
|
||||||
active_col,
|
active_col,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use sha2::{Digest, Sha256};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use notedeck_ui::images::fetch_binary_from_disk;
|
use notedeck::media::images::fetch_binary_from_disk;
|
||||||
|
|
||||||
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
||||||
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
||||||
@@ -143,7 +143,7 @@ pub fn nip96_upload(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Promise::from_ready(Err(Error::Generic(format!(
|
return Promise::from_ready(Err(Error::Generic(format!(
|
||||||
"could not read contents of file to upload: {e}"
|
"could not read contents of file to upload: {e}"
|
||||||
))))
|
))));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
|
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
|
||||||
profile::EditProfileView,
|
profile::EditProfileView,
|
||||||
search::{FocusState, SearchView},
|
search::{FocusState, SearchView},
|
||||||
settings::{SettingsAction, ShowNoteClientOptions},
|
settings::SettingsAction,
|
||||||
support::SupportView,
|
support::SupportView,
|
||||||
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
|
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
|
||||||
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
|
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
|
||||||
@@ -37,7 +37,6 @@ use notedeck::{
|
|||||||
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
|
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
|
||||||
RelayAction,
|
RelayAction,
|
||||||
};
|
};
|
||||||
use notedeck_ui::NoteOptions;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
/// The result of processing a nav response
|
/// The result of processing a nav response
|
||||||
@@ -459,6 +458,7 @@ fn process_render_nav_action(
|
|||||||
ctx.global_wallet,
|
ctx.global_wallet,
|
||||||
ctx.zaps,
|
ctx.zaps,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
|
&mut app.view_state,
|
||||||
ui,
|
ui,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -486,7 +486,7 @@ fn process_render_nav_action(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
RenderNavAction::SettingsAction(action) => {
|
RenderNavAction::SettingsAction(action) => {
|
||||||
action.process_settings_action(app, ctx.theme, ctx.i18n, ctx.img_cache, ui.ctx())
|
action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -545,6 +545,8 @@ fn render_nav_body(
|
|||||||
scroll_to_top,
|
scroll_to_top,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.timeline_cache.set_fresh(kind);
|
||||||
|
|
||||||
// always clear the scroll_to_top request
|
// always clear the scroll_to_top request
|
||||||
if scroll_to_top {
|
if scroll_to_top {
|
||||||
app.options.remove(AppOptions::ScrollToTop);
|
app.options.remove(AppOptions::ScrollToTop);
|
||||||
@@ -581,35 +583,14 @@ fn render_nav_body(
|
|||||||
.ui(ui)
|
.ui(ui)
|
||||||
.map(RenderNavAction::RelayAction),
|
.map(RenderNavAction::RelayAction),
|
||||||
|
|
||||||
Route::Settings => {
|
Route::Settings => SettingsView::new(
|
||||||
let mut show_note_client = if app.note_options.contains(NoteOptions::ShowNoteClientTop)
|
ctx.settings.get_settings_mut(),
|
||||||
{
|
&mut note_context,
|
||||||
ShowNoteClientOptions::Top
|
&mut app.note_options,
|
||||||
} else if app.note_options.contains(NoteOptions::ShowNoteClientBottom) {
|
&mut app.jobs,
|
||||||
ShowNoteClientOptions::Bottom
|
|
||||||
} else {
|
|
||||||
ShowNoteClientOptions::Hide
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut theme: String = (if ui.visuals().dark_mode {
|
|
||||||
"Dark"
|
|
||||||
} else {
|
|
||||||
"Light"
|
|
||||||
})
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let mut selected_language: String = ctx.i18n.get_current_locale().to_string();
|
|
||||||
|
|
||||||
SettingsView::new(
|
|
||||||
ctx.img_cache,
|
|
||||||
&mut selected_language,
|
|
||||||
&mut theme,
|
|
||||||
&mut show_note_client,
|
|
||||||
ctx.i18n,
|
|
||||||
)
|
)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.map(RenderNavAction::SettingsAction)
|
.map(RenderNavAction::SettingsAction),
|
||||||
}
|
|
||||||
Route::Reply(id) => {
|
Route::Reply(id) => {
|
||||||
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
||||||
txn
|
txn
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
use egui::{text::LayoutJob, TextBuffer, TextFormat};
|
use egui::{
|
||||||
|
text::{CCursor, CCursorRange, LayoutJob},
|
||||||
|
text_edit::TextEditOutput,
|
||||||
|
TextBuffer, TextEdit, TextFormat,
|
||||||
|
};
|
||||||
use enostr::{FullKeypair, Pubkey};
|
use enostr::{FullKeypair, Pubkey};
|
||||||
use nostrdb::{Note, NoteBuilder, NoteReply};
|
use nostrdb::{Note, NoteBuilder, NoteReply};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -270,6 +274,36 @@ impl Default for PostBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// New cursor index (indexed by characters) after operation is performed
|
||||||
|
#[must_use = "must call MentionSelectedResponse::process"]
|
||||||
|
pub struct MentionSelectedResponse {
|
||||||
|
pub next_cursor_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MentionSelectedResponse {
|
||||||
|
pub fn process(&self, ctx: &egui::Context, text_edit_output: &TextEditOutput) {
|
||||||
|
let text_edit_id = text_edit_output.response.id;
|
||||||
|
let Some(mut before_state) = TextEdit::load_state(ctx, text_edit_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_cursor = text_edit_output
|
||||||
|
.galley
|
||||||
|
.from_ccursor(CCursor::new(self.next_cursor_index));
|
||||||
|
new_cursor.ccursor.prefer_next_row = true;
|
||||||
|
|
||||||
|
before_state
|
||||||
|
.cursor
|
||||||
|
.set_char_range(Some(CCursorRange::one(CCursor::new(
|
||||||
|
self.next_cursor_index,
|
||||||
|
))));
|
||||||
|
|
||||||
|
ctx.memory_mut(|mem| mem.request_focus(text_edit_id));
|
||||||
|
|
||||||
|
TextEdit::store_state(ctx, text_edit_id, before_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PostBuffer {
|
impl PostBuffer {
|
||||||
pub fn get_new_mentions_key(&mut self) -> usize {
|
pub fn get_new_mentions_key(&mut self) -> usize {
|
||||||
let prev = self.mentions_key;
|
let prev = self.mentions_key;
|
||||||
@@ -319,15 +353,21 @@ impl PostBuffer {
|
|||||||
mention_key: usize,
|
mention_key: usize,
|
||||||
full_name: &str,
|
full_name: &str,
|
||||||
pk: Pubkey,
|
pk: Pubkey,
|
||||||
) {
|
) -> Option<MentionSelectedResponse> {
|
||||||
if let Some(info) = self.mentions.get(&mention_key) {
|
let Some(info) = self.mentions.get(&mention_key) else {
|
||||||
let text_start_index = info.start_index + 1;
|
|
||||||
self.delete_char_range(text_start_index..info.end_index);
|
|
||||||
self.insert_text(full_name, text_start_index);
|
|
||||||
self.select_full_mention(mention_key, pk);
|
|
||||||
} else {
|
|
||||||
error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions);
|
error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions);
|
||||||
}
|
return None;
|
||||||
|
};
|
||||||
|
let text_start_index = info.start_index + 1; // increment by one to exclude the mention indicator, '@'
|
||||||
|
self.delete_char_range(text_start_index..info.end_index);
|
||||||
|
let text_chars_inserted = self.insert_text(full_name, text_start_index);
|
||||||
|
self.select_full_mention(mention_key, pk);
|
||||||
|
|
||||||
|
let space_chars_inserted = self.insert_text(" ", text_start_index + text_chars_inserted);
|
||||||
|
|
||||||
|
Some(MentionSelectedResponse {
|
||||||
|
next_cursor_index: text_start_index + text_chars_inserted + space_chars_inserted,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_mention(&mut self, mention_key: usize) {
|
pub fn delete_mention(&mut self, mention_key: usize) {
|
||||||
@@ -919,7 +959,7 @@ mod tests {
|
|||||||
buf.select_mention_and_replace_name(0, "jb55", JB55());
|
buf.select_mention_and_replace_name(0, "jb55", JB55());
|
||||||
assert_eq!(buf.as_str(), "@jb55 ");
|
assert_eq!(buf.as_str(), "@jb55 ");
|
||||||
|
|
||||||
buf.insert_text(" test", 5);
|
buf.insert_text("test", 6);
|
||||||
assert_eq!(buf.as_str(), "@jb55 test");
|
assert_eq!(buf.as_str(), "@jb55 test");
|
||||||
|
|
||||||
assert_eq!(buf.mentions.len(), 1);
|
assert_eq!(buf.mentions.len(), 1);
|
||||||
@@ -1201,16 +1241,20 @@ mod tests {
|
|||||||
|
|
||||||
buf.insert_text("@jb", 0);
|
buf.insert_text("@jb", 0);
|
||||||
buf.select_mention_and_replace_name(0, "jb55", JB55());
|
buf.select_mention_and_replace_name(0, "jb55", JB55());
|
||||||
buf.insert_text(" test ", 5);
|
buf.insert_text("test ", 6);
|
||||||
|
assert_eq!(buf.as_str(), "@jb55 test ");
|
||||||
buf.insert_text("@kernel", 11);
|
buf.insert_text("@kernel", 11);
|
||||||
buf.select_mention_and_replace_name(1, "KernelKind", KK());
|
buf.select_mention_and_replace_name(1, "KernelKind", KK());
|
||||||
buf.insert_text(" test", 22);
|
assert_eq!(buf.as_str(), "@jb55 test @KernelKind ");
|
||||||
|
|
||||||
|
buf.insert_text("test", 23);
|
||||||
assert_eq!(buf.as_str(), "@jb55 test @KernelKind test");
|
assert_eq!(buf.as_str(), "@jb55 test @KernelKind test");
|
||||||
|
|
||||||
assert_eq!(buf.mentions.len(), 2);
|
assert_eq!(buf.mentions.len(), 2);
|
||||||
|
|
||||||
buf.insert_text(" ", 5);
|
|
||||||
buf.insert_text("@els", 6);
|
buf.insert_text("@els", 6);
|
||||||
|
assert_eq!(buf.as_str(), "@jb55 @elstest @KernelKind test");
|
||||||
|
|
||||||
assert_eq!(buf.mentions.len(), 3);
|
assert_eq!(buf.mentions.len(), 3);
|
||||||
assert_eq!(buf.mentions.get(&2).unwrap().bounds(), 6..10);
|
assert_eq!(buf.mentions.get(&2).unwrap().bounds(), 6..10);
|
||||||
buf.select_mention_and_replace_name(2, "elsat", JB55());
|
buf.select_mention_and_replace_name(2, "elsat", JB55());
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl Support {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static MAX_LOG_LINES: usize = 500;
|
static MAX_LOG_LINES: usize = 500;
|
||||||
static SUPPORT_EMAIL: &str = "support@damus.io";
|
pub static SUPPORT_EMAIL: &str = "support+notedeck@damus.io";
|
||||||
static EMAIL_TEMPLATE: &str = concat!("version ", env!("CARGO_PKG_VERSION"), "\nCommit hash: ", env!("GIT_COMMIT_HASH"), "\n\nDescribe the bug you have encountered:\n<-- your statement here -->\n\n===== Paste your log below =====\n\n");
|
static EMAIL_TEMPLATE: &str = concat!("version ", env!("CARGO_PKG_VERSION"), "\nCommit hash: ", env!("GIT_COMMIT_HASH"), "\n\nDescribe the bug you have encountered:\n<-- your statement here -->\n\n===== Paste your log below =====\n\n");
|
||||||
|
|
||||||
impl Support {
|
impl Support {
|
||||||
|
|||||||
@@ -221,6 +221,14 @@ impl TimelineCache {
|
|||||||
pub fn num_timelines(&self) -> usize {
|
pub fn num_timelines(&self) -> usize {
|
||||||
self.timelines.len()
|
self.timelines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_fresh(&mut self, kind: &TimelineKind) {
|
||||||
|
let Some(tl) = self.get_mut(kind) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
tl.current_view_mut().freshness.set_fresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look for new thread notes since our last fetch
|
/// Look for new thread notes since our last fetch
|
||||||
|
|||||||
@@ -471,11 +471,9 @@ impl TimelineKind {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// TODO: still need to update this to fetch likes, zaps, etc
|
// TODO: still need to update this to fetch likes, zaps, etc
|
||||||
TimelineKind::Notifications(pubkey) => FilterState::ready(vec![Filter::new()
|
TimelineKind::Notifications(pubkey) => {
|
||||||
.pubkeys([pubkey.bytes()])
|
FilterState::ready(vec![notifications_filter(pubkey)])
|
||||||
.kinds([1])
|
}
|
||||||
.limit(default_limit())
|
|
||||||
.build()]),
|
|
||||||
|
|
||||||
TimelineKind::Hashtag(hashtag) => {
|
TimelineKind::Hashtag(hashtag) => {
|
||||||
let filters = hashtag
|
let filters = hashtag
|
||||||
@@ -573,11 +571,7 @@ impl TimelineKind {
|
|||||||
)),
|
)),
|
||||||
|
|
||||||
TimelineKind::Notifications(pk) => {
|
TimelineKind::Notifications(pk) => {
|
||||||
let notifications_filter = Filter::new()
|
let notifications_filter = notifications_filter(&pk);
|
||||||
.pubkeys([pk.bytes()])
|
|
||||||
.kinds([1])
|
|
||||||
.limit(default_limit())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Some(Timeline::new(
|
Some(Timeline::new(
|
||||||
TimelineKind::notifications(pk),
|
TimelineKind::notifications(pk),
|
||||||
@@ -628,6 +622,14 @@ impl TimelineKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn notifications_filter(pk: &Pubkey) -> Filter {
|
||||||
|
Filter::new()
|
||||||
|
.pubkeys([pk.bytes()])
|
||||||
|
.kinds([1])
|
||||||
|
.limit(default_limit())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TitleNeedsDb<'a> {
|
pub struct TitleNeedsDb<'a> {
|
||||||
kind: &'a TimelineKind,
|
kind: &'a TimelineKind,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
|
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
contacts::hybrid_contacts_filter,
|
contacts::hybrid_contacts_filter,
|
||||||
|
debouncer::Debouncer,
|
||||||
filter::{self, HybridFilter},
|
filter::{self, HybridFilter},
|
||||||
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
|
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
|
||||||
NoteCache, NoteRef, UnknownIds,
|
NoteCache, NoteRef, UnknownIds,
|
||||||
@@ -16,8 +17,11 @@ use notedeck::{
|
|||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::{PoolRelay, Pubkey, RelayPool};
|
use enostr::{PoolRelay, Pubkey, RelayPool};
|
||||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
||||||
use std::cell::RefCell;
|
use std::{
|
||||||
use std::rc::Rc;
|
cell::RefCell,
|
||||||
|
time::{Duration, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use std::{rc::Rc, time::SystemTime};
|
||||||
|
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
@@ -103,6 +107,7 @@ pub struct TimelineTab {
|
|||||||
pub selection: i32,
|
pub selection: i32,
|
||||||
pub filter: ViewFilter,
|
pub filter: ViewFilter,
|
||||||
pub list: Rc<RefCell<VirtualList>>,
|
pub list: Rc<RefCell<VirtualList>>,
|
||||||
|
pub freshness: NotesFreshness,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimelineTab {
|
impl TimelineTab {
|
||||||
@@ -138,6 +143,7 @@ impl TimelineTab {
|
|||||||
selection,
|
selection,
|
||||||
filter,
|
filter,
|
||||||
list,
|
list,
|
||||||
|
freshness: NotesFreshness::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,3 +786,101 @@ pub fn is_timeline_ready(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NotesFreshness {
|
||||||
|
debouncer: Debouncer,
|
||||||
|
state: NotesFreshnessState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum NotesFreshnessState {
|
||||||
|
Fresh {
|
||||||
|
timestamp_viewed: u64,
|
||||||
|
},
|
||||||
|
Stale {
|
||||||
|
have_unseen: bool,
|
||||||
|
timestamp_last_viewed: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotesFreshness {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
debouncer: Debouncer::new(Duration::from_secs(2)),
|
||||||
|
state: NotesFreshnessState::Stale {
|
||||||
|
have_unseen: true,
|
||||||
|
timestamp_last_viewed: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotesFreshness {
|
||||||
|
pub fn set_fresh(&mut self) {
|
||||||
|
if !self.debouncer.should_act() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.state = NotesFreshnessState::Fresh {
|
||||||
|
timestamp_viewed: timestamp_now(),
|
||||||
|
};
|
||||||
|
self.debouncer.bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, check_have_unseen: impl FnOnce(u64) -> bool) {
|
||||||
|
if !self.debouncer.should_act() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match &self.state {
|
||||||
|
NotesFreshnessState::Fresh { timestamp_viewed } => {
|
||||||
|
let Ok(dur) = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH + Duration::from_secs(*timestamp_viewed))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if dur > Duration::from_secs(2) {
|
||||||
|
self.state = NotesFreshnessState::Stale {
|
||||||
|
have_unseen: check_have_unseen(*timestamp_viewed),
|
||||||
|
timestamp_last_viewed: *timestamp_viewed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotesFreshnessState::Stale {
|
||||||
|
have_unseen,
|
||||||
|
timestamp_last_viewed,
|
||||||
|
} => {
|
||||||
|
if *have_unseen {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = NotesFreshnessState::Stale {
|
||||||
|
have_unseen: check_have_unseen(*timestamp_last_viewed),
|
||||||
|
timestamp_last_viewed: *timestamp_last_viewed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.debouncer.bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_unseen(&self) -> bool {
|
||||||
|
match &self.state {
|
||||||
|
NotesFreshnessState::Fresh {
|
||||||
|
timestamp_viewed: _,
|
||||||
|
} => false,
|
||||||
|
NotesFreshnessState::Stale {
|
||||||
|
have_unseen,
|
||||||
|
timestamp_last_viewed: _,
|
||||||
|
} => *have_unseen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_now() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or(Duration::ZERO)
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use notedeck::NoteContext;
|
use notedeck::{JobsCache, NoteContext};
|
||||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
use notedeck_ui::NoteOptions;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_timeline_route(
|
pub fn render_timeline_route(
|
||||||
@@ -81,6 +81,9 @@ pub fn render_thread_route(
|
|||||||
// default truncated everywher eelse
|
// default truncated everywher eelse
|
||||||
note_options.set(NoteOptions::Truncate, false);
|
note_options.set(NoteOptions::Truncate, false);
|
||||||
|
|
||||||
|
// We need the reply lines in threads
|
||||||
|
note_options.set(NoteOptions::Wide, false);
|
||||||
|
|
||||||
ui::ThreadView::new(
|
ui::ThreadView::new(
|
||||||
threads,
|
threads,
|
||||||
selection.selected_or_root(),
|
selection.selected_or_root(),
|
||||||
|
|||||||
+27
-14
@@ -11,19 +11,21 @@ use notedeck_ui::{
|
|||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub struct SearchResultsView<'a> {
|
/// Displays user profiles for the user to pick from.
|
||||||
|
/// Useful for manually typing a username and selecting the profile desired
|
||||||
|
pub struct MentionPickerView<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
img_cache: &'a mut Images,
|
img_cache: &'a mut Images,
|
||||||
results: &'a Vec<&'a [u8; 32]>,
|
results: &'a Vec<&'a [u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SearchResultsResponse {
|
pub enum MentionPickerResponse {
|
||||||
SelectResult(Option<usize>),
|
SelectResult(Option<usize>),
|
||||||
DeleteMention,
|
DeleteMention,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SearchResultsView<'a> {
|
impl<'a> MentionPickerView<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
img_cache: &'a mut Images,
|
img_cache: &'a mut Images,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
@@ -38,8 +40,8 @@ impl<'a> SearchResultsView<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ui: &mut egui::Ui, width: f32) -> SearchResultsResponse {
|
fn show(&mut self, ui: &mut egui::Ui, width: f32) -> MentionPickerResponse {
|
||||||
let mut search_results_selection = None;
|
let mut selection = None;
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
for (i, res) in self.results.iter().enumerate() {
|
for (i, res) in self.results.iter().enumerate() {
|
||||||
let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) {
|
let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) {
|
||||||
@@ -54,16 +56,16 @@ impl<'a> SearchResultsView<'a> {
|
|||||||
.add(user_result(&profile, self.img_cache, i, width))
|
.add(user_result(&profile, self.img_cache, i, width))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
search_results_selection = Some(i)
|
selection = Some(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
SearchResultsResponse::SelectResult(search_results_selection)
|
MentionPickerResponse::SelectResult(selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> SearchResultsResponse {
|
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> MentionPickerResponse {
|
||||||
let widget_id = ui.id().with("search_results");
|
let widget_id = ui.id().with("mention_results");
|
||||||
let area_resp = egui::Area::new(widget_id)
|
let area_resp = egui::Area::new(widget_id)
|
||||||
.order(egui::Order::Foreground)
|
.order(egui::Order::Foreground)
|
||||||
.fixed_pos(rect.left_top())
|
.fixed_pos(rect.left_top())
|
||||||
@@ -72,10 +74,10 @@ impl<'a> SearchResultsView<'a> {
|
|||||||
let inner_margin_size = 8.0;
|
let inner_margin_size = 8.0;
|
||||||
egui::Frame::NONE
|
egui::Frame::NONE
|
||||||
.fill(ui.visuals().panel_fill)
|
.fill(ui.visuals().panel_fill)
|
||||||
.inner_margin(inner_margin_size)
|
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let width = rect.width() - (2.0 * inner_margin_size);
|
let width = rect.width() - (2.0 * inner_margin_size);
|
||||||
|
|
||||||
|
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
|
||||||
let close_button_resp = {
|
let close_button_resp = {
|
||||||
let close_button_size = 16.0;
|
let close_button_size = 16.0;
|
||||||
let (close_section_rect, _) = ui.allocate_exact_size(
|
let (close_section_rect, _) = ui.allocate_exact_size(
|
||||||
@@ -95,16 +97,16 @@ impl<'a> SearchResultsView<'a> {
|
|||||||
.inner
|
.inner
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
|
||||||
|
|
||||||
let scroll_resp = ScrollArea::vertical()
|
let scroll_resp = ScrollArea::vertical()
|
||||||
.max_width(width)
|
.max_width(rect.width())
|
||||||
.auto_shrink(Vec2b::FALSE)
|
.auto_shrink(Vec2b::FALSE)
|
||||||
.show(ui, |ui| self.show(ui, width));
|
.show(ui, |ui| self.show(ui, width));
|
||||||
ui.advance_cursor_after_rect(rect);
|
ui.advance_cursor_after_rect(rect);
|
||||||
|
|
||||||
if close_button_resp {
|
if close_button_resp {
|
||||||
SearchResultsResponse::DeleteMention
|
MentionPickerResponse::DeleteMention
|
||||||
} else {
|
} else {
|
||||||
scroll_resp.inner
|
scroll_resp.inner
|
||||||
}
|
}
|
||||||
@@ -128,7 +130,18 @@ fn user_result<'a>(
|
|||||||
let spacing = 8.0;
|
let spacing = 8.0;
|
||||||
let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
||||||
|
|
||||||
let helper = AnimationHelper::new(ui, ("user_result", index), vec2(width, max_image));
|
let animation_rect = {
|
||||||
|
let max_width = ui.available_width();
|
||||||
|
let extra_width = (max_width - width) / 2.0;
|
||||||
|
let left = ui.cursor().left();
|
||||||
|
let (rect, _) =
|
||||||
|
ui.allocate_exact_size(vec2(width + extra_width, max_image), egui::Sense::click());
|
||||||
|
|
||||||
|
let (_, right) = rect.split_left_right_at_x(left + extra_width);
|
||||||
|
right
|
||||||
|
};
|
||||||
|
|
||||||
|
let helper = AnimationHelper::new_from_rect(ui, ("user_result", index), animation_rect);
|
||||||
|
|
||||||
let icon_rect = {
|
let icon_rect = {
|
||||||
let r = helper.get_animation_rect();
|
let r = helper.get_animation_rect();
|
||||||
@@ -5,13 +5,13 @@ pub mod column;
|
|||||||
pub mod configure_deck;
|
pub mod configure_deck;
|
||||||
pub mod edit_deck;
|
pub mod edit_deck;
|
||||||
pub mod images;
|
pub mod images;
|
||||||
|
pub mod mentions_picker;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod search_results;
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod side_panel;
|
pub mod side_panel;
|
||||||
pub mod support;
|
pub mod support;
|
||||||
@@ -26,6 +26,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig};
|
|||||||
pub use profile::ProfileView;
|
pub use profile::ProfileView;
|
||||||
pub use relay::RelayView;
|
pub use relay::RelayView;
|
||||||
pub use settings::SettingsView;
|
pub use settings::SettingsView;
|
||||||
|
pub use settings::ShowSourceClientOption;
|
||||||
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
||||||
pub use thread::ThreadView;
|
pub use thread::ThreadView;
|
||||||
pub use timeline::TimelineView;
|
pub use timeline::TimelineView;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::draft::{Draft, Drafts, MentionHint};
|
|||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
||||||
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
||||||
use crate::ui::search_results::SearchResultsView;
|
use crate::ui::mentions_picker::MentionPickerView;
|
||||||
use crate::ui::{self, Preview, PreviewConfig};
|
use crate::ui::{self, Preview, PreviewConfig};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
|
||||||
@@ -14,13 +14,12 @@ use egui::{
|
|||||||
};
|
};
|
||||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
use notedeck::media::gif::ensure_latest_texture;
|
||||||
|
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
|
||||||
|
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
app_images,
|
app_images,
|
||||||
blur::PixelDimensions,
|
|
||||||
context_menu::{input_context, PasteBehavior},
|
context_menu::{input_context, PasteBehavior},
|
||||||
gif::{handle_repaint, retrieve_latest_texture},
|
|
||||||
images::{get_render_state, RenderState},
|
|
||||||
jobs::JobsCache,
|
|
||||||
note::render_note_preview,
|
note::render_note_preview,
|
||||||
NoteOptions, ProfilePic,
|
NoteOptions, ProfilePic,
|
||||||
};
|
};
|
||||||
@@ -219,6 +218,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
out.response
|
out.response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Displays the mention picker and handles when one is selected.
|
||||||
fn show_mention_hints(
|
fn show_mention_hints(
|
||||||
&mut self,
|
&mut self,
|
||||||
txn: &nostrdb::Transaction,
|
txn: &nostrdb::Transaction,
|
||||||
@@ -274,7 +274,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let resp = SearchResultsView::new(
|
let resp = MentionPickerView::new(
|
||||||
self.note_context.img_cache,
|
self.note_context.img_cache,
|
||||||
self.note_context.ndb,
|
self.note_context.ndb,
|
||||||
txn,
|
txn,
|
||||||
@@ -282,26 +282,35 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
)
|
)
|
||||||
.show_in_rect(hint_rect, ui);
|
.show_in_rect(hint_rect, ui);
|
||||||
|
|
||||||
|
let mut selection_made = None;
|
||||||
match resp {
|
match resp {
|
||||||
ui::search_results::SearchResultsResponse::SelectResult(selection) => {
|
ui::mentions_picker::MentionPickerResponse::SelectResult(selection) => {
|
||||||
if let Some(hint_index) = selection {
|
if let Some(hint_index) = selection {
|
||||||
if let Some(pk) = res.get(hint_index) {
|
if let Some(pk) = res.get(hint_index) {
|
||||||
let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk);
|
let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk);
|
||||||
|
|
||||||
|
if let Some(made_selection) =
|
||||||
self.draft.buffer.select_mention_and_replace_name(
|
self.draft.buffer.select_mention_and_replace_name(
|
||||||
mention.index,
|
mention.index,
|
||||||
get_display_name(record.ok().as_ref()).name(),
|
get_display_name(record.ok().as_ref()).name(),
|
||||||
Pubkey::new(**pk),
|
Pubkey::new(**pk),
|
||||||
);
|
)
|
||||||
|
{
|
||||||
|
selection_made = Some(made_selection);
|
||||||
|
}
|
||||||
self.draft.cur_mention_hint = None;
|
self.draft.cur_mention_hint = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui::search_results::SearchResultsResponse::DeleteMention => {
|
ui::mentions_picker::MentionPickerResponse::DeleteMention => {
|
||||||
self.draft.buffer.delete_mention(mention.index)
|
self.draft.buffer.delete_mention(mention.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(selection) = selection_made {
|
||||||
|
selection.process(ui.ctx(), textedit_output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focused(&self, ui: &egui::Ui) -> bool {
|
fn focused(&self, ui: &egui::Ui) -> bool {
|
||||||
@@ -471,7 +480,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
self.note_context.img_cache,
|
self.note_context.img_cache,
|
||||||
cache_type,
|
cache_type,
|
||||||
url,
|
url,
|
||||||
notedeck_ui::images::ImageType::Content(Some((width, height))),
|
notedeck::ImageType::Content(Some((width, height))),
|
||||||
);
|
);
|
||||||
|
|
||||||
render_post_view_media(
|
render_post_view_media(
|
||||||
@@ -595,12 +604,10 @@ fn render_post_view_media(
|
|||||||
.to_points(ui.pixels_per_point())
|
.to_points(ui.pixels_per_point())
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
let texture_handle = handle_repaint(
|
let texture_handle =
|
||||||
ui,
|
ensure_latest_texture(ui, url, render_state.gifs, renderable_media);
|
||||||
retrieve_latest_texture(url, render_state.gifs, renderable_media),
|
|
||||||
);
|
|
||||||
let img_resp = ui.add(
|
let img_resp = ui.add(
|
||||||
egui::Image::new(texture_handle)
|
egui::Image::new(&texture_handle)
|
||||||
.max_size(size)
|
.max_size(size)
|
||||||
.corner_radius(12.0),
|
.corner_radius(12.0),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use crate::{
|
|||||||
|
|
||||||
use egui::ScrollArea;
|
use egui::ScrollArea;
|
||||||
use enostr::{FilledKeypair, NoteId};
|
use enostr::{FilledKeypair, NoteId};
|
||||||
use notedeck::NoteContext;
|
use notedeck::{JobsCache, NoteContext};
|
||||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
use notedeck_ui::NoteOptions;
|
||||||
|
|
||||||
pub struct QuoteRepostView<'a, 'd> {
|
pub struct QuoteRepostView<'a, 'd> {
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ use crate::ui::{
|
|||||||
|
|
||||||
use egui::{Rect, Response, ScrollArea, Ui};
|
use egui::{Rect, Response, ScrollArea, Ui};
|
||||||
use enostr::{FilledKeypair, NoteId};
|
use enostr::{FilledKeypair, NoteId};
|
||||||
use notedeck::NoteContext;
|
use notedeck::{JobsCache, NoteContext};
|
||||||
use notedeck_ui::jobs::JobsCache;
|
|
||||||
use notedeck_ui::{NoteOptions, NoteView, ProfilePic};
|
use notedeck_ui::{NoteOptions, NoteView, ProfilePic};
|
||||||
|
|
||||||
pub struct PostReplyView<'a, 'd> {
|
pub struct PostReplyView<'a, 'd> {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use enostr::Pubkey;
|
|||||||
use nostrdb::{ProfileRecord, Transaction};
|
use nostrdb::{ProfileRecord, Transaction};
|
||||||
use notedeck::{tr, Localization};
|
use notedeck::{tr, Localization};
|
||||||
use notedeck_ui::profile::follow_button;
|
use notedeck_ui::profile::follow_button;
|
||||||
|
use robius_open::Uri;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -13,12 +14,11 @@ use crate::{
|
|||||||
ui::timeline::{tabs_ui, TimelineTabView},
|
ui::timeline::{tabs_ui, TimelineTabView},
|
||||||
};
|
};
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
name::get_display_name, profile::get_profile_url, IsFollowing, NoteAction, NoteContext,
|
name::get_display_name, profile::get_profile_url, IsFollowing, JobsCache, NoteAction,
|
||||||
NotedeckTextStyle,
|
NoteContext, NotedeckTextStyle,
|
||||||
};
|
};
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
app_images,
|
app_images,
|
||||||
jobs::JobsCache,
|
|
||||||
profile::{about_section_widget, banner, display_name_widget},
|
profile::{about_section_widget, banner, display_name_widget},
|
||||||
NoteOptions, ProfilePic,
|
NoteOptions, ProfilePic,
|
||||||
};
|
};
|
||||||
@@ -286,8 +286,8 @@ fn handle_link(ui: &mut egui::Ui, website_url: &str) {
|
|||||||
.interact(Sense::click())
|
.interact(Sense::click())
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
if let Err(e) = open::that(website_url) {
|
if let Err(e) = Uri::new(website_url).open() {
|
||||||
error!("Failed to open URL {} because: {}", website_url, e);
|
error!("Failed to open URL {} because: {:?}", website_url, e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ use state::TypingType;
|
|||||||
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
|
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
|
||||||
use egui_winit::clipboard::Clipboard;
|
use egui_winit::clipboard::Clipboard;
|
||||||
use nostrdb::{Filter, Ndb, Transaction};
|
use nostrdb::{Filter, Ndb, Transaction};
|
||||||
use notedeck::{tr, tr_plural, Localization, NoteAction, NoteContext, NoteRef};
|
use notedeck::{tr, tr_plural, JobsCache, Localization, NoteAction, NoteContext, NoteRef};
|
||||||
|
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
context_menu::{input_context, PasteBehavior},
|
context_menu::{input_context, PasteBehavior},
|
||||||
icons::search_icon,
|
icons::search_icon,
|
||||||
jobs::JobsCache,
|
|
||||||
padding, NoteOptions,
|
padding, NoteOptions,
|
||||||
};
|
};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -19,7 +19,7 @@ mod state;
|
|||||||
|
|
||||||
pub use state::{FocusState, SearchQueryState, SearchState};
|
pub use state::{FocusState, SearchQueryState, SearchState};
|
||||||
|
|
||||||
use super::search_results::{SearchResultsResponse, SearchResultsView};
|
use super::mentions_picker::{MentionPickerResponse, MentionPickerView};
|
||||||
|
|
||||||
pub struct SearchView<'a, 'd> {
|
pub struct SearchView<'a, 'd> {
|
||||||
query: &'a mut SearchQueryState,
|
query: &'a mut SearchQueryState,
|
||||||
@@ -76,7 +76,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
|||||||
break 's;
|
break 's;
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_res = SearchResultsView::new(
|
let search_res = MentionPickerView::new(
|
||||||
self.note_context.img_cache,
|
self.note_context.img_cache,
|
||||||
self.note_context.ndb,
|
self.note_context.ndb,
|
||||||
self.txn,
|
self.txn,
|
||||||
@@ -85,7 +85,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
|||||||
.show_in_rect(ui.available_rect_before_wrap(), ui);
|
.show_in_rect(ui.available_rect_before_wrap(), ui);
|
||||||
|
|
||||||
search_action = match search_res {
|
search_action = match search_res {
|
||||||
SearchResultsResponse::SelectResult(Some(index)) => {
|
MentionPickerResponse::SelectResult(Some(index)) => {
|
||||||
let Some(pk_bytes) = results.get(index) else {
|
let Some(pk_bytes) = results.get(index) else {
|
||||||
break 's;
|
break 's;
|
||||||
};
|
};
|
||||||
@@ -103,8 +103,8 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
|||||||
new_search_text: format!("@{username}"),
|
new_search_text: format!("@{username}"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SearchResultsResponse::DeleteMention => Some(SearchAction::CloseMention),
|
MentionPickerResponse::DeleteMention => Some(SearchAction::CloseMention),
|
||||||
SearchResultsResponse::SelectResult(None) => break 's,
|
MentionPickerResponse::SelectResult(None) => break 's,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
SearchState::PerformSearch(search_type) => {
|
SearchState::PerformSearch(search_type) => {
|
||||||
|
|||||||
@@ -1,22 +1,115 @@
|
|||||||
use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ThemePreference};
|
use egui::{
|
||||||
use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, ThemeHandler};
|
vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference,
|
||||||
use notedeck_ui::NoteOptions;
|
};
|
||||||
|
use enostr::NoteId;
|
||||||
|
use nostrdb::Transaction;
|
||||||
|
use notedeck::{
|
||||||
|
tr,
|
||||||
|
ui::{is_narrow, richtext_small},
|
||||||
|
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
|
||||||
|
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||||
|
};
|
||||||
|
use notedeck_ui::{NoteOptions, NoteView};
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
|
|
||||||
use crate::{nav::RouterAction, Damus, Route};
|
use crate::{nav::RouterAction, Damus, Route};
|
||||||
|
|
||||||
|
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
|
||||||
|
|
||||||
|
const THEME_LIGHT: &str = "Light";
|
||||||
|
const THEME_DARK: &str = "Dark";
|
||||||
|
|
||||||
|
const MIN_ZOOM: f32 = 0.5;
|
||||||
|
const MAX_ZOOM: f32 = 3.0;
|
||||||
|
const ZOOM_STEP: f32 = 0.1;
|
||||||
|
const RESET_ZOOM: f32 = 1.0;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Display)]
|
#[derive(Clone, Copy, PartialEq, Eq, Display)]
|
||||||
pub enum ShowNoteClientOptions {
|
pub enum ShowSourceClientOption {
|
||||||
Hide,
|
Hide,
|
||||||
Top,
|
Top,
|
||||||
Bottom,
|
Bottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ShowSourceClientOption> for String {
|
||||||
|
fn from(show_option: ShowSourceClientOption) -> Self {
|
||||||
|
match show_option {
|
||||||
|
ShowSourceClientOption::Hide => "hide".to_string(),
|
||||||
|
ShowSourceClientOption::Top => "top".to_string(),
|
||||||
|
ShowSourceClientOption::Bottom => "bottom".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NoteOptions> for ShowSourceClientOption {
|
||||||
|
fn from(note_options: NoteOptions) -> Self {
|
||||||
|
if note_options.contains(NoteOptions::ShowNoteClientTop) {
|
||||||
|
ShowSourceClientOption::Top
|
||||||
|
} else if note_options.contains(NoteOptions::ShowNoteClientBottom) {
|
||||||
|
ShowSourceClientOption::Bottom
|
||||||
|
} else {
|
||||||
|
ShowSourceClientOption::Hide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ShowSourceClientOption {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"hide" => Self::Hide,
|
||||||
|
"top" => Self::Top,
|
||||||
|
"bottom" => Self::Bottom,
|
||||||
|
_ => Self::Hide, // default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShowSourceClientOption {
|
||||||
|
pub fn set_note_options(self, note_options: &mut NoteOptions) {
|
||||||
|
match self {
|
||||||
|
Self::Hide => {
|
||||||
|
note_options.set(NoteOptions::ShowNoteClientTop, false);
|
||||||
|
note_options.set(NoteOptions::ShowNoteClientBottom, false);
|
||||||
|
}
|
||||||
|
Self::Bottom => {
|
||||||
|
note_options.set(NoteOptions::ShowNoteClientTop, false);
|
||||||
|
note_options.set(NoteOptions::ShowNoteClientBottom, true);
|
||||||
|
}
|
||||||
|
Self::Top => {
|
||||||
|
note_options.set(NoteOptions::ShowNoteClientTop, true);
|
||||||
|
note_options.set(NoteOptions::ShowNoteClientBottom, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self, i18n: &mut Localization) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Hide => tr!(
|
||||||
|
i18n,
|
||||||
|
"Hide",
|
||||||
|
"Option in settings section to hide the source client label in note display"
|
||||||
|
),
|
||||||
|
Self::Top => tr!(
|
||||||
|
i18n,
|
||||||
|
"Top",
|
||||||
|
"Option in settings section to show the source client label at the top of the note"
|
||||||
|
),
|
||||||
|
Self::Bottom => tr!(
|
||||||
|
i18n,
|
||||||
|
"Bottom",
|
||||||
|
"Option in settings section to show the source client label at the bottom of the note"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum SettingsAction {
|
pub enum SettingsAction {
|
||||||
SetZoom(f32),
|
SetZoomFactor(f32),
|
||||||
SetTheme(ThemePreference),
|
SetTheme(ThemePreference),
|
||||||
SetShowNoteClient(ShowNoteClientOptions),
|
SetShowSourceClient(ShowSourceClientOption),
|
||||||
SetLocale(LanguageIdentifier),
|
SetLocale(LanguageIdentifier),
|
||||||
|
SetRepliestNewestFirst(bool),
|
||||||
|
SetNoteBodyFontSize(f32),
|
||||||
OpenRelays,
|
OpenRelays,
|
||||||
OpenCacheFolder,
|
OpenCacheFolder,
|
||||||
ClearCacheFolder,
|
ClearCacheFolder,
|
||||||
@@ -26,7 +119,7 @@ impl SettingsAction {
|
|||||||
pub fn process_settings_action<'a>(
|
pub fn process_settings_action<'a>(
|
||||||
self,
|
self,
|
||||||
app: &mut Damus,
|
app: &mut Damus,
|
||||||
theme_handler: &'a mut ThemeHandler,
|
settings: &'a mut SettingsHandler,
|
||||||
i18n: &'a mut Localization,
|
i18n: &'a mut Localization,
|
||||||
img_cache: &mut Images,
|
img_cache: &mut Images,
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
@@ -34,152 +127,195 @@ impl SettingsAction {
|
|||||||
let mut route_action: Option<RouterAction> = None;
|
let mut route_action: Option<RouterAction> = None;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
SettingsAction::OpenRelays => {
|
Self::OpenRelays => {
|
||||||
route_action = Some(RouterAction::route_to(Route::Relays));
|
route_action = Some(RouterAction::route_to(Route::Relays));
|
||||||
}
|
}
|
||||||
SettingsAction::SetZoom(zoom_level) => {
|
Self::SetZoomFactor(zoom_factor) => {
|
||||||
ctx.set_zoom_factor(zoom_level);
|
ctx.set_zoom_factor(zoom_factor);
|
||||||
|
settings.set_zoom_factor(zoom_factor);
|
||||||
}
|
}
|
||||||
SettingsAction::SetShowNoteClient(newvalue) => match newvalue {
|
Self::SetShowSourceClient(option) => {
|
||||||
ShowNoteClientOptions::Hide => {
|
option.set_note_options(&mut app.note_options);
|
||||||
app.note_options.set(NoteOptions::ShowNoteClientTop, false);
|
|
||||||
app.note_options
|
settings.set_show_source_client(option);
|
||||||
.set(NoteOptions::ShowNoteClientBottom, false);
|
|
||||||
}
|
}
|
||||||
ShowNoteClientOptions::Bottom => {
|
Self::SetTheme(theme) => {
|
||||||
app.note_options.set(NoteOptions::ShowNoteClientTop, false);
|
ctx.set_theme(theme);
|
||||||
app.note_options
|
settings.set_theme(theme);
|
||||||
.set(NoteOptions::ShowNoteClientBottom, true);
|
|
||||||
}
|
}
|
||||||
ShowNoteClientOptions::Top => {
|
Self::SetLocale(language) => {
|
||||||
app.note_options.set(NoteOptions::ShowNoteClientTop, true);
|
if i18n.set_locale(language.clone()).is_ok() {
|
||||||
app.note_options
|
settings.set_locale(language.to_string());
|
||||||
.set(NoteOptions::ShowNoteClientBottom, false);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
SettingsAction::SetTheme(theme) => {
|
|
||||||
ctx.options_mut(|o| {
|
|
||||||
o.theme_preference = theme;
|
|
||||||
});
|
|
||||||
theme_handler.save(theme);
|
|
||||||
}
|
}
|
||||||
SettingsAction::SetLocale(language) => {
|
Self::SetRepliestNewestFirst(value) => {
|
||||||
_ = i18n.set_locale(language);
|
app.note_options.set(NoteOptions::RepliesNewestFirst, value);
|
||||||
|
settings.set_show_replies_newest_first(value);
|
||||||
}
|
}
|
||||||
SettingsAction::OpenCacheFolder => {
|
Self::OpenCacheFolder => {
|
||||||
use opener;
|
use opener;
|
||||||
let _ = opener::open(img_cache.base_path.clone());
|
let _ = opener::open(img_cache.base_path.clone());
|
||||||
}
|
}
|
||||||
SettingsAction::ClearCacheFolder => {
|
Self::ClearCacheFolder => {
|
||||||
let _ = img_cache.clear_folder_contents();
|
let _ = img_cache.clear_folder_contents();
|
||||||
}
|
}
|
||||||
|
Self::SetNoteBodyFontSize(size) => {
|
||||||
|
let mut style = (*ctx.style()).clone();
|
||||||
|
style.text_styles.insert(
|
||||||
|
NotedeckTextStyle::NoteBody.text_style(),
|
||||||
|
FontId::proportional(size),
|
||||||
|
);
|
||||||
|
ctx.set_style(style);
|
||||||
|
|
||||||
|
settings.set_note_body_font_size(size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
route_action
|
route_action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SettingsView<'a> {
|
pub struct SettingsView<'a> {
|
||||||
theme: &'a mut String,
|
settings: &'a mut Settings,
|
||||||
selected_language: &'a mut String,
|
note_context: &'a mut NoteContext<'a>,
|
||||||
show_note_client: &'a mut ShowNoteClientOptions,
|
note_options: &'a mut NoteOptions,
|
||||||
i18n: &'a mut Localization,
|
jobs: &'a mut JobsCache,
|
||||||
img_cache: &'a mut Images,
|
}
|
||||||
|
|
||||||
|
fn settings_group<S>(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui))
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
Frame::group(ui.style())
|
||||||
|
.fill(ui.style().visuals.widgets.open.bg_fill)
|
||||||
|
.inner_margin(10.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()));
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
|
||||||
|
|
||||||
|
contents(ui)
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SettingsView<'a> {
|
impl<'a> SettingsView<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
img_cache: &'a mut Images,
|
settings: &'a mut Settings,
|
||||||
selected_language: &'a mut String,
|
note_context: &'a mut NoteContext<'a>,
|
||||||
theme: &'a mut String,
|
note_options: &'a mut NoteOptions,
|
||||||
show_note_client: &'a mut ShowNoteClientOptions,
|
jobs: &'a mut JobsCache,
|
||||||
i18n: &'a mut Localization,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
show_note_client,
|
settings,
|
||||||
theme,
|
note_context,
|
||||||
img_cache,
|
note_options,
|
||||||
selected_language,
|
jobs,
|
||||||
i18n,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the localized name for a language identifier
|
/// Get the localized name for a language identifier
|
||||||
fn get_selected_language_name(&mut self) -> String {
|
fn get_selected_language_name(&mut self) -> String {
|
||||||
if let Ok(lang_id) = self.selected_language.parse::<LanguageIdentifier>() {
|
if let Ok(lang_id) = self.settings.locale.parse::<LanguageIdentifier>() {
|
||||||
self.i18n
|
self.note_context
|
||||||
|
.i18n
|
||||||
.get_locale_native_name(&lang_id)
|
.get_locale_native_name(&lang_id)
|
||||||
.map(|s| s.to_owned())
|
.map(|s| s.to_owned())
|
||||||
.unwrap_or_else(|| lang_id.to_string())
|
.unwrap_or_else(|| lang_id.to_string())
|
||||||
} else {
|
} else {
|
||||||
self.selected_language.clone()
|
self.settings.locale.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the localized label for ShowNoteClientOptions
|
pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||||
fn get_show_note_client_label(&mut self, option: ShowNoteClientOptions) -> String {
|
|
||||||
match option {
|
|
||||||
ShowNoteClientOptions::Hide => tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Hide",
|
|
||||||
"Option in settings section to hide the source client label in note display"
|
|
||||||
),
|
|
||||||
ShowNoteClientOptions::Top => tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Top",
|
|
||||||
"Option in settings section to show the source client label at the top of the note"
|
|
||||||
),
|
|
||||||
ShowNoteClientOptions::Bottom => tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Bottom",
|
|
||||||
"Option in settings section to show the source client label at the bottom of the note"
|
|
||||||
),
|
|
||||||
}.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
|
||||||
let id = ui.id();
|
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
let title = tr!(
|
||||||
Frame::default()
|
self.note_context.i18n,
|
||||||
.inner_margin(Margin::symmetric(10, 10))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
Frame::group(ui.style())
|
|
||||||
.fill(ui.style().visuals.widgets.open.bg_fill)
|
|
||||||
.inner_margin(10.0)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new(tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Appearance",
|
"Appearance",
|
||||||
"Label for appearance settings section"
|
"Label for appearance settings section",
|
||||||
))
|
|
||||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
|
||||||
);
|
);
|
||||||
|
settings_group(ui, title, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"Font size:",
|
||||||
|
"Label for font size, Appearance settings section",
|
||||||
|
)));
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
egui::Slider::new(&mut self.settings.note_body_font_size, 8.0..=32.0)
|
||||||
|
.text(""),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
action = Some(SettingsAction::SetNoteBodyFontSize(
|
||||||
|
self.settings.note_body_font_size,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"Reset",
|
||||||
|
"Label for reset note body font size, Appearance settings section",
|
||||||
|
)))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(SettingsAction::SetNoteBodyFontSize(
|
||||||
|
DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let txn = Transaction::new(self.note_context.ndb).unwrap();
|
||||||
|
if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) {
|
||||||
|
if let Ok(preview_note) =
|
||||||
|
self.note_context.ndb.get_note_by_id(&txn, note_id.bytes())
|
||||||
|
{
|
||||||
|
notedeck_ui::padding(8.0, ui, |ui| {
|
||||||
|
if is_narrow(ui.ctx()) {
|
||||||
|
ui.set_max_width(ui.available_width());
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteView::new(
|
||||||
|
self.note_context,
|
||||||
|
&preview_note,
|
||||||
|
*self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
)
|
||||||
|
.actionbar(false)
|
||||||
|
.options_button(false)
|
||||||
|
.show(ui);
|
||||||
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let current_zoom = ui.ctx().zoom_factor();
|
let current_zoom = ui.ctx().zoom_factor();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(richtext_small(tr!(
|
||||||
RichText::new(tr!(
|
self.note_context.i18n,
|
||||||
self.i18n,
|
|
||||||
"Zoom Level:",
|
"Zoom Level:",
|
||||||
"Label for zoom level, Appearance settings section"
|
"Label for zoom level, Appearance settings section",
|
||||||
))
|
)));
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
);
|
let min_reached = current_zoom <= MIN_ZOOM;
|
||||||
|
let max_reached = current_zoom >= MAX_ZOOM;
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.button(
|
.add_enabled(
|
||||||
RichText::new("-")
|
!min_reached,
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
Button::new(
|
||||||
|
RichText::new("-").text_style(NotedeckTextStyle::Small.text_style()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
let new_zoom = (current_zoom - 0.1).max(0.1);
|
let new_zoom = (current_zoom - ZOOM_STEP).max(MIN_ZOOM);
|
||||||
action = Some(SettingsAction::SetZoom(new_zoom));
|
action = Some(SettingsAction::SetZoomFactor(new_zoom));
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.label(
|
ui.label(
|
||||||
@@ -188,96 +324,89 @@ impl<'a> SettingsView<'a> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.button(
|
.add_enabled(
|
||||||
RichText::new("+")
|
!max_reached,
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
Button::new(
|
||||||
|
RichText::new("+").text_style(NotedeckTextStyle::Small.text_style()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
let new_zoom = (current_zoom + 0.1).min(10.0);
|
let new_zoom = (current_zoom + ZOOM_STEP).min(MAX_ZOOM);
|
||||||
action = Some(SettingsAction::SetZoom(new_zoom));
|
action = Some(SettingsAction::SetZoomFactor(new_zoom));
|
||||||
};
|
};
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.button(
|
.button(richtext_small(tr!(
|
||||||
RichText::new(tr!(
|
self.note_context.i18n,
|
||||||
self.i18n,
|
|
||||||
"Reset",
|
"Reset",
|
||||||
"Label for reset zoom level, Appearance settings section"
|
"Label for reset zoom level, Appearance settings section",
|
||||||
))
|
)))
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
)
|
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
action = Some(SettingsAction::SetZoom(1.0));
|
action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(richtext_small(tr!(
|
||||||
RichText::new(tr!(
|
self.note_context.i18n,
|
||||||
self.i18n,
|
|
||||||
"Language:",
|
"Language:",
|
||||||
"Label for language, Appearance settings section"
|
"Label for language, Appearance settings section",
|
||||||
))
|
)));
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
);
|
//
|
||||||
ComboBox::from_label("")
|
ComboBox::from_label("")
|
||||||
.selected_text(self.get_selected_language_name())
|
.selected_text(self.get_selected_language_name())
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for lang in self.i18n.get_available_locales() {
|
for lang in self.note_context.i18n.get_available_locales() {
|
||||||
let name = self.i18n
|
let name = self
|
||||||
|
.note_context
|
||||||
|
.i18n
|
||||||
.get_locale_native_name(lang)
|
.get_locale_native_name(lang)
|
||||||
.map(|s| s.to_owned())
|
.map(|s| s.to_owned())
|
||||||
.unwrap_or_else(|| lang.to_string());
|
.unwrap_or_else(|| lang.to_string());
|
||||||
if ui
|
if ui
|
||||||
.selectable_value(
|
.selectable_value(&mut self.settings.locale, lang.to_string(), name)
|
||||||
self.selected_language,
|
|
||||||
lang.to_string(),
|
|
||||||
&name,
|
|
||||||
)
|
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
action = Some(SettingsAction::SetLocale(lang.to_owned()))
|
action = Some(SettingsAction::SetLocale(lang.to_owned()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(richtext_small(tr!(
|
||||||
RichText::new(tr!(
|
self.note_context.i18n,
|
||||||
self.i18n,
|
|
||||||
"Theme:",
|
"Theme:",
|
||||||
"Label for theme, Appearance settings section"
|
"Label for theme, Appearance settings section",
|
||||||
))
|
)));
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
);
|
|
||||||
if ui
|
if ui
|
||||||
.selectable_value(
|
.selectable_value(
|
||||||
self.theme,
|
&mut self.settings.theme,
|
||||||
"Light".into(),
|
ThemePreference::Light,
|
||||||
RichText::new(tr!(
|
richtext_small(tr!(
|
||||||
self.i18n,
|
self.note_context.i18n,
|
||||||
"Light",
|
THEME_LIGHT,
|
||||||
"Label for Theme Light, Appearance settings section"
|
"Label for Theme Light, Appearance settings section",
|
||||||
))
|
)),
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
action = Some(SettingsAction::SetTheme(ThemePreference::Light));
|
action = Some(SettingsAction::SetTheme(ThemePreference::Light));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.selectable_value(
|
.selectable_value(
|
||||||
self.theme,
|
&mut self.settings.theme,
|
||||||
"Dark".into(),
|
ThemePreference::Dark,
|
||||||
RichText::new(tr!(
|
richtext_small(tr!(
|
||||||
self.i18n,
|
self.note_context.i18n,
|
||||||
"Dark",
|
THEME_DARK,
|
||||||
"Label for Theme Dark, Appearance settings section"
|
"Label for Theme Dark, Appearance settings section",
|
||||||
))
|
)),
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
@@ -285,49 +414,42 @@ impl<'a> SettingsView<'a> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(5.0);
|
action
|
||||||
|
}
|
||||||
|
|
||||||
Frame::group(ui.style())
|
pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||||
.fill(ui.style().visuals.widgets.open.bg_fill)
|
let id = ui.id();
|
||||||
.inner_margin(10.0)
|
let mut action: Option<SettingsAction> = None;
|
||||||
.show(ui, |ui| {
|
let title = tr!(
|
||||||
ui.label(
|
self.note_context.i18n,
|
||||||
RichText::new(tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Storage",
|
"Storage",
|
||||||
"Label for storage settings section"
|
"Label for storage settings section"
|
||||||
))
|
|
||||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
|
||||||
);
|
);
|
||||||
ui.separator();
|
settings_group(ui, title, |ui| {
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
|
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
let static_imgs_size = self
|
let static_imgs_size = self
|
||||||
|
.note_context
|
||||||
.img_cache
|
.img_cache
|
||||||
.static_imgs
|
.static_imgs
|
||||||
.cache_size
|
.cache_size
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let gifs_size = self.img_cache.gifs.cache_size.lock().unwrap();
|
let gifs_size = self.note_context.img_cache.gifs.cache_size.lock().unwrap();
|
||||||
|
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{} {}",
|
RichText::new(format!(
|
||||||
|
"{} {}",
|
||||||
tr!(
|
tr!(
|
||||||
self.i18n,
|
self.note_context.i18n,
|
||||||
"Image cache size:",
|
"Image cache size:",
|
||||||
"Label for Image cache size, Storage settings section"
|
"Label for Image cache size, Storage settings section"
|
||||||
),
|
),
|
||||||
format_size(
|
format_size(
|
||||||
[static_imgs_size, gifs_size]
|
[static_imgs_size, gifs_size]
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0_u64, |acc, cur| acc
|
.fold(0_u64, |acc, cur| acc + cur.unwrap_or_default())
|
||||||
+ cur.unwrap_or_default())
|
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||||
@@ -335,19 +457,24 @@ impl<'a> SettingsView<'a> {
|
|||||||
|
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
if !notedeck::ui::is_compiled_as_mobile() &&
|
if !notedeck::ui::is_compiled_as_mobile()
|
||||||
ui.button(RichText::new(tr!(self.i18n, "View folder:", "Label for view folder button, Storage settings section"))
|
&& ui
|
||||||
.text_style(NotedeckTextStyle::Small.text_style())).clicked() {
|
.button(richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"View folder",
|
||||||
|
"Label for view folder button, Storage settings section",
|
||||||
|
)))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
action = Some(SettingsAction::OpenCacheFolder);
|
action = Some(SettingsAction::OpenCacheFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
let clearcache_resp = ui.button(
|
let clearcache_resp = ui.button(
|
||||||
RichText::new(tr!(
|
richtext_small(tr!(
|
||||||
self.i18n,
|
self.note_context.i18n,
|
||||||
"Clear cache",
|
"Clear cache",
|
||||||
"Label for clear cache button, Storage settings section"
|
"Label for clear cache button, Storage settings section",
|
||||||
))
|
))
|
||||||
.text_style(NotedeckTextStyle::Small.text_style())
|
|
||||||
.color(Color32::LIGHT_RED),
|
.color(Color32::LIGHT_RED),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -360,7 +487,7 @@ impl<'a> SettingsView<'a> {
|
|||||||
let mut confirm_pressed = false;
|
let mut confirm_pressed = false;
|
||||||
clearcache_resp.show_tooltip_ui(|ui| {
|
clearcache_resp.show_tooltip_ui(|ui| {
|
||||||
let confirm_resp = ui.button(tr!(
|
let confirm_resp = ui.button(tr!(
|
||||||
self.i18n,
|
self.note_context.i18n,
|
||||||
"Confirm",
|
"Confirm",
|
||||||
"Label for confirm clear cache, Storage settings section"
|
"Label for confirm clear cache, Storage settings section"
|
||||||
));
|
));
|
||||||
@@ -368,97 +495,143 @@ impl<'a> SettingsView<'a> {
|
|||||||
confirm_pressed = true;
|
confirm_pressed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if confirm_resp.clicked() || ui.button(tr!(
|
if confirm_resp.clicked()
|
||||||
self.i18n,
|
|| ui
|
||||||
|
.button(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
"Cancel",
|
"Cancel",
|
||||||
"Label for cancel clear cache, Storage settings section"
|
"Label for cancel clear cache, Storage settings section"
|
||||||
)).clicked() {
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
|
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if confirm_pressed {
|
if confirm_pressed {
|
||||||
action = Some(SettingsAction::ClearCacheFolder);
|
action = Some(SettingsAction::ClearCacheFolder);
|
||||||
} else if !confirm_pressed
|
} else if !confirm_pressed && clearcache_resp.clicked_elsewhere() {
|
||||||
&& clearcache_resp.clicked_elsewhere()
|
|
||||||
{
|
|
||||||
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
|
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(5.0);
|
action
|
||||||
|
}
|
||||||
|
|
||||||
Frame::group(ui.style())
|
fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||||
.fill(ui.style().visuals.widgets.open.bg_fill)
|
let mut action = None;
|
||||||
.inner_margin(10.0)
|
|
||||||
.show(ui, |ui| {
|
let title = tr!(
|
||||||
ui.label(
|
self.note_context.i18n,
|
||||||
RichText::new(tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Others",
|
"Others",
|
||||||
"Label for others settings section"
|
"Label for others settings section"
|
||||||
))
|
|
||||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
|
||||||
);
|
);
|
||||||
ui.separator();
|
settings_group(ui, title, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
|
ui.label(richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
ui.horizontal_wrapped(|ui| {
|
"Sort replies newest first",
|
||||||
ui.label(
|
"Label for Sort replies newest first, others settings section",
|
||||||
RichText::new(
|
)));
|
||||||
tr!(
|
|
||||||
self.i18n,
|
|
||||||
"Show source client",
|
|
||||||
"Label for Show source client, others settings section"
|
|
||||||
))
|
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
);
|
|
||||||
|
|
||||||
for option in [
|
|
||||||
ShowNoteClientOptions::Hide,
|
|
||||||
ShowNoteClientOptions::Top,
|
|
||||||
ShowNoteClientOptions::Bottom,
|
|
||||||
] {
|
|
||||||
let label = self.get_show_note_client_label(option);
|
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.selectable_value(
|
.toggle_value(
|
||||||
self.show_note_client,
|
&mut self.settings.show_replies_newest_first,
|
||||||
option,
|
RichText::new(tr!(self.note_context.i18n, "ON", "ON"))
|
||||||
RichText::new(label)
|
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||||
)
|
)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
action = Some(SettingsAction::SetShowNoteClient(option));
|
action = Some(SettingsAction::SetRepliestNewestFirst(
|
||||||
}
|
self.settings.show_replies_newest_first,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.label(richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"Source client",
|
||||||
|
"Label for Source client, others settings section",
|
||||||
|
)));
|
||||||
|
|
||||||
|
for option in [
|
||||||
|
ShowSourceClientOption::Hide,
|
||||||
|
ShowSourceClientOption::Top,
|
||||||
|
ShowSourceClientOption::Bottom,
|
||||||
|
] {
|
||||||
|
let mut current: ShowSourceClientOption =
|
||||||
|
self.settings.show_source_client.clone().into();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.selectable_value(
|
||||||
|
&mut current,
|
||||||
|
option,
|
||||||
|
RichText::new(option.label(self.note_context.i18n))
|
||||||
|
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
action = Some(SettingsAction::SetShowSourceClient(option));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(10.0);
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.add_sized(
|
.add_sized(
|
||||||
[ui.available_width(), 30.0],
|
[ui.available_width(), 30.0],
|
||||||
Button::new(
|
Button::new(richtext_small(tr!(
|
||||||
RichText::new(tr!(
|
self.note_context.i18n,
|
||||||
self.i18n,
|
|
||||||
"Configure relays",
|
"Configure relays",
|
||||||
"Label for configure relays, settings section"
|
"Label for configure relays, settings section",
|
||||||
))
|
))),
|
||||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
action = Some(SettingsAction::OpenRelays);
|
action = Some(SettingsAction::OpenRelays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||||
|
let mut action: Option<SettingsAction> = None;
|
||||||
|
|
||||||
|
Frame::default()
|
||||||
|
.inner_margin(Margin::symmetric(10, 10))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
if let Some(new_action) = self.appearance_section(ui) {
|
||||||
|
action = Some(new_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
if let Some(new_action) = self.storage_section(ui) {
|
||||||
|
action = Some(new_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
if let Some(new_action) = self.other_options_section(ui) {
|
||||||
|
action = Some(new_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
if let Some(new_action) = self.manage_relays_section(ui) {
|
||||||
|
action = Some(new_action);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
action
|
action
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
use crate::support::{Support, SUPPORT_EMAIL};
|
||||||
use egui::{vec2, Button, Label, Layout, RichText};
|
use egui::{vec2, Button, Label, Layout, RichText};
|
||||||
use notedeck::{tr, Localization, NamedFontFamily, NotedeckTextStyle};
|
use notedeck::{tr, Localization, NamedFontFamily, NotedeckTextStyle};
|
||||||
use notedeck_ui::{colors::PINK, padding};
|
use notedeck_ui::{colors::PINK, padding};
|
||||||
|
use robius_open::Uri;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::support::Support;
|
|
||||||
|
|
||||||
pub struct SupportView<'a> {
|
pub struct SupportView<'a> {
|
||||||
support: &'a mut Support,
|
support: &'a mut Support,
|
||||||
i18n: &'a mut Localization,
|
i18n: &'a mut Localization,
|
||||||
@@ -44,15 +44,21 @@ impl<'a> SupportView<'a> {
|
|||||||
"Open your default email client to get help from the Damus team",
|
"Open your default email client to get help from the Damus team",
|
||||||
"Instruction to open email client"
|
"Instruction to open email client"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.label(tr!(self.i18n, "Support email:", "Support email address",));
|
||||||
|
ui.label(RichText::new(SUPPORT_EMAIL).color(PINK))
|
||||||
|
});
|
||||||
|
|
||||||
let size = vec2(120.0, 40.0);
|
let size = vec2(120.0, 40.0);
|
||||||
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||||
let font_size =
|
let font_size =
|
||||||
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
||||||
let button_resp = ui.add(open_email_button(self.i18n, font_size, size));
|
let button_resp = ui.add(open_email_button(self.i18n, font_size, size));
|
||||||
if button_resp.clicked() {
|
if button_resp.clicked() {
|
||||||
if let Err(e) = open::that(self.support.get_mailto_url()) {
|
if let Err(e) = Uri::new(self.support.get_mailto_url()).open() {
|
||||||
error!(
|
error!(
|
||||||
"Failed to open URL {} because: {}",
|
"Failed to open URL {} because: {:?}",
|
||||||
self.support.get_mailto_url(),
|
self.support.get_mailto_url(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use egui::InnerResponse;
|
|||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use nostrdb::{Note, Transaction};
|
use nostrdb::{Note, Transaction};
|
||||||
use notedeck::note::root_note_id_from_selected_id;
|
use notedeck::note::root_note_id_from_selected_id;
|
||||||
|
use notedeck::JobsCache;
|
||||||
use notedeck::{NoteAction, NoteContext};
|
use notedeck::{NoteAction, NoteContext};
|
||||||
use notedeck_ui::jobs::JobsCache;
|
|
||||||
use notedeck_ui::note::NoteResponse;
|
use notedeck_ui::note::NoteResponse;
|
||||||
use notedeck_ui::{NoteOptions, NoteView};
|
use notedeck_ui::{NoteOptions, NoteView};
|
||||||
|
|
||||||
@@ -115,7 +115,10 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.list;
|
.list;
|
||||||
|
|
||||||
let notes = note_builder.into_notes(&mut self.threads.seen_flags);
|
let notes = note_builder.into_notes(
|
||||||
|
self.note_options.contains(NoteOptions::RepliesNewestFirst),
|
||||||
|
&mut self.threads.seen_flags,
|
||||||
|
);
|
||||||
|
|
||||||
if !full_chain {
|
if !full_chain {
|
||||||
// TODO(kernelkind): insert UI denoting we don't have the full chain yet
|
// TODO(kernelkind): insert UI denoting we don't have the full chain yet
|
||||||
@@ -223,7 +226,11 @@ impl<'a> ThreadNoteBuilder<'a> {
|
|||||||
self.replies.push(note);
|
self.replies.push(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_notes(mut self, seen_flags: &mut NoteSeenFlags) -> ThreadNotes<'a> {
|
pub fn into_notes(
|
||||||
|
mut self,
|
||||||
|
replies_newer_first: bool,
|
||||||
|
seen_flags: &mut NoteSeenFlags,
|
||||||
|
) -> ThreadNotes<'a> {
|
||||||
let mut notes = Vec::new();
|
let mut notes = Vec::new();
|
||||||
|
|
||||||
let selected_is_root = self.chain.is_empty();
|
let selected_is_root = self.chain.is_empty();
|
||||||
@@ -246,6 +253,11 @@ impl<'a> ThreadNoteBuilder<'a> {
|
|||||||
unread_and_have_replies: false,
|
unread_and_have_replies: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if replies_newer_first {
|
||||||
|
self.replies
|
||||||
|
.sort_by_key(|b| std::cmp::Reverse(b.created_at()));
|
||||||
|
}
|
||||||
|
|
||||||
for reply in self.replies {
|
for reply in self.replies {
|
||||||
notes.push(ThreadNote {
|
notes.push(ThreadNote {
|
||||||
unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false),
|
unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use egui::{vec2, Direction, Layout, Pos2, Stroke};
|
|||||||
use egui_tabs::TabColor;
|
use egui_tabs::TabColor;
|
||||||
use nostrdb::Transaction;
|
use nostrdb::Transaction;
|
||||||
use notedeck::ui::is_narrow;
|
use notedeck::ui::is_narrow;
|
||||||
use notedeck_ui::jobs::JobsCache;
|
use notedeck::JobsCache;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ use crate::deck_state::DeckState;
|
|||||||
use crate::login_manager::AcquireKeyState;
|
use crate::login_manager::AcquireKeyState;
|
||||||
use crate::ui::search::SearchQueryState;
|
use crate::ui::search::SearchQueryState;
|
||||||
use enostr::ProfileState;
|
use enostr::ProfileState;
|
||||||
|
use notedeck_ui::media::MediaViewerState;
|
||||||
|
|
||||||
/// Various state for views
|
/// Various state for views
|
||||||
|
///
|
||||||
|
/// TODO(jb55): we likely want to encapsulate these better,
|
||||||
|
/// or at least document where they are used
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ViewState {
|
pub struct ViewState {
|
||||||
pub login: AcquireKeyState,
|
pub login: AcquireKeyState,
|
||||||
@@ -16,6 +20,11 @@ pub struct ViewState {
|
|||||||
pub id_string_map: HashMap<egui::Id, String>,
|
pub id_string_map: HashMap<egui::Id, String>,
|
||||||
pub searches: HashMap<egui::Id, SearchQueryState>,
|
pub searches: HashMap<egui::Id, SearchQueryState>,
|
||||||
pub pubkey_to_profile_state: HashMap<Pubkey, ProfileState>,
|
pub pubkey_to_profile_state: HashMap<Pubkey, ProfileState>,
|
||||||
|
|
||||||
|
/// Keeps track of what urls we are actively viewing in the
|
||||||
|
/// fullscreen media viewier, as well as any other state we want to
|
||||||
|
/// keep track of
|
||||||
|
pub media_viewer: MediaViewerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewState {
|
impl ViewState {
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ use egui_wgpu::RenderState;
|
|||||||
use enostr::KeypairUnowned;
|
use enostr::KeypairUnowned;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use nostrdb::Transaction;
|
use nostrdb::Transaction;
|
||||||
use notedeck::{AppAction, AppContext};
|
use notedeck::{AppAction, AppContext, JobsCache};
|
||||||
use notedeck_ui::jobs::JobsCache;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
use std::sync::mpsc::{self, Receiver};
|
use std::sync::mpsc::{self, Receiver};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
|
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::{tr, Accounts, AppContext, Images, Localization, NoteAction, NoteContext};
|
use notedeck::{
|
||||||
use notedeck_ui::{app_images, icons::search_icon, jobs::JobsCache, NoteOptions, ProfilePic};
|
tr, Accounts, AppContext, Images, JobsCache, Localization, NoteAction, NoteContext,
|
||||||
|
};
|
||||||
|
use notedeck_ui::{app_images, icons::search_icon, NoteOptions, ProfilePic};
|
||||||
|
|
||||||
/// DaveUi holds all of the data it needs to render itself
|
/// DaveUi holds all of the data it needs to render itself
|
||||||
pub struct DaveUi<'a> {
|
pub struct DaveUi<'a> {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "notedeck_notebook"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jsoncanvas = { git = "https://github.com/jb55/jsoncanvas", rev = "ae60f96e4d022cf037e086b793cacc3225bc14e5" }
|
||||||
|
notedeck = { workspace = true }
|
||||||
|
egui = { workspace = true }
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
fn debug_slider(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
id: egui::Id,
|
||||||
|
point: Pos2,
|
||||||
|
initial: f32,
|
||||||
|
range: std::ops::RangeInclusive<f32>,
|
||||||
|
) -> f32 {
|
||||||
|
let mut val = ui.data_mut(|d| *d.get_temp_mut_or::<f32>(id, initial));
|
||||||
|
let nudge = vec2(10.0, 10.0);
|
||||||
|
let slider = Rect::from_min_max(point - nudge, point + nudge);
|
||||||
|
let label = Rect::from_min_max(point + nudge * 2.0, point - nudge * 2.0);
|
||||||
|
|
||||||
|
let old_val = val;
|
||||||
|
ui.put(slider, egui::Slider::new(&mut val, range));
|
||||||
|
ui.put(label, egui::Label::new(format!("{val}")));
|
||||||
|
|
||||||
|
if val != old_val {
|
||||||
|
ui.data_mut(|d| d.insert_temp(id, val))
|
||||||
|
}
|
||||||
|
|
||||||
|
val
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use crate::ui::{edge_ui, node_ui};
|
||||||
|
use egui::{Pos2, Rect};
|
||||||
|
use jsoncanvas::JsonCanvas;
|
||||||
|
use notedeck::{AppAction, AppContext};
|
||||||
|
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
pub struct Notebook {
|
||||||
|
canvas: JsonCanvas,
|
||||||
|
scene_rect: Rect,
|
||||||
|
loaded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notebook {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Notebook::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Notebook {
|
||||||
|
fn default() -> Self {
|
||||||
|
Notebook {
|
||||||
|
canvas: demo_canvas(),
|
||||||
|
scene_rect: Rect::from_min_max(Pos2::ZERO, Pos2::ZERO),
|
||||||
|
loaded: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl notedeck::App for Notebook {
|
||||||
|
fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
|
||||||
|
//let app_action: Option<AppAction> = None;
|
||||||
|
|
||||||
|
if !self.loaded {
|
||||||
|
self.scene_rect = ui.available_rect_before_wrap();
|
||||||
|
self.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::Scene::new().show(ui, &mut self.scene_rect, |ui| {
|
||||||
|
// render nodes
|
||||||
|
for (_node_id, node) in self.canvas.get_nodes().iter() {
|
||||||
|
let _resp = node_ui(ui, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render edges
|
||||||
|
for (_edge_id, edge) in self.canvas.get_edges().iter() {
|
||||||
|
let _resp = edge_ui(ui, self.canvas.get_nodes(), edge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo_canvas() -> JsonCanvas {
|
||||||
|
let demo_json: String = include_str!("../demo.canvas").to_string();
|
||||||
|
|
||||||
|
let canvas: JsonCanvas = demo_json.parse().unwrap_or_else(|_| JsonCanvas::default());
|
||||||
|
canvas
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
use egui::{Align, Label, Pos2, Rect, Shape, Stroke, TextWrapMode, epaint::CubicBezierShape, vec2};
|
||||||
|
use jsoncanvas::{
|
||||||
|
FileNode, GroupNode, LinkNode, Node, NodeId, TextNode,
|
||||||
|
edge::{Edge, Side},
|
||||||
|
node::GenericNode,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops::Neg;
|
||||||
|
|
||||||
|
fn node_rect(node: &GenericNode) -> Rect {
|
||||||
|
let x = node.x as f32;
|
||||||
|
let y = node.y as f32;
|
||||||
|
let width = node.width as f32;
|
||||||
|
let height = node.height as f32;
|
||||||
|
|
||||||
|
let min = Pos2::new(x, y);
|
||||||
|
let max = Pos2::new(x + width, y + height);
|
||||||
|
|
||||||
|
Rect::from_min_max(min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn side_point(side: &Side, node: &GenericNode) -> Pos2 {
|
||||||
|
let rect = node_rect(node);
|
||||||
|
|
||||||
|
match side {
|
||||||
|
Side::Top => rect.center_top(),
|
||||||
|
Side::Left => rect.left_center(),
|
||||||
|
Side::Right => rect.right_center(),
|
||||||
|
Side::Bottom => rect.center_bottom(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a unit vector pointing outward from the given side
|
||||||
|
fn side_tangent(side: &Side) -> egui::Vec2 {
|
||||||
|
match side {
|
||||||
|
Side::Top => vec2(0.0, -1.0),
|
||||||
|
Side::Bottom => vec2(0.0, 1.0),
|
||||||
|
Side::Left => vec2(-1.0, 0.0),
|
||||||
|
Side::Right => vec2(1.0, 0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edge_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
nodes: &HashMap<NodeId, Node>,
|
||||||
|
edge: &Edge,
|
||||||
|
) -> Option<egui::Response> {
|
||||||
|
let from_node = nodes.get(edge.from_node())?;
|
||||||
|
let to_node = nodes.get(edge.to_node())?;
|
||||||
|
let to_side = edge.to_side()?;
|
||||||
|
let from_side = edge.from_side()?;
|
||||||
|
|
||||||
|
// anchor from-side
|
||||||
|
let p0 = side_point(from_side, from_node.node());
|
||||||
|
|
||||||
|
// anchor b
|
||||||
|
let to_anchor = side_point(to_side, to_node.node());
|
||||||
|
|
||||||
|
// to-point is slightly offset to accomidate arrow
|
||||||
|
let p3 = to_anchor + side_tangent(to_side) * 2.0;
|
||||||
|
|
||||||
|
// bend debug
|
||||||
|
//let bend = debug_slider(ui, ui.id().with("bend"), p3, 0.25, 0.0..=1.0);
|
||||||
|
let bend = 0.28;
|
||||||
|
|
||||||
|
// How far to pull the tangents.
|
||||||
|
// ¼ of the distance between anchors feels very “Obsidian”.
|
||||||
|
let d = (p3 - p0).length() * bend;
|
||||||
|
|
||||||
|
// c1 = anchor A + (outward tangent) * d
|
||||||
|
let c1 = p0 + side_tangent(from_side) * d;
|
||||||
|
|
||||||
|
// c2 = anchor B + (inward tangent) * d
|
||||||
|
let c2 = p3 - side_tangent(to_side).neg() * d;
|
||||||
|
|
||||||
|
let color = ui.visuals().noninteractive().bg_stroke.color;
|
||||||
|
let stroke = egui::Stroke::new(4.0, color);
|
||||||
|
let bezier = CubicBezierShape::from_points_stroke([p0, c1, c2, p3], false, color, stroke);
|
||||||
|
|
||||||
|
ui.painter().add(Shape::CubicBezier(bezier));
|
||||||
|
arrow_ui(ui, to_side, to_anchor, color);
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paint a tiny triangular “arrow”.
|
||||||
|
///
|
||||||
|
/// * `ui` – the egui `Ui` you’re painting in
|
||||||
|
/// * `side` – which edge of the box we’re attaching to
|
||||||
|
/// * `point` – the exact spot on that edge the arrow’s tip should touch
|
||||||
|
/// * `fill` – colour to fill the arrow with (usually your popup’s background)
|
||||||
|
pub fn arrow_ui(ui: &mut egui::Ui, side: &Side, point: Pos2, fill: egui::Color32) {
|
||||||
|
let len: f32 = 12.0; // distance from tip to base
|
||||||
|
let width: f32 = 16.0; // length of the base
|
||||||
|
let stroke: f32 = 1.0; // length of the base
|
||||||
|
|
||||||
|
let verts = match side {
|
||||||
|
Side::Top => [
|
||||||
|
point, // tip
|
||||||
|
Pos2::new(point.x - width * 0.5, point.y - len), // base‑left (above)
|
||||||
|
Pos2::new(point.x + width * 0.5, point.y - len), // base‑right (above)
|
||||||
|
],
|
||||||
|
Side::Bottom => [
|
||||||
|
point,
|
||||||
|
Pos2::new(point.x + width * 0.5, point.y + len), // below
|
||||||
|
Pos2::new(point.x - width * 0.5, point.y + len),
|
||||||
|
],
|
||||||
|
Side::Left => [
|
||||||
|
point,
|
||||||
|
Pos2::new(point.x - len, point.y + width * 0.5), // left
|
||||||
|
Pos2::new(point.x - len, point.y - width * 0.5),
|
||||||
|
],
|
||||||
|
Side::Right => [
|
||||||
|
point,
|
||||||
|
Pos2::new(point.x + len, point.y - width * 0.5), // right
|
||||||
|
Pos2::new(point.x + len, point.y + width * 0.5),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().add(egui::Shape::convex_polygon(
|
||||||
|
verts.to_vec(),
|
||||||
|
fill,
|
||||||
|
Stroke::new(stroke, fill), // add a stroke here if you want an outline
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_ui(ui: &mut egui::Ui, node: &Node) -> egui::Response {
|
||||||
|
match node {
|
||||||
|
Node::Text(text_node) => text_node_ui(ui, text_node),
|
||||||
|
Node::File(file_node) => file_node_ui(ui, file_node),
|
||||||
|
Node::Link(link_node) => link_node_ui(ui, link_node),
|
||||||
|
Node::Group(group_node) => group_node_ui(ui, group_node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_node_ui(ui: &mut egui::Ui, node: &TextNode) -> egui::Response {
|
||||||
|
node_box_ui(ui, node.node(), |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.with_layout(egui::Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
ui.add(Label::new(node.text()).wrap_mode(TextWrapMode::Wrap))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.inner
|
||||||
|
.response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_node_ui(ui: &mut egui::Ui, node: &FileNode) -> egui::Response {
|
||||||
|
node_box_ui(ui, node.node(), |ui| ui.label("file node"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_node_ui(ui: &mut egui::Ui, node: &LinkNode) -> egui::Response {
|
||||||
|
node_box_ui(ui, node.node(), |ui| ui.label("link node"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn group_node_ui(ui: &mut egui::Ui, node: &GroupNode) -> egui::Response {
|
||||||
|
node_box_ui(ui, node.node(), |ui| ui.label("group node"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_box_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
node: &GenericNode,
|
||||||
|
contents: impl FnOnce(&mut egui::Ui) -> egui::Response,
|
||||||
|
) -> egui::Response {
|
||||||
|
let pos = node_rect(node);
|
||||||
|
|
||||||
|
ui.put(pos, |ui: &mut egui::Ui| {
|
||||||
|
egui::Frame::default()
|
||||||
|
.fill(ui.visuals().noninteractive().weak_bg_fill)
|
||||||
|
.inner_margin(egui::Margin::same(16))
|
||||||
|
.corner_radius(egui::CornerRadius::same(10))
|
||||||
|
.stroke(egui::Stroke::new(
|
||||||
|
2.0,
|
||||||
|
ui.visuals().noninteractive().bg_stroke.color,
|
||||||
|
))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let rect = ui.available_rect_before_wrap();
|
||||||
|
ui.allocate_at_least(ui.available_size(), egui::Sense::click());
|
||||||
|
ui.put(rect, contents);
|
||||||
|
})
|
||||||
|
.response
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -21,5 +21,3 @@ image = { workspace = true }
|
|||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
enostr = { workspace = true }
|
enostr = { workspace = true }
|
||||||
hashbrown = { workspace = true }
|
hashbrown = { workspace = true }
|
||||||
|
|
||||||
blurhash = "0.2.3"
|
|
||||||
|
|||||||
@@ -1,510 +1 @@
|
|||||||
use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
|
|
||||||
use image::codecs::gif::GifDecoder;
|
|
||||||
use image::imageops::FilterType;
|
|
||||||
use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
|
|
||||||
use notedeck::{
|
|
||||||
Animation, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache, MediaCacheType,
|
|
||||||
TextureFrame, TextureState, TexturedImage,
|
|
||||||
};
|
|
||||||
use poll_promise::Promise;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::io::Cursor;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::path::{self, Path};
|
|
||||||
use std::sync::mpsc;
|
|
||||||
use std::sync::mpsc::SyncSender;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
// NOTE(jb55): chatgpt wrote this because I was too dumb to
|
|
||||||
pub fn aspect_fill(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
sense: Sense,
|
|
||||||
texture_id: egui::TextureId,
|
|
||||||
aspect_ratio: f32,
|
|
||||||
) -> egui::Response {
|
|
||||||
let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
|
|
||||||
let frame_ratio = frame.width() / frame.height();
|
|
||||||
|
|
||||||
let (width, height) = if frame_ratio > aspect_ratio {
|
|
||||||
// Frame is wider than the content
|
|
||||||
(frame.width(), frame.width() / aspect_ratio)
|
|
||||||
} else {
|
|
||||||
// Frame is taller than the content
|
|
||||||
(frame.height() * aspect_ratio, frame.height())
|
|
||||||
};
|
|
||||||
|
|
||||||
let content_rect = Rect::from_min_size(
|
|
||||||
frame.min
|
|
||||||
+ egui::vec2(
|
|
||||||
(frame.width() - width) / 2.0,
|
|
||||||
(frame.height() - height) / 2.0,
|
|
||||||
),
|
|
||||||
egui::vec2(width, height),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the clipping rectangle to the frame
|
|
||||||
//let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
|
|
||||||
//ui.set_clip_rect(frame);
|
|
||||||
|
|
||||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
|
||||||
|
|
||||||
let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
|
|
||||||
|
|
||||||
// Draw the texture within the calculated rect, potentially clipping it
|
|
||||||
painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
|
|
||||||
painter.image(texture_id, content_rect, uv, Color32::WHITE);
|
|
||||||
|
|
||||||
// Restore the original clipping rectangle
|
|
||||||
//ui.set_clip_rect(clip_rect);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
#[profiling::function]
|
|
||||||
pub fn round_image(image: &mut ColorImage) {
|
|
||||||
// The radius to the edge of of the avatar circle
|
|
||||||
let edge_radius = image.size[0] as f32 / 2.0;
|
|
||||||
let edge_radius_squared = edge_radius * edge_radius;
|
|
||||||
|
|
||||||
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
|
|
||||||
// y coordinate
|
|
||||||
let uy = pixnum / image.size[0];
|
|
||||||
let y = uy as f32;
|
|
||||||
let y_offset = edge_radius - y;
|
|
||||||
|
|
||||||
// x coordinate
|
|
||||||
let ux = pixnum % image.size[0];
|
|
||||||
let x = ux as f32;
|
|
||||||
let x_offset = edge_radius - x;
|
|
||||||
|
|
||||||
// The radius to this pixel (may be inside or outside the circle)
|
|
||||||
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
|
|
||||||
|
|
||||||
// If inside of the avatar circle
|
|
||||||
if pixel_radius_squared <= edge_radius_squared {
|
|
||||||
// squareroot to find how many pixels we are from the edge
|
|
||||||
let pixel_radius: f32 = pixel_radius_squared.sqrt();
|
|
||||||
let distance = edge_radius - pixel_radius;
|
|
||||||
|
|
||||||
// If we are within 1 pixel of the edge, we should fade, to
|
|
||||||
// antialias the edge of the circle. 1 pixel from the edge should
|
|
||||||
// be 100% of the original color, and right on the edge should be
|
|
||||||
// 0% of the original color.
|
|
||||||
if distance <= 1.0 {
|
|
||||||
*pixel = Color32::from_rgba_premultiplied(
|
|
||||||
(pixel.r() as f32 * distance) as u8,
|
|
||||||
(pixel.g() as f32 * distance) as u8,
|
|
||||||
(pixel.b() as f32 * distance) as u8,
|
|
||||||
(pixel.a() as f32 * distance) as u8,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Outside of the avatar circle
|
|
||||||
*pixel = Color32::TRANSPARENT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the image's longest dimension is greater than max_edge, downscale
|
|
||||||
fn resize_image_if_too_big(
|
|
||||||
image: image::DynamicImage,
|
|
||||||
max_edge: u32,
|
|
||||||
filter: FilterType,
|
|
||||||
) -> image::DynamicImage {
|
|
||||||
// if we have no size hint, resize to something reasonable
|
|
||||||
let w = image.width();
|
|
||||||
let h = image.height();
|
|
||||||
let long = w.max(h);
|
|
||||||
|
|
||||||
if long > max_edge {
|
|
||||||
let scale = max_edge as f32 / long as f32;
|
|
||||||
let new_w = (w as f32 * scale).round() as u32;
|
|
||||||
let new_h = (h as f32 * scale).round() as u32;
|
|
||||||
|
|
||||||
image.resize(new_w, new_h, filter)
|
|
||||||
} else {
|
|
||||||
image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Process an image, resizing so we don't blow up video memory or even crash
|
|
||||||
///
|
|
||||||
/// For profile pictures, make them round and small to fit the size hint
|
|
||||||
/// For everything else, either:
|
|
||||||
///
|
|
||||||
/// - resize to the size hint
|
|
||||||
/// - keep the size if the longest dimension is less than MAX_IMG_LENGTH
|
|
||||||
/// - resize if any larger, using [`resize_image_if_too_big`]
|
|
||||||
///
|
|
||||||
#[profiling::function]
|
|
||||||
fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
|
|
||||||
const MAX_IMG_LENGTH: u32 = 512;
|
|
||||||
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
|
|
||||||
|
|
||||||
match imgtyp {
|
|
||||||
ImageType::Content(size_hint) => {
|
|
||||||
let image = match size_hint {
|
|
||||||
None => resize_image_if_too_big(image, MAX_IMG_LENGTH, FILTER_TYPE),
|
|
||||||
Some((w, h)) => image.resize(w, h, FILTER_TYPE),
|
|
||||||
};
|
|
||||||
|
|
||||||
let image_buffer = image.into_rgba8();
|
|
||||||
ColorImage::from_rgba_unmultiplied(
|
|
||||||
[
|
|
||||||
image_buffer.width() as usize,
|
|
||||||
image_buffer.height() as usize,
|
|
||||||
],
|
|
||||||
image_buffer.as_flat_samples().as_slice(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ImageType::Profile(size) => {
|
|
||||||
// Crop square
|
|
||||||
let smaller = image.width().min(image.height());
|
|
||||||
|
|
||||||
if image.width() > smaller {
|
|
||||||
let excess = image.width() - smaller;
|
|
||||||
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
|
|
||||||
} else if image.height() > smaller {
|
|
||||||
let excess = image.height() - smaller;
|
|
||||||
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
|
|
||||||
}
|
|
||||||
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
|
|
||||||
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
|
|
||||||
let mut color_image = ColorImage::from_rgba_unmultiplied(
|
|
||||||
[
|
|
||||||
image_buffer.width() as usize,
|
|
||||||
image_buffer.height() as usize,
|
|
||||||
],
|
|
||||||
image_buffer.as_flat_samples().as_slice(),
|
|
||||||
);
|
|
||||||
round_image(&mut color_image);
|
|
||||||
color_image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[profiling::function]
|
|
||||||
fn parse_img_response(
|
|
||||||
response: ehttp::Response,
|
|
||||||
imgtyp: ImageType,
|
|
||||||
) -> Result<ColorImage, notedeck::Error> {
|
|
||||||
let content_type = response.content_type().unwrap_or_default();
|
|
||||||
let size_hint = match imgtyp {
|
|
||||||
ImageType::Profile(size) => SizeHint::Size(size, size),
|
|
||||||
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
|
|
||||||
ImageType::Content(None) => SizeHint::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if content_type.starts_with("image/svg") {
|
|
||||||
profiling::scope!("load_svg");
|
|
||||||
|
|
||||||
let mut color_image =
|
|
||||||
egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
|
|
||||||
round_image(&mut color_image);
|
|
||||||
Ok(color_image)
|
|
||||||
} else if content_type.starts_with("image/") {
|
|
||||||
profiling::scope!("load_from_memory");
|
|
||||||
let dyn_image = image::load_from_memory(&response.bytes)?;
|
|
||||||
Ok(process_image(imgtyp, dyn_image))
|
|
||||||
} else {
|
|
||||||
Err(format!("Expected image, found content-type {content_type:?}").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_img_from_disk(
|
|
||||||
ctx: &egui::Context,
|
|
||||||
url: &str,
|
|
||||||
path: &path::Path,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
) -> Promise<Option<Result<TexturedImage, notedeck::Error>>> {
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
let url = url.to_owned();
|
|
||||||
let path = path.to_owned();
|
|
||||||
|
|
||||||
Promise::spawn_async(async move {
|
|
||||||
Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn async_fetch_img_from_disk(
|
|
||||||
ctx: egui::Context,
|
|
||||||
url: String,
|
|
||||||
path: &path::Path,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
) -> Result<TexturedImage, notedeck::Error> {
|
|
||||||
match cache_type {
|
|
||||||
MediaCacheType::Image => {
|
|
||||||
let data = fs::read(path).await?;
|
|
||||||
let image_buffer = image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
|
|
||||||
|
|
||||||
let img = buffer_to_color_image(
|
|
||||||
image_buffer.as_flat_samples_u8(),
|
|
||||||
image_buffer.width(),
|
|
||||||
image_buffer.height(),
|
|
||||||
);
|
|
||||||
Ok(TexturedImage::Static(ctx.load_texture(
|
|
||||||
&url,
|
|
||||||
img,
|
|
||||||
Default::default(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
MediaCacheType::Gif => {
|
|
||||||
let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
|
|
||||||
generate_gif(ctx, url, path, gif_bytes, false, |i| {
|
|
||||||
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_gif(
|
|
||||||
ctx: egui::Context,
|
|
||||||
url: String,
|
|
||||||
path: &path::Path,
|
|
||||||
data: Vec<u8>,
|
|
||||||
write_to_disk: bool,
|
|
||||||
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
|
|
||||||
) -> Result<TexturedImage, notedeck::Error> {
|
|
||||||
let decoder = {
|
|
||||||
let reader = Cursor::new(data.as_slice());
|
|
||||||
GifDecoder::new(reader)?
|
|
||||||
};
|
|
||||||
let (tex_input, tex_output) = mpsc::sync_channel(4);
|
|
||||||
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
|
|
||||||
let (inp, out) = mpsc::sync_channel(4);
|
|
||||||
(Some(inp), Some(out))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut frames: VecDeque<Frame> = decoder
|
|
||||||
.into_frames()
|
|
||||||
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
|
|
||||||
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
|
|
||||||
|
|
||||||
let first_frame = frames.pop_front().map(|frame| {
|
|
||||||
generate_animation_frame(
|
|
||||||
&ctx,
|
|
||||||
&url,
|
|
||||||
0,
|
|
||||||
frame,
|
|
||||||
maybe_encoder_input.as_ref(),
|
|
||||||
process_to_egui,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let cur_url = url.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
for (index, frame) in frames.into_iter().enumerate() {
|
|
||||||
let texture_frame = generate_animation_frame(
|
|
||||||
&ctx,
|
|
||||||
&cur_url,
|
|
||||||
index,
|
|
||||||
frame,
|
|
||||||
maybe_encoder_input.as_ref(),
|
|
||||||
process_to_egui,
|
|
||||||
);
|
|
||||||
|
|
||||||
if tex_input.send(texture_frame).is_err() {
|
|
||||||
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(encoder_output) = maybe_encoder_output {
|
|
||||||
let path = path.to_owned();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
let mut imgs = Vec::new();
|
|
||||||
while let Ok(img) = encoder_output.recv() {
|
|
||||||
imgs.push(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
|
|
||||||
tracing::error!("Could not write gif to disk: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
first_frame.map_or_else(
|
|
||||||
|| {
|
|
||||||
Err(notedeck::Error::Generic(
|
|
||||||
"first frame not found for gif".to_owned(),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
|first_frame| {
|
|
||||||
Ok(TexturedImage::Animated(Animation {
|
|
||||||
other_frames: Default::default(),
|
|
||||||
receiver: Some(tex_output),
|
|
||||||
first_frame,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_animation_frame(
|
|
||||||
ctx: &egui::Context,
|
|
||||||
url: &str,
|
|
||||||
index: usize,
|
|
||||||
frame: image::Frame,
|
|
||||||
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
|
|
||||||
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
|
|
||||||
) -> TextureFrame {
|
|
||||||
let delay = Duration::from(frame.delay());
|
|
||||||
let img = DynamicImage::ImageRgba8(frame.into_buffer());
|
|
||||||
let color_img = process_to_egui(img);
|
|
||||||
|
|
||||||
if let Some(sender) = maybe_encoder_input {
|
|
||||||
if let Err(e) = sender.send(ImageFrame {
|
|
||||||
delay,
|
|
||||||
image: color_img.clone(),
|
|
||||||
}) {
|
|
||||||
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureFrame {
|
|
||||||
delay,
|
|
||||||
texture: ctx.load_texture(format!("{url}{index}"), color_img, Default::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn buffer_to_color_image(
|
|
||||||
samples: Option<FlatSamples<&[u8]>>,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
) -> ColorImage {
|
|
||||||
// TODO(jb55): remove unwrap here
|
|
||||||
let flat_samples = samples.unwrap();
|
|
||||||
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>, notedeck::Error> {
|
|
||||||
std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Controls type-specific handling
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum ImageType {
|
|
||||||
/// Profile Image (size)
|
|
||||||
Profile(u32),
|
|
||||||
/// Content Image with optional size hint
|
|
||||||
Content(Option<(u32, u32)>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_img(
|
|
||||||
img_cache_path: &Path,
|
|
||||||
ctx: &egui::Context,
|
|
||||||
url: &str,
|
|
||||||
imgtyp: ImageType,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
) -> Promise<Option<Result<TexturedImage, notedeck::Error>>> {
|
|
||||||
let key = MediaCache::key(url);
|
|
||||||
let path = img_cache_path.join(key);
|
|
||||||
|
|
||||||
if path.exists() {
|
|
||||||
fetch_img_from_disk(ctx, url, &path, cache_type)
|
|
||||||
} else {
|
|
||||||
fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fetch image from local cache
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_img_from_net(
|
|
||||||
cache_path: &path::Path,
|
|
||||||
ctx: &egui::Context,
|
|
||||||
url: &str,
|
|
||||||
imgtyp: ImageType,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
) -> Promise<Option<Result<TexturedImage, notedeck::Error>>> {
|
|
||||||
let (sender, promise) = Promise::new();
|
|
||||||
let request = ehttp::Request::get(url);
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
let cloned_url = url.to_owned();
|
|
||||||
let cache_path = cache_path.to_owned();
|
|
||||||
ehttp::fetch(request, move |response| {
|
|
||||||
let handle = response.map_err(notedeck::Error::Generic).and_then(|resp| {
|
|
||||||
match cache_type {
|
|
||||||
MediaCacheType::Image => {
|
|
||||||
let img = parse_img_response(resp, imgtyp);
|
|
||||||
img.map(|img| {
|
|
||||||
let texture_handle =
|
|
||||||
ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
|
||||||
|
|
||||||
// write to disk
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
MediaCache::write(&cache_path, &cloned_url, img)
|
|
||||||
});
|
|
||||||
|
|
||||||
TexturedImage::Static(texture_handle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
MediaCacheType::Gif => {
|
|
||||||
let gif_bytes = resp.bytes;
|
|
||||||
generate_gif(
|
|
||||||
ctx.clone(),
|
|
||||||
cloned_url,
|
|
||||||
&cache_path,
|
|
||||||
gif_bytes,
|
|
||||||
true,
|
|
||||||
move |img| process_image(imgtyp, img),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sender.send(Some(handle)); // send the results back to the UI thread.
|
|
||||||
ctx.request_repaint();
|
|
||||||
});
|
|
||||||
|
|
||||||
promise
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_render_state<'a>(
|
|
||||||
ctx: &Context,
|
|
||||||
images: &'a mut Images,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
url: &str,
|
|
||||||
img_type: ImageType,
|
|
||||||
) -> RenderState<'a> {
|
|
||||||
let cache = match cache_type {
|
|
||||||
MediaCacheType::Image => &mut images.static_imgs,
|
|
||||||
MediaCacheType::Gif => &mut images.gifs,
|
|
||||||
};
|
|
||||||
|
|
||||||
let cur_state = cache.textures_cache.handle_and_get_or_insert(url, || {
|
|
||||||
crate::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
|
|
||||||
});
|
|
||||||
|
|
||||||
RenderState {
|
|
||||||
texture_state: cur_state,
|
|
||||||
gifs: &mut images.gif_states,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LoadableRenderState<'a> {
|
|
||||||
pub texture_state: LoadableTextureState<'a>,
|
|
||||||
pub gifs: &'a mut GifStateMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RenderState<'a> {
|
|
||||||
pub texture_state: TextureState<'a>,
|
|
||||||
pub gifs: &'a mut GifStateMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_no_pfp_promise(
|
|
||||||
ctx: &Context,
|
|
||||||
cache: &MediaCache,
|
|
||||||
) -> Promise<Option<Result<TexturedImage, notedeck::Error>>> {
|
|
||||||
crate::images::fetch_img(
|
|
||||||
&cache.cache_dir,
|
|
||||||
ctx,
|
|
||||||
notedeck::profile::no_pfp_url(),
|
|
||||||
ImageType::Profile(128),
|
|
||||||
MediaCacheType::Image,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod app_images;
|
pub mod app_images;
|
||||||
pub mod blur;
|
|
||||||
pub mod colors;
|
pub mod colors;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
pub mod gif;
|
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod jobs;
|
pub mod media;
|
||||||
pub mod mention;
|
pub mod mention;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod viewer;
|
||||||
|
|
||||||
|
pub use viewer::{MediaViewer, MediaViewerFlags, MediaViewerState};
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
/// Spiral layout for media galleries
|
||||||
|
|
||||||
|
use egui::{pos2, vec2, Color32, Rect, Sense, TextureId, Vec2};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct ImageItem {
|
||||||
|
pub texture: TextureId,
|
||||||
|
pub ar: f32, // width / height (must be > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Placed {
|
||||||
|
texture: TextureId,
|
||||||
|
rect: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LayoutParams {
|
||||||
|
pub gutter: f32,
|
||||||
|
pub h_min: f32,
|
||||||
|
pub h_max: f32,
|
||||||
|
pub w_min: f32,
|
||||||
|
pub w_max: f32,
|
||||||
|
pub seed_center: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_spiral(images: &[ImageItem], params: LayoutParams) -> (Vec<Placed>, Vec2) {
|
||||||
|
if images.is_empty() {
|
||||||
|
return (Vec::new(), vec2(0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let eps = f32::EPSILON;
|
||||||
|
let g = params.gutter.max(0.0);
|
||||||
|
let h_min = params.h_min.max(1.0);
|
||||||
|
let h_max = params.h_max.max(h_min);
|
||||||
|
let w_min = params.w_min.max(1.0);
|
||||||
|
let w_max = params.w_max.max(w_min);
|
||||||
|
|
||||||
|
let mut placed = Vec::with_capacity(images.len());
|
||||||
|
|
||||||
|
// Build around origin; normalize at the end.
|
||||||
|
let mut x_min = 0.0f32;
|
||||||
|
let mut x_max = 0.0f32;
|
||||||
|
let mut y_min = 0.0f32;
|
||||||
|
let mut y_max = 0.0f32;
|
||||||
|
|
||||||
|
// dir: 0 right-col, 1 top-row, 2 left-col, 3 bottom-row
|
||||||
|
let mut dir = 0usize;
|
||||||
|
let mut i = 0usize;
|
||||||
|
|
||||||
|
// Optional seed: center a single image
|
||||||
|
if params.seed_center && i < images.len() {
|
||||||
|
let ar = images[i].ar.max(eps);
|
||||||
|
let h = ((h_min + h_max) * 0.5).clamp(h_min, h_max);
|
||||||
|
let w = ar * h;
|
||||||
|
|
||||||
|
let rect = Rect::from_center_size(pos2(0.0, 0.0), vec2(w, h));
|
||||||
|
placed.push(Placed { texture: images[i].texture, rect });
|
||||||
|
|
||||||
|
x_min = rect.min.x;
|
||||||
|
x_max = rect.max.x;
|
||||||
|
y_min = rect.min.y;
|
||||||
|
y_max = rect.max.y;
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
dir = 1; // start by adding a row above
|
||||||
|
} else {
|
||||||
|
// ensure non-empty bbox for the first strip
|
||||||
|
x_min = 0.0; x_max = 1.0; y_min = 0.0; y_max = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers -------------------------------------------------------------
|
||||||
|
|
||||||
|
// Choose how many items fit and the strip size S (W for column, H for row).
|
||||||
|
fn choose_k<F: Fn(&ImageItem) -> f32>(
|
||||||
|
images: &[ImageItem],
|
||||||
|
L: f32,
|
||||||
|
g: f32,
|
||||||
|
s_min: f32,
|
||||||
|
s_max: f32,
|
||||||
|
weight: F,
|
||||||
|
) -> (usize, f32) {
|
||||||
|
// prefix sums of weights (sum over first k items)
|
||||||
|
let mut pref = Vec::with_capacity(images.len() + 1);
|
||||||
|
pref.push(0.0);
|
||||||
|
for im in images {
|
||||||
|
pref.push(pref.last().copied().unwrap_or(0.0) + weight(im));
|
||||||
|
}
|
||||||
|
|
||||||
|
let k_max = images.len().max(1);
|
||||||
|
let mut chosen_k = 1usize;
|
||||||
|
let mut chosen_s = f32::NAN;
|
||||||
|
|
||||||
|
for k in 1..=k_max {
|
||||||
|
let L_eff = (L - g * (k as f32 - 1.0)).max(1.0);
|
||||||
|
let sum_w = pref[k].max(f32::EPSILON);
|
||||||
|
let s = (L_eff / sum_w).max(1.0);
|
||||||
|
|
||||||
|
if s > s_max && k < k_max {
|
||||||
|
continue; // too big; add one more to thin the strip
|
||||||
|
}
|
||||||
|
if s < s_min {
|
||||||
|
// prefer one fewer if possible
|
||||||
|
if k > 1 {
|
||||||
|
let k2 = k - 1;
|
||||||
|
let L_eff2 = (L - g * (k2 as f32 - 1.0)).max(1.0);
|
||||||
|
let sum_w2 = pref[k2].max(f32::EPSILON);
|
||||||
|
chosen_k = k2;
|
||||||
|
chosen_s = (L_eff2 / sum_w2).max(1.0);
|
||||||
|
} else {
|
||||||
|
chosen_k = 1;
|
||||||
|
chosen_s = s_min;
|
||||||
|
}
|
||||||
|
return (chosen_k, chosen_s);
|
||||||
|
}
|
||||||
|
return (k, s); // within bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fell through: use k_max and clamp
|
||||||
|
let L_eff = (L - g * (k_max as f32 - 1.0)).max(1.0);
|
||||||
|
let sum_w = pref[k_max].max(f32::EPSILON);
|
||||||
|
let s = (L_eff / sum_w).clamp(s_min, s_max);
|
||||||
|
(k_max, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place a column (top→bottom). Returns the new right/left edge.
|
||||||
|
fn place_column(
|
||||||
|
placed: &mut Vec<Placed>,
|
||||||
|
strip: &[ImageItem],
|
||||||
|
W: f32,
|
||||||
|
x: f32,
|
||||||
|
y_top: f32,
|
||||||
|
g: f32,
|
||||||
|
) -> f32 {
|
||||||
|
let mut y = y_top;
|
||||||
|
for (idx, im) in strip.iter().enumerate() {
|
||||||
|
let h = (W / im.ar.max(f32::EPSILON)).max(1.0);
|
||||||
|
let rect = Rect::from_min_size(pos2(x, y), vec2(W, h));
|
||||||
|
placed.push(Placed { texture: im.texture, rect });
|
||||||
|
y += h;
|
||||||
|
if idx + 1 != strip.len() { y += g; }
|
||||||
|
}
|
||||||
|
x + W
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place a row (left→right). Returns the new top/bottom edge.
|
||||||
|
fn place_row(
|
||||||
|
placed: &mut Vec<Placed>,
|
||||||
|
strip: &[ImageItem],
|
||||||
|
H: f32,
|
||||||
|
x_left: f32,
|
||||||
|
y: f32,
|
||||||
|
g: f32,
|
||||||
|
) -> f32 {
|
||||||
|
let mut x = x_left;
|
||||||
|
for (idx, im) in strip.iter().enumerate() {
|
||||||
|
let w = (im.ar.max(f32::EPSILON) * H).max(1.0);
|
||||||
|
let rect = Rect::from_min_size(pos2(x, y), vec2(w, H));
|
||||||
|
placed.push(Placed { texture: im.texture, rect });
|
||||||
|
x += w;
|
||||||
|
if idx + 1 != strip.len() { x += g; }
|
||||||
|
}
|
||||||
|
y + H
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- main loop -----------------------------------------------------------
|
||||||
|
|
||||||
|
while i < images.len() {
|
||||||
|
let remaining = &images[i..];
|
||||||
|
|
||||||
|
if dir % 2 == 0 {
|
||||||
|
// COLUMN (dir 0: right, 2: left)
|
||||||
|
let L = (y_max - y_min).max(1.0);
|
||||||
|
let (k, W) = choose_k(
|
||||||
|
remaining,
|
||||||
|
L, g, w_min, w_max,
|
||||||
|
|im| 1.0 / im.ar.max(f32::EPSILON),
|
||||||
|
);
|
||||||
|
|
||||||
|
let x = if dir == 0 { x_max + g } else { x_min - g - W };
|
||||||
|
let new_edge = place_column(&mut placed, &remaining[..k], W, x, y_min, g);
|
||||||
|
if dir == 0 { x_max = new_edge; } else { x_min = x; }
|
||||||
|
i += k;
|
||||||
|
} else {
|
||||||
|
// ROW (dir 1: top, 3: bottom)
|
||||||
|
let L = (x_max - x_min).max(1.0);
|
||||||
|
let (k, H) = choose_k(
|
||||||
|
remaining,
|
||||||
|
L, g, h_min, h_max,
|
||||||
|
|im| im.ar.max(f32::EPSILON),
|
||||||
|
);
|
||||||
|
|
||||||
|
let y = if dir == 1 { y_max + g } else { y_min - g - H };
|
||||||
|
let new_edge = place_row(&mut placed, &remaining[..k], H, x_min, y, g);
|
||||||
|
if dir == 1 { y_max = new_edge; } else { y_min = y; }
|
||||||
|
i += k;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = (dir + 1) % 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize so bbox top-left is (0,0)
|
||||||
|
let shift = vec2(-x_min, -y_min);
|
||||||
|
for p in &mut placed {
|
||||||
|
p.rect = p.rect.translate(shift);
|
||||||
|
}
|
||||||
|
let total_size = vec2(x_max - x_min, y_max - y_min);
|
||||||
|
(placed, total_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spiral_gallery(ui: &mut egui::Ui, images: &[ImageItem], params: LayoutParams) {
|
||||||
|
use egui::{ScrollArea, Stroke};
|
||||||
|
|
||||||
|
let (placed, size) = layout_spiral(images, params);
|
||||||
|
|
||||||
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
|
let (rect, _resp) = ui.allocate_exact_size(size, Sense::hover());
|
||||||
|
let painter = ui.painter_at(rect);
|
||||||
|
painter.rect_stroke(
|
||||||
|
Rect::from_min_size(rect.min, size),
|
||||||
|
0.0,
|
||||||
|
Stroke::new(1.0, Color32::DARK_GRAY),
|
||||||
|
);
|
||||||
|
|
||||||
|
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||||
|
for p in &placed {
|
||||||
|
let r = Rect::from_min_max(rect.min + p.rect.min.to_vec2(),
|
||||||
|
rect.min + p.rect.max.to_vec2());
|
||||||
|
painter.image(p.texture, r, uv, Color32::WHITE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
use bitflags::bitflags;
|
||||||
|
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
|
||||||
|
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
||||||
|
use notedeck::{ImageType, Images};
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct MediaViewerFlags: u64 {
|
||||||
|
/// Open the media viewer fullscreen
|
||||||
|
const Fullscreen = 1 << 0;
|
||||||
|
|
||||||
|
/// Enable a transition animation
|
||||||
|
const Transition = 1 << 1;
|
||||||
|
|
||||||
|
/// Are we open or closed?
|
||||||
|
const Open = 1 << 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State used in the MediaViewer ui widget.
|
||||||
|
pub struct MediaViewerState {
|
||||||
|
/// When
|
||||||
|
pub media_info: ViewMediaInfo,
|
||||||
|
pub scene_rect: Option<Rect>,
|
||||||
|
pub flags: MediaViewerFlags,
|
||||||
|
pub anim_id: egui::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MediaViewerState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
anim_id: egui::Id::new("notedeck-fullscreen-media-viewer"),
|
||||||
|
media_info: Default::default(),
|
||||||
|
scene_rect: None,
|
||||||
|
flags: MediaViewerFlags::Transition | MediaViewerFlags::Fullscreen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaViewerState {
|
||||||
|
pub fn new(anim_id: egui::Id) -> Self {
|
||||||
|
Self {
|
||||||
|
anim_id,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much is our media viewer open
|
||||||
|
pub fn open_amount(&self, ui: &mut egui::Ui) -> f32 {
|
||||||
|
ui.ctx().animate_bool_with_time_and_easing(
|
||||||
|
self.anim_id,
|
||||||
|
self.flags.contains(MediaViewerFlags::Open),
|
||||||
|
0.3,
|
||||||
|
egui::emath::easing::cubic_out,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should we show the control even if we're closed?
|
||||||
|
/// Needed for transition animation
|
||||||
|
pub fn should_show(&self, ui: &mut egui::Ui) -> bool {
|
||||||
|
if self.flags.contains(MediaViewerFlags::Open) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are closing
|
||||||
|
self.open_amount(ui) > 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A panning, scrolling, optionally fullscreen, and tiling media viewer
|
||||||
|
pub struct MediaViewer<'a> {
|
||||||
|
state: &'a mut MediaViewerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MediaViewer<'a> {
|
||||||
|
pub fn new(state: &'a mut MediaViewerState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this
|
||||||
|
pub fn fullscreen(self, enable: bool) -> Self {
|
||||||
|
self.state.flags.set(MediaViewerFlags::Fullscreen, enable);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable open transition animation
|
||||||
|
pub fn transition(self, enable: bool) -> Self {
|
||||||
|
self.state.flags.set(MediaViewerFlags::Transition, enable);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
|
||||||
|
if self.state.flags.contains(MediaViewerFlags::Fullscreen) {
|
||||||
|
egui::Window::new("Media Viewer")
|
||||||
|
.title_bar(false)
|
||||||
|
.fixed_size(ui.ctx().screen_rect().size())
|
||||||
|
.fixed_pos(ui.ctx().screen_rect().min)
|
||||||
|
.frame(egui::Frame::NONE)
|
||||||
|
.show(ui.ctx(), |ui| self.ui_content(images, ui))
|
||||||
|
.unwrap() // SAFETY: we are always open
|
||||||
|
.inner
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
self.ui_content(images, ui)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_content(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
|
||||||
|
let avail_rect = ui.available_rect_before_wrap();
|
||||||
|
|
||||||
|
let scene_rect = if let Some(scene_rect) = self.state.scene_rect {
|
||||||
|
scene_rect
|
||||||
|
} else {
|
||||||
|
self.state.scene_rect = Some(avail_rect);
|
||||||
|
avail_rect
|
||||||
|
};
|
||||||
|
|
||||||
|
let zoom_range: egui::Rangef = (0.0..=10.0).into();
|
||||||
|
|
||||||
|
let is_open = self.state.flags.contains(MediaViewerFlags::Open);
|
||||||
|
let can_transition = self.state.flags.contains(MediaViewerFlags::Transition);
|
||||||
|
let open_amount = self.state.open_amount(ui);
|
||||||
|
let transitioning = if !can_transition {
|
||||||
|
false
|
||||||
|
} else if is_open {
|
||||||
|
open_amount < 1.0
|
||||||
|
} else {
|
||||||
|
open_amount > 0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut trans_rect = if transitioning {
|
||||||
|
let clicked_img = &self.state.media_info.clicked_media();
|
||||||
|
let src_pos = &clicked_img.original_position;
|
||||||
|
let in_scene_pos = Self::first_image_rect(ui, clicked_img, images);
|
||||||
|
transition_scene_rect(
|
||||||
|
&avail_rect,
|
||||||
|
&zoom_range,
|
||||||
|
&in_scene_pos,
|
||||||
|
src_pos,
|
||||||
|
open_amount,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
scene_rect
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
avail_rect,
|
||||||
|
0.0,
|
||||||
|
egui::Color32::from_black_alpha((200.0 * open_amount) as u8),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scene = egui::Scene::new().zoom_range(zoom_range);
|
||||||
|
|
||||||
|
// We are opening, so lock controls
|
||||||
|
/* TODO(jb55): 0.32
|
||||||
|
if transitioning {
|
||||||
|
scene = scene.sense(egui::Sense::hover());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let resp = scene.show(ui, &mut trans_rect, |ui| {
|
||||||
|
Self::render_image_tiles(&self.state.media_info.medias, images, ui, open_amount);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state.scene_rect = Some(trans_rect);
|
||||||
|
|
||||||
|
resp.response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rect of the first image to be placed.
|
||||||
|
/// This is mainly used for the transition animation
|
||||||
|
///
|
||||||
|
/// TODO(jb55): replace this with a "placed" variant once
|
||||||
|
/// we have image layouts
|
||||||
|
fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
|
||||||
|
// fetch image texture
|
||||||
|
let Some(texture) = images.latest_texture(ui, &media.url, ImageType::Content(None)) else {
|
||||||
|
tracing::error!("could not get latest texture in first_image_rect");
|
||||||
|
return Rect::ZERO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// the area the next image will be put in.
|
||||||
|
let mut img_rect = ui.available_rect_before_wrap();
|
||||||
|
|
||||||
|
let size = texture.size_vec2();
|
||||||
|
img_rect.set_height(size.y);
|
||||||
|
img_rect.set_width(size.x);
|
||||||
|
img_rect
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Tile a scene with images.
|
||||||
|
///
|
||||||
|
/// TODO(jb55): Let's improve image tiling over time, spiraling outward. We
|
||||||
|
/// should have a way to click "next" and have the scene smoothly transition and
|
||||||
|
/// focus on the next image
|
||||||
|
fn render_image_tiles(
|
||||||
|
infos: &[MediaInfo],
|
||||||
|
images: &mut Images,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
open_amount: f32,
|
||||||
|
) {
|
||||||
|
for info in infos {
|
||||||
|
let url = &info.url;
|
||||||
|
|
||||||
|
// fetch image texture
|
||||||
|
let Some(texture) = images.latest_texture(ui, url, ImageType::Content(None)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// the area the next image will be put in.
|
||||||
|
let mut img_rect = ui.available_rect_before_wrap();
|
||||||
|
/*
|
||||||
|
if !ui.is_rect_visible(img_rect) {
|
||||||
|
// just stop rendering images if we're going out of the scene
|
||||||
|
// basic culling when we have lots of images
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
{
|
||||||
|
let size = texture.size_vec2();
|
||||||
|
img_rect.set_height(size.y);
|
||||||
|
img_rect.set_width(size.x);
|
||||||
|
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||||
|
|
||||||
|
// image actions
|
||||||
|
//let response = ui.interact(render_rect, carousel_id.with("img"), Sense::click());
|
||||||
|
|
||||||
|
/*
|
||||||
|
if response.clicked() {
|
||||||
|
} else if background_response.clicked() {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Paint image
|
||||||
|
ui.painter().image(
|
||||||
|
texture.id(),
|
||||||
|
img_rect,
|
||||||
|
uv,
|
||||||
|
Color32::from_white_alpha((open_amount * 255.0) as u8),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.advance_cursor_after_rect(img_rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: lerp a TSTransform (uniform scale + translation)
|
||||||
|
fn lerp_ts(a: TSTransform, b: TSTransform, t: f32) -> TSTransform {
|
||||||
|
let s = egui::lerp(a.scaling..=b.scaling, t);
|
||||||
|
let p = a.translation + (b.translation - a.translation) * t;
|
||||||
|
TSTransform {
|
||||||
|
scaling: s,
|
||||||
|
translation: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the open/close amount and transition rect
|
||||||
|
pub fn transition_scene_rect(
|
||||||
|
outer_rect: &Rect,
|
||||||
|
zoom_range: &Rangef,
|
||||||
|
image_rect_in_scene: &Rect, // e.g. Rect::from_min_size(Pos2::ZERO, image_size)
|
||||||
|
timeline_global_rect: &Rect, // saved from timeline Response.rect
|
||||||
|
open_amt: f32, // stable ID per media item
|
||||||
|
) -> Rect {
|
||||||
|
// Compute the two endpoints:
|
||||||
|
let from = fit_to_rect_in_scene(timeline_global_rect, image_rect_in_scene, zoom_range);
|
||||||
|
let to = fit_to_rect_in_scene(outer_rect, image_rect_in_scene, zoom_range);
|
||||||
|
|
||||||
|
// Interpolate transform and convert to scene_rect expected by Scene::show:
|
||||||
|
let lerped = lerp_ts(from, to, open_amt);
|
||||||
|
|
||||||
|
lerped.inverse() * (*outer_rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a transformation that fits a given scene rectangle into the available screen size.
|
||||||
|
///
|
||||||
|
/// The resulting visual scene bounds can be larger, due to letterboxing.
|
||||||
|
///
|
||||||
|
/// Returns the transformation from `scene` to `global` coordinates.
|
||||||
|
fn fit_to_rect_in_scene(
|
||||||
|
rect_in_global: &Rect,
|
||||||
|
rect_in_scene: &Rect,
|
||||||
|
zoom_range: &Rangef,
|
||||||
|
) -> TSTransform {
|
||||||
|
// Compute the scale factor to fit the bounding rectangle into the available screen size:
|
||||||
|
let scale = rect_in_global.size() / rect_in_scene.size();
|
||||||
|
|
||||||
|
// Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
|
||||||
|
let scale = scale.min_elem();
|
||||||
|
|
||||||
|
// Clamp scale to what is allowed
|
||||||
|
let scale = zoom_range.clamp(scale);
|
||||||
|
|
||||||
|
// Compute the translation to center the bounding rect in the screen:
|
||||||
|
let center_in_global = rect_in_global.center().to_vec2();
|
||||||
|
let center_scene = rect_in_scene.center().to_vec2();
|
||||||
|
|
||||||
|
// Set the transformation to scale and then translate to center.
|
||||||
|
TSTransform::from_translation(center_in_global - scale * center_scene)
|
||||||
|
* TSTransform::from_scaling(scale)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use crate::ProfilePreview;
|
|||||||
use egui::Sense;
|
use egui::Sense;
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::{name::get_display_name, Images, NoteAction};
|
use notedeck::{name::get_display_name, Images, NoteAction, NotedeckTextStyle};
|
||||||
|
|
||||||
pub struct Mention<'a> {
|
pub struct Mention<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
@@ -75,7 +75,9 @@ fn mention_ui(
|
|||||||
get_display_name(profile.as_ref()).username_or_displayname()
|
get_display_name(profile.as_ref()).username_or_displayname()
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut text = egui::RichText::new(name).color(link_color);
|
let mut text = egui::RichText::new(name)
|
||||||
|
.color(link_color)
|
||||||
|
.text_style(NotedeckTextStyle::NoteBody.text_style());
|
||||||
if let Some(size) = size {
|
if let Some(size) = size {
|
||||||
text = text.size(size);
|
text = text.size(size);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
use std::cell::OnceCell;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
blur::imeta_blurhashes,
|
|
||||||
jobs::JobsCache,
|
|
||||||
note::{NoteAction, NoteOptions, NoteResponse, NoteView},
|
note::{NoteAction, NoteOptions, NoteResponse, NoteView},
|
||||||
secondary_label,
|
secondary_label,
|
||||||
};
|
};
|
||||||
|
use notedeck::{JobsCache, RenderableMedia};
|
||||||
|
|
||||||
use egui::{Color32, Hyperlink, RichText};
|
use egui::{Color32, Hyperlink, Label, RichText};
|
||||||
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
|
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use notedeck::{IsFollowing, NoteCache, NoteContext};
|
use super::media::image_carousel;
|
||||||
|
use notedeck::{update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle};
|
||||||
use super::media::{find_renderable_media, image_carousel, RenderableMedia};
|
|
||||||
|
|
||||||
pub struct NoteContents<'a, 'd> {
|
pub struct NoteContents<'a, 'd> {
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
@@ -127,11 +123,11 @@ pub fn render_note_preview(
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
pub fn render_note_contents(
|
pub fn render_note_contents<'a>(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
note_context: &mut NoteContext,
|
note_context: &mut NoteContext,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
note: &Note,
|
note: &'a Note,
|
||||||
options: NoteOptions,
|
options: NoteOptions,
|
||||||
jobs: &mut JobsCache,
|
jobs: &mut JobsCache,
|
||||||
) -> NoteResponse {
|
) -> NoteResponse {
|
||||||
@@ -152,7 +148,6 @@ pub fn render_note_contents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut supported_medias: Vec<RenderableMedia> = vec![];
|
let mut supported_medias: Vec<RenderableMedia> = vec![];
|
||||||
let blurhashes = OnceCell::new();
|
|
||||||
|
|
||||||
let response = ui.horizontal_wrapped(|ui| {
|
let response = ui.horizontal_wrapped(|ui| {
|
||||||
let blocks = if let Ok(blocks) = note_context.ndb.get_blocks_by_key(txn, note_key) {
|
let blocks = if let Ok(blocks) = note_context.ndb.get_blocks_by_key(txn, note_key) {
|
||||||
@@ -163,9 +158,7 @@ pub fn render_note_contents(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
'block_loop: for block in blocks.iter(note) {
|
||||||
|
|
||||||
for block in blocks.iter(note) {
|
|
||||||
match block.blocktype() {
|
match block.blocktype() {
|
||||||
BlockType::MentionBech32 => match block.as_mention().unwrap() {
|
BlockType::MentionBech32 => match block.as_mention().unwrap() {
|
||||||
Mention::Profile(profile) => {
|
Mention::Profile(profile) => {
|
||||||
@@ -205,13 +198,24 @@ pub fn render_note_contents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
ui.colored_label(link_color, format!("@{}", &block.as_str()[..16]));
|
ui.colored_label(
|
||||||
|
link_color,
|
||||||
|
RichText::new(format!("@{}", &block.as_str()[..16]))
|
||||||
|
.text_style(NotedeckTextStyle::NoteBody.text_style()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
BlockType::Hashtag => {
|
BlockType::Hashtag => {
|
||||||
|
if block.as_str().trim().is_empty() {
|
||||||
|
continue 'block_loop;
|
||||||
|
}
|
||||||
let resp = ui
|
let resp = ui
|
||||||
.colored_label(link_color, format!("#{}", block.as_str()))
|
.colored_label(
|
||||||
|
link_color,
|
||||||
|
RichText::new(format!("#{}", block.as_str()))
|
||||||
|
.text_style(NotedeckTextStyle::NoteBody.text_style()),
|
||||||
|
)
|
||||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
if resp.clicked() {
|
if resp.clicked() {
|
||||||
@@ -223,21 +227,26 @@ pub fn render_note_contents(
|
|||||||
let mut found_supported = || -> bool {
|
let mut found_supported = || -> bool {
|
||||||
let url = block.as_str();
|
let url = block.as_str();
|
||||||
|
|
||||||
let blurs = blurhashes.get_or_init(|| imeta_blurhashes(note));
|
if !note_context.img_cache.metadata.contains_key(url) {
|
||||||
|
update_imeta_blurhashes(note, &mut note_context.img_cache.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
let Some(media_type) =
|
let Some(media) = note_context.img_cache.get_renderable_media(url) else {
|
||||||
find_renderable_media(&mut note_context.img_cache.urls, blurs, url)
|
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
supported_medias.push(media_type);
|
supported_medias.push(media);
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
if hide_media || !found_supported() {
|
if hide_media || !found_supported() {
|
||||||
|
if block.as_str().trim().is_empty() {
|
||||||
|
continue 'block_loop;
|
||||||
|
}
|
||||||
ui.add(Hyperlink::from_label_and_url(
|
ui.add(Hyperlink::from_label_and_url(
|
||||||
RichText::new(block.as_str()).color(link_color),
|
RichText::new(block.as_str())
|
||||||
|
.color(link_color)
|
||||||
|
.text_style(NotedeckTextStyle::NoteBody.text_style()),
|
||||||
block.as_str(),
|
block.as_str(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -263,17 +272,28 @@ pub fn render_note_contents(
|
|||||||
current_len += block_str.len();
|
current_len += block_str.len();
|
||||||
block_str
|
block_str
|
||||||
};
|
};
|
||||||
|
if block_str.trim().is_empty() {
|
||||||
|
continue 'block_loop;
|
||||||
|
}
|
||||||
if options.contains(NoteOptions::ScrambleText) {
|
if options.contains(NoteOptions::ScrambleText) {
|
||||||
ui.add(
|
ui.add(
|
||||||
egui::Label::new(rot13(block_str))
|
Label::new(
|
||||||
|
RichText::new(rot13(block_str))
|
||||||
|
.text_style(NotedeckTextStyle::NoteBody.text_style()),
|
||||||
|
)
|
||||||
.wrap()
|
.wrap()
|
||||||
.selectable(selectable),
|
.selectable(selectable),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.add(egui::Label::new(block_str).wrap().selectable(selectable));
|
ui.add(
|
||||||
|
Label::new(
|
||||||
|
RichText::new(block_str)
|
||||||
|
.text_style(NotedeckTextStyle::NoteBody.text_style()),
|
||||||
|
)
|
||||||
|
.wrap()
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't render any more blocks
|
// don't render any more blocks
|
||||||
if truncate {
|
if truncate {
|
||||||
break;
|
break;
|
||||||
@@ -311,6 +331,7 @@ pub fn render_note_contents(
|
|||||||
.key
|
.key
|
||||||
.pubkey
|
.pubkey
|
||||||
.bytes();
|
.bytes();
|
||||||
|
|
||||||
let trusted_media = is_self
|
let trusted_media = is_self
|
||||||
|| note_context
|
|| note_context
|
||||||
.accounts
|
.accounts
|
||||||
@@ -327,6 +348,7 @@ pub fn render_note_contents(
|
|||||||
carousel_id,
|
carousel_id,
|
||||||
trusted_media,
|
trusted_media,
|
||||||
note_context.i18n,
|
note_context.i18n,
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ pub mod media;
|
|||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod reply_description;
|
pub mod reply_description;
|
||||||
|
|
||||||
use crate::jobs::JobsCache;
|
|
||||||
use crate::{app_images, secondary_label};
|
use crate::{app_images, secondary_label};
|
||||||
use crate::{
|
use crate::{
|
||||||
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
|
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
|
||||||
@@ -14,13 +13,14 @@ use crate::{
|
|||||||
pub use contents::{render_note_contents, render_note_preview, NoteContents};
|
pub use contents::{render_note_contents, render_note_preview, NoteContents};
|
||||||
pub use context::NoteContextButton;
|
pub use context::NoteContextButton;
|
||||||
use notedeck::get_current_wallet;
|
use notedeck::get_current_wallet;
|
||||||
use notedeck::note::MediaAction;
|
|
||||||
use notedeck::note::ZapTargetAmount;
|
use notedeck::note::ZapTargetAmount;
|
||||||
use notedeck::ui::is_narrow;
|
use notedeck::ui::is_narrow;
|
||||||
use notedeck::Accounts;
|
use notedeck::Accounts;
|
||||||
use notedeck::GlobalWallet;
|
use notedeck::GlobalWallet;
|
||||||
use notedeck::Images;
|
use notedeck::Images;
|
||||||
|
use notedeck::JobsCache;
|
||||||
use notedeck::Localization;
|
use notedeck::Localization;
|
||||||
|
use notedeck::MediaAction;
|
||||||
pub use options::NoteOptions;
|
pub use options::NoteOptions;
|
||||||
pub use reply_description::reply_desc;
|
pub use reply_description::reply_desc;
|
||||||
|
|
||||||
@@ -344,7 +344,12 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
1.0,
|
1.0,
|
||||||
ui.visuals().noninteractive().bg_stroke.color,
|
ui.visuals().noninteractive().bg_stroke.color,
|
||||||
))
|
))
|
||||||
.show(ui, |ui| self.show_impl(ui))
|
.show(ui, |ui| {
|
||||||
|
if is_narrow(ui.ctx()) {
|
||||||
|
ui.set_width(ui.available_width());
|
||||||
|
}
|
||||||
|
self.show_impl(ui)
|
||||||
|
})
|
||||||
.inner
|
.inner
|
||||||
} else {
|
} else {
|
||||||
self.show_impl(ui)
|
self.show_impl(ui)
|
||||||
@@ -454,7 +459,14 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
note_action = contents.action.or(note_action);
|
note_action = contents.action.or(note_action);
|
||||||
|
|
||||||
if self.options().contains(NoteOptions::ActionBar) {
|
if self.options().contains(NoteOptions::ActionBar) {
|
||||||
note_action = render_note_actionbar(
|
note_action = ui
|
||||||
|
.horizontal_wrapped(|ui| {
|
||||||
|
// NOTE(jb55): without this we get a weird artifact where
|
||||||
|
// there subsequent lines start sinking leftward off the screen.
|
||||||
|
// question: WTF? question 2: WHY?
|
||||||
|
ui.allocate_space(egui::vec2(0.0, 0.0));
|
||||||
|
|
||||||
|
render_note_actionbar(
|
||||||
ui,
|
ui,
|
||||||
get_zapper(
|
get_zapper(
|
||||||
self.note_context.accounts,
|
self.note_context.accounts,
|
||||||
@@ -466,6 +478,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
note_key,
|
note_key,
|
||||||
self.note_context.i18n,
|
self.note_context.i18n,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.inner
|
.inner
|
||||||
.or(note_action);
|
.or(note_action);
|
||||||
}
|
}
|
||||||
@@ -531,7 +544,9 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
note_action = contents.action.or(note_action);
|
note_action = contents.action.or(note_action);
|
||||||
|
|
||||||
if self.options().contains(NoteOptions::ActionBar) {
|
if self.options().contains(NoteOptions::ActionBar) {
|
||||||
note_action = render_note_actionbar(
|
note_action = ui
|
||||||
|
.horizontal_wrapped(|ui| {
|
||||||
|
render_note_actionbar(
|
||||||
ui,
|
ui,
|
||||||
get_zapper(
|
get_zapper(
|
||||||
self.note_context.accounts,
|
self.note_context.accounts,
|
||||||
@@ -543,6 +558,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
note_key,
|
note_key,
|
||||||
self.note_context.i18n,
|
self.note_context.i18n,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.inner
|
.inner
|
||||||
.or(note_action);
|
.or(note_action);
|
||||||
}
|
}
|
||||||
@@ -781,8 +797,7 @@ fn render_note_actionbar(
|
|||||||
note_pubkey: &[u8; 32],
|
note_pubkey: &[u8; 32],
|
||||||
note_key: NoteKey,
|
note_key: NoteKey,
|
||||||
i18n: &mut Localization,
|
i18n: &mut Localization,
|
||||||
) -> egui::InnerResponse<Option<NoteAction>> {
|
) -> Option<NoteAction> {
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.set_min_height(26.0);
|
ui.set_min_height(26.0);
|
||||||
ui.spacing_mut().item_spacing.x = 24.0;
|
ui.spacing_mut().item_spacing.x = 24.0;
|
||||||
|
|
||||||
@@ -825,8 +840,7 @@ fn render_note_actionbar(
|
|||||||
match zap_state {
|
match zap_state {
|
||||||
Ok(any_zap_state) => ui.add(zap_button(i18n, any_zap_state, note_id)),
|
Ok(any_zap_state) => ui.add(zap_button(i18n, any_zap_state, note_id)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let (rect, _) =
|
let (rect, _) = ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
|
||||||
ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
|
|
||||||
ui.add(x_button(rect)).on_hover_text(err.to_string())
|
ui.add(x_button(rect)).on_hover_text(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -845,7 +859,6 @@ fn render_note_actionbar(
|
|||||||
target,
|
target,
|
||||||
specified_msats: None,
|
specified_msats: None,
|
||||||
})))
|
})))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ bitflags! {
|
|||||||
/// Show note's client in the note header
|
/// Show note's client in the note header
|
||||||
const ShowNoteClientTop = 1 << 12;
|
const ShowNoteClientTop = 1 << 12;
|
||||||
const ShowNoteClientBottom = 1 << 13;
|
const ShowNoteClientBottom = 1 << 13;
|
||||||
|
|
||||||
|
const RepliesNewestFirst = 1 << 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use egui::{Label, RichText, Sense};
|
|||||||
use nostrdb::{NoteReply, Transaction};
|
use nostrdb::{NoteReply, Transaction};
|
||||||
|
|
||||||
use super::NoteOptions;
|
use super::NoteOptions;
|
||||||
use crate::{jobs::JobsCache, note::NoteView, Mention};
|
use crate::{note::NoteView, Mention};
|
||||||
use notedeck::{tr, NoteAction, NoteContext};
|
use notedeck::{tr, JobsCache, NoteAction, NoteContext};
|
||||||
|
|
||||||
// Rich text segment types for internationalized rendering
|
// Rich text segment types for internationalized rendering
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ pub fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui:
|
|||||||
banner_url
|
banner_url
|
||||||
.and_then(|url| banner_texture(ui, url))
|
.and_then(|url| banner_texture(ui, url))
|
||||||
.map(|texture| {
|
.map(|texture| {
|
||||||
crate::images::aspect_fill(
|
notedeck::media::images::aspect_fill(
|
||||||
ui,
|
ui,
|
||||||
egui::Sense::hover(),
|
egui::Sense::hover(),
|
||||||
texture.id,
|
texture.id,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
|
||||||
use crate::images::{fetch_no_pfp_promise, get_render_state, ImageType};
|
|
||||||
use egui::{vec2, InnerResponse, Sense, Stroke, TextureHandle};
|
use egui::{vec2, InnerResponse, Sense, Stroke, TextureHandle};
|
||||||
|
|
||||||
use notedeck::note::MediaAction;
|
use notedeck::get_render_state;
|
||||||
|
use notedeck::media::gif::ensure_latest_texture;
|
||||||
|
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
||||||
|
use notedeck::MediaAction;
|
||||||
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
|
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
|
||||||
|
|
||||||
pub struct ProfilePic<'cache, 'url> {
|
pub struct ProfilePic<'cache, 'url> {
|
||||||
@@ -140,12 +141,9 @@ fn render_pfp(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
notedeck::TextureState::Loaded(textured_image) => {
|
notedeck::TextureState::Loaded(textured_image) => {
|
||||||
let texture_handle = handle_repaint(
|
let texture_handle = ensure_latest_texture(ui, url, cur_state.gifs, textured_image);
|
||||||
ui,
|
|
||||||
retrieve_latest_texture(url, cur_state.gifs, textured_image),
|
|
||||||
);
|
|
||||||
|
|
||||||
egui::InnerResponse::new(None, pfp_image(ui, texture_handle, ui_size, border, sense))
|
egui::InnerResponse::new(None, pfp_image(ui, &texture_handle, ui_size, border, sense))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user