Compare commits
189 Commits
2025-07-23
...
2025-08-19
| Author | SHA1 | Date | |
|---|---|---|---|
|
b84ad4f1cd
|
|||
|
736ce50f64
|
|||
|
|
e9ca793509 | ||
|
|
ea65af8d5b | ||
|
|
11aa2142cf | ||
|
|
6ee2b28e70 | ||
|
|
31ee64827a | ||
|
|
f243adc855 | ||
|
|
5224a5d8ae | ||
|
|
2c96dd99a8 | ||
|
|
e7843bad2f | ||
|
|
c2f012ff75 | ||
|
|
76fd7a9753 | ||
|
|
8b5464641d | ||
|
|
c06d18f76b | ||
|
|
84e60e0642 | ||
|
|
23f35c60bb | ||
|
|
30c2ebdcc2 | ||
|
|
1658600604 | ||
|
|
529377a706 | ||
|
|
30af03cfcc | ||
|
|
bb878d3772 | ||
|
|
5c9eb492b6 | ||
|
|
0b584a773f | ||
|
|
78504a6673 | ||
|
|
ae204cbd5c | ||
|
|
7d4e9799e5 | ||
|
|
55d7cd3379 | ||
|
|
697040d862 | ||
|
|
49866418a6 | ||
|
|
9b784dfdf7 | ||
|
|
c1d6c0f535 | ||
|
|
1a93663b1a | ||
|
|
4992e25b3a | ||
|
|
7b1ace328f | ||
|
|
2973a0c6c5 | ||
|
|
4f63629715 | ||
|
|
686dea9831 | ||
|
|
01171ff9d7 | ||
|
|
b421e7e45f | ||
|
|
ccc188c0ae | ||
|
|
86641c6121 | ||
|
|
77ac91e810 | ||
|
|
3aa4d00053 | ||
|
|
9ef72ec7de | ||
|
|
1566cd5cf4 | ||
|
|
bdcd31cda0 | ||
|
|
a782d01ec2 | ||
|
|
8d4c0cfdbe | ||
|
|
f8f720c193 | ||
|
|
2a439b1f30 | ||
|
|
8399c951fa | ||
|
|
ac1bbeac1b | ||
|
|
dc91b6ffae | ||
|
|
28bd13d110 | ||
|
|
0b12b08c59 | ||
|
|
c79d5f1b9e | ||
|
|
507cf113a3 | ||
|
|
b750c0a927 | ||
|
|
49ef85aef6 | ||
|
|
29f59459d2 | ||
|
|
cd0bd53b3d | ||
|
|
5c0546deab | ||
|
|
1469f9a074 | ||
|
|
3d8018bb9a | ||
|
|
361d0e3708 | ||
|
|
c5df47dc73 | ||
|
|
ea85799007 | ||
|
|
9ba071c5ed | ||
|
|
81393f8468 | ||
|
|
87d9308435 | ||
|
|
1f8fd395ed | ||
|
|
2f3a3de7cc | ||
|
|
35e9354217 | ||
|
|
08a97c946d | ||
|
|
2fde5addeb | ||
|
04f5725a9d
|
|||
|
59199d8197
|
|||
|
|
f77e7898b6 | ||
|
e6a27a53fe
|
|||
|
|
8138a0a1ca | ||
|
|
2444e24fb5 | ||
|
|
fc509b1b26 | ||
|
|
1fd92e9e00 | ||
|
|
382ef772f5 | ||
|
|
53b4a8da5c | ||
|
|
cb72592f4b | ||
|
|
c60e1af3eb | ||
|
|
87cb5ed515 | ||
|
|
9cbba37507 | ||
|
|
b94e715539 | ||
|
|
d12f66e5cd | ||
|
|
e8be471608 | ||
|
|
97d15e41e7 | ||
|
|
ea5c876da6 | ||
|
|
75eefcbf72 | ||
|
|
54b86ee5a6 | ||
|
|
f6c44bba8a | ||
|
|
3451206f1a | ||
|
|
0770bab37c | ||
|
|
48a11b9bab | ||
|
|
603de6bbab | ||
|
|
571bf35109 | ||
|
|
0dda26791a | ||
|
|
7e73ed2760 | ||
|
|
2fb9470ee6 | ||
|
|
af2c556700 | ||
|
|
27df33dc83 | ||
|
|
2edc19fbcc | ||
|
|
edf0e2498b | ||
|
|
ad35547582 | ||
|
|
24f70930eb | ||
|
|
5b1bc442d4 | ||
|
|
391abe817d | ||
|
|
30eb2e0258 | ||
|
|
21fe3527a8 | ||
|
|
249e166a95 | ||
|
|
3f9d030046 | ||
|
fa13884908
|
|||
|
f8ae0825c4
|
|||
|
|
26ece3bc05 | ||
|
|
a64ff3b630 | ||
|
|
ab84304265 | ||
|
|
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 |
3
.envrc
3
.envrc
@@ -18,4 +18,7 @@ export OLLAMA_HOST=http://ollama.jb55.com
|
||||
|
||||
# simple todo reminders
|
||||
export TODO_FILE=TODO
|
||||
|
||||
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
|
||||
|
||||
2>/dev/null todo.sh ls || :
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ scripts/macos_build_secrets.sh
|
||||
/tags
|
||||
.zed
|
||||
.lsp
|
||||
.idea
|
||||
local.properties
|
||||
398
Cargo.lock
generated
398
Cargo.lock
generated
@@ -105,7 +105,8 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5#a8948332c7c551303d32eb26a59d0abd676e47a5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
|
||||
dependencies = [
|
||||
"android-properties",
|
||||
"bitflags 2.9.1",
|
||||
@@ -125,7 +126,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9#c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9"
|
||||
source = "git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805#4ee16f1585e4a75031dc10785163d4b920f95805"
|
||||
dependencies = [
|
||||
"android-properties",
|
||||
"bitflags 2.9.1",
|
||||
@@ -192,7 +193,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.1",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
@@ -765,6 +766,25 @@ dependencies = [
|
||||
"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]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -989,6 +1009,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1244,6 +1265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1381,7 +1403,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
|
||||
source = "git+https://github.com/damus-io/winit?rev=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
|
||||
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
@@ -1389,20 +1411,26 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"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=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eframe"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1438,24 +1466,25 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
"backtrace",
|
||||
"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=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||
"epaint",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
"serde",
|
||||
"similar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui-wgpu"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1474,7 +1503,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui-winit"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"arboard",
|
||||
@@ -1492,7 +1521,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_extras"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"egui",
|
||||
@@ -1509,7 +1538,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1526,7 +1555,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_nav"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/damus-io/egui-nav?rev=3c67eb6298edbff36d46546897cfac33df4f04db#3c67eb6298edbff36d46546897cfac33df4f04db"
|
||||
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
@@ -1588,7 +1617,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde",
|
||||
@@ -1606,7 +1635,7 @@ version = "0.3.0"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"ewebsock",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"hex",
|
||||
"mio",
|
||||
"nostr 0.37.0",
|
||||
@@ -1686,13 +1715,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
"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=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||
"epaint_default_fonts",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
@@ -1704,7 +1733,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "epaint_default_fonts"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||
|
||||
[[package]]
|
||||
name = "equator"
|
||||
@@ -2280,7 +2309,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"gpu-descriptor-types",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2302,6 +2331,18 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
@@ -2330,6 +2371,9 @@ name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
@@ -2346,6 +2390,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
@@ -2507,6 +2562,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
@@ -2665,6 +2730,17 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
@@ -2672,7 +2748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2744,25 +2820,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -2879,6 +2936,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -2969,6 +3039,7 @@ dependencies = [
|
||||
"bech32",
|
||||
"bitcoin",
|
||||
"lightning-types",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3004,6 +3075,22 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||
|
||||
[[package]]
|
||||
name = "lnsocket"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "724c7fba2188a49ab31316e52dd410d4d3168b8e6482aa2ac3889dd840d28712"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"hashbrown 0.13.2",
|
||||
"hex",
|
||||
"lightning-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
@@ -3201,7 +3288,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
@@ -3418,27 +3505,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck"
|
||||
version = "0.5.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
|
||||
"base32",
|
||||
"bech32",
|
||||
"bincode",
|
||||
"bitflags 2.9.1",
|
||||
"blurhash",
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui-winit",
|
||||
"egui_extras",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"fluent",
|
||||
"fluent-langneg",
|
||||
"fluent-resmgr",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"hex",
|
||||
"image",
|
||||
"indexmap 2.9.0",
|
||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lightning-invoice",
|
||||
"md5",
|
||||
"mime_guess",
|
||||
"ndk-context",
|
||||
"nostr 0.37.0",
|
||||
"nostrdb",
|
||||
"nwc",
|
||||
@@ -3466,8 +3561,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_chrome"
|
||||
version = "0.5.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui-winit",
|
||||
@@ -3475,8 +3571,10 @@ dependencies = [
|
||||
"egui_tabs",
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
"notedeck_clndash",
|
||||
"notedeck_columns",
|
||||
"notedeck_dave",
|
||||
"notedeck_notebook",
|
||||
"notedeck_ui",
|
||||
"profiling",
|
||||
"puffin",
|
||||
@@ -3493,9 +3591,28 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_clndash"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"hex",
|
||||
"lightning-invoice",
|
||||
"lnsocket",
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
"notedeck_ui",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_columns"
|
||||
version = "0.5.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bech32",
|
||||
@@ -3510,16 +3627,17 @@ dependencies = [
|
||||
"egui_virtual_list",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"hex",
|
||||
"human_format",
|
||||
"image",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ndk-context",
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
"notedeck_ui",
|
||||
"oot_bitset",
|
||||
"open",
|
||||
"opener",
|
||||
"poll-promise",
|
||||
"pretty_assertions",
|
||||
@@ -3528,6 +3646,7 @@ dependencies = [
|
||||
"puffin_egui",
|
||||
"rfd",
|
||||
"rmpv",
|
||||
"robius-open",
|
||||
"security-framework 2.11.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3549,7 +3668,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_dave"
|
||||
version = "0.5.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"async-openai",
|
||||
"bytemuck",
|
||||
@@ -3557,6 +3676,7 @@ dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"egui_extras",
|
||||
"enostr",
|
||||
"futures",
|
||||
"hex",
|
||||
@@ -3571,19 +3691,27 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_notebook"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"jsoncanvas",
|
||||
"notedeck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_ui"
|
||||
version = "0.5.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"blurhash",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui-winit",
|
||||
"egui_extras",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"image",
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
@@ -4012,17 +4140,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "opener"
|
||||
version = "0.8.2"
|
||||
@@ -4135,12 +4252,6 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -4429,7 +4540,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"natord",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
@@ -4757,6 +4868,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@@ -4948,6 +5079,30 @@ dependencies = [
|
||||
"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]]
|
||||
name = "roxmltree"
|
||||
version = "0.19.0"
|
||||
@@ -5094,6 +5249,30 @@ dependencies = [
|
||||
"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]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -5247,7 +5426,7 @@ version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -5286,6 +5465,38 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -5347,6 +5558,12 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.2"
|
||||
@@ -5880,7 +6097,7 @@ version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -6663,7 +6880,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"naga",
|
||||
"once_cell",
|
||||
@@ -6784,6 +7001,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
@@ -6803,6 +7030,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
@@ -6879,6 +7116,15 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
@@ -7206,10 +7452,10 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
[[package]]
|
||||
name = "winit"
|
||||
version = "0.30.8"
|
||||
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
|
||||
source = "git+https://github.com/damus-io/winit?rev=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9)",
|
||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
|
||||
"atomic-waker",
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
@@ -7261,7 +7507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5)",
|
||||
"android-activity 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atomic-waker",
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
|
||||
35
Cargo.toml
35
Cargo.toml
@@ -1,18 +1,21 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
package.version = "0.5.8"
|
||||
package.version = "0.7.1"
|
||||
members = [
|
||||
"crates/notedeck",
|
||||
"crates/notedeck_chrome",
|
||||
"crates/notedeck_columns",
|
||||
"crates/notedeck_dave",
|
||||
"crates/notedeck_notebook",
|
||||
"crates/notedeck_ui",
|
||||
"crates/notedeck_clndash",
|
||||
|
||||
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
|
||||
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
opener = "0.8.2"
|
||||
chrono = "0.4.40"
|
||||
base32 = "0.4.0"
|
||||
base64 = "0.22.1"
|
||||
rmpv = "1.3.0"
|
||||
@@ -24,7 +27,7 @@ egui = { version = "0.31.1", features = ["serde"] }
|
||||
egui-wgpu = "0.31.1"
|
||||
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
||||
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "3c67eb6298edbff36d46546897cfac33df4f04db" }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "de6e2d51892478fdd516df166f866e64dedbae07" }
|
||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
||||
#egui_virtual_list = "0.6.0"
|
||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
||||
@@ -34,7 +37,7 @@ ewebsock = { version = "0.2.0", features = ["tls"] }
|
||||
fluent = "0.17.0"
|
||||
fluent-resmgr = "0.0.8"
|
||||
fluent-langneg = "0.13"
|
||||
hex = "0.4.3"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||
indexmap = "2.6.0"
|
||||
log = "0.4.17"
|
||||
@@ -46,12 +49,14 @@ nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b
|
||||
#nostrdb = "0.6.1"
|
||||
notedeck = { path = "crates/notedeck" }
|
||||
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
||||
notedeck_clndash = { path = "crates/notedeck_clndash" }
|
||||
notedeck_columns = { path = "crates/notedeck_columns" }
|
||||
notedeck_dave = { path = "crates/notedeck_dave" }
|
||||
notedeck_notebook = { path = "crates/notedeck_notebook" }
|
||||
notedeck_ui = { path = "crates/notedeck_ui" }
|
||||
tokenator = { path = "crates/tokenator" }
|
||||
once_cell = "1.19.0"
|
||||
open = "5.3.0"
|
||||
robius-open = "0.1"
|
||||
poll-promise = { version = "0.3.0", features = ["tokio"] }
|
||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||
@@ -76,12 +81,14 @@ mime_guess = "2.0.5"
|
||||
pretty_assertions = "1.4.1"
|
||||
jni = "0.21.1"
|
||||
profiling = "1.0"
|
||||
lightning-invoice = "0.33.1"
|
||||
lightning-invoice = { version = "0.33.1", features = ["serde"] }
|
||||
secp256k1 = "0.30.0"
|
||||
hashbrown = "0.15.2"
|
||||
openai-api-rs = "6.0.3"
|
||||
re_memory = "0.23.4"
|
||||
oot_bitset = "0.1.1"
|
||||
blurhash = "0.2.3"
|
||||
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "4ee16f1585e4a75031dc10785163d4b920f95805", features = [ "game-activity" ] }
|
||||
|
||||
[profile.small]
|
||||
inherits = 'release'
|
||||
@@ -99,15 +106,15 @@ strip = true # Strip symbols from binary*
|
||||
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
||||
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
||||
|
||||
egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||
eframe = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||
epaint = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", 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 = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
|
||||
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
|
||||
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "a8948332c7c551303d32eb26a59d0abd676e47a5" }
|
||||
#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" }
|
||||
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }
|
||||
|
||||
2
Makefile
2
Makefile
@@ -27,4 +27,4 @@ push-android-config:
|
||||
android: jni
|
||||
cd $(ANDROID_DIR) && ./gradlew installDebug
|
||||
adb shell am start -n com.damus.notedeck/.MainActivity
|
||||
adb logcat -v color -s RustStdoutStderr -s threaded_app | tee logcat.txt
|
||||
adb logcat -v color -s GameActivity -s RustStdoutStderr -s threaded_app | tee logcat.txt
|
||||
|
||||
57
assets/icons/clnlogo.svg
Normal file
57
assets/icons/clnlogo.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="256mm"
|
||||
height="256mm"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="clnlogo.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.078823"
|
||||
inkscape:cx="396.72867"
|
||||
inkscape:cy="561.25984"
|
||||
inkscape:window-width="2020"
|
||||
inkscape:window-height="1420"
|
||||
inkscape:window-x="270"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
|
||||
style="display:inline">
|
||||
<g
|
||||
id="g4"
|
||||
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
|
||||
<path
|
||||
class="st1"
|
||||
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
|
||||
id="path3"
|
||||
style="fill:#f0d003" />
|
||||
<path
|
||||
fill="#fffae6"
|
||||
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
|
||||
id="path4" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/icons/like_icon_4x.png
Normal file
BIN
assets/icons/like_icon_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -45,6 +45,8 @@ Algo_2452 = Algorithmus
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Menge
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = Darstellung
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Fragen
|
||||
# Placeholder text for Dave AI input field
|
||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Senden
|
||||
Broadcast_Local_7e50 = Lokal senden
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Abbrechen
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = Abbrechen
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = Zwischenspeicher leeren
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Notiz erstellen
|
||||
# Label for configure relays, settings section
|
||||
Configure_relays_d156 = Relays konfigurieren
|
||||
# Label for confirm clear cache, Storage settings section
|
||||
Confirm_9d9d = Bestätigen
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Bestätigen
|
||||
# Status label for connected relay
|
||||
@@ -88,19 +98,19 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = Text kopieren
|
||||
# Relative time in days
|
||||
count_d_b9be = { $count }Tg.
|
||||
count_d_b9be = { $count }T
|
||||
# Relative time in hours
|
||||
count_h_3ecb = { $count }Std.
|
||||
count_h_3ecb = { $count }h
|
||||
# Relative time in minutes
|
||||
count_m_b41e = { $count }Min.
|
||||
count_m_b41e = { $count }min
|
||||
# Relative time in months
|
||||
count_mo_7aba = { $count }Mon.
|
||||
count_mo_7aba = { $count }M
|
||||
# Relative time in seconds
|
||||
count_s_aa26 = { $count }Sek.
|
||||
count_s_aa26 = { $count }s
|
||||
# Relative time in weeks
|
||||
count_w_7468 = { $count }Wo.
|
||||
count_w_7468 = { $count }W
|
||||
# Relative time in years
|
||||
count_y_9408 = { $count }J.
|
||||
count_y_9408 = { $count }J
|
||||
# Button to create a new account
|
||||
Create_Account_6994 = Konto erstellen
|
||||
# Button label to create a new deck
|
||||
@@ -111,6 +121,8 @@ Custom_a69e = Benutzerdefiniert
|
||||
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Damus Support
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Dunkel
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Deck-Name
|
||||
# Label for decks section in side panel
|
||||
@@ -151,12 +163,16 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Profil finden
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Schriftgröße:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
Home_8c19 = Startseite
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Symbol
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Bildcache Größe:
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individuell
|
||||
# Error message for invalid zap amount
|
||||
@@ -177,8 +193,12 @@ k_50K_c2dc = 50K
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Sprache:
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Letzte Notiz pro Profil
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = Hell
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
||||
# Login page title
|
||||
@@ -216,11 +236,17 @@ Notifications_d673 = Benachrichtigungen
|
||||
# Title for notifications column
|
||||
Notifications_ef56 = Benachrichtigungen
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = Jetzt
|
||||
now_2181 = Gerade eben
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = An
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Neue Leute finden
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = E-Mail öffnen
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
|
||||
# Label for others settings section
|
||||
Others_7267 = Andere
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
|
||||
# Error message for missing deck name
|
||||
@@ -267,6 +293,10 @@ replying_to_a_note_e0bc = Antwort auf eine Notiz
|
||||
Repost_this_note_8e56 = Diese Notiz teilen
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Teilen
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Zurücksetzen
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Zurücksetzen
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
@@ -287,8 +317,12 @@ Searching_for___query_5d18 = Suche nach '{ $query }'
|
||||
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Alle auswählen
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Senden
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Einstellungen
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
|
||||
# Button label to sign out of account
|
||||
@@ -297,6 +331,8 @@ Sign_out_337b = Abmelden
|
||||
Someone_else_s_Notes_7e5f = Notizen anderer Profile
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Neueste Antworten zuerst sortieren:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
|
||||
# Description for hashtags column
|
||||
@@ -315,10 +351,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Ben
|
||||
Step_1_8656 = Schritt 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Schritt 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Speicher
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
|
||||
# Support email address
|
||||
Support_email_44d9 = E-Mail Support:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
||||
# Hover text for light mode toggle button
|
||||
@@ -327,6 +367,8 @@ Switch_to_light_mode_72ce = Zum Hellmodus wechseln
|
||||
Tap_to_Load_4b05 = Zum Laden antippen
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Design:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Unterhaltung
|
||||
# Link text for thread references
|
||||
@@ -341,6 +383,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das ak
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
|
||||
# Profile username field label
|
||||
Username_daa7 = Benutzername
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Ordner anzeigen
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Wallet
|
||||
# Hint for deck name input field
|
||||
@@ -359,6 +403,8 @@ Your_Notifications_080d = Deine Benachrichtigungen
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Zappe diese Notiz
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = Zoomstufe:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
|
||||
@@ -79,9 +79,6 @@ Banner_52ef = Banner
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = Bottom
|
||||
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Broadcast
|
||||
|
||||
@@ -241,12 +238,12 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Find User
|
||||
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Font size:
|
||||
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = Hide
|
||||
|
||||
# Title for Home column
|
||||
Home_8c19 = Home
|
||||
|
||||
@@ -352,6 +349,12 @@ Notifications_ef56 = Notifications
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = now
|
||||
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = On
|
||||
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Onboarding
|
||||
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Open Email
|
||||
|
||||
@@ -430,6 +433,9 @@ Repost_this_note_8e56 = Repost this note
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Reposted
|
||||
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Reset
|
||||
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Reset
|
||||
|
||||
@@ -463,15 +469,15 @@ See_notes_from_your_contacts_ac16 = See notes from your contacts
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = See the whole nostr universe
|
||||
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Select All
|
||||
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Send
|
||||
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Settings
|
||||
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = Show source client
|
||||
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
|
||||
|
||||
@@ -484,6 +490,9 @@ Someone_else_s_Notes_7e5f = Someone else's Notes
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Someone else's Notifications
|
||||
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
||||
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list
|
||||
|
||||
@@ -520,6 +529,9 @@ Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes
|
||||
|
||||
# Support email address
|
||||
Support_email_44d9 = Support email:
|
||||
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Switch to dark mode
|
||||
|
||||
@@ -541,9 +553,6 @@ Thread_0f20 = Thread
|
||||
# Link text for thread references
|
||||
thread_ad1f = thread
|
||||
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = Top
|
||||
|
||||
# Title for universe column
|
||||
Universe_e01e = Universe
|
||||
|
||||
@@ -560,7 +569,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at
|
||||
Username_daa7 = Username
|
||||
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = View folder:
|
||||
View_folder_9742 = View folder
|
||||
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Wallet
|
||||
|
||||
@@ -79,9 +79,6 @@ Banner_52ef = {"["}Bàññér{"]"}
|
||||
# Beta version label
|
||||
BETA_8e5d = {"["}BÉTÀ{"]"}
|
||||
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = {"["}Bóttóm{"]"}
|
||||
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
|
||||
|
||||
@@ -241,12 +238,12 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
# Label for find user button
|
||||
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
|
||||
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = {"["}Fóñt sízé:{"]"}
|
||||
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
|
||||
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = {"["}Hídé{"]"}
|
||||
|
||||
# Title for Home column
|
||||
Home_8c19 = {"["}Hómé{"]"}
|
||||
|
||||
@@ -352,6 +349,12 @@ Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = {"["}ñów{"]"}
|
||||
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = {"["}Óñ{"]"}
|
||||
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = {"["}Óñbóàrdíñg{"]"}
|
||||
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
|
||||
|
||||
@@ -430,6 +433,9 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = {"["}Répóstéd{"]"}
|
||||
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = {"["}Rését{"]"}
|
||||
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = {"["}Rését{"]"}
|
||||
|
||||
@@ -463,15 +469,15 @@ See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàç
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"}
|
||||
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = {"["}Séléçt Àll{"]"}
|
||||
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = {"["}Séñd{"]"}
|
||||
|
||||
# Column title for app settings
|
||||
Settings_7a4f = {"["}Séttíñgs{"]"}
|
||||
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = {"["}Shów sóúrçé çlíéñt{"]"}
|
||||
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
|
||||
|
||||
@@ -484,6 +490,9 @@ Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"}
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
|
||||
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = {"["}Sórt réplíés ñéwést fírst:{"]"}
|
||||
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"}
|
||||
|
||||
@@ -520,6 +529,9 @@ Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé él
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
|
||||
|
||||
# Support email address
|
||||
Support_email_44d9 = {"["}Súppórt émàíl:{"]"}
|
||||
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
|
||||
|
||||
@@ -541,9 +553,6 @@ Thread_0f20 = {"["}Thréàd{"]"}
|
||||
# Link text for thread references
|
||||
thread_ad1f = {"["}thréàd{"]"}
|
||||
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = {"["}Tóp{"]"}
|
||||
|
||||
# Title for universe column
|
||||
Universe_e01e = {"["}Úñívérsé{"]"}
|
||||
|
||||
@@ -560,7 +569,7 @@ username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username
|
||||
Username_daa7 = {"["}Úsérñàmé{"]"}
|
||||
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = {"["}Víéw fóldér:{"]"}
|
||||
View_folder_9742 = {"["}Víéw fóldér{"]"}
|
||||
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = {"["}Wàllét{"]"}
|
||||
|
||||
@@ -45,6 +45,8 @@ Algo_2452 = Algo
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Cantidad
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = Aspecto
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Preguntar
|
||||
# Placeholder text for Dave AI input field
|
||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Transmitir
|
||||
Broadcast_Local_7e50 = Transmitir localmente
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Cancelar
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = Cancelar
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = Limpiar caché
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Haz clic para editar
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Redactar nota
|
||||
# Label for configure relays, settings section
|
||||
Configure_relays_d156 = Configurar relés
|
||||
# Label for confirm clear cache, Storage settings section
|
||||
Confirm_9d9d = Confirmar
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Confirmar
|
||||
# Status label for connected relay
|
||||
@@ -111,6 +121,8 @@ Custom_a69e = Personalizado
|
||||
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Ayuda de Damus
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Oscuro
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Nombre del deck
|
||||
# Label for decks section in side panel
|
||||
@@ -149,12 +161,16 @@ Enter_your_key_0fca = Ingresa tu clave
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Buscar usuario
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Font size:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
Home_8c19 = Inicio
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Ícono
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Tamaño de caché de imágenes:
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
# Error message for invalid zap amount
|
||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 50.000
|
||||
k_5K_f7e6 = 5.000
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Idioma:
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Última nota por usuario
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = Claro
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||
# Login page title
|
||||
@@ -215,10 +235,14 @@ Notifications_d673 = Notificaciones
|
||||
Notifications_ef56 = Notificaciones
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = ahora
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = On
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir correo electrónico
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
||||
# Label for others settings section
|
||||
Others_7267 = Otros
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||
# Error message for missing deck name
|
||||
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = respondiendo a una nota
|
||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicadas de nuevo
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Reset
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Restablecer
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = ¿Encontraste un error?
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Configuración
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
||||
# Button label to sign out of account
|
||||
@@ -295,6 +325,8 @@ Sign_out_337b = Cerrar sesión
|
||||
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||
# Description for hashtags column
|
||||
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con
|
||||
Step_1_8656 = Paso 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Paso 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Almacenamiento
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||
# Support email address
|
||||
Support_email_44d9 = Support email:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||
# Hover text for light mode toggle button
|
||||
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = Cambiar a modo claro
|
||||
Tap_to_Load_4b05 = Toca para cargar
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Tema:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Conversación
|
||||
# Link text for thread references
|
||||
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Usar esta billetera solo par
|
||||
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||
# Profile username field label
|
||||
Username_daa7 = Nombre de usuario
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Ver carpeta
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Billetera
|
||||
# Hint for deck name input field
|
||||
@@ -357,6 +397,8 @@ Your_Notifications_080d = Tus notificaciones
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Enviar un zap a esta nota
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = Nivel de zoom:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ Algo_2452 = Algo
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Cantidad
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = Aspecto
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Preguntar
|
||||
# Placeholder text for Dave AI input field
|
||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Transmitir
|
||||
Broadcast_Local_7e50 = Transmitir localmente
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Cancelar
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = Cancelar
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = Limpiar caché
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Haz clic para editar
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Redactar nota
|
||||
# Label for configure relays, settings section
|
||||
Configure_relays_d156 = Configurar relés
|
||||
# Label for confirm clear cache, Storage settings section
|
||||
Confirm_9d9d = Confirmar
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Confirmar
|
||||
# Status label for connected relay
|
||||
@@ -111,6 +121,8 @@ Custom_a69e = Personalizado
|
||||
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Ayuda de Damus
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Oscuro
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Nombre del deck
|
||||
# Label for decks section in side panel
|
||||
@@ -149,12 +161,16 @@ Enter_your_key_0fca = Ingresa tu clave
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Buscar usuario
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Font size:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
Home_8c19 = Inicio
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Icono
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Tamaño de caché de imágenes:
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
# Error message for invalid zap amount
|
||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 50.000
|
||||
k_5K_f7e6 = 5.000
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Idioma:
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Última nota por usuario
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = Claro
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||
# Login page title
|
||||
@@ -215,10 +235,14 @@ Notifications_d673 = Notificaciones
|
||||
Notifications_ef56 = Notificaciones
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = ahora
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = On
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir correo electrónico
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
||||
# Label for others settings section
|
||||
Others_7267 = Otros
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||
# Error message for missing deck name
|
||||
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = respondiendo a una nota
|
||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicadas de nuevo
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Reset
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Restablecer
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = ¿Has encontrado un error?
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Configuración
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
||||
# Button label to sign out of account
|
||||
@@ -295,6 +325,8 @@ Sign_out_337b = Cerrar sesión
|
||||
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||
# Description for hashtags column
|
||||
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con
|
||||
Step_1_8656 = Paso 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Paso 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Almacenamiento
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||
# Support email address
|
||||
Support_email_44d9 = Support email:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||
# Hover text for light mode toggle button
|
||||
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = Cambiar a modo claro
|
||||
Tap_to_Load_4b05 = Toca para cargar
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Tema:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Conversación
|
||||
# Link text for thread references
|
||||
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Usar este monedero solo para
|
||||
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||
# Profile username field label
|
||||
Username_daa7 = Nombre de usuario
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Ver carpeta
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Monedero
|
||||
# Hint for deck name input field
|
||||
@@ -357,6 +397,8 @@ Your_Notifications_080d = Tus notificaciones
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Enviar un zap a esta nota
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = Nivel de zoom:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
|
||||
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
|
||||
Banner_52ef = Bannière
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = En bas
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Diffusion
|
||||
# Broadcast the note only to local network relays
|
||||
@@ -163,10 +161,10 @@ Enter_your_key_0fca = Entrez votre clé
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Trouver un utilisateur
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Taille du texte :
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = Masquer
|
||||
# Title for Home column
|
||||
Home_8c19 = Accueil
|
||||
# Label for deck icon selection
|
||||
@@ -237,6 +235,10 @@ Notifications_d673 = Notifications
|
||||
Notifications_ef56 = Notifications
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = maintenant
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = Activé
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Utilisateurs recommandés
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Ouvrir Email
|
||||
# Instruction to open email client
|
||||
@@ -289,6 +291,8 @@ replying_to_a_note_e0bc = répondre à une note
|
||||
Repost_this_note_8e56 = Republier cette note
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Republier
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Réinitialiser
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Réinitialiser
|
||||
# Heading for support section
|
||||
@@ -311,12 +315,12 @@ Searching_for___query_5d18 = Recherche par '{ $query }'
|
||||
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Tout sélectionner
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Envoyer
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Paramètres
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = Afficher le client source
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
|
||||
# Button label to sign out of account
|
||||
@@ -325,6 +329,8 @@ Sign_out_337b = Se déconnecter
|
||||
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Trier les réponses les plus récentes en premier :
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
|
||||
# Description for hashtags column
|
||||
@@ -349,6 +355,8 @@ Storage_ed65 = Stockage
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
|
||||
# Support email address
|
||||
Support_email_44d9 = Adresse email de l'assistance :
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Passer en mode sombre
|
||||
# Hover text for light mode toggle button
|
||||
@@ -363,8 +371,6 @@ Theme_4aac = Thème :
|
||||
Thread_0f20 = Fil
|
||||
# Link text for thread references
|
||||
thread_ad1f = fil
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = En haut
|
||||
# Title for universe column
|
||||
Universe_e01e = Universel
|
||||
# Column title for universe feed
|
||||
@@ -376,7 +382,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
||||
# Profile username field label
|
||||
Username_daa7 = Nom d'utilisateur
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Voir le dossier :
|
||||
View_folder_9742 = Voir le dossier
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Portefeuille
|
||||
# Hint for deck name input field
|
||||
|
||||
410
assets/translations/ja/main.ftl
Normal file
410
assets/translations/ja/main.ftl
Normal file
@@ -0,0 +1,410 @@
|
||||
# Main translation file for Notedeck
|
||||
# This file contains common UI strings used throughout the application
|
||||
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
|
||||
|
||||
|
||||
# Regular strings
|
||||
|
||||
# Profile about/bio field label
|
||||
About_00c0 = 概要
|
||||
# Column title for account management
|
||||
Accounts_f018 = アカウント
|
||||
# Button label to add a relay
|
||||
Add_269d = 追加
|
||||
# Label for add column button
|
||||
Add_47df = 追加
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = このアカウントでのみ使用される別のウォレットを追加
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = 続行するにはウォレットを追加してください
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = アカウントを追加
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = アカウントの追加
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = アルゴカラムの追加
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = カラムの追加
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = デッキの追加
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = 外部通知カラムの追加
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = ハッシュタグカラムの追加
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = 最後の投稿カラムの追加
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = 外部通知カラムの追加
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = リレーを追加
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = ウォレットを追加
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = アルゴ
|
||||
# Description for algorithmic feeds column
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 投稿の発見に役立つアルゴリズムフィードです
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = 金額
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = 外観
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = 質問
|
||||
# Placeholder text for Dave AI input field
|
||||
Ask_dave_anything_33d1 = Dave に何でも質問してみましょう…
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = バナー
|
||||
# Beta version label
|
||||
BETA_8e5d = ベータ
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = ブロードキャスト
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = ローカルにブロードキャスト
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = キャンセル
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = キャンセル
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = キャッシュを消去
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = クリックして編集
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = メモの作成
|
||||
# Label for configure relays, settings section
|
||||
Configure_relays_d156 = リレーを設定
|
||||
# Label for confirm clear cache, Storage settings section
|
||||
Confirm_9d9d = 決定
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = 決定
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = 接続済
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = 接続中…
|
||||
# Title for contact list column
|
||||
Contact_List_f85a = フォロイーリスト
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = フォロー
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = フォロー (最後の投稿)
|
||||
# Button label to copy logs
|
||||
Copy_a688 = コピー
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = リンクをコピー
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = 投稿 ID をコピー
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = 投稿の JSON をコピー
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = 公開鍵をコピー
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = テキストをコピー
|
||||
# Relative time in days
|
||||
count_d_b9be = { $count }日
|
||||
# Relative time in hours
|
||||
count_h_3ecb = { $count }時間
|
||||
# Relative time in minutes
|
||||
count_m_b41e = { $count }分
|
||||
# Relative time in months
|
||||
count_mo_7aba = { $count }ヶ月
|
||||
# Relative time in seconds
|
||||
count_s_aa26 = { $count }秒
|
||||
# Relative time in weeks
|
||||
count_w_7468 = { $count }週間
|
||||
# Relative time in years
|
||||
count_y_9408 = { $count }年
|
||||
# Button to create a new account
|
||||
Create_Account_6994 = アカウントを作成
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = デッキを作成
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = カスタマイズ
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Zap 金額をカスタマイズ
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Damus サポート
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = ダーク
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = デッキ名
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = デッキ
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Zap ごとのデフォルトの金額:
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = 既定のデッキ
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = デッキを削除
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = このカラムを削除します
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = ウォレットを削除
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = 表示名
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" が識別に使用されます
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = デッキの編集
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = デッキを編集
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = プロファイルを編集
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = プロファイルの編集
|
||||
# Placeholder for hashtag input field
|
||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 必要なハッシュタグをここに入力してください (複数スペースで区切る場合)
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = ここにリレーを入力してください
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = ユーザーの鍵 (npub, hex, nip05) を入力してください...
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = 鍵を入力してください
|
||||
# Instructions for entering Nostr credentials
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 公開鍵 (npub)、nostr アドレス (例: { $address })、秘密鍵 (nsec) を入力してください。 投稿、返信などを行うには秘密鍵を入力する必要があります。
|
||||
# Label for find user button
|
||||
Find_User_bd12 = ユーザーを探す
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = フォントサイズ:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = ハッシュタグ
|
||||
# Title for Home column
|
||||
Home_8c19 = ホーム
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = アイコン
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = 画像キャッシュのサイズ:
|
||||
# Title for individual user column
|
||||
Individual_b776 = 個人用
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = 無効な金額です
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = 無効な鍵です。
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = 無効な NWC URI です
|
||||
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_100K_686c = 100K
|
||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_10K_f7e6 = 10K
|
||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_20K_4977 = 20K
|
||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_50K_c2dc = 50K
|
||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = 投稿と返信を記録します
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = 言語:
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = ユーザーごとの最後の投稿
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = ライト
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = ライトニングネットワークアドレス (lud16)
|
||||
# Login page title
|
||||
Login_9eef = ログイン
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = 今すぐログイン — レッツゴー!
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = フォローしていない人のメディアです
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = このカラムを別の位置に移動します
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = あなたのデッキ
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = Nostr は初めてですか?
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Nostr アドレス (NIP-05)
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = ノス民
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = 未接続
|
||||
# Link text for note references
|
||||
note_cad6 = 投稿
|
||||
# Beta product warning message
|
||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck はベータ製品です。問題が発生した場合はサポートに問い合わせてください。
|
||||
# Filter label for notes only view
|
||||
Notes_03fb = 投稿
|
||||
# Label for notes-only filter
|
||||
Notes_60d2 = 投稿
|
||||
# Filter label for notes and replies view
|
||||
Notes___Replies_1ec2 = 投稿 & 返信
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = 投稿 & 返信
|
||||
# Column title for notifications
|
||||
Notifications_d673 = 通知
|
||||
# Title for notifications column
|
||||
Notifications_ef56 = 通知
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = たった今
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = 有効
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = メールを開く
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = デフォルトのメールクライアントを開いて、Damus チームのヘルプを表示しましょう。
|
||||
# Label for others settings section
|
||||
Others_7267 = その他
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = ここに NWC の URI を貼り付けてください...
|
||||
# Error message for missing deck name
|
||||
Please_create_a_name_for_the_deck_38e7 = デッキの名前を作成してください。
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = デッキの名前を作成してアイコンを選択してください。
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = アイコンを選択してください。
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = すぐに投稿
|
||||
# Instruction for copying logs
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 下のボタンを押して、最新のログをシステムのクリップボードにコピーします。その後、メールに貼り付けてください。
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = プロフィール写真
|
||||
# Column title for quote composition
|
||||
Quote_475c = 引用
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = 不明な投稿の引用です
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = 読み取り専用
|
||||
# Column title for relay management
|
||||
Relays_9d89 = リレー
|
||||
# Label for relay list section
|
||||
Relays_ad5e = リレー
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = 返信
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = この投稿に返信
|
||||
# Error message when reply note cannot be found
|
||||
Reply_to_unknown_note_4401 = 不明な投稿に返信しています
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = { $user } に返信
|
||||
# Template for replying to user in unknown thread
|
||||
replying_to__user__in_someone_s_thread_e148 = 誰かのスレッドで { $user } に返信
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = { $user }の { $note } の { $thread_user }の { $thread } に返信
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = { $user }の { $note } に返信
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = { $user }の { $thread } に返信
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = 投稿に返信
|
||||
# Hover text for repost button
|
||||
Repost_this_note_8e56 = このメモを再投稿
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = 再投稿
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = リセット
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = リセット
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = バグに遭遇しましたか?
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
SATS_45d7 = SATS
|
||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
||||
sats_e5ec = sats
|
||||
# Button to save default zap amount
|
||||
Save_6f7c = 保存
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = 変更を保存
|
||||
# Column title for search page
|
||||
Search_c573 = 検索
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = 投稿を検索しましょう...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = 「{ $query }」を検索中
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = フォローしている人の投稿を表示
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = 全ユニバースを表示します
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = 送信
|
||||
# Column title for app settings
|
||||
Settings_7a4f = 設定
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 一覧から各ユーザーの最後の投稿を表示する
|
||||
# Button label to sign out of account
|
||||
Sign_out_337b = サインアウト
|
||||
# Title for someone else's notes column
|
||||
Someone_else_s_Notes_7e5f = 他の人の投稿
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = 他の人の通知
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = 最新の返信を最初に並べ替え:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = フォローリストにある各ユーザーの最後の投稿を取得します
|
||||
# Description for hashtags column
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = 特定のハッシュタグで最新の情報を受け取ります
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = 通知とメンションの最新の情報を受け取ります
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 他のユーザーの投稿と返信の最新の情報を受け取ります
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 他のユーザーの投稿と返信の最新の情報を受け取ります
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 投稿と返信の最新の情報を受け取ります
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = あなたの通知とメンションの最新の情報を受け取ります
|
||||
# Step 1 label in support instructions
|
||||
Step_1_8656 = ステップ 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = ステップ 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = ストレージ
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = 他のユーザー投稿の購読
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = 投稿の購読
|
||||
# Support email address
|
||||
Support_email_44d9 = サポートメール:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = ダークモードに切り替える
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = ライトモードに切り替える
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = タップして読み込む
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI アシスタントトライアルが終了しました: (テストしていただきありがとうございます! Zap 対応デイブは近日公開予定です!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = テーマ:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = スレッド
|
||||
# Link text for thread references
|
||||
thread_ad1f = スレッド
|
||||
# Title for universe column
|
||||
Universe_e01e = ユニバース
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = ユニバース
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = このウォレットを現在のアカウントにのみ使用する
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $domain }" の "{ $username }" が識別に使用されます
|
||||
# Profile username field label
|
||||
Username_daa7 = ユーザー名
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = フォルダを表示
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = ウォレット
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = 短い名前を推奨しています
|
||||
# Profile website field label
|
||||
Website_7980 = Web サイト
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = アツい一言をどうぞ...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = ここに鍵を入力...
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = 投稿
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = 通知
|
||||
# Heading for zap (tip) action
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = この投稿に Zap
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = 拡大率:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[one] { $query } の結果を '{ $count }' 件取得しました
|
||||
*[other] ' { $query } の結果を '{ $count }' 件取得しました
|
||||
}
|
||||
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = Perguntar ao Dave
|
||||
Banner_52ef = Destaque
|
||||
# Beta version label
|
||||
BETA_8e5d = Beta
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = Abaixo
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Encaminhar
|
||||
# Broadcast the note only to local network relays
|
||||
@@ -163,10 +161,10 @@ Enter_your_key_0fca = Sua chave aqui
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Pesquisar usuário
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Tamanho da letra
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = #
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = Ocultar
|
||||
# Title for Home column
|
||||
Home_8c19 = Início
|
||||
# Label for deck icon selection
|
||||
@@ -237,6 +235,10 @@ Notifications_d673 = Notificações
|
||||
Notifications_ef56 = Notificações
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = Agora
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = Ligar
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Interação
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir E-mail
|
||||
# Instruction to open email client
|
||||
@@ -289,6 +291,8 @@ replying_to_a_note_e0bc = Respondendo nota
|
||||
Repost_this_note_8e56 = Republicar nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicada
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Redefinir
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Resetar
|
||||
# Heading for support section
|
||||
@@ -311,12 +315,12 @@ Searching_for___query_5d18 = Pesquisando por '{ $query }'
|
||||
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Selecionar todos
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Configurações
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = Mostrar cliente de origem
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
|
||||
# Button label to sign out of account
|
||||
@@ -325,6 +329,8 @@ Sign_out_337b = Sair
|
||||
Someone_else_s_Notes_7e5f = Notas de outra pessoa
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes primeiro:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
|
||||
# Description for hashtags column
|
||||
@@ -349,6 +355,8 @@ Storage_ed65 = Armazenamento
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
|
||||
# Support email address
|
||||
Support_email_44d9 = E-mail de suporte
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Mudar para modo escuro
|
||||
# Hover text for light mode toggle button
|
||||
@@ -363,8 +371,6 @@ Theme_4aac = Tema:
|
||||
Thread_0f20 = Fio
|
||||
# Link text for thread references
|
||||
thread_ad1f = Fio
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = Topo
|
||||
# Title for universe column
|
||||
Universe_e01e = Universo
|
||||
# Column title for universe feed
|
||||
@@ -376,7 +382,7 @@ username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username
|
||||
# Profile username field label
|
||||
Username_daa7 = Usuário
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Visualizar pasta:
|
||||
View_folder_9742 = Visualizar pasta
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Carteira
|
||||
# Hint for deck name input field
|
||||
|
||||
414
assets/translations/pt-PT/main.ftl
Normal file
414
assets/translations/pt-PT/main.ftl
Normal file
@@ -0,0 +1,414 @@
|
||||
# Main translation file for Notedeck
|
||||
# This file contains common UI strings used throughout the application
|
||||
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
|
||||
|
||||
|
||||
# Regular strings
|
||||
|
||||
# Profile about/bio field label
|
||||
About_00c0 = Sobre
|
||||
# Column title for account management
|
||||
Accounts_f018 = Contas
|
||||
# Button label to add a relay
|
||||
Add_269d = Adicionar
|
||||
# Label for add column button
|
||||
Add_47df = Adicionar
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar uma carteira diferente que será usada apenas para esta conta
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = Adicionar uma carteira para continuar
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = Adicionar conta
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Adicionar conta
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Adicionar coluna
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Adicionar aba
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = Adicionar relay
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = Adicionar carteira
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = Algoritmo
|
||||
# Description for algorithmic feeds column
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Fontes de algoritmo para ajudar na descoberta de notas
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Quantia
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = Aparência
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Perguntar
|
||||
# Placeholder text for Dave AI input field
|
||||
Ask_dave_anything_33d1 = Perguntar qualquer coisa...
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = Faixa
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Transmissão
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = Transmissão local
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Cancelar
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = Cancelar
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = Limpar cache
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Clica para editar
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Compor nota
|
||||
# Label for configure relays, settings section
|
||||
Configure_relays_d156 = Configurar relays
|
||||
# Label for confirm clear cache, Storage settings section
|
||||
Confirm_9d9d = Confirmar
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Confirmar
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = Conectado
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = A conectar...
|
||||
# Title for contact list column
|
||||
Contact_List_f85a = Lista de contactos
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = Contactos
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = Contactos (últimas notas)
|
||||
# Button label to copy logs
|
||||
Copy_a688 = Copiar
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = Copiar link
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = Copiar ID da nota
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copiar JSON da nota
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copiar chave pública
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = Copiar texto
|
||||
# Relative time in days
|
||||
count_d_b9be = { $count }d
|
||||
# Relative time in hours
|
||||
count_h_3ecb = { $count }h
|
||||
# Relative time in minutes
|
||||
count_m_b41e = { $count }m
|
||||
# Relative time in months
|
||||
count_mo_7aba = { $count } mês(es)
|
||||
# Relative time in seconds
|
||||
count_s_aa26 = { $count } s
|
||||
# Relative time in weeks
|
||||
count_w_7468 = { $count } semana(s)
|
||||
# Relative time in years
|
||||
count_y_9408 = { $count } ano(s)
|
||||
# Button to create a new account
|
||||
Create_Account_6994 = Criar conta
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = Criar aba
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Personalizadas
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Personalizar valor do zap
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Suporte Damus
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Modo escuro
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Nome da aba
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = ABAS
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Valor padrão por zap:
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Aba padrão
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = Excluir aba
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = Apagar esta coluna
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = Eliminar carteira
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = Nome a mostrar
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Editar aba
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Editar aba
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = Editar perfil
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = Editar perfil
|
||||
# Placeholder for hashtag input field
|
||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Insere aqui os marcadores desejados (para múltiplos com espaços separados)
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = Insere aqui o relay
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Insere aqui a chave de utilizador (npub, hex, nip05)
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = Insere a tua chave
|
||||
# Instructions for entering Nostr credentials
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insere a tua chave públca (npub), endereço nostr (por exemplo { $address }), ou chave privada (nsec). Tens de inserir a tua chave pública para publicar, responder, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Encontrar utilizador
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Tamanho da letra:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Marcadores
|
||||
# Title for Home column
|
||||
Home_8c19 = Início
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Ícone
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Tamanho do cache da imagem:
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = Quantia inválida
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = Chave inválida.
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = NWC URI inválido.
|
||||
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_100K_686c = 100K
|
||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_10K_f7e6 = 10K
|
||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_20K_4977 = 20K
|
||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_50K_c2dc = 50K
|
||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Idioma:
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Última nota por utilizador
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = Modo claro
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Endereço da rede Lightning (lud16)
|
||||
# Login page title
|
||||
Login_9eef = Iniciar sessão
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = Entra agora — vamos fazer isto!
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Minha aba
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = Nov@ no Nostr?
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (identificação NIP-05)
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = nostrich
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = Não conectado
|
||||
# Link text for note references
|
||||
note_cad6 = nota
|
||||
# Beta product warning message
|
||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere bugs e contacte-nos quando tiver problemas.
|
||||
# Filter label for notes only view
|
||||
Notes_03fb = Notas
|
||||
# Label for notes-only filter
|
||||
Notes_60d2 = Notas
|
||||
# Filter label for notes and replies view
|
||||
Notes___Replies_1ec2 = Notas e respostas
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notas e respostas
|
||||
# Column title for notifications
|
||||
Notifications_d673 = Notificações
|
||||
# Title for notifications column
|
||||
Notifications_ef56 = Notificações
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = agora
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = Ativado
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Introdução
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir e-mail
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre o teu cliente de e-mail padrão para obteres ajuda da equipa Damus
|
||||
# Label for others settings section
|
||||
Others_7267 = Outros
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Cola o teu NWC URI aqui...
|
||||
# Error message for missing deck name
|
||||
Please_create_a_name_for_the_deck_38e7 = Cria um nome para a aba.
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Cria um nome para a aba e seleciona um ícone.
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = Seleciona um ícone.
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = Publicar agora
|
||||
# Instruction for copying logs
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Foto de perfil
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citação
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = Somente leitura
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relays
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relays
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Responder
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = Responder a esta nota
|
||||
# Error message when reply note cannot be found
|
||||
Reply_to_unknown_note_4401 = Responder a nota desconhecida
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = responder a { $user }
|
||||
# Template for replying to user in unknown thread
|
||||
replying_to__user__in_someone_s_thread_e148 = responder a { $user } no tópico de alguém
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondendo à { $note } de { $user } no { $thread } de { $thread_user }
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = respondendo à { $note } de { $user }
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = respondendo ao { $thread } de { $user }
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = respondendo a uma nota
|
||||
# Hover text for repost button
|
||||
Repost_this_note_8e56 = Republicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Republicado
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Redefinir
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Redefinir
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = Encontraste um bug?
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
SATS_45d7 = SATS
|
||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
||||
sats_e5ec = sats
|
||||
# Button to save default zap amount
|
||||
Save_6f7c = Guardar
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = Guardar alterações
|
||||
# Column title for search page
|
||||
Search_c573 = Procurar
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = Procurar notas...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Procurando por '{ $query }'
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Ver notas de todo o universo nostr
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Selecionar todos
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Configurações
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada utilizador a partir de uma lista
|
||||
# Button label to sign out of account
|
||||
Sign_out_337b = Terminar sessão
|
||||
# Title for someone else's notes column
|
||||
Someone_else_s_Notes_7e5f = Notas de outra pessoa
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes antes:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Origem da última nota para cada utilizador na minha lista
|
||||
# Description for hashtags column
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Atualizações com um dado marcador
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Atualizações com notificações e menções
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Atualizar-me de notas e respostas de outra pessoa
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Atualizar-me de notificações e menções de outra pessoa
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Atualizar-me de notas e respostas de outra pessoa
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Atualizar-me de notificações e menções
|
||||
# Step 1 label in support instructions
|
||||
Step_1_8656 = Passo 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Passo 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Armazenamento
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Subscrever as notas de outra pessoa
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Subscrever as notas de alguém
|
||||
# Support email address
|
||||
Support_email_44d9 = E-mail de suporte:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Mudar para o modo escuro
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = Mudar para o modo claro
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = Toca para carregar
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nost terminou :(. Obrigado por testares! Dave com ativação de ZAPS em breve!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Tema:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Tópico
|
||||
# Link text for thread references
|
||||
thread_ad1f = tópico
|
||||
# Title for universe column
|
||||
Universe_e01e = Universo
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = Universo
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = Usar esta carteira apenas para a conta atual
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" em "{ $domain }" será usado para identificação
|
||||
# Profile username field label
|
||||
Username_daa7 = Nome de utilizador
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Ver pasta
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Carteira
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Recomendamos nomes curtos
|
||||
# Profile website field label
|
||||
Website_7980 = Website
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = Escreve uma nota sonante aqui...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = A tua chave aqui...
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = Minhas notas
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = Minhas notificações
|
||||
# Heading for zap (tip) action
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Enviar zaps a esta nota
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = Nível de zoom:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[one] { $count } resultado obtido para '{ $query }'
|
||||
*[other] { $count } resultados obtidos para '{ $query }'
|
||||
}
|
||||
@@ -46,7 +46,7 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลก
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = จำนวน
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = รูปลักษณ์
|
||||
Appearance_4c7f = ลักษณะ
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = ถาม
|
||||
# Placeholder text for Dave AI input field
|
||||
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = ถามเดฟได้ทุกเรื่อง.
|
||||
Banner_52ef = ภาพปก
|
||||
# Beta version label
|
||||
BETA_8e5d = เบต้า
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = ด้านล่าง
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = เผยแพร่
|
||||
# Broadcast the note only to local network relays
|
||||
@@ -92,11 +90,11 @@ Copy_a688 = คัดลอก
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = คัดลอกลิงก์
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = คัดลอก ID โน้ต
|
||||
Copy_Note_ID_6b45 = คัดลอก โน้ต ID
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = คัดลอก Pubkey
|
||||
Copy_Pubkey_9cc4 = คัดลอก npub
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = คัดลอกข้อความ
|
||||
# Relative time in days
|
||||
@@ -165,10 +163,10 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
|
||||
# Label for find user button
|
||||
Find_User_bd12 = ค้นหาผู้ใช้
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = ขนาดตัวอักษร:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = แฮชแท็ก
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = ซ่อน
|
||||
# Title for Home column
|
||||
Home_8c19 = หน้าแรก
|
||||
# Label for deck icon selection
|
||||
@@ -239,6 +237,10 @@ Notifications_d673 = การแจ้งเตือน
|
||||
Notifications_ef56 = การแจ้งเตือน
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = เมื่อสักครู่
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = เปิด
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = เริ่มใช้
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = เปิดอีเมล
|
||||
# Instruction to open email client
|
||||
@@ -254,7 +256,7 @@ Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาต
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = กรุณาเลือกไอคอน
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = โพสต์เลย
|
||||
Post_now_8a49 = โพสต์
|
||||
# Instruction for copying logs
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ
|
||||
# Profile picture URL field label
|
||||
@@ -291,6 +293,8 @@ replying_to_a_note_e0bc = ตอบกลับโน้ต
|
||||
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = รีโพสต์แล้ว
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = รีเซ็ต
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = รีเซ็ต
|
||||
# Heading for support section
|
||||
@@ -313,12 +317,12 @@ Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
|
||||
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = เลือกทั้งหมด
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = ส่ง
|
||||
# Column title for app settings
|
||||
Settings_7a4f = การตั้งค่า
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = แสดงไคลเอนต์ต้นทาง
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
|
||||
# Button label to sign out of account
|
||||
@@ -327,6 +331,8 @@ Sign_out_337b = ออกจากระบบ
|
||||
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = เรียงการตอบกลับจากใหม่ที่สุดไปเก่าที่สุด
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
|
||||
# Description for hashtags column
|
||||
@@ -351,6 +357,8 @@ Storage_ed65 = พื้นที่จัดเก็บ
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
|
||||
# Support email address
|
||||
Support_email_44d9 = อีเมลฝ่ายสนับสนุน:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
|
||||
# Hover text for light mode toggle button
|
||||
@@ -365,8 +373,6 @@ Theme_4aac = ธีม:
|
||||
Thread_0f20 = เธรด
|
||||
# Link text for thread references
|
||||
thread_ad1f = เธรด
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = ด้านบน
|
||||
# Title for universe column
|
||||
Universe_e01e = จักรวาล
|
||||
# Column title for universe feed
|
||||
@@ -374,11 +380,11 @@ Universe_ffaa = จักรวาล
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" ที่ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" @ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
|
||||
# Profile username field label
|
||||
Username_daa7 = ชื่อผู้ใช้
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = ดูโฟลเดอร์:
|
||||
View_folder_9742 = ดูโฟลเดอร์
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = วอลเล็ต
|
||||
# Hint for deck name input field
|
||||
@@ -386,7 +392,7 @@ We_recommend_short_names_083e = เราแนะนำให้ใช้ชื
|
||||
# Profile website field label
|
||||
Website_7980 = เว็บไซต์
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = เขียนโน้ตปังๆ ที่นี่...
|
||||
Write_a_banger_note_here_bad2 = เขียนโน้ตที่นี่...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
|
||||
# Title for your notes column
|
||||
|
||||
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = 向 Dave 提问任何问题…
|
||||
Banner_52ef = 横幅
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = 底部
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = 广播
|
||||
# Broadcast the note only to local network relays
|
||||
@@ -163,10 +161,10 @@ Enter_your_key_0fca = 请输入你的密钥
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥(npub)、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
|
||||
# Label for find user button
|
||||
Find_User_bd12 = 查找用户
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = 字体大小:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = 标签
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = 隐藏
|
||||
# Title for Home column
|
||||
Home_8c19 = 主页
|
||||
# Label for deck icon selection
|
||||
@@ -237,6 +235,8 @@ Notifications_d673 = 通知
|
||||
Notifications_ef56 = 通知
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = 刚刚
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = 开启
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = 打开电子邮箱
|
||||
# Instruction to open email client
|
||||
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = 正在回复笔记
|
||||
Repost_this_note_8e56 = 转发此笔记
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = 已转发
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = 重置
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = 重置
|
||||
# Heading for support section
|
||||
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
|
||||
Send_1ea4 = 发送
|
||||
# Column title for app settings
|
||||
Settings_7a4f = 设置
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = 显示来源客户端
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
|
||||
# Button label to sign out of account
|
||||
@@ -325,6 +325,8 @@ Sign_out_337b = 登出
|
||||
Someone_else_s_Notes_7e5f = 其他人的笔记
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = 其他人的通知
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = 按最新排序回复:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 获取你的联系人列表中每个用户的最新一条笔记
|
||||
# Description for hashtags column
|
||||
@@ -349,6 +351,8 @@ Storage_ed65 = 存储
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
|
||||
# Support email address
|
||||
Support_email_44d9 = 支持电子邮件:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = 切换到暗色模式
|
||||
# Hover text for light mode toggle button
|
||||
@@ -363,8 +367,6 @@ Theme_4aac = 主题:
|
||||
Thread_0f20 = 帖子
|
||||
# Link text for thread references
|
||||
thread_ad1f = 帖子
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = 顶部
|
||||
# Title for universe column
|
||||
Universe_e01e = 宇宙
|
||||
# Column title for universe feed
|
||||
@@ -376,7 +378,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
||||
# Profile username field label
|
||||
Username_daa7 = 用户名
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = 查看文件夹:
|
||||
View_folder_9742 = 查看文件夹
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = 钱包
|
||||
# Hint for deck name input field
|
||||
|
||||
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = 向 Dave 提問任何問題...
|
||||
Banner_52ef = 橫幅
|
||||
# Beta version label
|
||||
BETA_8e5d = 測試版
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = 底部
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = 廣播
|
||||
# Broadcast the note only to local network relays
|
||||
@@ -163,10 +161,10 @@ Enter_your_key_0fca = 請輸入你的密鑰
|
||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰(npub)、nostr 地址(如 { $address })、或私鑰(nsec)。你必須輸入你的私鑰才能發貼、回覆等等。
|
||||
# Label for find user button
|
||||
Find_User_bd12 = 查找用戶
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = 字體大小:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = 標籤
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = 隱藏
|
||||
# Title for Home column
|
||||
Home_8c19 = 主頁
|
||||
# Label for deck icon selection
|
||||
@@ -237,6 +235,8 @@ Notifications_d673 = 通知
|
||||
Notifications_ef56 = 通知
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = 剛剛
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = 開啟
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = 打開電子郵箱
|
||||
# Instruction to open email client
|
||||
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = 正在回覆筆記
|
||||
Repost_this_note_8e56 = 轉發此筆記
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = 已轉發
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = 重置
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = 重置
|
||||
# Heading for support section
|
||||
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
|
||||
Send_1ea4 = 發送
|
||||
# Column title for app settings
|
||||
Settings_7a4f = 設置
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = 顯示來源客戶端
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
|
||||
# Button label to sign out of account
|
||||
@@ -325,6 +325,8 @@ Sign_out_337b = 登出
|
||||
Someone_else_s_Notes_7e5f = 其他人的筆記
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = 其他人的通知
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = 按最新排序回覆:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 獲取你的聯繫人列表中每個用戶的最新一條筆記
|
||||
# Description for hashtags column
|
||||
@@ -349,6 +351,8 @@ Storage_ed65 = 儲存
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
|
||||
# Support email address
|
||||
Support_email_44d9 = 支持電子郵件:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = 切換到暗色模式
|
||||
# Hover text for light mode toggle button
|
||||
@@ -363,8 +367,6 @@ Theme_4aac = 主題:
|
||||
Thread_0f20 = 串文
|
||||
# Link text for thread references
|
||||
thread_ad1f = 串文
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = 頂部
|
||||
# Title for universe column
|
||||
Universe_e01e = 宇宙
|
||||
# Column title for universe feed
|
||||
@@ -376,7 +378,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
||||
# Profile username field label
|
||||
Username_daa7 = 用戶名
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = 查看文件夾:
|
||||
View_folder_9742 = 查看文件夾
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = 錢包
|
||||
# Hint for deck name input field
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn setup_multicast_relay(
|
||||
std::thread::spawn(move || {
|
||||
let mut events = Events::with_capacity(1);
|
||||
loop {
|
||||
if let Err(err) = poll.poll(&mut events, Some(Duration::from_millis(100))) {
|
||||
if let Err(err) = poll.poll(&mut events, None) {
|
||||
error!("multicast socket poll error: {err}. ending multicast poller.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ impl From<RelayEvent<'_>> for OwnedRelayEvent {
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub struct RelaySub {
|
||||
pub struct _RelaySub {
|
||||
pub(crate) subid: String,
|
||||
pub(crate) filter: String,
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ nostrdb = { workspace = true }
|
||||
jni = { workspace = true }
|
||||
url = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
blurhash = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
enostr = { workspace = true }
|
||||
nostr = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
image = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
@@ -45,7 +47,11 @@ fluent-langneg = { workspace = true }
|
||||
unic-langid = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
md5 = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
regex = "1"
|
||||
chrono = { workspace = true }
|
||||
indexmap = {workspace = true}
|
||||
crossbeam-channel = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
@@ -53,6 +59,8 @@ tokio = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = { workspace = true }
|
||||
android-activity = { workspace = true }
|
||||
ndk-context = "0.1"
|
||||
|
||||
[features]
|
||||
puffin = ["puffin_egui", "dep:puffin"]
|
||||
|
||||
@@ -267,6 +267,11 @@ impl Accounts {
|
||||
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
|
||||
}
|
||||
|
||||
pub fn mute(&self) -> Box<Arc<crate::Muted>> {
|
||||
let account_data = self.get_selected_account_data();
|
||||
Box::new(Arc::clone(&account_data.muted.muted))
|
||||
}
|
||||
|
||||
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
|
||||
let data = &self.get_selected_account().data;
|
||||
// send the active account's relay list subscription
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Contacts {
|
||||
|
||||
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||
let binding = ndb
|
||||
.query(txn, &[self.filter.clone()], 1)
|
||||
.query(txn, std::slice::from_ref(&self.filter), 1)
|
||||
.expect("query user relays results");
|
||||
|
||||
let Some(res) = binding.first() else {
|
||||
|
||||
@@ -33,7 +33,7 @@ impl AccountMutedData {
|
||||
.limit()
|
||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let nks = ndb
|
||||
.query(txn, &[self.filter.clone()], lim)
|
||||
.query(txn, std::slice::from_ref(&self.filter), lim)
|
||||
.expect("query user muted results")
|
||||
.iter()
|
||||
.map(|qr| qr.note_key)
|
||||
|
||||
@@ -36,7 +36,7 @@ impl AccountRelayData {
|
||||
.limit()
|
||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let nks = ndb
|
||||
.query(txn, &[self.filter.clone()], lim)
|
||||
.query(txn, std::slice::from_ref(&self.filter), lim)
|
||||
.expect("query user relays results")
|
||||
.iter()
|
||||
.map(|qr| qr.note_key)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::account::FALLBACK_PUBKEY;
|
||||
use crate::i18n::Localization;
|
||||
use crate::persist::{AppSizeHandler, ZoomHandler};
|
||||
use crate::persist::{AppSizeHandler, SettingsHandler};
|
||||
use crate::wallet::GlobalWallet;
|
||||
use crate::zaps::Zaps;
|
||||
use crate::Error;
|
||||
use crate::JobPool;
|
||||
use crate::NotedeckOptions;
|
||||
use crate::{
|
||||
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
|
||||
UnknownIds,
|
||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
|
||||
};
|
||||
use egui::Margin;
|
||||
use egui::ThemePreference;
|
||||
@@ -19,6 +20,10 @@ use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use tracing::{error, info};
|
||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_activity::AndroidApp;
|
||||
|
||||
pub enum AppAction {
|
||||
Note(NoteAction),
|
||||
@@ -40,9 +45,8 @@ pub struct Notedeck {
|
||||
global_wallet: GlobalWallet,
|
||||
path: DataPath,
|
||||
args: Args,
|
||||
theme: ThemeHandler,
|
||||
settings: SettingsHandler,
|
||||
app: Option<Rc<RefCell<dyn App>>>,
|
||||
zoom: ZoomHandler,
|
||||
app_size: AppSizeHandler,
|
||||
unrecognized_args: BTreeSet<String>,
|
||||
clipboard: Clipboard,
|
||||
@@ -50,6 +54,9 @@ pub struct Notedeck {
|
||||
frame_history: FrameHistory,
|
||||
job_pool: JobPool,
|
||||
i18n: Localization,
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
android_app: Option<AndroidApp>,
|
||||
}
|
||||
|
||||
/// Our chrome, which is basically nothing
|
||||
@@ -99,10 +106,18 @@ impl eframe::App for Notedeck {
|
||||
|
||||
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);
|
||||
|
||||
if self.args.relay_debug {
|
||||
if self.args.options.contains(NotedeckOptions::RelayDebug) {
|
||||
if self.pool.debug.is_none() {
|
||||
self.pool.use_debug();
|
||||
}
|
||||
@@ -129,6 +144,11 @@ fn setup_puffin() {
|
||||
}
|
||||
|
||||
impl Notedeck {
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn set_android_context(&mut self, context: AndroidApp) {
|
||||
self.android_app = Some(context);
|
||||
}
|
||||
|
||||
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
|
||||
#[cfg(feature = "puffin")]
|
||||
setup_puffin();
|
||||
@@ -159,10 +179,11 @@ impl Notedeck {
|
||||
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 keystore = if parsed_args.use_keystore {
|
||||
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
|
||||
let keys_path = path.path(DataPathType::Keys);
|
||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||
Some(AccountStorage::new(
|
||||
@@ -213,12 +234,8 @@ impl Notedeck {
|
||||
|
||||
let img_cache = Images::new(img_cache_dir);
|
||||
let note_cache = NoteCache::default();
|
||||
let zoom = ZoomHandler::new(&path);
|
||||
let app_size = AppSizeHandler::new(&path);
|
||||
|
||||
if let Some(z) = zoom.get_zoom_factor() {
|
||||
ctx.set_zoom_factor(z);
|
||||
}
|
||||
let app_size = AppSizeHandler::new(&path);
|
||||
|
||||
// migrate
|
||||
if let Err(e) = img_cache.migrate_v0() {
|
||||
@@ -231,15 +248,22 @@ impl Notedeck {
|
||||
|
||||
// Initialize localization
|
||||
let mut i18n = Localization::new();
|
||||
|
||||
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
|
||||
settings.locale().parse();
|
||||
|
||||
if let Ok(setting_locale) = setting_locale {
|
||||
if let Err(err) = i18n.set_locale(setting_locale) {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(locale) = &parsed_args.locale {
|
||||
if let Err(err) = i18n.set_locale(locale.to_owned()) {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize global i18n context
|
||||
//crate::i18n::init_global_i18n(i18n.clone());
|
||||
|
||||
Self {
|
||||
ndb,
|
||||
img_cache,
|
||||
@@ -250,9 +274,8 @@ impl Notedeck {
|
||||
global_wallet,
|
||||
path: path.clone(),
|
||||
args: parsed_args,
|
||||
theme,
|
||||
settings,
|
||||
app: None,
|
||||
zoom,
|
||||
app_size,
|
||||
unrecognized_args,
|
||||
frame_history: FrameHistory::default(),
|
||||
@@ -260,9 +283,49 @@ impl Notedeck {
|
||||
zaps,
|
||||
job_pool,
|
||||
i18n,
|
||||
#[cfg(target_os = "android")]
|
||||
android_app: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.set_app(app);
|
||||
self
|
||||
@@ -279,12 +342,14 @@ impl Notedeck {
|
||||
global_wallet: &mut self.global_wallet,
|
||||
path: &self.path,
|
||||
args: &self.args,
|
||||
theme: &mut self.theme,
|
||||
settings: &mut self.settings,
|
||||
clipboard: &mut self.clipboard,
|
||||
zaps: &mut self.zaps,
|
||||
frame_history: &mut self.frame_history,
|
||||
job_pool: &mut self.job_pool,
|
||||
i18n: &mut self.i18n,
|
||||
#[cfg(target_os = "android")]
|
||||
android: self.android_app.as_ref().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +362,15 @@ impl Notedeck {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::NotedeckOptions;
|
||||
use enostr::{Keypair, Pubkey, SecretKey};
|
||||
use tracing::error;
|
||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||
|
||||
pub struct Args {
|
||||
pub relays: Vec<String>,
|
||||
pub is_mobile: Option<bool>,
|
||||
pub locale: Option<LanguageIdentifier>,
|
||||
pub show_note_client: bool,
|
||||
pub keys: Vec<Keypair>,
|
||||
pub light: bool,
|
||||
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 options: NotedeckOptions,
|
||||
pub dbpath: Option<String>,
|
||||
pub datapath: Option<String>,
|
||||
}
|
||||
@@ -28,14 +20,8 @@ impl Args {
|
||||
let mut unrecognized_args = BTreeSet::new();
|
||||
let mut res = Args {
|
||||
relays: vec![],
|
||||
is_mobile: None,
|
||||
keys: vec![],
|
||||
light: false,
|
||||
show_note_client: false,
|
||||
debug: false,
|
||||
relay_debug: false,
|
||||
tests: false,
|
||||
use_keystore: true,
|
||||
options: NotedeckOptions::default(),
|
||||
dbpath: None,
|
||||
datapath: None,
|
||||
locale: None,
|
||||
@@ -47,9 +33,9 @@ impl Args {
|
||||
let arg = &args[i];
|
||||
|
||||
if arg == "--mobile" {
|
||||
res.is_mobile = Some(true);
|
||||
res.options.set(NotedeckOptions::Mobile, true);
|
||||
} else if arg == "--light" {
|
||||
res.light = true;
|
||||
res.options.set(NotedeckOptions::LightTheme, true);
|
||||
} else if arg == "--locale" {
|
||||
i += 1;
|
||||
let Some(locale) = args.get(i) else {
|
||||
@@ -68,11 +54,11 @@ impl Args {
|
||||
}
|
||||
}
|
||||
} else if arg == "--dark" {
|
||||
res.light = false;
|
||||
res.options.set(NotedeckOptions::LightTheme, false);
|
||||
} else if arg == "--debug" {
|
||||
res.debug = true;
|
||||
res.options.set(NotedeckOptions::Debug, true);
|
||||
} else if arg == "--testrunner" {
|
||||
res.tests = true;
|
||||
res.options.set(NotedeckOptions::Tests, true);
|
||||
} else if arg == "--pub" || arg == "--npub" {
|
||||
i += 1;
|
||||
let pubstr = if let Some(next_arg) = args.get(i) {
|
||||
@@ -135,11 +121,13 @@ impl Args {
|
||||
};
|
||||
res.relays.push(relay.clone());
|
||||
} else if arg == "--no-keystore" {
|
||||
res.use_keystore = false;
|
||||
res.options.set(NotedeckOptions::UseKeystore, true);
|
||||
} else if arg == "--relay-debug" {
|
||||
res.relay_debug = true;
|
||||
} else if arg == "--show-note-client" {
|
||||
res.show_note_client = true;
|
||||
res.options.set(NotedeckOptions::RelayDebug, true);
|
||||
} else if arg == "--notebook" {
|
||||
res.options.set(NotedeckOptions::FeatureNotebook, true);
|
||||
} else if arg == "--clndash" {
|
||||
res.options.set(NotedeckOptions::FeatureClnDash, true);
|
||||
} else {
|
||||
unrecognized_args.insert(arg.clone());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
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,
|
||||
};
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
@@ -8,6 +8,9 @@ use egui_winit::clipboard::Clipboard;
|
||||
use enostr::RelayPool;
|
||||
use nostrdb::Ndb;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_activity::AndroidApp;
|
||||
use egui::{Pos2, Rect};
|
||||
// TODO: make this interface more sandboxed
|
||||
|
||||
pub struct AppContext<'a> {
|
||||
@@ -20,10 +23,68 @@ pub struct AppContext<'a> {
|
||||
pub global_wallet: &'a mut GlobalWallet,
|
||||
pub path: &'a DataPath,
|
||||
pub args: &'a Args,
|
||||
pub theme: &'a mut ThemeHandler,
|
||||
pub settings: &'a mut SettingsHandler,
|
||||
pub clipboard: &'a mut Clipboard,
|
||||
pub zaps: &'a mut Zaps,
|
||||
pub frame_history: &'a mut FrameHistory,
|
||||
pub job_pool: &'a mut JobPool,
|
||||
pub i18n: &'a mut Localization,
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub android: AndroidApp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SoftKeyboardContext {
|
||||
Virtual,
|
||||
Platform { ppp: f32 },
|
||||
}
|
||||
|
||||
impl SoftKeyboardContext {
|
||||
pub fn platform(context: &egui::Context) -> Self {
|
||||
Self::Platform {
|
||||
ppp: context.pixels_per_point(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AppContext<'a> {
|
||||
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
|
||||
match ctx {
|
||||
SoftKeyboardContext::Virtual => {
|
||||
let height = 400.0;
|
||||
skb_rect_from_screen_rect(screen_rect, height)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
SoftKeyboardContext::Platform { ppp } => {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use android_activity::InsetType;
|
||||
|
||||
// not sure why I need this, it seems to be consistently off by some amount of
|
||||
// pixels ?
|
||||
let fudge = 0.0;
|
||||
|
||||
let inset = self.android.get_window_insets(InsetType::Ime);
|
||||
let height = (inset.bottom as f32 / ppp) - fudge;
|
||||
skb_rect_from_screen_rect(screen_rect, height)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option<Rect> {
|
||||
if height == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let min = Pos2::new(0.0, screen_rect.max.y - height);
|
||||
Some(Rect::from_min_max(min, screen_rect.max))
|
||||
}
|
||||
|
||||
@@ -86,6 +86,13 @@ impl FilterStates {
|
||||
}
|
||||
self.states.insert(relay, state);
|
||||
}
|
||||
|
||||
/// For contacts, since that sub is managed elsewhere
|
||||
pub fn set_all_states(&mut self, state: FilterState) {
|
||||
for cur_state in self.states.values_mut() {
|
||||
*cur_state = state.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We may need to fetch some data from relays before our filter is ready.
|
||||
@@ -176,21 +183,24 @@ pub fn should_since_optimize(limit: u64, num_notes: usize) -> bool {
|
||||
limit as usize <= num_notes
|
||||
}
|
||||
|
||||
pub fn since_optimize_filter_with(filter: Filter, notes: &[NoteRef], since_gap: u64) -> Filter {
|
||||
pub fn since_optimize_filter_with(
|
||||
filter: Filter,
|
||||
latest_note: Option<&NoteRef>,
|
||||
since_gap: u64,
|
||||
) -> Filter {
|
||||
// Get the latest entry in the events
|
||||
if notes.is_empty() {
|
||||
let Some(latest) = latest_note else {
|
||||
return filter;
|
||||
}
|
||||
};
|
||||
|
||||
// get the latest note
|
||||
let latest = notes[0];
|
||||
let since = latest.created_at - since_gap;
|
||||
|
||||
filter.since_mut(since)
|
||||
}
|
||||
|
||||
pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
|
||||
since_optimize_filter_with(filter, notes, 60)
|
||||
pub fn since_optimize_filter(filter: Filter, latest: Option<&NoteRef>) -> Filter {
|
||||
since_optimize_filter_with(filter, latest, 60)
|
||||
}
|
||||
|
||||
pub fn default_limit() -> u64 {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use crate::{ui, NotedeckTextStyle};
|
||||
use egui::FontData;
|
||||
use egui::FontDefinitions;
|
||||
use egui::FontTweak;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum NamedFontFamily {
|
||||
Medium,
|
||||
@@ -31,6 +36,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
||||
NotedeckTextStyle::Button => 13.0,
|
||||
NotedeckTextStyle::Small => 12.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::Small => 12.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ const DE: LanguageIdentifier = langid!("de");
|
||||
const ES_419: LanguageIdentifier = langid!("es-419");
|
||||
const ES_ES: LanguageIdentifier = langid!("es-ES");
|
||||
const FR: LanguageIdentifier = langid!("fr");
|
||||
const JA: LanguageIdentifier = langid!("ja");
|
||||
const PT_BR: LanguageIdentifier = langid!("pt-BR");
|
||||
const PT_PT: LanguageIdentifier = langid!("pt-PT");
|
||||
const TH: LanguageIdentifier = langid!("th");
|
||||
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
|
||||
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
|
||||
const NUM_FTLS: usize = 10;
|
||||
const NUM_FTLS: usize = 12;
|
||||
|
||||
const EN_US_NATIVE_NAME: &str = "English (US)";
|
||||
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
|
||||
@@ -23,7 +25,9 @@ const DE_NATIVE_NAME: &str = "Deutsch";
|
||||
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
|
||||
const ES_ES_NATIVE_NAME: &str = "Español (España)";
|
||||
const FR_NATIVE_NAME: &str = "Français";
|
||||
const JA_NATIVE_NAME: &str = "日本語";
|
||||
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
|
||||
const PT_PT_NATIVE_NAME: &str = "Português (Portugal)";
|
||||
const TH_NATIVE_NAME: &str = "ภาษาไทย";
|
||||
const ZH_CN_NATIVE_NAME: &str = "简体中文";
|
||||
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
|
||||
@@ -58,10 +62,18 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
|
||||
identifier: FR,
|
||||
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: JA,
|
||||
ftl: include_str!("../../../../assets/translations/ja/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: PT_BR,
|
||||
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: PT_PT,
|
||||
ftl: include_str!("../../../../assets/translations/pt-PT/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: TH,
|
||||
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
|
||||
@@ -113,7 +125,9 @@ impl Default for Localization {
|
||||
ES_419.clone(),
|
||||
ES_ES.clone(),
|
||||
FR.clone(),
|
||||
JA.clone(),
|
||||
PT_BR.clone(),
|
||||
PT_PT.clone(),
|
||||
TH.clone(),
|
||||
ZH_CN.clone(),
|
||||
ZH_TW.clone(),
|
||||
@@ -126,7 +140,9 @@ impl Default for Localization {
|
||||
(ES_419, ES_419_NATIVE_NAME.to_owned()),
|
||||
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
|
||||
(FR, FR_NATIVE_NAME.to_owned()),
|
||||
(JA, JA_NATIVE_NAME.to_owned()),
|
||||
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
|
||||
(PT_PT, PT_PT_NATIVE_NAME.to_owned()),
|
||||
(TH, TH_NATIVE_NAME.to_owned()),
|
||||
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
|
||||
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use crate::media::gif::ensure_latest_texture_from_cache;
|
||||
use crate::media::images::ImageType;
|
||||
use crate::media::AnimationMode;
|
||||
use crate::urls::{UrlCache, UrlMimes};
|
||||
use crate::ImageMetadata;
|
||||
use crate::ObfuscationType;
|
||||
use crate::RenderableMedia;
|
||||
use crate::Result;
|
||||
use egui::TextureHandle;
|
||||
use image::{Delay, Frame};
|
||||
@@ -21,7 +27,7 @@ use tracing::warn;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TexturesCache {
|
||||
cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||
}
|
||||
|
||||
impl TexturesCache {
|
||||
@@ -29,7 +35,7 @@ impl TexturesCache {
|
||||
&mut self,
|
||||
url: &str,
|
||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||
) -> LoadableTextureState {
|
||||
) -> LoadableTextureState<'_> {
|
||||
let internal = self.handle_and_get_state_internal(url, true, closure);
|
||||
|
||||
internal.into()
|
||||
@@ -39,7 +45,7 @@ impl TexturesCache {
|
||||
&mut self,
|
||||
url: &str,
|
||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||
) -> TextureState {
|
||||
) -> TextureState<'_> {
|
||||
let internal = self.handle_and_get_state_internal(url, false, closure);
|
||||
|
||||
internal.into()
|
||||
@@ -90,7 +96,7 @@ impl TexturesCache {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> {
|
||||
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
|
||||
self.cache.get_mut(url).map(|state| {
|
||||
handle_occupied(state, true);
|
||||
state.into()
|
||||
@@ -141,6 +147,12 @@ pub enum TextureState<'a> {
|
||||
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> {
|
||||
fn from(value: &'a mut TextureStateInternal) -> Self {
|
||||
match value {
|
||||
@@ -402,6 +414,8 @@ pub struct Images {
|
||||
pub static_imgs: MediaCache,
|
||||
pub gifs: MediaCache,
|
||||
pub urls: UrlMimes,
|
||||
/// cached imeta data
|
||||
pub metadata: HashMap<String, ImageMetadata>,
|
||||
pub gif_states: GifStateMap,
|
||||
}
|
||||
|
||||
@@ -414,6 +428,7 @@ impl Images {
|
||||
gifs: MediaCache::new(&path, MediaCacheType::Gif),
|
||||
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
||||
gif_states: Default::default(),
|
||||
metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +437,65 @@ impl Images {
|
||||
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,
|
||||
animation_mode: AnimationMode,
|
||||
) -> 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,
|
||||
animation_mode,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => &self.static_imgs,
|
||||
@@ -465,3 +539,35 @@ pub struct GifState {
|
||||
pub next_frame_time: Option<SystemTime>,
|
||||
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 {
|
||||
let (tx, rx) = mpsc::channel::<Job>();
|
||||
|
||||
// TODO(jb55) why not mpmc here !???
|
||||
let arc_rx = Arc::new(Mutex::new(rx));
|
||||
for _ in 0..num_threads {
|
||||
let arc_rx_clone = arc_rx.clone();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::JobPool;
|
||||
use egui::TextureHandle;
|
||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||
use notedeck::JobPool;
|
||||
use poll_promise::Promise;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -12,16 +12,21 @@ mod frame_history;
|
||||
pub mod i18n;
|
||||
mod imgcache;
|
||||
mod job_pool;
|
||||
mod jobs;
|
||||
pub mod media;
|
||||
mod muted;
|
||||
pub mod name;
|
||||
mod nip51_set;
|
||||
pub mod note;
|
||||
mod notecache;
|
||||
mod options;
|
||||
mod persist;
|
||||
pub mod platform;
|
||||
pub mod profile;
|
||||
pub mod relay_debug;
|
||||
pub mod relayspec;
|
||||
mod result;
|
||||
mod setup;
|
||||
pub mod storage;
|
||||
mod style;
|
||||
pub mod theme;
|
||||
@@ -41,23 +46,33 @@ pub use account::relay::RelayAction;
|
||||
pub use account::FALLBACK_PUBKEY;
|
||||
pub use app::{App, AppAction, Notedeck};
|
||||
pub use args::Args;
|
||||
pub use context::AppContext;
|
||||
pub use context::{AppContext, SoftKeyboardContext};
|
||||
pub use error::{show_one_error_message, Error, FilterError, ZapError};
|
||||
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
||||
pub use fonts::NamedFontFamily;
|
||||
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
|
||||
pub use imgcache::{
|
||||
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
|
||||
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
|
||||
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
|
||||
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
|
||||
TexturedImage, TexturesCache,
|
||||
};
|
||||
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 name::NostrName;
|
||||
pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache};
|
||||
pub use note::{
|
||||
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
|
||||
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
||||
};
|
||||
pub use notecache::{CachedNote, NoteCache};
|
||||
pub use options::NotedeckOptions;
|
||||
pub use persist::*;
|
||||
pub use profile::get_profile_url;
|
||||
pub use relay_debug::RelayDebugView;
|
||||
@@ -67,6 +82,7 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
|
||||
pub use style::NotedeckTextStyle;
|
||||
pub use theme::ColorTheme;
|
||||
pub use time::time_ago_since;
|
||||
pub use time::time_format;
|
||||
pub use timecache::TimeCached;
|
||||
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
||||
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
||||
|
||||
127
crates/notedeck/src/media/action.rs
Normal file
127
crates/notedeck/src/media/action.rs
Normal file
@@ -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};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Blur<'a> {
|
||||
pub blurhash: &'a str,
|
||||
pub struct ImageMetadata {
|
||||
pub blurhash: String,
|
||||
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl PointDimensions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Blur<'_> {
|
||||
impl ImageMetadata {
|
||||
pub fn scaled_pixel_dimensions(
|
||||
&self,
|
||||
ui: &egui::Ui,
|
||||
@@ -75,9 +75,8 @@ impl Blur<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
||||
let mut blurs = HashMap::new();
|
||||
|
||||
/// Find blurhashes in image metadata and update our cache
|
||||
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
|
||||
for tag in note.tags() {
|
||||
let mut tag_iter = tag.into_iter();
|
||||
if tag_iter
|
||||
@@ -93,13 +92,11 @@ pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
||||
continue;
|
||||
};
|
||||
|
||||
blurs.insert(url, blur);
|
||||
blurs.insert(url.to_string(), blur);
|
||||
}
|
||||
|
||||
blurs
|
||||
}
|
||||
|
||||
fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
||||
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
|
||||
let mut url = None;
|
||||
let mut blurhash = None;
|
||||
let mut dims = None;
|
||||
@@ -138,21 +135,21 @@ fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
||||
});
|
||||
|
||||
Some((
|
||||
url,
|
||||
Blur {
|
||||
blurhash,
|
||||
url.to_string(),
|
||||
ImageMetadata {
|
||||
blurhash: blurhash.to_string(),
|
||||
dimensions,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ObfuscationType<'a> {
|
||||
Blurhash(Blur<'a>),
|
||||
pub enum ObfuscationType {
|
||||
Blurhash(ImageMetadata),
|
||||
Default,
|
||||
}
|
||||
|
||||
pub(crate) fn compute_blurhash(
|
||||
pub fn compute_blurhash(
|
||||
params: Option<JobParamsOwned>,
|
||||
dims: PixelDimensions,
|
||||
) -> Result<Job, JobError> {
|
||||
@@ -185,9 +182,9 @@ fn generate_blurhash_texturehandle(
|
||||
url: &str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> notedeck::Result<egui::TextureHandle> {
|
||||
) -> Result<egui::TextureHandle, crate::Error> {
|
||||
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);
|
||||
Ok(ctx.load_texture(url, img, Default::default()))
|
||||
164
crates/notedeck/src/media/gif.rs
Normal file
164
crates/notedeck/src/media/gif.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use std::{
|
||||
sync::mpsc::TryRecvError,
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
|
||||
use crate::media::AnimationMode;
|
||||
use crate::Animation;
|
||||
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
||||
use egui::TextureHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn ensure_latest_texture_from_cache(
|
||||
ui: &egui::Ui,
|
||||
url: &str,
|
||||
gifs: &mut GifStateMap,
|
||||
textures: &mut TexturesCache,
|
||||
animation_mode: AnimationMode,
|
||||
) -> Option<TextureHandle> {
|
||||
let tstate = textures.cache.get_mut(url)?;
|
||||
|
||||
let TextureState::Loaded(img) = tstate.into() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
|
||||
}
|
||||
|
||||
struct ProcessedGifFrame {
|
||||
texture: TextureHandle,
|
||||
maybe_new_state: Option<GifState>,
|
||||
repaint_at: Option<SystemTime>,
|
||||
}
|
||||
|
||||
/// Process a gif state frame, and optionally present a new
|
||||
/// state and when to repaint it
|
||||
fn process_gif_frame(
|
||||
animation: &Animation,
|
||||
frame_state: Option<&GifState>,
|
||||
animation_mode: AnimationMode,
|
||||
) -> ProcessedGifFrame {
|
||||
let now = Instant::now();
|
||||
|
||||
match frame_state {
|
||||
Some(prev_state) => {
|
||||
let should_advance = animation_mode.can_animate()
|
||||
&& (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
|
||||
|
||||
if should_advance {
|
||||
let maybe_new_index = if animation.receiver.is_some()
|
||||
|| prev_state.last_frame_index < animation.num_frames() - 1
|
||||
{
|
||||
prev_state.last_frame_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
match animation.get_frame(maybe_new_index) {
|
||||
Some(frame) => {
|
||||
let next_frame_time = match animation_mode {
|
||||
AnimationMode::Continuous { fps } => match fps {
|
||||
Some(fps) => {
|
||||
let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
|
||||
SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
|
||||
}
|
||||
None => SystemTime::now().checked_add(frame.delay),
|
||||
},
|
||||
|
||||
AnimationMode::NoAnimation | AnimationMode::Reactive => None,
|
||||
};
|
||||
|
||||
ProcessedGifFrame {
|
||||
texture: frame.texture.clone(),
|
||||
maybe_new_state: Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: frame.delay,
|
||||
next_frame_time,
|
||||
last_frame_index: maybe_new_index,
|
||||
}),
|
||||
repaint_at: next_frame_time,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let (texture, maybe_new_state) =
|
||||
match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (frame.texture.clone(), None),
|
||||
None => (animation.first_frame.texture.clone(), None),
|
||||
};
|
||||
|
||||
ProcessedGifFrame {
|
||||
texture,
|
||||
maybe_new_state,
|
||||
repaint_at: prev_state.next_frame_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (texture, maybe_new_state) =
|
||||
match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (frame.texture.clone(), None),
|
||||
None => (animation.first_frame.texture.clone(), None),
|
||||
};
|
||||
|
||||
ProcessedGifFrame {
|
||||
texture,
|
||||
maybe_new_state,
|
||||
repaint_at: prev_state.next_frame_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
None => ProcessedGifFrame {
|
||||
texture: animation.first_frame.texture.clone(),
|
||||
maybe_new_state: Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: animation.first_frame.delay,
|
||||
next_frame_time: None,
|
||||
last_frame_index: 0,
|
||||
}),
|
||||
repaint_at: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_latest_texture(
|
||||
ui: &egui::Ui,
|
||||
url: &str,
|
||||
gifs: &mut GifStateMap,
|
||||
img: &mut TexturedImage,
|
||||
animation_mode: AnimationMode,
|
||||
) -> TextureHandle {
|
||||
match img {
|
||||
TexturedImage::Static(handle) => handle.clone(),
|
||||
TexturedImage::Animated(animation) => {
|
||||
if let Some(receiver) = &animation.receiver {
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(frame) => animation.other_frames.push(frame),
|
||||
Err(TryRecvError::Empty) => {
|
||||
break;
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
animation.receiver = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
|
||||
|
||||
if let Some(new_state) = next_state.maybe_new_state {
|
||||
gifs.insert(url.to_owned(), new_state);
|
||||
}
|
||||
|
||||
if let Some(repaint) = next_state.repaint_at {
|
||||
tracing::trace!("requesting repaint for {url} after {repaint:?}");
|
||||
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
|
||||
ui.ctx().request_repaint_after(dur);
|
||||
}
|
||||
}
|
||||
|
||||
next_state.texture
|
||||
}
|
||||
}
|
||||
}
|
||||
475
crates/notedeck/src/media/images.rs
Normal file
475
crates/notedeck/src/media/images.rs
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
1
crates/notedeck/src/media/imeta.rs
Normal file
1
crates/notedeck/src/media/imeta.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
32
crates/notedeck/src/media/mod.rs
Normal file
32
crates/notedeck/src/media/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum AnimationMode {
|
||||
/// Only render when scrolling, network activity, etc
|
||||
Reactive,
|
||||
|
||||
/// Continuous with an optional target fps
|
||||
Continuous { fps: Option<f32> },
|
||||
|
||||
/// Disable animation
|
||||
NoAnimation,
|
||||
}
|
||||
|
||||
impl AnimationMode {
|
||||
pub fn can_animate(&self) -> bool {
|
||||
!matches!(self, Self::NoAnimation)
|
||||
}
|
||||
}
|
||||
9
crates/notedeck/src/media/renderable.rs
Normal file
9
crates/notedeck/src/media/renderable.rs
Normal file
@@ -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,
|
||||
}
|
||||
@@ -80,4 +80,8 @@ impl Muted {
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_pk_muted(&self, pk: &[u8; 32]) -> bool {
|
||||
self.pubkeys.contains(pk)
|
||||
}
|
||||
}
|
||||
|
||||
206
crates/notedeck/src/nip51_set.rs
Normal file
206
crates/notedeck/src/nip51_set.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use enostr::{Pubkey, RelayPool};
|
||||
use indexmap::IndexMap;
|
||||
use nostrdb::{Filter, Ndb, Note, Transaction};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{UnifiedSubscription, UnknownIds};
|
||||
|
||||
/// Keeps track of most recent NIP-51 sets
|
||||
#[derive(Debug)]
|
||||
pub struct Nip51SetCache {
|
||||
pub sub: UnifiedSubscription,
|
||||
cached_notes: IndexMap<PackId, Nip51Set>,
|
||||
}
|
||||
|
||||
type PackId = String;
|
||||
|
||||
impl Nip51SetCache {
|
||||
pub fn new(
|
||||
pool: &mut RelayPool,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
unknown_ids: &mut UnknownIds,
|
||||
nip51_set_filter: Vec<Filter>,
|
||||
) -> Option<Self> {
|
||||
let subid = Uuid::new_v4().to_string();
|
||||
let mut cached_notes = IndexMap::default();
|
||||
|
||||
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
|
||||
Some(results.into_iter().map(|r| r.note).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(notes) = notes {
|
||||
add(notes, &mut cached_notes, ndb, txn, unknown_ids);
|
||||
}
|
||||
|
||||
let sub = match ndb.subscribe(&nip51_set_filter) {
|
||||
Ok(sub) => sub,
|
||||
Err(e) => {
|
||||
tracing::error!("Could not ndb subscribe: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
pool.subscribe(subid.clone(), nip51_set_filter);
|
||||
|
||||
Some(Self {
|
||||
sub: UnifiedSubscription {
|
||||
local: sub,
|
||||
remote: subid,
|
||||
},
|
||||
cached_notes,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll_for_notes(&mut self, ndb: &Ndb, unknown_ids: &mut UnknownIds) {
|
||||
let new_notes = ndb.poll_for_notes(self.sub.local, 5);
|
||||
|
||||
if new_notes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
let notes: Vec<Note> = new_notes
|
||||
.into_iter()
|
||||
.filter_map(|new_note_key| ndb.get_note_by_key(&txn, new_note_key).ok())
|
||||
.collect();
|
||||
|
||||
add(notes, &mut self.cached_notes, ndb, &txn, unknown_ids);
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
|
||||
self.cached_notes.values()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.cached_notes.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cached_notes.is_empty()
|
||||
}
|
||||
|
||||
pub fn at_index(&self, index: usize) -> Option<&Nip51Set> {
|
||||
self.cached_notes.get_index(index).map(|(_, s)| s)
|
||||
}
|
||||
}
|
||||
|
||||
fn add(
|
||||
notes: Vec<Note>,
|
||||
cache: &mut IndexMap<PackId, Nip51Set>,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
unknown_ids: &mut UnknownIds,
|
||||
) {
|
||||
for note in notes {
|
||||
let Some(new_pack) = create_nip51_set(note) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(cur_cached) = cache.get(&new_pack.identifier) {
|
||||
if new_pack.created_at <= cur_cached.created_at {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for pk in &new_pack.pks {
|
||||
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
|
||||
}
|
||||
|
||||
cache.insert(new_pack.identifier.clone(), new_pack);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_nip51_set(note: Note) -> Option<Nip51Set> {
|
||||
let mut identifier = None;
|
||||
let mut title = None;
|
||||
let mut image = None;
|
||||
let mut description = None;
|
||||
let mut pks = Vec::new();
|
||||
|
||||
for tag in note.tags() {
|
||||
if tag.count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(first) = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match first {
|
||||
"p" => {
|
||||
let Some(pk) = tag.get_id(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
pks.push(Pubkey::new(*pk));
|
||||
}
|
||||
"d" => {
|
||||
let Some(id) = tag.get_str(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
identifier = Some(id.to_owned());
|
||||
}
|
||||
"image" => {
|
||||
let Some(cur_img) = tag.get_str(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
image = Some(cur_img.to_owned());
|
||||
}
|
||||
"title" => {
|
||||
let Some(cur_title) = tag.get_str(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
title = Some(cur_title.to_owned());
|
||||
}
|
||||
"description" => {
|
||||
let Some(cur_desc) = tag.get_str(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
description = Some(cur_desc.to_owned());
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let identifier = identifier?;
|
||||
|
||||
Some(Nip51Set {
|
||||
identifier,
|
||||
title,
|
||||
image,
|
||||
description,
|
||||
pks,
|
||||
created_at: note.created_at(),
|
||||
})
|
||||
}
|
||||
|
||||
/// NIP-51 Set. Read only (do not use for writing)
|
||||
pub struct Nip51Set {
|
||||
pub identifier: String, // 'd' tag
|
||||
pub title: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub pks: Vec<Pubkey>,
|
||||
created_at: u64,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Nip51Set {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Nip51Set")
|
||||
.field("identifier", &self.identifier)
|
||||
.field("title", &self.title)
|
||||
.field("image", &self.image)
|
||||
.field("description", &self.description)
|
||||
.field("pks", &self.pks.len())
|
||||
.field("created_at", &self.created_at)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::context::ContextSelection;
|
||||
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
|
||||
use crate::{zaps::NoteZapTargetOwned, MediaAction};
|
||||
use egui::Vec2;
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use poll_promise::Promise;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScrollInfo {
|
||||
@@ -25,7 +24,11 @@ pub enum NoteAction {
|
||||
Profile(Pubkey),
|
||||
|
||||
/// User has clicked a note link
|
||||
Note { note_id: NoteId, preview: bool },
|
||||
Note {
|
||||
note_id: NoteId,
|
||||
preview: bool,
|
||||
scroll_offset: f32,
|
||||
},
|
||||
|
||||
/// User has selected some context option
|
||||
Context(ContextSelection),
|
||||
@@ -45,6 +48,7 @@ impl NoteAction {
|
||||
NoteAction::Note {
|
||||
note_id: id,
|
||||
preview: false,
|
||||
scroll_offset: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,62 +65,3 @@ pub struct ZapTargetAmount {
|
||||
pub target: NoteZapTargetOwned,
|
||||
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 context;
|
||||
|
||||
pub use action::{MediaAction, NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
||||
|
||||
use crate::Accounts;
|
||||
|
||||
39
crates/notedeck/src/options.rs
Normal file
39
crates/notedeck/src/options.rs
Normal file
@@ -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;
|
||||
|
||||
/// Simulate is_compiled_as_mobile ?
|
||||
const Mobile = 1 << 6;
|
||||
|
||||
// ===== Feature Flags ======
|
||||
/// Is notebook enabled?
|
||||
const FeatureNotebook = 1 << 32;
|
||||
|
||||
/// Is clndash enabled?
|
||||
const FeatureClnDash = 1 << 33;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NotedeckOptions {
|
||||
fn default() -> Self {
|
||||
NotedeckOptions::UseKeystore
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
mod app_size;
|
||||
mod theme_handler;
|
||||
mod settings_handler;
|
||||
mod token_handler;
|
||||
mod zoom;
|
||||
|
||||
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 zoom::ZoomHandler;
|
||||
|
||||
253
crates/notedeck/src/persist/settings_handler.rs
Normal file
253
crates/notedeck/src/persist/settings_handler.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
use crate::platform::{file::emit_selected_file, SelectedMedia};
|
||||
use jni::{
|
||||
objects::{JByteArray, JClass, JObject, JObjectArray, JString},
|
||||
JNIEnv,
|
||||
};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub fn get_jvm() -> jni::JavaVM {
|
||||
unsafe { jni::JavaVM::from_raw(ndk_context::android_context().vm().cast()) }.unwrap()
|
||||
}
|
||||
|
||||
// Thread-safe static global
|
||||
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
@@ -16,7 +25,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
|
||||
debug!("updating virtual keyboard height {}", height);
|
||||
|
||||
// Convert and store atomically
|
||||
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
|
||||
KEYBOARD_HEIGHT.store(height.max(0), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Gets the current Android virtual keyboard height. Useful for transforming
|
||||
@@ -24,3 +33,80 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
|
||||
pub fn virtual_keyboard_height() -> i32 {
|
||||
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedFailed(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
juri: JString,
|
||||
je: JString,
|
||||
) {
|
||||
let _uri: String = env.get_string(&juri).unwrap().into();
|
||||
let _error: String = env.get_string(&je).unwrap().into();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedWithContent(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
// [display_name, size, mime_type]
|
||||
juri_info: JObjectArray,
|
||||
jcontent: JByteArray,
|
||||
) {
|
||||
debug!("File picked with content");
|
||||
|
||||
let display_name: Option<String> = {
|
||||
let obj = env.get_object_array_element(&juri_info, 0).unwrap();
|
||||
if obj.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(env.get_string(&JString::from(obj)).unwrap().into())
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(display_name) = display_name {
|
||||
let length = env.get_array_length(&jcontent).unwrap() as usize;
|
||||
let mut content: Vec<i8> = vec![0; length];
|
||||
env.get_byte_array_region(&jcontent, 0, &mut content)
|
||||
.unwrap();
|
||||
|
||||
debug!("selected file: {display_name:?} ({length:?} bytes)",);
|
||||
|
||||
emit_selected_file(SelectedMedia::from_bytes(
|
||||
display_name,
|
||||
content.into_iter().map(|b| b as u8).collect(),
|
||||
));
|
||||
} else {
|
||||
error!("Received null file name");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_open_file_picker() {
|
||||
match open_file_picker() {
|
||||
Ok(()) => {
|
||||
info!("File picker opened successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to open file picker: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_file_picker() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
// Get the Java VM from AndroidApp
|
||||
let vm = get_jvm();
|
||||
|
||||
// Attach current thread to get JNI environment
|
||||
let mut env = vm.attach_current_thread()?;
|
||||
|
||||
let context = unsafe { JObject::from_raw(ndk_context::android_context().context().cast()) };
|
||||
// Call the openFilePicker method on the MainActivity
|
||||
env.call_method(
|
||||
context,
|
||||
"openFilePicker",
|
||||
"()V", // Method signature: no parameters, void return
|
||||
&[], // No arguments
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
99
crates/notedeck/src/platform/file.rs
Normal file
99
crates/notedeck/src/platform/file.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{Error, SupportedMimeType};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MediaFrom {
|
||||
PathBuf(PathBuf),
|
||||
Memory(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SelectedMedia {
|
||||
pub from: MediaFrom,
|
||||
pub file_name: String,
|
||||
pub media_type: SupportedMimeType,
|
||||
}
|
||||
|
||||
impl SelectedMedia {
|
||||
pub fn from_path(path: PathBuf) -> Result<Self, Error> {
|
||||
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
||||
let media_type = SupportedMimeType::from_extension(ex)?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(&format!("file.{ex}"))
|
||||
.to_owned();
|
||||
|
||||
Ok(SelectedMedia {
|
||||
from: MediaFrom::PathBuf(path),
|
||||
file_name,
|
||||
media_type,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"{path:?} does not have an extension"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(file_name: String, content: Vec<u8>) -> Result<Self, Error> {
|
||||
if let Some(ex) = PathBuf::from_str(&file_name)
|
||||
.unwrap()
|
||||
.extension()
|
||||
.and_then(|f| f.to_str())
|
||||
{
|
||||
let media_type = SupportedMimeType::from_extension(ex)?;
|
||||
|
||||
Ok(SelectedMedia {
|
||||
from: MediaFrom::Memory(content),
|
||||
file_name,
|
||||
media_type,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"{file_name:?} does not have an extension"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelectedMediaChannel {
|
||||
sender: Sender<Result<SelectedMedia, Error>>,
|
||||
receiver: Receiver<Result<SelectedMedia, Error>>,
|
||||
}
|
||||
|
||||
impl Default for SelectedMediaChannel {
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = unbounded();
|
||||
Self { sender, receiver }
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedMediaChannel {
|
||||
pub fn new_selected_file(&self, media: Result<SelectedMedia, Error>) {
|
||||
let _ = self.sender.send(media);
|
||||
}
|
||||
|
||||
pub fn try_receive(&self) -> Option<Result<SelectedMedia, Error>> {
|
||||
self.receiver.try_recv().ok()
|
||||
}
|
||||
|
||||
pub fn receive(&self) -> Option<Result<SelectedMedia, Error>> {
|
||||
self.receiver.recv().ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub static SELECTED_MEDIA_CHANNEL: Lazy<SelectedMediaChannel> =
|
||||
Lazy::new(SelectedMediaChannel::default);
|
||||
|
||||
pub fn emit_selected_file(media: Result<SelectedMedia, Error>) {
|
||||
SELECTED_MEDIA_CHANNEL.new_selected_file(media);
|
||||
}
|
||||
|
||||
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
|
||||
SELECTED_MEDIA_CHANNEL.try_receive()
|
||||
}
|
||||
@@ -1,12 +1,39 @@
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod android;
|
||||
use crate::{platform::file::SelectedMedia, Error};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn virtual_keyboard_height() -> i32 {
|
||||
pub mod android;
|
||||
pub mod file;
|
||||
|
||||
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
|
||||
file::get_next_selected_file()
|
||||
}
|
||||
|
||||
const VIRT_HEIGHT: i32 = 400;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn virtual_keyboard_height(virt: bool) -> i32 {
|
||||
if virt {
|
||||
VIRT_HEIGHT
|
||||
} else {
|
||||
android::virtual_keyboard_height()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn virtual_keyboard_height() -> i32 {
|
||||
pub fn virtual_keyboard_height(virt: bool) -> i32 {
|
||||
if virt {
|
||||
VIRT_HEIGHT
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn virtual_keyboard_rect(ui: &egui::Ui, virt: bool) -> Option<egui::Rect> {
|
||||
let height = virtual_keyboard_height(virt);
|
||||
if height <= 0 {
|
||||
return None;
|
||||
}
|
||||
let screen_rect = ui.ctx().screen_rect();
|
||||
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
|
||||
Some(egui::Rect::from_min_max(min, screen_rect.max))
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ impl PartialEq for RelaySpec {
|
||||
|
||||
impl Eq for RelaySpec {}
|
||||
|
||||
#[allow(clippy::non_canonical_partial_ord_impl)]
|
||||
impl PartialOrd for RelaySpec {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.url.cmp(&other.url))
|
||||
|
||||
46
crates/notedeck/src/setup.rs
Normal file
46
crates/notedeck/src/setup.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
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(is_mobile);
|
||||
|
||||
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,
|
||||
Small,
|
||||
Tiny,
|
||||
NoteBody,
|
||||
}
|
||||
|
||||
impl NotedeckTextStyle {
|
||||
@@ -29,6 +30,7 @@ impl NotedeckTextStyle {
|
||||
Self::Button => TextStyle::Button,
|
||||
Self::Small => TextStyle::Small,
|
||||
Self::Tiny => TextStyle::Name("Tiny".into()),
|
||||
Self::NoteBody => TextStyle::Name("NoteBody".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ impl NotedeckTextStyle {
|
||||
Self::Button => FontFamily::Proportional,
|
||||
Self::Small => FontFamily::Proportional,
|
||||
Self::Tiny => FontFamily::Proportional,
|
||||
Self::NoteBody => FontFamily::Proportional,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
use egui::{
|
||||
style::{Selection, WidgetVisuals, Widgets},
|
||||
Color32, CornerRadius, Stroke, Visuals,
|
||||
};
|
||||
use crate::{fonts, NotedeckTextStyle};
|
||||
use egui::style::Interaction;
|
||||
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 {
|
||||
// VISUALS
|
||||
@@ -86,3 +114,131 @@ pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
|
||||
..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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{tr, Localization};
|
||||
use chrono::DateTime;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Time duration constants in seconds
|
||||
@@ -83,6 +84,14 @@ fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String
|
||||
}
|
||||
}
|
||||
|
||||
pub fn time_format(_i18n: &mut Localization, timestamp: u64) -> String {
|
||||
// TODO: format this using the selected locale
|
||||
DateTime::from_timestamp(timestamp as i64, 0)
|
||||
.unwrap()
|
||||
.format("%l:%M %p %b %d, %Y")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
||||
@@ -2,16 +2,16 @@ use crate::debouncer::Debouncer;
|
||||
use crate::{storage, DataPath, DataPathType, Directory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
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,
|
||||
file_name: String,
|
||||
debouncer: Debouncer,
|
||||
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 {
|
||||
let directory = Directory::new(path.path(path_type));
|
||||
let delay = Duration::from_millis(1000);
|
||||
@@ -30,11 +30,11 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
||||
self
|
||||
}
|
||||
|
||||
// returns whether successful
|
||||
/// Returns whether it actually wrote the new value
|
||||
pub fn try_save(&mut self, cur_item: T) -> bool {
|
||||
if self.debouncer.should_act() {
|
||||
if let Some(saved_item) = self.saved_item {
|
||||
if saved_item != cur_item {
|
||||
if let Some(ref saved_item) = self.saved_item {
|
||||
if *saved_item != cur_item {
|
||||
return self.save(cur_item);
|
||||
}
|
||||
} else {
|
||||
@@ -45,8 +45,8 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
||||
}
|
||||
|
||||
pub fn get_item(&self) -> Option<T> {
|
||||
if self.saved_item.is_some() {
|
||||
return self.saved_item;
|
||||
if let Some(ref item) = self.saved_item {
|
||||
return Some(item.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) {
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
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
|
||||
/// contexts, but with the nuance that we may also have a wide android tablet.
|
||||
pub fn is_narrow(ctx: &egui::Context) -> bool {
|
||||
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 {
|
||||
is_compiled_as_mobile()
|
||||
pub fn is_oled(is_mobile_override: bool) -> bool {
|
||||
is_mobile_override || is_compiled_as_mobile()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -195,13 +195,13 @@ impl UnknownIds {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &Pubkey) {
|
||||
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &[u8; 32]) {
|
||||
// we already have this profile, skip
|
||||
if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
let unknown_id = UnknownId::Pubkey(*pubkey);
|
||||
let unknown_id = UnknownId::Pubkey(Pubkey::new(*pubkey));
|
||||
if self.ids.contains_key(&unknown_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,9 @@ impl SupportedMimeType {
|
||||
{
|
||||
Ok(Self { mime })
|
||||
} else {
|
||||
Err(Error::Generic("Unsupported mime type".to_owned()))
|
||||
Err(Error::Generic(
|
||||
format!("{extension} Unsupported mime type",),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ impl UserAccount {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keypair(&self) -> KeypairUnowned {
|
||||
pub fn keypair(&self) -> KeypairUnowned<'_> {
|
||||
KeypairUnowned {
|
||||
pubkey: &self.key.pubkey,
|
||||
secret_key: self.key.secret_key.as_ref(),
|
||||
|
||||
@@ -9,6 +9,7 @@ license = "GPLv3"
|
||||
description = "The nostr browser"
|
||||
|
||||
[dependencies]
|
||||
bitflags = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui_tabs = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
@@ -16,6 +17,8 @@ egui = { workspace = true }
|
||||
notedeck_columns = { workspace = true }
|
||||
notedeck_ui = { workspace = true }
|
||||
notedeck_dave = { workspace = true }
|
||||
notedeck_notebook = { workspace = true }
|
||||
notedeck_clndash = { workspace = true }
|
||||
notedeck = { workspace = true }
|
||||
nostrdb = { workspace = true }
|
||||
puffin = { workspace = true, optional = true }
|
||||
@@ -63,6 +66,12 @@ short_description = "The nostr browser"
|
||||
identifier = "com.damus.notedeck"
|
||||
icon = ["assets/app_icon.icns"]
|
||||
|
||||
[package.metadata.android.manifest.queries]
|
||||
intent = [
|
||||
{ action = ["android.intent.action.MAIN"] },
|
||||
]
|
||||
|
||||
|
||||
[package.metadata.android]
|
||||
package = "com.damus.app"
|
||||
apk_name = "Notedeck"
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
>
|
||||
<intent-filter>
|
||||
@@ -23,9 +24,16 @@
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.level"
|
||||
android:required="true"
|
||||
android:version="1" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.damus.notedeck;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.Configuration;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
public class KeyboardHeightHelper {
|
||||
private static final String TAG = "KeyboardHeightHelper";
|
||||
private KeyboardHeightProvider keyboardHeightProvider;
|
||||
private Activity activity;
|
||||
|
||||
// Static JNI method not tied to any specific activity
|
||||
private static native void nativeKeyboardHeightChanged(int height);
|
||||
|
||||
public KeyboardHeightHelper(Activity activity) {
|
||||
this.activity = activity;
|
||||
keyboardHeightProvider = new KeyboardHeightProvider(activity);
|
||||
|
||||
// Create observer implementation
|
||||
KeyboardHeightObserver observer = (height, orientation) -> {
|
||||
Log.d(TAG, "Keyboard height: " + height + "px, orientation: " +
|
||||
(orientation == Configuration.ORIENTATION_PORTRAIT ? "portrait" : "landscape"));
|
||||
|
||||
// Call the generic native method
|
||||
nativeKeyboardHeightChanged(height);
|
||||
};
|
||||
|
||||
// Set up the provider
|
||||
keyboardHeightProvider.setKeyboardHeightObserver(observer);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
// Start the keyboard height provider after the view is ready
|
||||
final View contentView = activity.findViewById(android.R.id.content);
|
||||
contentView.post(() -> {
|
||||
keyboardHeightProvider.start();
|
||||
});
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
keyboardHeightProvider.setKeyboardHeightObserver(null);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
keyboardHeightProvider.close();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* This file is part of Siebe Projects samples.
|
||||
*
|
||||
* Siebe Projects samples is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Lesser GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Siebe Projects samples is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* Lesser GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Lesser GNU General Public License
|
||||
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.damus.notedeck;
|
||||
|
||||
/**
|
||||
* The observer that will be notified when the height of
|
||||
* the keyboard has changed
|
||||
*/
|
||||
public interface KeyboardHeightObserver {
|
||||
|
||||
/**
|
||||
* Called when the keyboard height has changed, 0 means keyboard is closed,
|
||||
* >= 1 means keyboard is opened.
|
||||
*
|
||||
* @param height The height of the keyboard in pixels
|
||||
* @param orientation The orientation either: Configuration.ORIENTATION_PORTRAIT or
|
||||
* Configuration.ORIENTATION_LANDSCAPE
|
||||
*/
|
||||
void onKeyboardHeightChanged(int height, int orientation);
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* This file is part of Siebe Projects samples.
|
||||
*
|
||||
* Siebe Projects samples is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Lesser GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Siebe Projects samples is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* Lesser GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Lesser GNU General Public License
|
||||
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.damus.notedeck;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Log;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
|
||||
/**
|
||||
* The keyboard height provider, this class uses a PopupWindow
|
||||
* to calculate the window height when the floating keyboard is opened and closed.
|
||||
*/
|
||||
public class KeyboardHeightProvider extends PopupWindow {
|
||||
|
||||
/** The tag for logging purposes */
|
||||
private final static String TAG = "sample_KeyboardHeightProvider";
|
||||
|
||||
/** The keyboard height observer */
|
||||
private KeyboardHeightObserver observer;
|
||||
|
||||
/** The cached landscape height of the keyboard */
|
||||
private int keyboardLandscapeHeight;
|
||||
|
||||
/** The cached portrait height of the keyboard */
|
||||
private int keyboardPortraitHeight;
|
||||
|
||||
/** The view that is used to calculate the keyboard height */
|
||||
private View popupView;
|
||||
|
||||
/** The parent view */
|
||||
private View parentView;
|
||||
|
||||
/** The root activity that uses this KeyboardHeightProvider */
|
||||
private Activity activity;
|
||||
|
||||
/**
|
||||
* Construct a new KeyboardHeightProvider
|
||||
*
|
||||
* @param activity The parent activity
|
||||
*/
|
||||
public KeyboardHeightProvider(Activity activity) {
|
||||
super(activity);
|
||||
this.activity = activity;
|
||||
|
||||
//LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
|
||||
//this.popupView = inflator.inflate(android.R.layout.popupwindow, null, false);
|
||||
this.popupView = new View(activity);
|
||||
setContentView(popupView);
|
||||
|
||||
setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE | LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
|
||||
|
||||
parentView = activity.findViewById(android.R.id.content);
|
||||
|
||||
setWidth(0);
|
||||
setHeight(LayoutParams.MATCH_PARENT);
|
||||
|
||||
popupView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (popupView != null) {
|
||||
handleOnGlobalLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the KeyboardHeightProvider, this must be called after the onResume of the Activity.
|
||||
* PopupWindows are not allowed to be registered before the onResume has finished
|
||||
* of the Activity.
|
||||
*/
|
||||
public void start() {
|
||||
|
||||
if (!isShowing() && parentView.getWindowToken() != null) {
|
||||
setBackgroundDrawable(new ColorDrawable(0));
|
||||
showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the keyboard height provider,
|
||||
* this provider will not be used anymore.
|
||||
*/
|
||||
public void close() {
|
||||
this.observer = null;
|
||||
dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the keyboard height observer to this provider. The
|
||||
* observer will be notified when the keyboard height has changed.
|
||||
* For example when the keyboard is opened or closed.
|
||||
*
|
||||
* @param observer The observer to be added to this provider.
|
||||
*/
|
||||
public void setKeyboardHeightObserver(KeyboardHeightObserver observer) {
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup window itself is as big as the window of the Activity.
|
||||
* The keyboard can then be calculated by extracting the popup view bottom
|
||||
* from the activity window height.
|
||||
*/
|
||||
private void handleOnGlobalLayout() {
|
||||
|
||||
Point screenSize = new Point();
|
||||
activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
|
||||
|
||||
Rect rect = new Rect();
|
||||
popupView.getWindowVisibleDisplayFrame(rect);
|
||||
|
||||
// REMIND, you may like to change this using the fullscreen size of the phone
|
||||
// and also using the status bar and navigation bar heights of the phone to calculate
|
||||
// the keyboard height. But this worked fine on a Nexus.
|
||||
int orientation = getScreenOrientation();
|
||||
int keyboardHeight = screenSize.y - rect.bottom;
|
||||
|
||||
if (keyboardHeight == 0) {
|
||||
notifyKeyboardHeightChanged(0, orientation);
|
||||
}
|
||||
else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
this.keyboardPortraitHeight = keyboardHeight;
|
||||
notifyKeyboardHeightChanged(keyboardPortraitHeight, orientation);
|
||||
}
|
||||
else {
|
||||
this.keyboardLandscapeHeight = keyboardHeight;
|
||||
notifyKeyboardHeightChanged(keyboardLandscapeHeight, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
private int getScreenOrientation() {
|
||||
return activity.getResources().getConfiguration().orientation;
|
||||
}
|
||||
|
||||
private void notifyKeyboardHeightChanged(int height, int orientation) {
|
||||
if (observer != null) {
|
||||
observer.onKeyboardHeightChanged(height, orientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.damus.notedeck;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.DisplayCutoutCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
@@ -15,13 +20,137 @@ import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.androidgamesdk.GameActivity;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class MainActivity extends GameActivity {
|
||||
static {
|
||||
System.loadLibrary("notedeck_chrome");
|
||||
static final int REQUEST_CODE_PICK_FILE = 420;
|
||||
|
||||
private native void nativeOnFilePickedFailed(String uri, String e);
|
||||
private native void nativeOnFilePickedWithContent(Object[] uri_info, byte[] content);
|
||||
|
||||
public void openFilePicker() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.setType("*/*");
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
|
||||
}
|
||||
|
||||
private native void nativeOnKeyboardHeightChanged(int height);
|
||||
private KeyboardHeightHelper keyboardHelper;
|
||||
private void setupInsets() {
|
||||
|
||||
// NOTE(jb55): This is needed for keyboard visibility. Without this the
|
||||
// window still gets the right insets, but they’re consumed before they
|
||||
// reach the NDK side.
|
||||
//WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
// NOTE(jb55): This is needed for keyboard visibility. If the bars are
|
||||
// permanently gone, Android routes the keyboard over the GL surface and
|
||||
// doesn’t change insets.
|
||||
//WindowInsetsControllerCompat ic = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||
//ic.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
|
||||
View content = getContent();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
mlp.topMargin = insets.top;
|
||||
mlp.leftMargin = insets.left;
|
||||
mlp.bottomMargin = insets.bottom;
|
||||
mlp.rightMargin = insets.right;
|
||||
v.setLayoutParams(mlp);
|
||||
|
||||
return windowInsets;
|
||||
});
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
}
|
||||
|
||||
private void processSelectedFile(Uri uri) {
|
||||
try {
|
||||
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
|
||||
} catch (Exception e) {
|
||||
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
|
||||
|
||||
nativeOnFilePickedFailed(uri.toString(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private Object[] getUriInfo(Uri uri) throws Exception {
|
||||
if (!uri.getScheme().equals("content")) {
|
||||
throw new Exception("uri should start with content://");
|
||||
}
|
||||
|
||||
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
Object[] info = new Object[3];
|
||||
|
||||
int col_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
info[0] = cursor.getString(col_idx);
|
||||
|
||||
col_idx = cursor.getColumnIndex(OpenableColumns.SIZE);
|
||||
info[1] = cursor.getLong(col_idx);
|
||||
|
||||
col_idx = cursor.getColumnIndex("mime_type");
|
||||
info[2] = cursor.getString(col_idx);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private byte[] readUriContent(Uri uri) {
|
||||
InputStream inputStream = null;
|
||||
ByteArrayOutputStream buffer = null;
|
||||
|
||||
try {
|
||||
inputStream = getContentResolver().openInputStream(uri);
|
||||
if (inputStream == null) {
|
||||
Log.e("MainActivity", "Could not open input stream for URI: " + uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
buffer = new ByteArrayOutputStream();
|
||||
byte[] data = new byte[8192]; // 8KB buffer
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = inputStream.read(data)) != -1) {
|
||||
buffer.write(data, 0, bytesRead);
|
||||
}
|
||||
|
||||
byte[] result = buffer.toByteArray();
|
||||
Log.d("MainActivity", "Successfully read " + result.length + " bytes");
|
||||
return result;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e("MainActivity", "IOException while reading URI: " + uri, e);
|
||||
return null;
|
||||
} catch (SecurityException e) {
|
||||
Log.e("MainActivity", "SecurityException while reading URI: " + uri, e);
|
||||
return null;
|
||||
} finally {
|
||||
// Close streams
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("MainActivity", "Error closing input stream", e);
|
||||
}
|
||||
}
|
||||
if (buffer != null) {
|
||||
try {
|
||||
buffer.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("MainActivity", "Error closing buffer", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -29,11 +158,32 @@ public class MainActivity extends GameActivity {
|
||||
|
||||
setupInsets();
|
||||
//setupFullscreen()
|
||||
keyboardHelper = new KeyboardHeightHelper(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
|
||||
if (data == null) return;
|
||||
|
||||
if (data.getClipData() != null) {
|
||||
// Multiple files selected
|
||||
ClipData clipData = data.getClipData();
|
||||
for (int i = 0; i < clipData.getItemCount(); i++) {
|
||||
Uri uri = clipData.getItemAt(i).getUri();
|
||||
processSelectedFile(uri);
|
||||
}
|
||||
} else if (data.getData() != null) {
|
||||
// Single file selected
|
||||
Uri uri = data.getData();
|
||||
processSelectedFile(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFullscreen() {
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
@@ -60,40 +210,19 @@ public class MainActivity extends GameActivity {
|
||||
return getWindow().getDecorView().findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
private void setupInsets() {
|
||||
View content = getContent();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
mlp.topMargin = insets.top;
|
||||
mlp.leftMargin = insets.left;
|
||||
mlp.bottomMargin = insets.bottom;
|
||||
mlp.rightMargin = insets.right;
|
||||
v.setLayoutParams(mlp);
|
||||
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
keyboardHelper.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
keyboardHelper.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
keyboardHelper.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,26 +2,21 @@
|
||||
//use egui_android::run_android;
|
||||
|
||||
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 tracing::error;
|
||||
|
||||
#[no_mangle]
|
||||
#[tokio::main]
|
||||
pub async fn android_main(app: AndroidApp) {
|
||||
pub async fn android_main(android_app: AndroidApp) {
|
||||
//use tracing_logcat::{LogcatMakeWriter, LogcatTag};
|
||||
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
//std::env::set_var("DAVE_ENDPOINT", "http://ollama.jb55.com/v1");
|
||||
//std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
"egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
|
||||
"egui=debug,egui-winit=debug,winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
|
||||
);
|
||||
|
||||
//std::env::set_var(
|
||||
@@ -46,7 +41,7 @@ pub async fn android_main(app: AndroidApp) {
|
||||
.with(fmt_layer)
|
||||
.init();
|
||||
|
||||
let path = app.internal_data_path().expect("data path");
|
||||
let path = android_app.internal_data_path().expect("data path");
|
||||
let mut options = eframe::NativeOptions {
|
||||
depth_buffer: 24,
|
||||
..eframe::NativeOptions::default()
|
||||
@@ -59,40 +54,20 @@ pub async fn android_main(app: AndroidApp) {
|
||||
// builder.with_android_app(app_clone_for_event_loop);
|
||||
//}));
|
||||
|
||||
options.android_app = Some(app.clone());
|
||||
options.android_app = Some(android_app.clone());
|
||||
|
||||
let app_args = get_app_args(app);
|
||||
let app_args = get_app_args();
|
||||
|
||||
let _res = eframe::run_native(
|
||||
"Damus Notedeck",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let ctx = &cc.egui_ctx;
|
||||
|
||||
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
||||
setup_chrome(ctx, ¬edeck.args(), notedeck.theme());
|
||||
|
||||
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_android_context(android_app);
|
||||
notedeck.setup(ctx);
|
||||
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
||||
notedeck.set_app(chrome);
|
||||
|
||||
Ok(Box::new(notedeck))
|
||||
@@ -129,7 +104,7 @@ Using internal storage would be better but it seems hard to get the config file
|
||||
the device ...
|
||||
*/
|
||||
|
||||
fn get_app_args(_app: AndroidApp) -> Vec<String> {
|
||||
fn get_app_args() -> Vec<String> {
|
||||
vec!["argv0-placeholder".to_string()]
|
||||
/*
|
||||
use serde_json::value;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use notedeck::{AppAction, AppContext};
|
||||
use notedeck_clndash::ClnDash;
|
||||
use notedeck_columns::Damus;
|
||||
use notedeck_dave::Dave;
|
||||
use notedeck_notebook::Notebook;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum NotedeckApp {
|
||||
Dave(Dave),
|
||||
Columns(Damus),
|
||||
Dave(Box<Dave>),
|
||||
Columns(Box<Damus>),
|
||||
Notebook(Box<Notebook>),
|
||||
ClnDash(Box<ClnDash>),
|
||||
Other(Box<dyn notedeck::App>),
|
||||
}
|
||||
|
||||
@@ -14,6 +18,8 @@ impl notedeck::App for NotedeckApp {
|
||||
match self {
|
||||
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
||||
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
||||
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
|
||||
NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
|
||||
NotedeckApp::Other(other) => other.update(ctx, ui),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,12 +1,12 @@
|
||||
pub mod fonts;
|
||||
pub mod setup;
|
||||
pub mod theme;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
|
||||
mod app;
|
||||
mod chrome;
|
||||
mod options;
|
||||
|
||||
pub use app::NotedeckApp;
|
||||
pub use chrome::Chrome;
|
||||
pub use options::ChromeOptions;
|
||||
|
||||
@@ -9,15 +9,8 @@ use re_memory::AccountingAllocator;
|
||||
static GLOBAL: AccountingAllocator<std::alloc::System> =
|
||||
AccountingAllocator::new(std::alloc::System);
|
||||
|
||||
use notedeck::enostr::Error;
|
||||
use notedeck::{DataPath, DataPathType, Notedeck};
|
||||
use notedeck_chrome::{
|
||||
setup::{generate_native_options, setup_chrome},
|
||||
Chrome, NotedeckApp,
|
||||
};
|
||||
use notedeck_columns::Damus;
|
||||
use notedeck_dave::Dave;
|
||||
use tracing::error;
|
||||
use notedeck_chrome::{setup::generate_native_options, Chrome};
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -93,29 +86,8 @@ async fn main() {
|
||||
let ctx = &cc.egui_ctx;
|
||||
|
||||
let mut notedeck = Notedeck::new(ctx, base_path, &args);
|
||||
|
||||
let mut chrome = Chrome::new();
|
||||
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.setup(ctx);
|
||||
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
|
||||
notedeck.set_app(chrome);
|
||||
|
||||
Ok(Box::new(notedeck))
|
||||
@@ -149,7 +121,8 @@ pub fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Damus, Notedeck};
|
||||
use super::Notedeck;
|
||||
use notedeck_columns::Damus;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn create_tmp_dir() -> PathBuf {
|
||||
@@ -210,7 +183,6 @@ mod tests {
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
|
||||
let unrecognized_args = notedeck.unrecognized_args().clone();
|
||||
let mut app_ctx = notedeck.app_context();
|
||||
let app = Damus::new(&mut app_ctx, &args);
|
||||
|
||||
|
||||
38
crates/notedeck_chrome/src/options.rs
Normal file
38
crates/notedeck_chrome/src/options.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ChromeOptions: u64 {
|
||||
/// Is the chrome currently open?
|
||||
const NoOptions = 0;
|
||||
|
||||
/// Is the chrome currently open?
|
||||
const IsOpen = 1 << 0;
|
||||
|
||||
/// Are we simulating a virtual keyboard? This is mostly for debugging
|
||||
/// if we are too lazy to open up a real mobile device with soft
|
||||
/// keyboard
|
||||
const VirtualKeyboard = 1 << 1;
|
||||
|
||||
/// Are we showing the memory debug window?
|
||||
const MemoryDebug = 1 << 2;
|
||||
|
||||
/// Repaint debug
|
||||
const RepaintDebug = 1 << 3;
|
||||
|
||||
/// We need soft keyboard visibility
|
||||
const KeyboardVisibility = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChromeOptions {
|
||||
fn default() -> Self {
|
||||
let mut options = ChromeOptions::NoOptions;
|
||||
options.set(
|
||||
ChromeOptions::IsOpen,
|
||||
!notedeck::ui::is_compiled_as_mobile(),
|
||||
);
|
||||
options
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,13 @@ impl PreviewRunner {
|
||||
"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));
|
||||
|
||||
|
||||
@@ -1,57 +1,6 @@
|
||||
use crate::{fonts, theme};
|
||||
|
||||
use eframe::NativeOptions;
|
||||
use egui::ThemePreference;
|
||||
use notedeck::{AppSizeHandler, DataPath};
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
21
crates/notedeck_clndash/Cargo.toml
Normal file
21
crates/notedeck_clndash/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "notedeck_clndash"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true }
|
||||
notedeck = { workspace = true }
|
||||
#notedeck_ui = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
lightning-invoice = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
nostrdb = { workspace = true }
|
||||
notedeck_ui = { workspace = true }
|
||||
|
||||
lnsocket = "0.5.1"
|
||||
77
crates/notedeck_clndash/README.md
Normal file
77
crates/notedeck_clndash/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# ⚡ clndash
|
||||
|
||||
Your Core Lightning dashboard, **without the server nonsense**.
|
||||
|
||||
clndash is a weird little experiment: a [notedeck][notedeck] app that talks to your node **directly over the Lightning Network** using [lnsocket][lnsocket] + [Commando][commando] RPCs.
|
||||
|
||||
No HTTP. No nginx. No VPS.
|
||||
Just open clndash, point it at your node, and boom — you’re in.
|
||||
|
||||
<img src="https://jb55.com/s/476285c50d06c3ce.png" width="50%" />
|
||||
|
||||
---
|
||||
|
||||
## 🤯 Why?
|
||||
|
||||
Because sometimes you just want to *see your channels* and *check invoices* without SSH-ing into a box and typing `lightning-cli`.
|
||||
|
||||
And because LN is already a secure, encrypted connection layer — why not just use that?
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Features (as of today)
|
||||
|
||||
* **Plug-and-play LN connection** – powered by [lnsocket][lnsocket]
|
||||
* **Commando RPC** – all dashboard data is fetched directly from your CLN node over Lightning
|
||||
* **Channel overview** – total capacity, inbound/outbound liquidity, largest channel, and pretty bars
|
||||
* **Invoices** – shows recent paid invoices (with zap previews if they came from Nostr)
|
||||
* **No extra daemons** – you don’t need to run a server to use it
|
||||
|
||||
---
|
||||
|
||||
## 🪄 Nostr Bonus
|
||||
|
||||
Because it’s a notedeck app, clndash can **render zaps** inline.
|
||||
Yes, your Core Lightning dashboard can now show you when someone on Nostr just sent you sats and why.
|
||||
|
||||
---
|
||||
|
||||
## 🏗 Still Baking
|
||||
|
||||
This is WIP.
|
||||
You’ll probably hit bugs. UI might be janky. Some features may vanish or suddenly mutate.
|
||||
|
||||
If you’re reading this and still excited — you’re the exact audience.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 How to connect
|
||||
|
||||
1. Get your node’s **public address** (host\:port) and a **Commando rune** with safe permissions.
|
||||
2. Set them as environment variables:
|
||||
|
||||
```bash
|
||||
export CLNDASH_ID="03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71"
|
||||
export CLNDASH_HOST="node.example.com:9735"
|
||||
export CLNDASH_RUNE="your_rune_here"
|
||||
```
|
||||
3. Run clndash inside notedeck by calling notedeck with the `--clndash` argument.
|
||||
4. Bask in the glow of real-time LN data over an LN connection.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
* Don’t give it a rune that can spend your funds.
|
||||
* Don’t blame me if you break something — this is experimental territory.
|
||||
* If it connects on the first try, buy yourself a beer.
|
||||
|
||||
---
|
||||
|
||||
If you like living on the edge of LN/Nostr tooling, you’ll like this.
|
||||
If you don’t… you’ll probably want to wait a bit.
|
||||
|
||||
|
||||
[commando]: https://docs.corelightning.org/reference/commando
|
||||
[lnsocket]: https://github.com/jb55/lnsocket-rs
|
||||
[notedeck]: https://github.com/damus-io/notedeck
|
||||
124
crates/notedeck_clndash/src/channels.rs
Normal file
124
crates/notedeck_clndash/src/channels.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use crate::event::LoadingState;
|
||||
use crate::ui;
|
||||
use egui::Color32;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ListPeerChannel {
|
||||
pub short_channel_id: String,
|
||||
pub our_reserve_msat: i64,
|
||||
pub to_us_msat: i64,
|
||||
pub total_msat: i64,
|
||||
pub their_reserve_msat: i64,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
pub to_us: i64,
|
||||
pub to_them: i64,
|
||||
pub original: ListPeerChannel,
|
||||
}
|
||||
|
||||
pub struct Channels {
|
||||
pub max_total_msat: i64,
|
||||
pub avail_in: i64,
|
||||
pub avail_out: i64,
|
||||
pub channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
pub fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
|
||||
match channels {
|
||||
LoadingState::Loaded(channels) => {
|
||||
if channels.channels.is_empty() {
|
||||
ui.label("no channels yet...");
|
||||
return;
|
||||
}
|
||||
|
||||
for channel in &channels.channels {
|
||||
channel_ui(ui, channel, channels.max_total_msat);
|
||||
}
|
||||
|
||||
ui.label(format!(
|
||||
"available out {}",
|
||||
ui::human_sat(channels.avail_out)
|
||||
));
|
||||
ui.label(format!("available in {}", ui::human_sat(channels.avail_in)));
|
||||
}
|
||||
LoadingState::Failed(err) => {
|
||||
ui.label(format!("error fetching channels: {err}"));
|
||||
}
|
||||
LoadingState::Loading => {
|
||||
ui.label("fetching channels...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
|
||||
// ---------- numbers ----------
|
||||
let short_channel_id = &c.original.short_channel_id;
|
||||
|
||||
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
|
||||
// Feel free to switch to log scaling if you have whales:
|
||||
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
|
||||
|
||||
// ---------- colors & style ----------
|
||||
let out_color = Color32::from_rgb(84, 69, 201); // blue
|
||||
let in_color = Color32::from_rgb(158, 56, 180); // purple
|
||||
|
||||
// Thickness scales with capacity, but keeps a nice minimum
|
||||
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
|
||||
let row_h = thickness + 14.0;
|
||||
|
||||
// ---------- layout ----------
|
||||
let (rect, response) = ui.allocate_exact_size(
|
||||
egui::vec2(ui.available_width(), row_h),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
let painter = ui.painter_at(rect);
|
||||
|
||||
let bar_rect = egui::Rect::from_min_max(
|
||||
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
|
||||
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
|
||||
);
|
||||
let corner_radius = (thickness * 0.5) as u8;
|
||||
let out_radius = egui::CornerRadius {
|
||||
ne: 0,
|
||||
nw: corner_radius,
|
||||
sw: corner_radius,
|
||||
se: 0,
|
||||
};
|
||||
let in_radius = egui::CornerRadius {
|
||||
ne: corner_radius,
|
||||
nw: 0,
|
||||
sw: 0,
|
||||
se: corner_radius,
|
||||
};
|
||||
/*
|
||||
painter.rect_filled(bar_rect, rounding, track_color);
|
||||
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
|
||||
*/
|
||||
|
||||
// Split widths
|
||||
let usable = (c.to_us + c.to_them).max(1) as f32;
|
||||
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
|
||||
let split_x = bar_rect.left() + out_w;
|
||||
|
||||
// Outbound fill (left)
|
||||
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
|
||||
if out_rect.width() > 0.5 {
|
||||
painter.rect_filled(out_rect, out_radius, out_color);
|
||||
}
|
||||
|
||||
// Inbound fill (right)
|
||||
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
|
||||
if in_rect.width() > 0.5 {
|
||||
painter.rect_filled(in_rect, in_radius, in_color);
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
response.on_hover_text_at_pointer(format!(
|
||||
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
|
||||
ui::human_sat(c.to_us),
|
||||
ui::human_sat(c.to_them),
|
||||
ui::human_sat(c.original.total_msat),
|
||||
));
|
||||
}
|
||||
80
crates/notedeck_clndash/src/event.rs
Normal file
80
crates/notedeck_clndash/src/event.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::channels::Channels;
|
||||
use crate::invoice::Invoice;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
pub enum ConnectionState {
|
||||
Dead(String),
|
||||
Connecting,
|
||||
Active,
|
||||
}
|
||||
pub enum LoadingState<T, E> {
|
||||
Loading,
|
||||
Failed(E),
|
||||
Loaded(T),
|
||||
}
|
||||
|
||||
impl<T, E> Default for LoadingState<T, E> {
|
||||
fn default() -> Self {
|
||||
Self::Loading
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> LoadingState<T, E> {
|
||||
fn _as_ref(&self) -> LoadingState<&T, &E> {
|
||||
match self {
|
||||
Self::Loading => LoadingState::<&T, &E>::Loading,
|
||||
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
|
||||
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
|
||||
match res {
|
||||
Ok(r) => LoadingState::Loaded(r),
|
||||
Err(err) => LoadingState::Failed(err),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fn unwrap(self) -> T {
|
||||
let Self::Loaded(t) = self else {
|
||||
panic!("unwrap in LoadingState");
|
||||
};
|
||||
|
||||
t
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct _WaitRequest {
|
||||
pub indexname: String,
|
||||
pub subsystem: String,
|
||||
pub nextvalue: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Request {
|
||||
GetInfo,
|
||||
ListPeerChannels,
|
||||
PaidInvoices(u32),
|
||||
}
|
||||
|
||||
/// Responses from the socket
|
||||
pub enum ClnResponse {
|
||||
GetInfo(Value),
|
||||
ListPeerChannels(Result<Channels, lnsocket::Error>),
|
||||
PaidInvoices(Result<Vec<Invoice>, lnsocket::Error>),
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
/// We lost the socket somehow
|
||||
Ended {
|
||||
reason: String,
|
||||
},
|
||||
|
||||
Connected,
|
||||
|
||||
Response(ClnResponse),
|
||||
}
|
||||
77
crates/notedeck_clndash/src/invoice.rs
Normal file
77
crates/notedeck_clndash/src/invoice.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::event::LoadingState;
|
||||
use crate::ui;
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use notedeck::AppContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Invoice {
|
||||
pub lastpay_index: Option<u64>,
|
||||
pub label: String,
|
||||
pub bolt11: Bolt11Invoice,
|
||||
pub payment_hash: String,
|
||||
pub amount_msat: u64,
|
||||
pub status: String,
|
||||
pub description: String,
|
||||
pub expires_at: u64,
|
||||
pub created_index: u64,
|
||||
pub updated_index: u64,
|
||||
}
|
||||
|
||||
pub fn invoices_ui(
|
||||
ui: &mut egui::Ui,
|
||||
invoice_notes: &HashMap<String, [u8; 32]>,
|
||||
ctx: &mut AppContext,
|
||||
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
|
||||
) {
|
||||
match invoices {
|
||||
LoadingState::Loading => {
|
||||
ui.label("loading invoices...");
|
||||
}
|
||||
|
||||
LoadingState::Failed(err) => {
|
||||
ui.label(format!("failed to load invoices: {err}"));
|
||||
}
|
||||
|
||||
LoadingState::Loaded(invoices) => {
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
|
||||
TableBuilder::new(ui)
|
||||
.column(Column::auto().resizable(true))
|
||||
.column(Column::remainder())
|
||||
.vscroll(false)
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.strong("description");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("amount");
|
||||
});
|
||||
})
|
||||
.body(|mut body| {
|
||||
for invoice in invoices {
|
||||
body.row(20.0, |mut row| {
|
||||
row.col(|ui| {
|
||||
if invoice.description.starts_with("{") {
|
||||
ui.label("Zap!").on_hover_ui_at_pointer(|ui| {
|
||||
ui::note_hover_ui(ui, &invoice.label, ctx, invoice_notes);
|
||||
});
|
||||
} else {
|
||||
ui.label(&invoice.description);
|
||||
}
|
||||
});
|
||||
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
|
||||
None => {
|
||||
ui.label("any");
|
||||
}
|
||||
Some(amt) => {
|
||||
ui.label(ui::human_verbose_sat(amt as i64));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
290
crates/notedeck_clndash/src/lib.rs
Normal file
290
crates/notedeck_clndash/src/lib.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use crate::channels::Channel;
|
||||
use crate::channels::Channels;
|
||||
use crate::channels::ListPeerChannel;
|
||||
use crate::event::ClnResponse;
|
||||
use crate::event::ConnectionState;
|
||||
use crate::event::Event;
|
||||
use crate::event::LoadingState;
|
||||
use crate::event::Request;
|
||||
use crate::invoice::Invoice;
|
||||
use crate::summary::Summary;
|
||||
use crate::watch::fetch_paid_invoices;
|
||||
|
||||
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
|
||||
use lnsocket::{CommandoClient, LNSocket};
|
||||
use nostrdb::Ndb;
|
||||
use notedeck::{AppAction, AppContext};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
|
||||
|
||||
mod channels;
|
||||
mod event;
|
||||
mod invoice;
|
||||
mod summary;
|
||||
mod ui;
|
||||
mod watch;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClnDash {
|
||||
initialized: bool,
|
||||
connection_state: ConnectionState,
|
||||
summary: LoadingState<Summary, lnsocket::Error>,
|
||||
get_info: LoadingState<String, lnsocket::Error>,
|
||||
channels: LoadingState<Channels, lnsocket::Error>,
|
||||
invoices: LoadingState<Vec<Invoice>, lnsocket::Error>,
|
||||
channel: Option<CommChannel>,
|
||||
last_summary: Option<Summary>,
|
||||
// invoice label to zapreq id
|
||||
invoice_zap_reqs: HashMap<String, [u8; 32]>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ZapReqId {
|
||||
#[serde(with = "hex::serde")]
|
||||
id: [u8; 32],
|
||||
}
|
||||
|
||||
impl Default for ConnectionState {
|
||||
fn default() -> Self {
|
||||
ConnectionState::Dead("uninitialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
struct CommChannel {
|
||||
req_tx: UnboundedSender<Request>,
|
||||
event_rx: UnboundedReceiver<Event>,
|
||||
}
|
||||
|
||||
impl notedeck::App for ClnDash {
|
||||
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
|
||||
if !self.initialized {
|
||||
self.connection_state = ConnectionState::Connecting;
|
||||
|
||||
self.setup_connection();
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
self.process_events(ctx.ndb);
|
||||
|
||||
self.show(ui, ctx);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ClnDash {
|
||||
fn show(&mut self, ui: &mut egui::Ui, ctx: &mut AppContext) {
|
||||
egui::Frame::new()
|
||||
.inner_margin(egui::Margin::same(20))
|
||||
.show(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui::connection_state_ui(ui, &self.connection_state);
|
||||
crate::summary::summary_ui(ui, self.last_summary.as_ref(), &self.summary);
|
||||
crate::invoice::invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
|
||||
crate::channels::channels_ui(ui, &self.channels);
|
||||
crate::ui::get_info_ui(ui, &self.get_info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_connection(&mut self) {
|
||||
let (req_tx, mut req_rx) = unbounded_channel::<Request>();
|
||||
let (event_tx, event_rx) = unbounded_channel::<Event>();
|
||||
self.channel = Some(CommChannel { req_tx, event_rx });
|
||||
|
||||
tokio::spawn(async move {
|
||||
let key = SecretKey::new(&mut rand::thread_rng());
|
||||
let their_pubkey = PublicKey::from_str(&std::env::var("CLNDASH_ID").unwrap_or(
|
||||
"03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71".to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let host = std::env::var("CLNDASH_HOST").unwrap_or("ln.damus.io:9735".to_string());
|
||||
let lnsocket = match LNSocket::connect_and_init(key, their_pubkey, &host).await {
|
||||
Err(err) => {
|
||||
let _ = event_tx.send(Event::Ended {
|
||||
reason: err.to_string(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Ok(lnsocket) => {
|
||||
let _ = event_tx.send(Event::Connected);
|
||||
lnsocket
|
||||
}
|
||||
};
|
||||
|
||||
let rune = std::env::var("CLNDASH_RUNE").unwrap_or(
|
||||
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
|
||||
);
|
||||
let commando = Arc::new(CommandoClient::spawn(lnsocket, &rune));
|
||||
|
||||
loop {
|
||||
match req_rx.recv().await {
|
||||
None => {
|
||||
let _ = event_tx.send(Event::Ended {
|
||||
reason: "channel dead?".to_string(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
Some(req) => {
|
||||
tracing::debug!("calling {req:?}");
|
||||
match req {
|
||||
Request::GetInfo => {
|
||||
let event_tx = event_tx.clone();
|
||||
let commando = commando.clone();
|
||||
tokio::spawn(async move {
|
||||
match commando.call("getinfo", json!({})).await {
|
||||
Ok(v) => {
|
||||
let _ = event_tx
|
||||
.send(Event::Response(ClnResponse::GetInfo(v)));
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("get_info error {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Request::PaidInvoices(n) => {
|
||||
let event_tx = event_tx.clone();
|
||||
let commando = commando.clone();
|
||||
tokio::spawn(async move {
|
||||
let invoices = fetch_paid_invoices(commando, n).await;
|
||||
let _ = event_tx
|
||||
.send(Event::Response(ClnResponse::PaidInvoices(invoices)));
|
||||
});
|
||||
}
|
||||
|
||||
Request::ListPeerChannels => {
|
||||
let event_tx = event_tx.clone();
|
||||
let commando = commando.clone();
|
||||
tokio::spawn(async move {
|
||||
let peer_channels =
|
||||
commando.call("listpeerchannels", json!({})).await;
|
||||
let channels = peer_channels.map(|v| {
|
||||
let peer_channels: Vec<ListPeerChannel> =
|
||||
serde_json::from_value(v["channels"].clone()).unwrap();
|
||||
to_channels(peer_channels)
|
||||
});
|
||||
let _ = event_tx.send(Event::Response(
|
||||
ClnResponse::ListPeerChannels(channels),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn process_events(&mut self, ndb: &Ndb) {
|
||||
let Some(channel) = &mut self.channel else {
|
||||
return;
|
||||
};
|
||||
|
||||
while let Ok(event) = channel.event_rx.try_recv() {
|
||||
match event {
|
||||
Event::Ended { reason } => {
|
||||
self.connection_state = ConnectionState::Dead(reason);
|
||||
}
|
||||
|
||||
Event::Connected => {
|
||||
self.connection_state = ConnectionState::Active;
|
||||
let _ = channel.req_tx.send(Request::GetInfo);
|
||||
let _ = channel.req_tx.send(Request::ListPeerChannels);
|
||||
let _ = channel.req_tx.send(Request::PaidInvoices(100));
|
||||
}
|
||||
|
||||
Event::Response(resp) => match resp {
|
||||
ClnResponse::ListPeerChannels(chans) => {
|
||||
if let LoadingState::Loaded(prev) = &self.channels {
|
||||
self.last_summary = Some(crate::summary::compute_summary(prev));
|
||||
}
|
||||
|
||||
self.summary = match &chans {
|
||||
Ok(chans) => {
|
||||
LoadingState::Loaded(crate::summary::compute_summary(chans))
|
||||
}
|
||||
Err(err) => LoadingState::Failed(err.clone()),
|
||||
};
|
||||
self.channels = LoadingState::from_result(chans);
|
||||
}
|
||||
|
||||
ClnResponse::GetInfo(value) => {
|
||||
let res = serde_json::to_string_pretty(&value);
|
||||
self.get_info =
|
||||
LoadingState::from_result(res.map_err(|_| lnsocket::Error::Json));
|
||||
}
|
||||
|
||||
ClnResponse::PaidInvoices(invoices) => {
|
||||
// process zap requests
|
||||
|
||||
if let Ok(invoices) = &invoices {
|
||||
for invoice in invoices {
|
||||
let zap_req_id: Option<ZapReqId> =
|
||||
serde_json::from_str(&invoice.description).ok();
|
||||
if let Some(zap_req_id) = zap_req_id {
|
||||
self.invoice_zap_reqs
|
||||
.insert(invoice.label.clone(), zap_req_id.id);
|
||||
let _ = ndb.process_event(&format!(
|
||||
"[\"EVENT\",\"a\",{}]",
|
||||
&invoice.description
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.invoices = LoadingState::from_result(invoices);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
|
||||
let mut avail_out: i64 = 0;
|
||||
let mut avail_in: i64 = 0;
|
||||
let mut max_total_msat: i64 = 0;
|
||||
|
||||
let mut channels: Vec<Channel> = peer_channels
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let to_us = (c.to_us_msat - c.our_reserve_msat).max(0);
|
||||
let to_them_raw = (c.total_msat - c.to_us_msat).max(0);
|
||||
let to_them = (to_them_raw - c.their_reserve_msat).max(0);
|
||||
|
||||
avail_out += to_us;
|
||||
avail_in += to_them;
|
||||
if c.total_msat > max_total_msat {
|
||||
max_total_msat = c.total_msat; // <-- max, not sum
|
||||
}
|
||||
|
||||
Channel {
|
||||
to_us,
|
||||
to_them,
|
||||
original: c,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
channels.sort_by(|a, b| {
|
||||
let a_capacity = a.to_them + a.to_us;
|
||||
let b_capacity = b.to_them + b.to_us;
|
||||
|
||||
a_capacity.partial_cmp(&b_capacity).unwrap().reverse()
|
||||
});
|
||||
|
||||
Channels {
|
||||
max_total_msat,
|
||||
avail_out,
|
||||
avail_in,
|
||||
channels,
|
||||
}
|
||||
}
|
||||
140
crates/notedeck_clndash/src/summary.rs
Normal file
140
crates/notedeck_clndash/src/summary.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use crate::channels::Channels;
|
||||
use crate::event::LoadingState;
|
||||
use crate::ui;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Summary {
|
||||
pub total_msat: i64,
|
||||
pub avail_out_msat: i64,
|
||||
pub avail_in_msat: i64,
|
||||
pub channel_count: usize,
|
||||
pub largest_msat: i64,
|
||||
pub outbound_pct: f32, // fraction of total capacity
|
||||
}
|
||||
|
||||
pub fn compute_summary(ch: &Channels) -> Summary {
|
||||
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
|
||||
let largest_msat: i64 = ch
|
||||
.channels
|
||||
.iter()
|
||||
.map(|c| c.original.total_msat)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let outbound_pct = if total_msat > 0 {
|
||||
ch.avail_out as f32 / total_msat as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Summary {
|
||||
total_msat,
|
||||
avail_out_msat: ch.avail_out,
|
||||
avail_in_msat: ch.avail_in,
|
||||
channel_count: ch.channels.len(),
|
||||
largest_msat,
|
||||
outbound_pct,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary_ui(
|
||||
ui: &mut egui::Ui,
|
||||
last_summary: Option<&Summary>,
|
||||
summary: &LoadingState<Summary, lnsocket::Error>,
|
||||
) {
|
||||
match summary {
|
||||
LoadingState::Loading => {
|
||||
ui.label("loading summary");
|
||||
}
|
||||
LoadingState::Failed(err) => {
|
||||
ui.label(format!("Failed to get summary: {err}"));
|
||||
}
|
||||
LoadingState::Loaded(summary) => {
|
||||
summary_cards_ui(ui, summary, last_summary);
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
|
||||
let old = prev.cloned().unwrap_or_default();
|
||||
let items: [(&str, String, Option<String>); 6] = [
|
||||
(
|
||||
"Total capacity",
|
||||
ui::human_sat(s.total_msat),
|
||||
prev.map(|_| ui::delta_str(s.total_msat, old.total_msat)),
|
||||
),
|
||||
(
|
||||
"Avail out",
|
||||
ui::human_sat(s.avail_out_msat),
|
||||
prev.map(|_| ui::delta_str(s.avail_out_msat, old.avail_out_msat)),
|
||||
),
|
||||
(
|
||||
"Avail in",
|
||||
ui::human_sat(s.avail_in_msat),
|
||||
prev.map(|_| ui::delta_str(s.avail_in_msat, old.avail_in_msat)),
|
||||
),
|
||||
("# Channels", s.channel_count.to_string(), None),
|
||||
("Largest", ui::human_sat(s.largest_msat), None),
|
||||
(
|
||||
"Outbound %",
|
||||
format!("{:.0}%", s.outbound_pct * 100.0),
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
// --- responsive columns ---
|
||||
let min_card = 160.0;
|
||||
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
|
||||
|
||||
egui::Grid::new("summary_grid")
|
||||
.num_columns(cols)
|
||||
.min_col_width(min_card)
|
||||
.spacing(egui::vec2(8.0, 8.0))
|
||||
.show(ui, |ui| {
|
||||
let items_len = items.len();
|
||||
for (i, (t, v, d)) in items.into_iter().enumerate() {
|
||||
card_cell(ui, t, v, d, min_card);
|
||||
|
||||
// End the row when we filled a row worth of cells
|
||||
if (i + 1) % cols == 0 {
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
// If the last row wasn't full, close it anyway
|
||||
if items_len % cols != 0 {
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
|
||||
let weak = ui.visuals().weak_text_color();
|
||||
egui::Frame::group(ui.style())
|
||||
.fill(ui.visuals().extreme_bg_color)
|
||||
.corner_radius(egui::CornerRadius::same(10))
|
||||
.inner_margin(egui::Margin::same(10))
|
||||
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
|
||||
.show(ui, |ui| {
|
||||
ui.set_min_width(min_card);
|
||||
ui.vertical(|ui| {
|
||||
ui.add(
|
||||
egui::Label::new(egui::RichText::new(title).small().color(weak))
|
||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.add(
|
||||
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
|
||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
||||
);
|
||||
if let Some(d) = delta {
|
||||
ui.add_space(2.0);
|
||||
ui.add(
|
||||
egui::Label::new(egui::RichText::new(d).small().color(weak))
|
||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.set_min_height(20.0);
|
||||
});
|
||||
}
|
||||
145
crates/notedeck_clndash/src/ui.rs
Normal file
145
crates/notedeck_clndash/src/ui.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::event::ConnectionState;
|
||||
use crate::event::LoadingState;
|
||||
use egui::Color32;
|
||||
use egui::Label;
|
||||
use egui::RichText;
|
||||
use egui::Widget;
|
||||
use notedeck::AppContext;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn note_hover_ui(
|
||||
ui: &mut egui::Ui,
|
||||
label: &str,
|
||||
ctx: &mut AppContext,
|
||||
invoice_notes: &HashMap<String, [u8; 32]>,
|
||||
) -> Option<notedeck::NoteAction> {
|
||||
let zap_req_id = invoice_notes.get(label)?;
|
||||
|
||||
let Ok(txn) = nostrdb::Transaction::new(ctx.ndb) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(zapreq_note) = ctx.ndb.get_note_by_id(&txn, zap_req_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
for tag in zapreq_note.tags() {
|
||||
let Some("e") = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(target_id) = tag.get_id(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(note) = ctx.ndb.get_note_by_id(&txn, target_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let author = ctx
|
||||
.ndb
|
||||
.get_profile_by_pubkey(&txn, zapreq_note.pubkey())
|
||||
.ok();
|
||||
|
||||
// TODO(jb55): make this less horrible
|
||||
let mut note_context = notedeck::NoteContext {
|
||||
ndb: ctx.ndb,
|
||||
accounts: ctx.accounts,
|
||||
img_cache: ctx.img_cache,
|
||||
note_cache: ctx.note_cache,
|
||||
zaps: ctx.zaps,
|
||||
pool: ctx.pool,
|
||||
job_pool: ctx.job_pool,
|
||||
unknown_ids: ctx.unknown_ids,
|
||||
clipboard: ctx.clipboard,
|
||||
i18n: ctx.i18n,
|
||||
global_wallet: ctx.global_wallet,
|
||||
};
|
||||
|
||||
let mut jobs = notedeck::JobsCache::default();
|
||||
let options = notedeck_ui::NoteOptions::default();
|
||||
|
||||
notedeck_ui::ProfilePic::from_profile_or_default(note_context.img_cache, author.as_ref())
|
||||
.ui(ui);
|
||||
|
||||
let nostr_name = notedeck::name::get_display_name(author.as_ref());
|
||||
ui.label(format!("{} zapped you", nostr_name.name()));
|
||||
|
||||
return notedeck_ui::NoteView::new(&mut note_context, ¬e, options, &mut jobs)
|
||||
.preview_style()
|
||||
.hide_media(true)
|
||||
.show(ui)
|
||||
.action;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
|
||||
ui.horizontal_wrapped(|ui| match info {
|
||||
LoadingState::Loading => {}
|
||||
LoadingState::Failed(err) => {
|
||||
ui.label(format!("failed to fetch node info: {err}"));
|
||||
}
|
||||
LoadingState::Loaded(info) => {
|
||||
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
|
||||
match state {
|
||||
ConnectionState::Active => {
|
||||
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
|
||||
}
|
||||
|
||||
ConnectionState::Connecting => {
|
||||
ui.add(Label::new(
|
||||
RichText::new("Connecting").color(Color32::YELLOW),
|
||||
));
|
||||
}
|
||||
|
||||
ConnectionState::Dead(reason) => {
|
||||
ui.add(Label::new(
|
||||
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- helper ----------
|
||||
pub fn human_sat(msat: i64) -> String {
|
||||
let sats = msat / 1000;
|
||||
if sats >= 1_000_000 {
|
||||
format!("{:.1}M", sats as f64 / 1_000_000.0)
|
||||
} else if sats >= 1_000 {
|
||||
format!("{:.1}k", sats as f64 / 1_000.0)
|
||||
} else {
|
||||
sats.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn human_verbose_sat(msat: i64) -> String {
|
||||
if msat < 1_000 {
|
||||
// less than 1 sat
|
||||
format!("{msat} msat")
|
||||
} else {
|
||||
let sats = msat / 1_000;
|
||||
if sats < 100_000_000 {
|
||||
// less than 1 BTC
|
||||
format!("{sats} sat")
|
||||
} else {
|
||||
let btc = sats / 100_000_000;
|
||||
format!("{btc} BTC")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delta_str(new: i64, old: i64) -> String {
|
||||
let d = new - old;
|
||||
match d.cmp(&0) {
|
||||
std::cmp::Ordering::Greater => format!("↑ {}", human_sat(d)),
|
||||
std::cmp::Ordering::Less => format!("↓ {}", human_sat(-d)),
|
||||
std::cmp::Ordering::Equal => "·".into(),
|
||||
}
|
||||
}
|
||||
198
crates/notedeck_clndash/src/watch.rs
Normal file
198
crates/notedeck_clndash/src/watch.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use crate::invoice::Invoice;
|
||||
use lnsocket::CallOpts;
|
||||
use lnsocket::CommandoClient;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdatedInvoicesResponse {
|
||||
updated: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PayIndexInvoices {
|
||||
invoices: Vec<PayIndexScan>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PayIndexScan {
|
||||
pay_index: Option<u64>,
|
||||
}
|
||||
|
||||
async fn find_lastpay_index(commando: Arc<CommandoClient>) -> Result<Option<u64>, lnsocket::Error> {
|
||||
const PAGE: u64 = 250;
|
||||
// 1) get the current updated tail
|
||||
let created_value = commando
|
||||
.call(
|
||||
"wait",
|
||||
json!({"subsystem":"invoices","indexname":"updated","nextvalue":0}),
|
||||
)
|
||||
.await?;
|
||||
let response: UpdatedInvoicesResponse =
|
||||
serde_json::from_value(created_value).map_err(|_| lnsocket::Error::Json)?;
|
||||
|
||||
// start our window at the tail
|
||||
let mut start_at = response
|
||||
.updated
|
||||
.saturating_add(1) // +1 because we want max(1, updated - PAGE + 1)
|
||||
.saturating_sub(PAGE)
|
||||
.max(1);
|
||||
|
||||
loop {
|
||||
// 2) fetch a window (indexed by "updated")
|
||||
let val = commando
|
||||
.call_with_opts(
|
||||
"listinvoices",
|
||||
json!({
|
||||
"index": "updated",
|
||||
"start": start_at,
|
||||
"limit": PAGE,
|
||||
}),
|
||||
// only fetch the one field we care about
|
||||
CallOpts::default().filter(json!({
|
||||
"invoices": [{"pay_index": true}]
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let parsed: PayIndexInvoices =
|
||||
serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
|
||||
|
||||
if let Some(pi) = parsed.invoices.iter().filter_map(|inv| inv.pay_index).max() {
|
||||
return Ok(Some(pi));
|
||||
}
|
||||
|
||||
// 4) no paid invoice in this slice—step back or bail
|
||||
if start_at == 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
start_at = start_at.saturating_sub(PAGE).max(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_paid_invoices(
|
||||
commando: Arc<CommandoClient>,
|
||||
limit: u32,
|
||||
) -> Result<Vec<Invoice>, lnsocket::Error> {
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
// look for an invoice with the last paid index
|
||||
let Some(lastpay_index) = find_lastpay_index(commando.clone()).await? else {
|
||||
// no paid invoices
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
let mut set: JoinSet<Result<Invoice, lnsocket::Error>> = JoinSet::new();
|
||||
let start = lastpay_index.saturating_sub(limit as u64);
|
||||
|
||||
// 3) Fire off at most `concurrency` `waitanyinvoice` calls at a time,
|
||||
// collect all successful responses into a Vec.
|
||||
// fire them ALL at once
|
||||
for idx in start..lastpay_index {
|
||||
let c = commando.clone();
|
||||
set.spawn(async move {
|
||||
let val = c
|
||||
.call(
|
||||
"waitanyinvoice",
|
||||
serde_json::json!({ "lastpay_index": idx }),
|
||||
)
|
||||
.await?;
|
||||
let parsed: Invoice = serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
|
||||
Ok(parsed)
|
||||
});
|
||||
}
|
||||
|
||||
let mut results = Vec::with_capacity(limit as usize);
|
||||
while let Some(res) = set.join_next().await {
|
||||
results.push(res.map_err(|_| lnsocket::Error::Io(std::io::ErrorKind::Interrupted))??);
|
||||
}
|
||||
|
||||
results.sort_by(|a, b| a.updated_index.cmp(&b.updated_index).reverse());
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// wip watch subsystem
|
||||
/*
|
||||
async fn watch_subsystem(
|
||||
commando: CommandoClient,
|
||||
subsystem: WaitSubsystem,
|
||||
index: WaitIndex,
|
||||
event_tx: UnboundedSender<Event>,
|
||||
mut cancel_rx: Receiver<()>,
|
||||
) {
|
||||
// Step 1: Fetch current index value so we can back up ~20
|
||||
let mut nextvalue: u64 = match commando
|
||||
.call(
|
||||
"wait",
|
||||
serde_json::json!({
|
||||
"indexname": index.as_str(),
|
||||
"subsystem": subsystem.as_str(),
|
||||
"nextvalue": 0
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => {
|
||||
// You showed the result has `updated` as the current highest index
|
||||
let current = v.get("updated").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
current.saturating_sub(20) // back up 20, clamp at 0
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("initial wait(…nextvalue=0) failed: {}", err);
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
// You can add a timeout to avoid hanging forever in weird network states.
|
||||
let fut = commando.call(
|
||||
"wait",
|
||||
serde_json::to_value(WaitRequest {
|
||||
indexname: "invoices".into(),
|
||||
subsystem: "lightningd".into(),
|
||||
nextvalue,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
tokio::select! {
|
||||
_ = &mut cancel_rx => {
|
||||
// graceful shutdown
|
||||
break;
|
||||
}
|
||||
|
||||
res = fut => {
|
||||
match res {
|
||||
Ok(v) => {
|
||||
// Typical shape: { "nextvalue": n, "invoicestatus": { ... } } (varies by plugin/index)
|
||||
// Adjust these lookups for your node’s actual wait payload.
|
||||
if let Some(nv) = v.get("nextvalue").and_then(|x| x.as_u64()) {
|
||||
nextvalue = nv + 1;
|
||||
} else {
|
||||
// Defensive: never get stuck — bump at least by 1
|
||||
nextvalue += 1;
|
||||
}
|
||||
|
||||
// Inspect/route
|
||||
let kind = v.get("status").and_then(|s| s.as_str());
|
||||
let ev = match kind {
|
||||
Some("paid") => ClnResponse::Invoice(InvoiceEvent::Paid(v.clone())),
|
||||
Some("created") => ClnResponse::Invoice(InvoiceEvent::Created(v.clone())),
|
||||
_ => ClnResponse::Invoice(InvoiceEvent::Other(v.clone())),
|
||||
};
|
||||
let _ = event_tx.send(Event::Response(ev));
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("wait(invoices) error: {err}");
|
||||
// small backoff so we don't tight-loop on persistent errors
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -10,6 +10,10 @@ description = "A tweetdeck-style notedeck app"
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = { workspace = true }
|
||||
ndk-context = "0.1"
|
||||
|
||||
[dependencies]
|
||||
opener = { workspace = true }
|
||||
rmpv = { workspace = true }
|
||||
@@ -32,7 +36,7 @@ image = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
nostrdb = { workspace = true }
|
||||
notedeck_ui = { workspace = true }
|
||||
open = { workspace = true }
|
||||
robius-open = { workspace = true }
|
||||
poll-promise = { workspace = true }
|
||||
puffin = { workspace = true, optional = true }
|
||||
puffin_egui = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use enostr::{FullKeypair, Pubkey};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
|
||||
use notedeck::{Accounts, AppContext, Localization, SingleUnkIdAction, UnknownIds};
|
||||
use notedeck::{Accounts, AppContext, JobsCache, Localization, SingleUnkIdAction, UnknownIds};
|
||||
use notedeck_ui::nip51_set::Nip51SetUiCache;
|
||||
|
||||
pub use crate::accounts::route::AccountsResponse;
|
||||
use crate::app::get_active_columns_mut;
|
||||
use crate::decks::DecksCache;
|
||||
use crate::onboarding::Onboarding;
|
||||
use crate::profile::send_new_contact_list;
|
||||
use crate::subscriptions::Subscriptions;
|
||||
use crate::ui::onboarding::{FollowPackOnboardingView, FollowPacksResponse, OnboardingResponse};
|
||||
use crate::{
|
||||
login_manager::AcquireKeyState,
|
||||
route::Route,
|
||||
timeline::TimelineCache,
|
||||
ui::{
|
||||
account_login_view::{AccountLoginResponse, AccountLoginView},
|
||||
accounts::{AccountsView, AccountsViewResponse},
|
||||
@@ -37,6 +41,7 @@ pub struct SwitchAccountAction {
|
||||
|
||||
/// The account to switch to
|
||||
pub switch_to: Pubkey,
|
||||
pub switching_to_new: bool,
|
||||
}
|
||||
|
||||
impl SwitchAccountAction {
|
||||
@@ -44,8 +49,14 @@ impl SwitchAccountAction {
|
||||
SwitchAccountAction {
|
||||
source_column,
|
||||
switch_to,
|
||||
switching_to_new: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switching_to_new(mut self) -> Self {
|
||||
self.switching_to_new = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -65,13 +76,13 @@ pub struct AddAccountAction {
|
||||
pub fn render_accounts_route(
|
||||
ui: &mut egui::Ui,
|
||||
app_ctx: &mut AppContext,
|
||||
col: usize,
|
||||
decks: &mut DecksCache,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
jobs: &mut JobsCache,
|
||||
login_state: &mut AcquireKeyState,
|
||||
onboarding: &mut Onboarding,
|
||||
follow_packs_ui: &mut Nip51SetUiCache,
|
||||
route: AccountsRoute,
|
||||
) -> AddAccountAction {
|
||||
let resp = match route {
|
||||
) -> Option<AccountsResponse> {
|
||||
match route {
|
||||
AccountsRoute::Accounts => AccountsView::new(
|
||||
app_ctx.ndb,
|
||||
app_ctx.accounts,
|
||||
@@ -80,47 +91,33 @@ pub fn render_accounts_route(
|
||||
)
|
||||
.ui(ui)
|
||||
.inner
|
||||
.map(AccountsRouteResponse::Accounts),
|
||||
|
||||
.map(AccountsRouteResponse::Accounts)
|
||||
.map(AccountsResponse::Account),
|
||||
AccountsRoute::AddAccount => {
|
||||
AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
|
||||
.ui(ui)
|
||||
.inner
|
||||
.map(AccountsRouteResponse::AddAccount)
|
||||
.map(AccountsResponse::Account)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(resp) = resp {
|
||||
match resp {
|
||||
AccountsRouteResponse::Accounts(response) => {
|
||||
let action = process_accounts_view_response(
|
||||
AccountsRoute::Onboarding => FollowPackOnboardingView::new(
|
||||
onboarding,
|
||||
follow_packs_ui,
|
||||
app_ctx.ndb,
|
||||
app_ctx.img_cache,
|
||||
app_ctx.i18n,
|
||||
app_ctx.accounts,
|
||||
decks,
|
||||
col,
|
||||
response,
|
||||
);
|
||||
AddAccountAction {
|
||||
accounts_action: action,
|
||||
unk_id_action: SingleUnkIdAction::no_action(),
|
||||
}
|
||||
}
|
||||
AccountsRouteResponse::AddAccount(response) => {
|
||||
let action =
|
||||
process_login_view_response(app_ctx, timeline_cache, decks, col, response);
|
||||
*login_state = Default::default();
|
||||
let router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
|
||||
.column_mut(col)
|
||||
.router_mut();
|
||||
router.go_back();
|
||||
action
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AddAccountAction {
|
||||
accounts_action: None,
|
||||
unk_id_action: SingleUnkIdAction::no_action(),
|
||||
app_ctx.job_pool,
|
||||
jobs,
|
||||
)
|
||||
.ui(ui)
|
||||
.map(|r| match r {
|
||||
OnboardingResponse::FollowPacks(follow_packs_response) => {
|
||||
AccountsResponse::Account(AccountsRouteResponse::AddAccount(
|
||||
AccountLoginResponse::Onboarding(follow_packs_response),
|
||||
))
|
||||
}
|
||||
OnboardingResponse::ViewProfile(pubkey) => AccountsResponse::ViewProfile(pubkey),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,31 +152,53 @@ pub fn process_accounts_view_response(
|
||||
|
||||
pub fn process_login_view_response(
|
||||
app_ctx: &mut AppContext,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
decks: &mut DecksCache,
|
||||
subs: &mut Subscriptions,
|
||||
onboarding: &mut Onboarding,
|
||||
col: usize,
|
||||
response: AccountLoginResponse,
|
||||
) -> AddAccountAction {
|
||||
let (r, pubkey) = match response {
|
||||
AccountLoginResponse::CreateNew => {
|
||||
let kp = FullKeypair::generate();
|
||||
let pubkey = kp.pubkey;
|
||||
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool);
|
||||
(app_ctx.accounts.add_account(kp.to_keypair()), pubkey)
|
||||
}
|
||||
AccountLoginResponse::LoginWith(keypair) => {
|
||||
let pubkey = keypair.pubkey;
|
||||
(app_ctx.accounts.add_account(keypair), pubkey)
|
||||
}
|
||||
};
|
||||
let cur_router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
|
||||
.column_mut(col)
|
||||
.router_mut();
|
||||
|
||||
decks.add_deck_default(app_ctx, timeline_cache, pubkey);
|
||||
let r = match response {
|
||||
AccountLoginResponse::LoginWith(keypair) => {
|
||||
cur_router.go_back();
|
||||
app_ctx.accounts.add_account(keypair)
|
||||
}
|
||||
AccountLoginResponse::CreatingNew => {
|
||||
cur_router.route_to(Route::Accounts(AccountsRoute::Onboarding));
|
||||
|
||||
onboarding.process(app_ctx.pool, app_ctx.ndb, subs, app_ctx.unknown_ids);
|
||||
|
||||
None
|
||||
}
|
||||
AccountLoginResponse::Onboarding(onboarding_response) => match onboarding_response {
|
||||
FollowPacksResponse::NoFollowPacks => {
|
||||
onboarding.process(app_ctx.pool, app_ctx.ndb, subs, app_ctx.unknown_ids);
|
||||
None
|
||||
}
|
||||
FollowPacksResponse::UserSelectedPacks(nip51_sets_ui_state) => {
|
||||
let pks_to_follow = nip51_sets_ui_state.get_all_selected();
|
||||
|
||||
let kp = FullKeypair::generate();
|
||||
|
||||
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool, pks_to_follow);
|
||||
cur_router.go_back();
|
||||
onboarding.end_onboarding(app_ctx.pool, app_ctx.ndb);
|
||||
|
||||
app_ctx.accounts.add_account(kp.to_keypair())
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(action) = r {
|
||||
AddAccountAction {
|
||||
accounts_action: Some(AccountsAction::Switch(SwitchAccountAction {
|
||||
source_column: col,
|
||||
switch_to: action.switch_to,
|
||||
switching_to_new: true,
|
||||
})),
|
||||
unk_id_action: action.unk_id_action,
|
||||
}
|
||||
@@ -190,3 +209,41 @@ pub fn process_login_view_response(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsRouteResponse {
|
||||
pub fn process(
|
||||
self,
|
||||
app_ctx: &mut AppContext,
|
||||
app: &mut crate::Damus,
|
||||
col: usize,
|
||||
) -> AddAccountAction {
|
||||
match self {
|
||||
AccountsRouteResponse::Accounts(response) => {
|
||||
let action = process_accounts_view_response(
|
||||
app_ctx.i18n,
|
||||
app_ctx.accounts,
|
||||
&mut app.decks_cache,
|
||||
col,
|
||||
response,
|
||||
);
|
||||
AddAccountAction {
|
||||
accounts_action: action,
|
||||
unk_id_action: notedeck::SingleUnkIdAction::no_action(),
|
||||
}
|
||||
}
|
||||
AccountsRouteResponse::AddAccount(response) => {
|
||||
let action = process_login_view_response(
|
||||
app_ctx,
|
||||
&mut app.decks_cache,
|
||||
&mut app.subscriptions,
|
||||
&mut app.onboarding,
|
||||
col,
|
||||
response,
|
||||
);
|
||||
app.view_state.login = Default::default();
|
||||
|
||||
action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,16 @@ pub enum AccountsRouteResponse {
|
||||
AddAccount(AccountLoginResponse),
|
||||
}
|
||||
|
||||
pub enum AccountsResponse {
|
||||
ViewProfile(enostr::Pubkey),
|
||||
Account(AccountsRouteResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum AccountsRoute {
|
||||
Accounts,
|
||||
AddAccount,
|
||||
Onboarding,
|
||||
}
|
||||
|
||||
impl AccountsRoute {
|
||||
@@ -19,6 +25,7 @@ impl AccountsRoute {
|
||||
match self {
|
||||
Self::Accounts => &["accounts", "show"],
|
||||
Self::AddAccount => &["accounts", "new"],
|
||||
Self::Onboarding => &["accounts", "onboarding"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
column::Columns,
|
||||
nav::{RouterAction, RouterType},
|
||||
route::Route,
|
||||
timeline::{
|
||||
thread::{
|
||||
selected_has_at_least_n_replies, InsertionResponse, NoteSeenFlags, ThreadNode, Threads,
|
||||
},
|
||||
ThreadSelection, TimelineCache, TimelineKind,
|
||||
thread::{selected_has_at_least_n_replies, NoteSeenFlags, ThreadNode, Threads},
|
||||
InsertionResponse, ThreadSelection, TimelineCache, TimelineKind,
|
||||
},
|
||||
view_state::ViewState,
|
||||
};
|
||||
|
||||
use enostr::{NoteId, Pubkey, RelayPool};
|
||||
@@ -16,6 +17,7 @@ use notedeck::{
|
||||
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
||||
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
||||
};
|
||||
use notedeck_ui::media::MediaViewerFlags;
|
||||
use tracing::error;
|
||||
|
||||
pub struct NewNotes {
|
||||
@@ -28,8 +30,9 @@ pub enum NotesOpenResult {
|
||||
Thread(NewThreadNotes),
|
||||
}
|
||||
|
||||
pub enum TimelineOpenResult {
|
||||
NewNotes(NewNotes),
|
||||
pub struct TimelineOpenResult {
|
||||
new_notes: Option<NewNotes>,
|
||||
new_pks: Option<HashSet<Pubkey>>,
|
||||
}
|
||||
|
||||
struct NoteActionResponse {
|
||||
@@ -51,6 +54,7 @@ fn execute_note_action(
|
||||
global_wallet: &mut GlobalWallet,
|
||||
zaps: &mut Zaps,
|
||||
images: &mut Images,
|
||||
view_state: &mut ViewState,
|
||||
router_type: RouterType,
|
||||
ui: &mut egui::Ui,
|
||||
col: usize,
|
||||
@@ -78,7 +82,11 @@ fn execute_note_action(
|
||||
.open(ndb, note_cache, txn, pool, &kind)
|
||||
.map(NotesOpenResult::Timeline);
|
||||
}
|
||||
NoteAction::Note { note_id, preview } => 'ex: {
|
||||
NoteAction::Note {
|
||||
note_id,
|
||||
preview,
|
||||
scroll_offset,
|
||||
} => 'ex: {
|
||||
let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id)
|
||||
else {
|
||||
tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes()));
|
||||
@@ -86,7 +94,15 @@ fn execute_note_action(
|
||||
};
|
||||
|
||||
timeline_res = threads
|
||||
.open(ndb, txn, pool, &thread_selection, preview, col)
|
||||
.open(
|
||||
ndb,
|
||||
txn,
|
||||
pool,
|
||||
&thread_selection,
|
||||
preview,
|
||||
col,
|
||||
scroll_offset,
|
||||
)
|
||||
.map(NotesOpenResult::Thread);
|
||||
|
||||
let route = Route::Thread(thread_selection);
|
||||
@@ -153,7 +169,16 @@ fn execute_note_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 +205,7 @@ pub fn execute_and_process_note_action(
|
||||
global_wallet: &mut GlobalWallet,
|
||||
zaps: &mut Zaps,
|
||||
images: &mut Images,
|
||||
view_state: &mut ViewState,
|
||||
ui: &mut egui::Ui,
|
||||
) -> Option<RouterAction> {
|
||||
let router_type = {
|
||||
@@ -204,6 +230,7 @@ pub fn execute_and_process_note_action(
|
||||
global_wallet,
|
||||
zaps,
|
||||
images,
|
||||
view_state,
|
||||
router_type,
|
||||
ui,
|
||||
col,
|
||||
@@ -244,7 +271,24 @@ fn clear_zap_error(sender: &Pubkey, zaps: &mut Zaps, target: &NoteZapTargetOwned
|
||||
|
||||
impl TimelineOpenResult {
|
||||
pub fn new_notes(notes: Vec<NoteKey>, id: TimelineKind) -> Self {
|
||||
Self::NewNotes(NewNotes::new(notes, id))
|
||||
Self {
|
||||
new_notes: Some(NewNotes { id, notes }),
|
||||
new_pks: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_pks(pks: HashSet<Pubkey>) -> Self {
|
||||
Self {
|
||||
new_notes: None,
|
||||
new_pks: Some(pks),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_pks(&mut self, pks: HashSet<Pubkey>) {
|
||||
match &mut self.new_pks {
|
||||
Some(cur_pks) => cur_pks.extend(pks),
|
||||
None => self.new_pks = Some(pks),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(
|
||||
@@ -255,11 +299,17 @@ impl TimelineOpenResult {
|
||||
storage: &mut TimelineCache,
|
||||
unknown_ids: &mut UnknownIds,
|
||||
) {
|
||||
match self {
|
||||
// update the thread for next render if we have new notes
|
||||
TimelineOpenResult::NewNotes(new_notes) => {
|
||||
if let Some(new_notes) = &self.new_notes {
|
||||
new_notes.process(storage, ndb, txn, unknown_ids, note_cache);
|
||||
}
|
||||
|
||||
let Some(pks) = &self.new_pks else {
|
||||
return;
|
||||
};
|
||||
|
||||
for pk in pks {
|
||||
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,7 +411,7 @@ pub fn process_thread_notes(
|
||||
created_at,
|
||||
};
|
||||
|
||||
if thread.replies.contains(¬e_ref) {
|
||||
if thread.replies.contains_key(¬e_ref.key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,28 +4,31 @@ use crate::{
|
||||
decks::{Decks, DecksCache},
|
||||
draft::Drafts,
|
||||
nav::{self, ProcessNavResult},
|
||||
onboarding::Onboarding,
|
||||
options::AppOptions,
|
||||
route::Route,
|
||||
storage,
|
||||
subscriptions::{SubKind, Subscriptions},
|
||||
support::Support,
|
||||
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
||||
ui::{self, DesktopSidePanel, SidePanelAction},
|
||||
toolbar::unseen_notification,
|
||||
ui::{self, toolbar::toolbar, DesktopSidePanel, SidePanelAction},
|
||||
view_state::ViewState,
|
||||
Result,
|
||||
};
|
||||
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{
|
||||
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::path::Path;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -56,18 +59,20 @@ pub struct Damus {
|
||||
pub note_options: NoteOptions,
|
||||
|
||||
pub unrecognized_args: BTreeSet<String>,
|
||||
|
||||
/// keep track of follow packs
|
||||
pub onboarding: Onboarding,
|
||||
}
|
||||
|
||||
fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
|
||||
fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
|
||||
for event in &input.raw.events {
|
||||
if let egui::Event::Key {
|
||||
key, pressed: true, ..
|
||||
} = event
|
||||
{
|
||||
match key {
|
||||
match event {
|
||||
egui::Event::Key { key, pressed, .. } if *pressed => match key {
|
||||
egui::Key::J => {
|
||||
columns.select_down();
|
||||
//columns.select_down();
|
||||
{}
|
||||
}
|
||||
/*
|
||||
egui::Key::K => {
|
||||
columns.select_up();
|
||||
}
|
||||
@@ -77,11 +82,18 @@ fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
|
||||
egui::Key::L => {
|
||||
columns.select_left();
|
||||
}
|
||||
*/
|
||||
egui::Key::BrowserBack | egui::Key::Escape => {
|
||||
columns.get_selected_router().go_back();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
egui::Event::InsetsChanged => {
|
||||
tracing::debug!("insets have changed!");
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +105,7 @@ fn try_process_event(
|
||||
) -> Result<()> {
|
||||
let current_columns =
|
||||
get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
|
||||
ctx.input(|i| handle_key_events(i, current_columns));
|
||||
ctx.input(|i| handle_egui_events(i, current_columns));
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let wakeup = move || {
|
||||
@@ -136,13 +148,14 @@ fn try_process_event(
|
||||
}
|
||||
}
|
||||
|
||||
for (_kind, timeline) in &mut damus.timeline_cache {
|
||||
for (kind, timeline) in &mut damus.timeline_cache {
|
||||
let is_ready = timeline::is_timeline_ready(
|
||||
app_ctx.ndb,
|
||||
app_ctx.pool,
|
||||
app_ctx.note_cache,
|
||||
timeline,
|
||||
app_ctx.accounts,
|
||||
app_ctx.unknown_ids,
|
||||
);
|
||||
|
||||
if is_ready {
|
||||
@@ -161,8 +174,15 @@ fn try_process_event(
|
||||
}
|
||||
} else {
|
||||
// TODO: show loading?
|
||||
if matches!(kind, TimelineKind::List(ListKind::Contact(_))) {
|
||||
timeline::fetch_contact_list(&mut damus.subscriptions, timeline, app_ctx.accounts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(follow_packs) = damus.onboarding.get_follow_packs_mut() {
|
||||
follow_packs.poll_for_notes(app_ctx.ndb, app_ctx.unknown_ids);
|
||||
}
|
||||
|
||||
if app_ctx.unknown_ids.ready_to_send() {
|
||||
unknown_id_send(app_ctx.unknown_ids, app_ctx.pool);
|
||||
@@ -203,6 +223,7 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con
|
||||
app_ctx.ndb,
|
||||
app_ctx.note_cache,
|
||||
&mut damus.timeline_cache,
|
||||
app_ctx.unknown_ids,
|
||||
) {
|
||||
warn!("update_damus init: {err}");
|
||||
}
|
||||
@@ -359,18 +380,54 @@ fn render_damus(
|
||||
app_ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
) -> Option<AppAction> {
|
||||
damus
|
||||
.note_options
|
||||
.set(NoteOptions::Wide, is_narrow(ui.ctx()));
|
||||
|
||||
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
|
||||
render_damus_mobile(damus, app_ctx, ui)
|
||||
} else {
|
||||
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
|
||||
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
||||
//ui.ctx().request_repaint_after(Duration::from_secs(5));
|
||||
|
||||
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 {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -404,6 +461,14 @@ impl Damus {
|
||||
let mut options = AppOptions::default();
|
||||
let tmp_columns = !parsed_args.columns.is_empty();
|
||||
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 {
|
||||
info!("DecksCache: loading from command line arguments");
|
||||
@@ -450,35 +515,8 @@ impl Damus {
|
||||
};
|
||||
|
||||
let support = Support::new(app_context.path);
|
||||
let mut note_options = NoteOptions::default();
|
||||
note_options.set(
|
||||
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 note_options = get_note_options(parsed_args, app_context.settings);
|
||||
let jobs = JobsCache::default();
|
||||
|
||||
let threads = Threads::default();
|
||||
|
||||
Self {
|
||||
@@ -495,6 +533,7 @@ impl Damus {
|
||||
unrecognized_args,
|
||||
jobs,
|
||||
threads,
|
||||
onboarding: Onboarding::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,6 +584,7 @@ impl Damus {
|
||||
unrecognized_args: BTreeSet::default(),
|
||||
jobs: JobsCache::default(),
|
||||
threads: Threads::default(),
|
||||
onboarding: Onboarding::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +595,36 @@ impl Damus {
|
||||
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
||||
&self.unrecognized_args
|
||||
}
|
||||
|
||||
pub fn toolbar_height() -> f32 {
|
||||
48.0
|
||||
}
|
||||
|
||||
pub fn initially_selected_toolbar_index() -> i32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
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::RepliesNewestFirst,
|
||||
settings_handler.show_replies_newest_first(),
|
||||
);
|
||||
note_options
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -574,10 +644,25 @@ fn render_damus_mobile(
|
||||
) -> Option<AppAction> {
|
||||
//let routes = app.timelines[0].routes.clone();
|
||||
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let mut app_action: Option<AppAction> = None;
|
||||
|
||||
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
||||
let mut app_action: Option<AppAction> = None;
|
||||
// don't show toolbar if soft keyboard is open
|
||||
let skb_rect = app_ctx.soft_keyboard_rect(
|
||||
ui.ctx().screen_rect(),
|
||||
notedeck::SoftKeyboardContext::platform(ui.ctx()),
|
||||
);
|
||||
let toolbar_height = if skb_rect.is_none() {
|
||||
Damus::toolbar_height()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder()) // top cell
|
||||
.size(Size::exact(toolbar_height)) // bottom cell
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
||||
let r = nav::render_nav(
|
||||
active_col,
|
||||
@@ -603,6 +688,27 @@ fn render_damus_mobile(
|
||||
}
|
||||
|
||||
hovering_post_button(ui, app, app_ctx, rect);
|
||||
});
|
||||
|
||||
strip.cell(|ui| 'brk: {
|
||||
if toolbar_height <= 0.0 {
|
||||
break 'brk;
|
||||
}
|
||||
|
||||
let unseen_notif = unseen_notification(
|
||||
app,
|
||||
app_ctx.ndb,
|
||||
app_ctx.accounts.get_selected_account().key.pubkey,
|
||||
);
|
||||
|
||||
if skb_rect.is_none() {
|
||||
let resp = toolbar(ui, unseen_notif);
|
||||
if let Some(action) = resp {
|
||||
action.process(app, app_ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app_action
|
||||
}
|
||||
@@ -618,8 +724,10 @@ fn hovering_post_button(
|
||||
let button_y = ui
|
||||
.ctx()
|
||||
.animate_bool_responsive(btn_id, should_show_compose);
|
||||
rect.min.x = rect.max.x - if is_narrow(ui.ctx()) { 60.0 } else { 100.0 };
|
||||
rect.min.y = rect.max.y - 100.0 * button_y;
|
||||
|
||||
rect.min.x = rect.max.x - (if is_narrow(ui.ctx()) { 60.0 } else { 100.0 } * button_y);
|
||||
rect.min.y = rect.max.y - 100.0;
|
||||
rect.max.x += 48.0 * (1.0 - button_y);
|
||||
|
||||
let darkmode = ui.ctx().style().visuals.dark_mode;
|
||||
|
||||
@@ -796,7 +904,13 @@ fn timelines_view(
|
||||
let mut save_cols = false;
|
||||
if let Some(action) = side_panel_action {
|
||||
save_cols = save_cols
|
||||
|| action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx, ui.ctx());
|
||||
|| action.process(
|
||||
&mut app.timeline_cache,
|
||||
&mut app.decks_cache,
|
||||
ctx,
|
||||
&mut app.subscriptions,
|
||||
ui.ctx(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut app_action: Option<AppAction> = None;
|
||||
|
||||
@@ -11,8 +11,6 @@ pub enum ColumnsFlag {
|
||||
Textmode,
|
||||
Scramble,
|
||||
NoMedia,
|
||||
ShowNoteClientTop,
|
||||
ShowNoteClientBottom,
|
||||
}
|
||||
|
||||
pub struct ColumnsArgs {
|
||||
@@ -54,10 +52,6 @@ impl ColumnsArgs {
|
||||
res.clear_flag(ColumnsFlag::SinceOptimize);
|
||||
} else if arg == "--scramble" {
|
||||
res.set_flag(ColumnsFlag::Scramble);
|
||||
} else if arg == "--show-note-client=top" {
|
||||
res.set_flag(ColumnsFlag::ShowNoteClientTop);
|
||||
} else if arg == "--show-note-client=bottom" {
|
||||
res.set_flag(ColumnsFlag::ShowNoteClientBottom);
|
||||
} else if arg == "--no-media" {
|
||||
res.set_flag(ColumnsFlag::NoMedia);
|
||||
} else if arg == "--filter" {
|
||||
@@ -146,7 +140,16 @@ impl ColumnsArgs {
|
||||
} else if column_name == "universe" {
|
||||
debug!("got universe column");
|
||||
res.columns
|
||||
.push(ArgColumn::Timeline(TimelineKind::Universe))
|
||||
.push(ArgColumn::Timeline(TimelineKind::Universe));
|
||||
} else if let Some(hashtag) = column_name.strip_prefix("hashtag:") {
|
||||
let hashtags: Vec<String> = hashtag
|
||||
.split(",")
|
||||
.map(str::trim)
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
res.columns
|
||||
.push(ArgColumn::Timeline(TimelineKind::Hashtag(hashtags)));
|
||||
} else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") {
|
||||
if let Ok(pubkey) = Pubkey::parse(profile_pk_str) {
|
||||
info!("got profile column for user {}", pubkey.hex());
|
||||
|
||||
@@ -75,12 +75,10 @@ impl Columns {
|
||||
/// Select the column based on the timeline kind.
|
||||
///
|
||||
/// TODO: add timeline if missing?
|
||||
pub fn select_by_kind(&mut self, kind: &TimelineKind) -> SelectionResult {
|
||||
pub fn select_by_route(&mut self, desired_route: Route) -> SelectionResult {
|
||||
for (i, col) in self.columns.iter().enumerate() {
|
||||
for route in col.router().routes() {
|
||||
if let Some(timeline) = route.timeline_id() {
|
||||
if timeline == kind {
|
||||
tracing::info!("selecting {kind:?} column");
|
||||
if *route == desired_route {
|
||||
if self.selected as usize == i {
|
||||
return SelectionResult::AlreadySelected(i);
|
||||
} else {
|
||||
@@ -90,10 +88,20 @@ impl Columns {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(&desired_route, Route::Timeline(_))
|
||||
|| matches!(&desired_route, Route::Thread(_))
|
||||
{
|
||||
// these require additional handling to add state
|
||||
tracing::error!("failed to select {desired_route:?} column");
|
||||
return SelectionResult::Failed;
|
||||
}
|
||||
|
||||
tracing::error!("failed to select {kind:?} column");
|
||||
SelectionResult::Failed
|
||||
self.add_column(Column::new(vec![desired_route]));
|
||||
|
||||
let selected_index = self.columns.len() - 1;
|
||||
self.select_column(selected_index as i32);
|
||||
SelectionResult::NewSelection(selected_index)
|
||||
}
|
||||
|
||||
pub fn add_new_timeline_column(
|
||||
|
||||
@@ -190,7 +190,7 @@ impl DecksCache {
|
||||
&self.fallback_pubkey
|
||||
}
|
||||
|
||||
pub fn get_all_decks_mut(&mut self) -> ValuesMut<Pubkey, Decks> {
|
||||
pub fn get_all_decks_mut(&mut self) -> ValuesMut<'_, Pubkey, Decks> {
|
||||
self.account_to_decks.values_mut()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod login_manager;
|
||||
mod media_upload;
|
||||
mod multi_subscriber;
|
||||
mod nav;
|
||||
mod onboarding;
|
||||
pub mod options;
|
||||
mod post;
|
||||
mod profile;
|
||||
@@ -27,6 +28,7 @@ mod subscriptions;
|
||||
mod support;
|
||||
mod test_data;
|
||||
pub mod timeline;
|
||||
mod toolbar;
|
||||
pub mod ui;
|
||||
mod unknowns;
|
||||
mod view_state;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
#![cfg_attr(target_os = "android", allow(dead_code, unused_variables))]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::Error;
|
||||
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||
use ehttp::Request;
|
||||
use nostrdb::{Note, NoteBuilder};
|
||||
use notedeck::SupportedMimeType;
|
||||
use notedeck::{
|
||||
media::images::fetch_binary_from_disk,
|
||||
platform::file::{MediaFrom, SelectedMedia},
|
||||
};
|
||||
use poll_promise::Promise;
|
||||
use sha2::{Digest, Sha256};
|
||||
use url::Url;
|
||||
|
||||
use crate::Error;
|
||||
use notedeck_ui::images::fetch_binary_from_disk;
|
||||
|
||||
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
||||
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
||||
|
||||
@@ -75,7 +74,7 @@ pub fn get_nostr_build_upload_url() -> Promise<Result<String, Error>> {
|
||||
get_upload_url_from_provider(NOSTR_BUILD_URL())
|
||||
}
|
||||
|
||||
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note {
|
||||
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note<'_> {
|
||||
NoteBuilder::new()
|
||||
.kind(27235)
|
||||
.start_tag()
|
||||
@@ -94,15 +93,15 @@ fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String
|
||||
|
||||
fn create_nip96_request(
|
||||
upload_url: &str,
|
||||
media_path: MediaPath,
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
file_contents: Vec<u8>,
|
||||
nip98_base64: &str,
|
||||
) -> ehttp::Request {
|
||||
let boundary = "----boundary";
|
||||
|
||||
let mut body = format!(
|
||||
"--{}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
|
||||
boundary, media_path.file_name, media_path.media_type.to_mime()
|
||||
"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{file_name}\"\r\nContent-Type: {media_type}\r\n\r\n",
|
||||
)
|
||||
.into_bytes();
|
||||
body.extend(file_contents);
|
||||
@@ -134,25 +133,14 @@ fn sha256_hex(contents: &Vec<u8>) -> String {
|
||||
pub fn nip96_upload(
|
||||
seckey: [u8; 32],
|
||||
upload_url: String,
|
||||
media_path: MediaPath,
|
||||
selected_media: SelectedMedia,
|
||||
) -> Promise<Result<Nip94Event, Error>> {
|
||||
let bytes_res = fetch_binary_from_disk(media_path.full_path.clone());
|
||||
|
||||
let file_bytes = match bytes_res {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return Promise::from_ready(Err(Error::Generic(format!(
|
||||
"could not read contents of file to upload: {e}"
|
||||
))))
|
||||
}
|
||||
};
|
||||
|
||||
internal_nip96_upload(seckey, upload_url, media_path, file_bytes)
|
||||
internal_nip96_upload(seckey, upload_url, selected_media)
|
||||
}
|
||||
|
||||
pub fn nostrbuild_nip96_upload(
|
||||
seckey: [u8; 32],
|
||||
media_path: MediaPath,
|
||||
selected_media: SelectedMedia,
|
||||
) -> Promise<Result<Nip94Event, Error>> {
|
||||
let (sender, promise) = Promise::new();
|
||||
std::thread::spawn(move || {
|
||||
@@ -166,7 +154,7 @@ pub fn nostrbuild_nip96_upload(
|
||||
}
|
||||
};
|
||||
|
||||
let res = nip96_upload(seckey, upload_url, media_path).block_and_take();
|
||||
let res = nip96_upload(seckey, upload_url, selected_media).block_and_take();
|
||||
sender.send(res);
|
||||
});
|
||||
promise
|
||||
@@ -175,9 +163,21 @@ pub fn nostrbuild_nip96_upload(
|
||||
fn internal_nip96_upload(
|
||||
seckey: [u8; 32],
|
||||
upload_url: String,
|
||||
media_path: MediaPath,
|
||||
file_contents: Vec<u8>,
|
||||
selected_media: SelectedMedia,
|
||||
) -> Promise<Result<Nip94Event, Error>> {
|
||||
let file_name = selected_media.file_name;
|
||||
let mime_type = selected_media.media_type.to_mime();
|
||||
let bytes_res = bytes_from_media(selected_media.from);
|
||||
|
||||
let file_contents = match bytes_res {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return Promise::from_ready(Err(Error::Generic(format!(
|
||||
"could not read contents of file to upload: {e}"
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
let file_hash = sha256_hex(&file_contents);
|
||||
let nip98_note = create_nip98_note(&seckey, upload_url.to_owned(), file_hash);
|
||||
|
||||
@@ -186,7 +186,13 @@ fn internal_nip96_upload(
|
||||
Err(e) => return Promise::from_ready(Err(Error::Generic(e.to_string()))),
|
||||
};
|
||||
|
||||
let request = create_nip96_request(&upload_url, media_path, file_contents, &nip98_base64);
|
||||
let request = create_nip96_request(
|
||||
&upload_url,
|
||||
&file_name,
|
||||
mime_type,
|
||||
file_contents,
|
||||
&nip98_base64,
|
||||
);
|
||||
|
||||
let (sender, promise) = Promise::new();
|
||||
|
||||
@@ -232,33 +238,10 @@ fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MediaPath {
|
||||
full_path: PathBuf,
|
||||
file_name: String,
|
||||
media_type: SupportedMimeType,
|
||||
}
|
||||
|
||||
impl MediaPath {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
||||
let media_type = SupportedMimeType::from_extension(ex)?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(&format!("file.{ex}"))
|
||||
.to_owned();
|
||||
|
||||
Ok(MediaPath {
|
||||
full_path: path,
|
||||
file_name,
|
||||
media_type,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"{path:?} does not have an extension"
|
||||
)))
|
||||
}
|
||||
pub fn bytes_from_media(media: MediaFrom) -> Result<Vec<u8>, notedeck::Error> {
|
||||
match media {
|
||||
MediaFrom::PathBuf(full_path) => fetch_binary_from_disk(full_path.clone()),
|
||||
MediaFrom::Memory(bytes) => Ok(bytes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +332,7 @@ mod tests {
|
||||
use enostr::FullKeypair;
|
||||
|
||||
use crate::media_upload::{
|
||||
get_upload_url_from_provider, nostrbuild_nip96_upload, MediaPath, NOSTR_BUILD_URL,
|
||||
get_upload_url_from_provider, nostrbuild_nip96_upload, SelectedMedia, NOSTR_BUILD_URL,
|
||||
};
|
||||
|
||||
use super::internal_nip96_upload;
|
||||
@@ -368,7 +351,7 @@ mod tests {
|
||||
fn test_internal_nip96() {
|
||||
// just a random image to test image upload
|
||||
let file_path = PathBuf::from_str("../../../assets/damus_rounded_80.png").unwrap();
|
||||
let media_path = MediaPath::new(file_path).unwrap();
|
||||
let selected_media = SelectedMedia::from_path(file_path).unwrap();
|
||||
let img_bytes = include_bytes!("../../../assets/damus_rounded_80.png");
|
||||
let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
|
||||
let kp = FullKeypair::generate();
|
||||
@@ -378,8 +361,7 @@ mod tests {
|
||||
let promise = internal_nip96_upload(
|
||||
kp.secret_key.secret_bytes(),
|
||||
upload_url.to_string(),
|
||||
media_path,
|
||||
img_bytes.to_vec(),
|
||||
selected_media,
|
||||
);
|
||||
let res = promise.block_until_ready();
|
||||
assert!(res.is_ok())
|
||||
@@ -395,11 +377,11 @@ mod tests {
|
||||
let file_path =
|
||||
fs::canonicalize(PathBuf::from_str("../../assets/damus_rounded_80.png").unwrap())
|
||||
.unwrap();
|
||||
let media_path = MediaPath::new(file_path).unwrap();
|
||||
let selected_media = SelectedMedia::from_path(file_path).unwrap();
|
||||
let kp = FullKeypair::generate();
|
||||
println!("Using pubkey: {:?}", kp.pubkey);
|
||||
|
||||
let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), media_path);
|
||||
let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), selected_media);
|
||||
|
||||
let out = promise.block_and_take();
|
||||
assert!(out.is_ok());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user