Compare commits
275 Commits
2025-07-22
...
2025-09-04
| Author | SHA1 | Date | |
|---|---|---|---|
|
97b6755504
|
|||
|
|
c765b031e9 | ||
|
|
024cf3ef91 | ||
|
|
3a0da9a3e0 | ||
|
|
10b62a073b | ||
|
|
ac212b96a6 | ||
|
|
637b05c1e2 | ||
|
|
f436b49fec | ||
|
|
04ce29d1dd | ||
|
|
ae1d5ab1c5 | ||
|
|
7caf77aa1c | ||
|
|
80ae489967 | ||
|
|
259c0b677a | ||
|
|
3b7f1f1b39 | ||
|
|
f2258ab16b | ||
|
|
571435cf85 | ||
|
|
8f8ff42156 | ||
|
|
3a95ba05a8 | ||
|
|
73e44d1497 | ||
|
|
43b98fc6ed | ||
|
|
95ee275153 | ||
|
|
8bc54cc519 | ||
|
|
5282373434 | ||
|
|
14c59a6c94 | ||
|
|
09238baee0 | ||
|
|
594072cfb8 | ||
|
|
2882b1c2d9 | ||
|
|
f4b8d235eb | ||
|
|
cf48b29fd8 | ||
|
|
2a7c5eb983 | ||
|
|
72d696beb2 | ||
|
|
dea695fa8e | ||
|
|
fc1caf5eb4 | ||
|
|
5539e4ef82 | ||
|
|
408afbda50 | ||
|
|
af4b896739 | ||
|
|
d448caa369 | ||
|
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 | ||
|
39e932c674
|
|||
|
6919460d18
|
|||
|
bf58fdce1f
|
|||
|
419102959f
|
|||
|
9bcbcae688
|
|||
|
|
5c8ab0ce07 | ||
|
|
590ffa0680 | ||
|
|
3d18db8fd2 | ||
|
|
661acb3a12 | ||
|
|
8306003f6f | ||
|
|
96ab4ee681 | ||
|
|
2524ff1061 | ||
|
|
eb0ab75e87 | ||
|
|
009b4cf6b0 | ||
|
|
f2e01f0e40 | ||
|
|
c891f8585d | ||
|
|
2648967d7b | ||
|
|
438dbb2397 | ||
|
|
2bd139ef9e | ||
|
|
cda0a68854 | ||
|
|
a555707f67 | ||
|
|
1601914b8b | ||
|
|
aac0f54991 | ||
|
|
8960b3f052 | ||
|
|
6db6cf7b7a | ||
|
|
0bc32272d2 | ||
|
|
b05d39cc81 | ||
|
|
7a83483758 | ||
|
|
1a3112d8ef | ||
|
|
c1d0ea1901 | ||
|
|
db6103d448 | ||
|
|
0f00dcf7a7 | ||
|
|
8f63546524 | ||
|
|
90975180f5 | ||
|
|
bd9a78b305 | ||
|
|
4e27c1f491 | ||
|
|
f9f8b3fe1b | ||
|
|
5ddd8660a3 | ||
|
|
fe30704496 | ||
|
|
e997f1bf68 | ||
|
|
ff0428550b | ||
|
|
da6ede5f69 | ||
|
|
56cbf68ea5 | ||
|
|
ebf31abafa | ||
|
|
e317c57769 | ||
|
|
f722a58d66 | ||
|
|
ffcd38ef96 | ||
|
|
088704a768 | ||
|
|
10eedc0ca6 | ||
|
|
ed38c75193 | ||
|
|
fdef74c353 | ||
|
|
030e4226f8 | ||
|
|
508d8dc0ba | ||
|
|
34afa755b8 | ||
|
|
45490c918d | ||
|
|
a31fdd3ed2 |
3
.envrc
3
.envrc
@@ -18,4 +18,7 @@ export OLLAMA_HOST=http://ollama.jb55.com
|
|||||||
|
|
||||||
# simple todo reminders
|
# simple todo reminders
|
||||||
export TODO_FILE=TODO
|
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>/dev/null todo.sh ls || :
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,3 +19,7 @@ queries/damus-notifs.json
|
|||||||
.direnv/
|
.direnv/
|
||||||
scripts/macos_build_secrets.sh
|
scripts/macos_build_secrets.sh
|
||||||
/tags
|
/tags
|
||||||
|
.zed
|
||||||
|
.lsp
|
||||||
|
.idea
|
||||||
|
local.properties
|
||||||
424
Cargo.lock
generated
424
Cargo.lock
generated
@@ -105,7 +105,8 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "android-activity"
|
name = "android-activity"
|
||||||
version = "0.6.0"
|
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 = [
|
dependencies = [
|
||||||
"android-properties",
|
"android-properties",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
@@ -125,7 +126,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "android-activity"
|
name = "android-activity"
|
||||||
version = "0.6.0"
|
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 = [
|
dependencies = [
|
||||||
"android-properties",
|
"android-properties",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
@@ -192,7 +193,7 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.1",
|
"objc2-foundation 0.3.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -765,6 +766,25 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-sys"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
|
||||||
|
dependencies = [
|
||||||
|
"objc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
|
||||||
|
dependencies = [
|
||||||
|
"block-sys",
|
||||||
|
"objc2 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -802,6 +822,17 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
|
checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.9",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "built"
|
name = "built"
|
||||||
version = "0.7.7"
|
version = "0.7.7"
|
||||||
@@ -978,6 +1009,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -1233,6 +1265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1370,7 +1403,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
version = "0.1.1"
|
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]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
@@ -1378,20 +1411,26 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eframe"
|
name = "eframe"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1427,24 +1466,25 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui"
|
name = "egui"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"ahash",
|
"ahash",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||||
"epaint",
|
"epaint",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"profiling",
|
"profiling",
|
||||||
"serde",
|
"serde",
|
||||||
|
"similar",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1463,7 +1503,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-winit"
|
name = "egui-winit"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"arboard",
|
"arboard",
|
||||||
@@ -1481,7 +1521,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_extras"
|
name = "egui_extras"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -1498,7 +1538,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_glow"
|
name = "egui_glow"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1515,7 +1555,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_nav"
|
name = "egui_nav"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/damus-io/egui-nav?rev=111de8ac40b5d18df53e9691eb18a50d49cb31d8#111de8ac40b5d18df53e9691eb18a50d49cb31d8"
|
source = "git+https://github.com/damus-io/egui-nav?rev=e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9#e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
@@ -1577,7 +1617,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "emath"
|
name = "emath"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1595,7 +1635,7 @@ version = "0.3.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bech32",
|
"bech32",
|
||||||
"ewebsock",
|
"ewebsock",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"mio",
|
"mio",
|
||||||
"nostr 0.37.0",
|
"nostr 0.37.0",
|
||||||
@@ -1675,13 +1715,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint"
|
name = "epaint"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"ecolor",
|
"ecolor",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||||
"epaint_default_fonts",
|
"epaint_default_fonts",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -1693,7 +1733,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint_default_fonts"
|
name = "epaint_default_fonts"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equator"
|
name = "equator"
|
||||||
@@ -2269,7 +2309,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"gpu-descriptor-types",
|
"gpu-descriptor-types",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2291,6 +2331,18 @@ dependencies = [
|
|||||||
"crunchy",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.4"
|
version = "0.15.4"
|
||||||
@@ -2319,6 +2371,9 @@ name = "hex"
|
|||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex-conservative"
|
name = "hex-conservative"
|
||||||
@@ -2335,6 +2390,17 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex_color"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex_lit"
|
name = "hex_lit"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2496,6 +2562,16 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icrate"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
|
||||||
|
dependencies = [
|
||||||
|
"block2 0.4.0",
|
||||||
|
"objc2 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -2654,6 +2730,17 @@ version = "1.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -2661,7 +2748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2733,25 +2820,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is-docker"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is-wsl"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
|
||||||
dependencies = [
|
|
||||||
"is-docker",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
@@ -2868,6 +2936,19 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsoncanvas"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "git+https://github.com/jb55/jsoncanvas?rev=ae60f96e4d022cf037e086b793cacc3225bc14e5#ae60f96e4d022cf037e086b793cacc3225bc14e5"
|
||||||
|
dependencies = [
|
||||||
|
"hex_color",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos-egl"
|
name = "khronos-egl"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -2958,6 +3039,7 @@ dependencies = [
|
|||||||
"bech32",
|
"bech32",
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"lightning-types",
|
"lightning-types",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2993,6 +3075,22 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
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]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -3190,7 +3288,7 @@ dependencies = [
|
|||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"hexf-parse",
|
"hexf-parse",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"log",
|
"log",
|
||||||
"rustc-hash 1.1.0",
|
"rustc-hash 1.1.0",
|
||||||
"spirv",
|
"spirv",
|
||||||
@@ -3304,6 +3402,15 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "normpath"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
@@ -3383,8 +3490,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostrdb"
|
name = "nostrdb"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "git+https://github.com/damus-io/nostrdb-rs?rev=a307f5d3863b5319c728b2782959839b8df544cb#a307f5d3863b5319c728b2782959839b8df544cb"
|
source = "git+https://github.com/damus-io/nostrdb-rs?rev=2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3#2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen",
|
"bindgen",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -3398,27 +3505,35 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck"
|
name = "notedeck"
|
||||||
version = "0.5.6"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
|
||||||
"base32",
|
"base32",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"blurhash",
|
||||||
|
"chrono",
|
||||||
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
|
"egui_extras",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"fluent",
|
"fluent",
|
||||||
"fluent-langneg",
|
"fluent-langneg",
|
||||||
"fluent-resmgr",
|
"fluent-resmgr",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
|
"indexmap 2.9.0",
|
||||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lightning-invoice",
|
"lightning-invoice",
|
||||||
"md5",
|
"md5",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"ndk-context",
|
||||||
"nostr 0.37.0",
|
"nostr 0.37.0",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"nwc",
|
"nwc",
|
||||||
@@ -3446,8 +3561,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_chrome"
|
name = "notedeck_chrome"
|
||||||
version = "0.5.6"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
@@ -3455,8 +3571,10 @@ dependencies = [
|
|||||||
"egui_tabs",
|
"egui_tabs",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
|
"notedeck_clndash",
|
||||||
"notedeck_columns",
|
"notedeck_columns",
|
||||||
"notedeck_dave",
|
"notedeck_dave",
|
||||||
|
"notedeck_notebook",
|
||||||
"notedeck_ui",
|
"notedeck_ui",
|
||||||
"profiling",
|
"profiling",
|
||||||
"puffin",
|
"puffin",
|
||||||
@@ -3473,9 +3591,28 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"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]]
|
[[package]]
|
||||||
name = "notedeck_columns"
|
name = "notedeck_columns"
|
||||||
version = "0.5.6"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
@@ -3490,16 +3627,18 @@ dependencies = [
|
|||||||
"egui_virtual_list",
|
"egui_virtual_list",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"human_format",
|
"human_format",
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ndk-context",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
"notedeck_ui",
|
"notedeck_ui",
|
||||||
"oot_bitset",
|
"oot_bitset",
|
||||||
"open",
|
"opener",
|
||||||
"poll-promise",
|
"poll-promise",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"profiling",
|
"profiling",
|
||||||
@@ -3507,6 +3646,7 @@ dependencies = [
|
|||||||
"puffin_egui",
|
"puffin_egui",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rmpv",
|
"rmpv",
|
||||||
|
"robius-open",
|
||||||
"security-framework 2.11.1",
|
"security-framework 2.11.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -3528,7 +3668,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_dave"
|
name = "notedeck_dave"
|
||||||
version = "0.5.6"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-openai",
|
"async-openai",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -3536,6 +3676,7 @@ dependencies = [
|
|||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
|
"egui_extras",
|
||||||
"enostr",
|
"enostr",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -3550,19 +3691,27 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notedeck_notebook"
|
||||||
|
version = "0.7.1"
|
||||||
|
dependencies = [
|
||||||
|
"egui",
|
||||||
|
"jsoncanvas",
|
||||||
|
"notedeck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_ui"
|
name = "notedeck_ui"
|
||||||
version = "0.5.6"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"blurhash",
|
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"hashbrown",
|
"hashbrown 0.15.4",
|
||||||
"image",
|
"image",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
@@ -3992,14 +4141,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "open"
|
name = "opener"
|
||||||
version = "5.3.2"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
|
checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"is-wsl",
|
"bstr",
|
||||||
"libc",
|
"normpath",
|
||||||
"pathdiff",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4103,12 +4252,6 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathdiff"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
@@ -4397,7 +4540,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"natord",
|
"natord",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -4725,6 +4868,26 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
|
||||||
|
dependencies = [
|
||||||
|
"ref-cast-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast-impl"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -4916,6 +5079,30 @@ dependencies = [
|
|||||||
"rmp",
|
"rmp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robius-android-env"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef"
|
||||||
|
dependencies = [
|
||||||
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ndk-context",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robius-open"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "243e2abbc8c1ca8ddc283056d4675b67e452fd527c3741c5318642da37840ff3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"icrate",
|
||||||
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"objc2 0.5.2",
|
||||||
|
"robius-android-env",
|
||||||
|
"windows 0.54.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@@ -5062,6 +5249,30 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -5215,7 +5426,7 @@ version = "1.0.140"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -5254,6 +5465,38 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.9.0",
|
||||||
|
"schemars 0.9.0",
|
||||||
|
"schemars 1.0.4",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -5315,6 +5558,12 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "similar"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simplecss"
|
name = "simplecss"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -5848,7 +6097,7 @@ version = "0.22.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
@@ -6631,7 +6880,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"document-features",
|
"document-features",
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"log",
|
"log",
|
||||||
"naga",
|
"naga",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -6752,6 +7001,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.54.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -6771,6 +7030,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result 0.1.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -6847,6 +7116,15 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -7174,10 +7452,10 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "winit"
|
name = "winit"
|
||||||
version = "0.30.8"
|
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 = [
|
dependencies = [
|
||||||
"ahash",
|
"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",
|
"atomic-waker",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
@@ -7229,7 +7507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
|
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"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",
|
"atomic-waker",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
|
|||||||
38
Cargo.toml
38
Cargo.toml
@@ -1,17 +1,21 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
package.version = "0.5.6"
|
package.version = "0.7.1"
|
||||||
members = [
|
members = [
|
||||||
"crates/notedeck",
|
"crates/notedeck",
|
||||||
"crates/notedeck_chrome",
|
"crates/notedeck_chrome",
|
||||||
"crates/notedeck_columns",
|
"crates/notedeck_columns",
|
||||||
"crates/notedeck_dave",
|
"crates/notedeck_dave",
|
||||||
|
"crates/notedeck_notebook",
|
||||||
"crates/notedeck_ui",
|
"crates/notedeck_ui",
|
||||||
|
"crates/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]
|
[workspace.dependencies]
|
||||||
|
opener = "0.8.2"
|
||||||
|
chrono = "0.4.40"
|
||||||
base32 = "0.4.0"
|
base32 = "0.4.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
rmpv = "1.3.0"
|
rmpv = "1.3.0"
|
||||||
@@ -23,7 +27,7 @@ egui = { version = "0.31.1", features = ["serde"] }
|
|||||||
egui-wgpu = "0.31.1"
|
egui-wgpu = "0.31.1"
|
||||||
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
||||||
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
||||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "111de8ac40b5d18df53e9691eb18a50d49cb31d8" }
|
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9" }
|
||||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
||||||
#egui_virtual_list = "0.6.0"
|
#egui_virtual_list = "0.6.0"
|
||||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
||||||
@@ -33,7 +37,7 @@ ewebsock = { version = "0.2.0", features = ["tls"] }
|
|||||||
fluent = "0.17.0"
|
fluent = "0.17.0"
|
||||||
fluent-resmgr = "0.0.8"
|
fluent-resmgr = "0.0.8"
|
||||||
fluent-langneg = "0.13"
|
fluent-langneg = "0.13"
|
||||||
hex = "0.4.3"
|
hex = { version = "0.4.3", features = ["serde"] }
|
||||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||||
indexmap = "2.6.0"
|
indexmap = "2.6.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
@@ -41,16 +45,18 @@ md5 = "0.7.0"
|
|||||||
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
|
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
|
||||||
nwc = "0.39.0"
|
nwc = "0.39.0"
|
||||||
mio = { version = "1.0.3", features = ["os-poll", "net"] }
|
mio = { version = "1.0.3", features = ["os-poll", "net"] }
|
||||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "a307f5d3863b5319c728b2782959839b8df544cb" }
|
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
|
||||||
#nostrdb = "0.6.1"
|
#nostrdb = "0.6.1"
|
||||||
notedeck = { path = "crates/notedeck" }
|
notedeck = { path = "crates/notedeck" }
|
||||||
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
||||||
|
notedeck_clndash = { path = "crates/notedeck_clndash" }
|
||||||
notedeck_columns = { path = "crates/notedeck_columns" }
|
notedeck_columns = { path = "crates/notedeck_columns" }
|
||||||
notedeck_dave = { path = "crates/notedeck_dave" }
|
notedeck_dave = { path = "crates/notedeck_dave" }
|
||||||
|
notedeck_notebook = { path = "crates/notedeck_notebook" }
|
||||||
notedeck_ui = { path = "crates/notedeck_ui" }
|
notedeck_ui = { path = "crates/notedeck_ui" }
|
||||||
tokenator = { path = "crates/tokenator" }
|
tokenator = { path = "crates/tokenator" }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
open = "5.3.0"
|
robius-open = "0.1"
|
||||||
poll-promise = { version = "0.3.0", features = ["tokio"] }
|
poll-promise = { version = "0.3.0", features = ["tokio"] }
|
||||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
@@ -75,12 +81,14 @@ mime_guess = "2.0.5"
|
|||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
jni = "0.21.1"
|
jni = "0.21.1"
|
||||||
profiling = "1.0"
|
profiling = "1.0"
|
||||||
lightning-invoice = "0.33.1"
|
lightning-invoice = { version = "0.33.1", features = ["serde"] }
|
||||||
secp256k1 = "0.30.0"
|
secp256k1 = "0.30.0"
|
||||||
hashbrown = "0.15.2"
|
hashbrown = "0.15.2"
|
||||||
openai-api-rs = "6.0.3"
|
openai-api-rs = "6.0.3"
|
||||||
re_memory = "0.23.4"
|
re_memory = "0.23.4"
|
||||||
oot_bitset = "0.1.1"
|
oot_bitset = "0.1.1"
|
||||||
|
blurhash = "0.2.3"
|
||||||
|
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "4ee16f1585e4a75031dc10785163d4b920f95805", features = [ "game-activity" ] }
|
||||||
|
|
||||||
[profile.small]
|
[profile.small]
|
||||||
inherits = 'release'
|
inherits = 'release'
|
||||||
@@ -98,15 +106,15 @@ strip = true # Strip symbols from binary*
|
|||||||
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
||||||
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
||||||
|
|
||||||
egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
eframe = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
epaint = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
|
#winit = { git = "https://github.com/damus-io/winit", rev = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
|
||||||
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
|
#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" }
|
#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
|
android: jni
|
||||||
cd $(ANDROID_DIR) && ./gradlew installDebug
|
cd $(ANDROID_DIR) && ./gradlew installDebug
|
||||||
adb shell am start -n com.damus.notedeck/.MainActivity
|
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
|
||||||
|
|||||||
11
android
Executable file
11
android
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
root_dir=$PWD
|
||||||
|
|
||||||
|
cargo ndk --target arm64-v8a -o ./crates/notedeck_chrome/android/app/src/main/jniLibs/ build --profile release
|
||||||
|
|
||||||
|
cd ./crates/notedeck_chrome/android
|
||||||
|
|
||||||
|
./gradlew build && ./gradlew installDebug
|
||||||
|
|
||||||
|
cd $root_dir
|
||||||
BIN
assets/icons/accounts.png
Normal file
BIN
assets/icons/accounts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
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 |
3
assets/icons/copy-to-clipboard.svg
Normal file
3
assets/icons/copy-to-clipboard.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.33337 5.33337V3.46671C5.33337 2.71997 5.33337 2.3466 5.4787 2.06139C5.60653 1.8105 5.8105 1.60653 6.06139 1.4787C6.3466 1.33337 6.71997 1.33337 7.46671 1.33337H12.5334C13.2801 1.33337 13.6535 1.33337 13.9387 1.4787C14.1896 1.60653 14.3936 1.8105 14.5214 2.06139C14.6667 2.3466 14.6667 2.71997 14.6667 3.46671V8.53337C14.6667 9.28011 14.6667 9.65351 14.5214 9.93871C14.3936 10.1896 14.1896 10.3936 13.9387 10.5214C13.6535 10.6667 13.2801 10.6667 12.5334 10.6667H10.6667M3.46671 14.6667H8.53337C9.28011 14.6667 9.65351 14.6667 9.93871 14.5214C10.1896 14.3936 10.3936 14.1896 10.5214 13.9387C10.6667 13.6535 10.6667 13.2801 10.6667 12.5334V7.46671C10.6667 6.71997 10.6667 6.3466 10.5214 6.06139C10.3936 5.8105 10.1896 5.60653 9.93871 5.4787C9.65351 5.33337 9.28011 5.33337 8.53337 5.33337H3.46671C2.71997 5.33337 2.3466 5.33337 2.06139 5.4787C1.8105 5.60653 1.60653 5.8105 1.4787 6.06139C1.33337 6.3466 1.33337 6.71997 1.33337 7.46671V12.5334C1.33337 13.2801 1.33337 13.6535 1.4787 13.9387C1.60653 14.1896 1.8105 14.3936 2.06139 14.5214C2.3466 14.6667 2.71997 14.6667 3.46671 14.6667Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 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 |
@@ -6,7 +6,7 @@
|
|||||||
# Regular strings
|
# Regular strings
|
||||||
|
|
||||||
# Profile about/bio field label
|
# Profile about/bio field label
|
||||||
About_00c0 = Über
|
About_00c0 = Über mich
|
||||||
# Column title for account management
|
# Column title for account management
|
||||||
Accounts_f018 = Konten
|
Accounts_f018 = Konten
|
||||||
# Button label to add a relay
|
# Button label to add a relay
|
||||||
@@ -45,6 +45,8 @@ Algo_2452 = Algorithmus
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Menge
|
Amount_70f0 = Menge
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = Darstellung
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Fragen
|
Ask_b7f4 = Fragen
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Senden
|
|||||||
Broadcast_Local_7e50 = Lokal senden
|
Broadcast_Local_7e50 = Lokal senden
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Abbrechen
|
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
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Notiz erstellen
|
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
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Bestätigen
|
Confirm_f8a6 = Bestätigen
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -88,19 +98,19 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
|
|||||||
# Copy the text content of the note to clipboard
|
# Copy the text content of the note to clipboard
|
||||||
Copy_Text_f81c = Text kopieren
|
Copy_Text_f81c = Text kopieren
|
||||||
# Relative time in days
|
# Relative time in days
|
||||||
count_d_b9be = { $count }T.
|
count_d_b9be = { $count }T
|
||||||
# Relative time in hours
|
# Relative time in hours
|
||||||
count_h_3ecb = { $count }Std.
|
count_h_3ecb = { $count }h
|
||||||
# Relative time in minutes
|
# Relative time in minutes
|
||||||
count_m_b41e = { $count }Min.
|
count_m_b41e = { $count }min
|
||||||
# Relative time in months
|
# Relative time in months
|
||||||
count_mo_7aba = { $count }Mon.
|
count_mo_7aba = { $count }M
|
||||||
# Relative time in seconds
|
# Relative time in seconds
|
||||||
count_s_aa26 = { $count }Sek.
|
count_s_aa26 = { $count }s
|
||||||
# Relative time in weeks
|
# Relative time in weeks
|
||||||
count_w_7468 = { $count }Wo.
|
count_w_7468 = { $count }W
|
||||||
# Relative time in years
|
# Relative time in years
|
||||||
count_y_9408 = { $count }J.
|
count_y_9408 = { $count }J
|
||||||
# Button to create a new account
|
# Button to create a new account
|
||||||
Create_Account_6994 = Konto erstellen
|
Create_Account_6994 = Konto erstellen
|
||||||
# Button label to create a new deck
|
# Button label to create a new deck
|
||||||
@@ -111,6 +121,8 @@ Custom_a69e = Benutzerdefiniert
|
|||||||
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Damus Support
|
Damus_Support_27c0 = Damus Support
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = Dunkel
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Deck-Name
|
Deck_name_cd32 = Deck-Name
|
||||||
# Label for decks section in side panel
|
# 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.
|
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Profil finden
|
Find_User_bd12 = Profil finden
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = Schriftgröße:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Startseite
|
Home_8c19 = Startseite
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Symbol
|
Icon_b0ab = Symbol
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = Bildcache Größe:
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = Individuell
|
Individual_b776 = Individuell
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -177,8 +193,12 @@ k_50K_c2dc = 50K
|
|||||||
k_5K_f7e6 = 5K
|
k_5K_f7e6 = 5K
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
|
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
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Letzte Notiz pro Profil
|
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
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -216,11 +236,17 @@ Notifications_d673 = Benachrichtigungen
|
|||||||
# Title for notifications column
|
# Title for notifications column
|
||||||
Notifications_ef56 = Benachrichtigungen
|
Notifications_ef56 = Benachrichtigungen
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# 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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = E-Mail öffnen
|
Open_Email_25e9 = E-Mail öffnen
|
||||||
# Instruction to open email client
|
# 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
|
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
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
|
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
|
||||||
# Error message for missing deck name
|
# 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
|
Repost_this_note_8e56 = Diese Notiz teilen
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Teilen
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
|
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# 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
|
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
|
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
|
# Button label to send a zap
|
||||||
Send_1ea4 = Senden
|
Send_1ea4 = Senden
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = Einstellungen
|
||||||
# Description for last note per user column
|
# 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
|
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
|
# Button label to sign out of account
|
||||||
@@ -297,6 +331,8 @@ Sign_out_337b = Abmelden
|
|||||||
Someone_else_s_Notes_7e5f = Notizen anderer Profile
|
Someone_else_s_Notes_7e5f = Notizen anderer Profile
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
|
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
|
# 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
|
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
|
# 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_1_8656 = Schritt 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Schritt 2
|
Step_2_d08d = Schritt 2
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = Speicher
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
|
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
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -327,8 +367,10 @@ Switch_to_light_mode_72ce = Zum Hellmodus wechseln
|
|||||||
Tap_to_Load_4b05 = Zum Laden antippen
|
Tap_to_Load_4b05 = Zum Laden antippen
|
||||||
# Message shown when Dave trial period has ended
|
# 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!
|
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
|
# Column title for note thread view
|
||||||
Thread_0f20 = Unterhaltungen
|
Thread_0f20 = Unterhaltung
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = Unterhaltung
|
thread_ad1f = Unterhaltung
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
@@ -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
|
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Benutzername
|
Username_daa7 = Benutzername
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = Ordner anzeigen
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Wallet
|
Wallet_5e50 = Wallet
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -359,6 +403,8 @@ Your_Notifications_080d = Deine Benachrichtigungen
|
|||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Zappe diese Notiz
|
Zap_this_note_42b2 = Zappe diese Notiz
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = Zoomstufe:
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = Add Hashtag Column
|
|||||||
# Column title for adding last notes column
|
# Column title for adding last notes column
|
||||||
Add_Last_Notes_Column_bbad = Add Last Notes Column
|
Add_Last_Notes_Column_bbad = Add Last Notes Column
|
||||||
|
|
||||||
|
# Tooltip text for adding a new deck button
|
||||||
|
Add_new_deck_f2fc = Add new deck
|
||||||
|
|
||||||
# Column title for adding notifications column
|
# Column title for adding notifications column
|
||||||
Add_Notifications_Column_79f8 = Add Notifications Column
|
Add_Notifications_Column_79f8 = Add Notifications Column
|
||||||
|
|
||||||
@@ -64,6 +67,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in no
|
|||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Amount
|
Amount_70f0 = Amount
|
||||||
|
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = Appearance
|
||||||
|
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Ask
|
Ask_b7f4 = Ask
|
||||||
|
|
||||||
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = Broadcast Local
|
|||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Cancel
|
Cancel_ed3b = Cancel
|
||||||
|
|
||||||
|
# Label for cancel clear cache, Storage settings section
|
||||||
|
Cancel_fd8b = Cancel
|
||||||
|
|
||||||
|
# Label for clear cache button, Storage settings section
|
||||||
|
Clear_cache_dccb = Clear cache
|
||||||
|
|
||||||
# Hover text for editable zap amount
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Click to edit
|
Click_to_edit_0414 = Click to edit
|
||||||
|
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Compose Note
|
Compose_Note_c094 = Compose Note
|
||||||
|
|
||||||
|
# Label for configure relays, settings section
|
||||||
|
Configure_relays_d156 = Configure relays
|
||||||
|
|
||||||
|
# Label for confirm clear cache, Storage settings section
|
||||||
|
Confirm_9d9d = Confirm
|
||||||
|
|
||||||
# Button label to confirm an action
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Confirm
|
Confirm_f8a6 = Confirm
|
||||||
|
|
||||||
@@ -121,6 +139,9 @@ Copy_Note_ID_6b45 = Copy Note ID
|
|||||||
# Copy the raw note data in JSON format to clipboard
|
# Copy the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = Copy Note JSON
|
Copy_Note_JSON_9e4e = Copy Note JSON
|
||||||
|
|
||||||
|
# Tooltip text for copying npub to clipboard
|
||||||
|
Copy_npub_to_clipboard_c105 = Copy npub to clipboard
|
||||||
|
|
||||||
# Copy the author's public key to clipboard
|
# Copy the author's public key to clipboard
|
||||||
Copy_Pubkey_9cc4 = Copy Pubkey
|
Copy_Pubkey_9cc4 = Copy Pubkey
|
||||||
|
|
||||||
@@ -163,6 +184,9 @@ Customize_Zap_Amount_cfc4 = Customize Zap Amount
|
|||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Damus Support
|
Damus_Support_27c0 = Damus Support
|
||||||
|
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = Dark
|
||||||
|
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Deck name
|
Deck_name_cd32 = Deck name
|
||||||
|
|
||||||
@@ -190,6 +214,9 @@ Display_name_f9d9 = Display name
|
|||||||
# Domain identification message
|
# Domain identification message
|
||||||
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
|
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
|
||||||
|
|
||||||
|
# Button to indicate that the user is done going through the onboarding process.
|
||||||
|
Done_50dd = Done
|
||||||
|
|
||||||
# Column title for editing deck
|
# Column title for editing deck
|
||||||
Edit_Deck_4018 = Edit Deck
|
Edit_Deck_4018 = Edit Deck
|
||||||
|
|
||||||
@@ -220,6 +247,9 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
|||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Find User
|
Find_User_bd12 = Find User
|
||||||
|
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = Font size:
|
||||||
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
|
|
||||||
@@ -229,6 +259,9 @@ Home_8c19 = Home
|
|||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Icon
|
Icon_b0ab = Icon
|
||||||
|
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = Image cache size:
|
||||||
|
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = Individual
|
Individual_b776 = Individual
|
||||||
|
|
||||||
@@ -259,9 +292,18 @@ k_5K_f7e6 = 5K
|
|||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
|
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
|
||||||
|
|
||||||
|
# label for keys setting section
|
||||||
|
Keys_435f = Keys
|
||||||
|
|
||||||
|
# Label for language, Appearance settings section
|
||||||
|
Language_e264 = Language:
|
||||||
|
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Last Note per User
|
Last_Note_per_User_17ad = Last Note per User
|
||||||
|
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = Light
|
||||||
|
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Lightning network address (lud16)
|
Lightning_network_address__lud16_ea51 = Lightning network address (lud16)
|
||||||
|
|
||||||
@@ -280,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = Moves this column to another positi
|
|||||||
# Title for the user's deck
|
# Title for the user's deck
|
||||||
My_Deck_4ac5 = My Deck
|
My_Deck_4ac5 = My Deck
|
||||||
|
|
||||||
|
# reaction from user to a note you were tagged in
|
||||||
|
name__reacted_to_a_note_you_were_tagged_in_4b62 = {$name} reacted to a note you were tagged in
|
||||||
|
|
||||||
|
# reaction from user to your note
|
||||||
|
name__reacted_to_your_note_ead9 = {$name} reacted to your note
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_a_note_you_were_tagged_in_1379 = {$name} reposted a note you were tagged in
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_your_note_1379 = {$name} reposted your note
|
||||||
|
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||||
New_to_Nostr_a2fd = New to Nostr?
|
New_to_Nostr_a2fd = New to Nostr?
|
||||||
|
|
||||||
@@ -319,12 +373,21 @@ Notifications_ef56 = Notifications
|
|||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = now
|
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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Open Email
|
Open_Email_25e9 = Open Email
|
||||||
|
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Open your default email client to get help from the Damus team
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Open your default email client to get help from the Damus team
|
||||||
|
|
||||||
|
# Label for others settings section
|
||||||
|
Others_7267 = Others
|
||||||
|
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Paste your NWC URI here...
|
Paste_your_NWC_URI_here_b471 = Paste your NWC URI here...
|
||||||
|
|
||||||
@@ -346,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
|
|||||||
# Profile picture URL field label
|
# Profile picture URL field label
|
||||||
Profile_picture_81ff = Profile picture
|
Profile_picture_81ff = Profile picture
|
||||||
|
|
||||||
|
# label describing public key
|
||||||
|
PUBLIC_ACCOUNT_ID_4394 = PUBLIC ACCOUNT ID
|
||||||
|
|
||||||
# Column title for quote composition
|
# Column title for quote composition
|
||||||
Quote_475c = Quote
|
Quote_475c = Quote
|
||||||
|
|
||||||
@@ -394,6 +460,12 @@ Repost_this_note_8e56 = Repost this note
|
|||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Reposted
|
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
|
||||||
|
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = Running into a bug?
|
Running_into_a_bug_1796 = Running into a bug?
|
||||||
|
|
||||||
@@ -418,15 +490,24 @@ Search_notes_42a6 = Search notes...
|
|||||||
# Search in progress message
|
# Search in progress message
|
||||||
Searching_for___query_5d18 = Searching for '{$query}'
|
Searching_for___query_5d18 = Searching for '{$query}'
|
||||||
|
|
||||||
|
# label describing secret key
|
||||||
|
SECRET_ACCOUNT_LOGIN_KEY_8440 = SECRET ACCOUNT LOGIN KEY
|
||||||
|
|
||||||
# Description for Home column
|
# Description for Home column
|
||||||
See_notes_from_your_contacts_ac16 = See notes from your contacts
|
See_notes_from_your_contacts_ac16 = See notes from your contacts
|
||||||
|
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = See the whole nostr universe
|
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
|
# Button label to send a zap
|
||||||
Send_1ea4 = Send
|
Send_1ea4 = Send
|
||||||
|
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = Settings
|
||||||
|
|
||||||
# Description for last note per user column
|
# 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
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
|
||||||
|
|
||||||
@@ -439,6 +520,9 @@ Someone_else_s_Notes_7e5f = Someone else's Notes
|
|||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Someone else's Notifications
|
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
|
# 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
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list
|
||||||
|
|
||||||
@@ -466,12 +550,18 @@ Step_1_8656 = Step 1
|
|||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Step 2
|
Step_2_d08d = Step 2
|
||||||
|
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = Storage
|
||||||
|
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
|
Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
|
||||||
|
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes
|
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
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Switch to dark mode
|
Switch_to_dark_mode_4dec = Switch to dark mode
|
||||||
|
|
||||||
@@ -484,6 +574,9 @@ Tap_to_Load_4b05 = Tap to Load
|
|||||||
# Message shown when Dave trial period has ended
|
# 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 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!
|
||||||
|
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = Theme:
|
||||||
|
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = Thread
|
Thread_0f20 = Thread
|
||||||
|
|
||||||
@@ -505,6 +598,9 @@ username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at
|
|||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Username
|
Username_daa7 = Username
|
||||||
|
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = View folder
|
||||||
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Wallet
|
Wallet_5e50 = Wallet
|
||||||
|
|
||||||
@@ -532,6 +628,9 @@ Zap_16b4 = Zap
|
|||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Zap this note
|
Zap_this_note_42b2 = Zap this note
|
||||||
|
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = Zoom Level:
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
# Search results count
|
# Search results count
|
||||||
@@ -540,3 +639,35 @@ Got__count__results_for___query_85fb =
|
|||||||
[one] Got {$count} result for '{$query}'
|
[one] Got {$count} result for '{$query}'
|
||||||
*[other] Got {$count} results for '{$query}'
|
*[other] Got {$count} results for '{$query}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# amount of reactions a note you were tagged in received
|
||||||
|
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reacted to a note you were tagged in
|
||||||
|
*[other] {$name} and {$count} others reacted to a note you were tagged in
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reactions your note received
|
||||||
|
name__and__count__others_reacted_to_your_note_0f6a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reacted to your note
|
||||||
|
*[other] {$name} and {$count} others reacted to your note
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts a note you were tagged in received
|
||||||
|
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reposted a note you were tagged in
|
||||||
|
*[other] {$name} and {$count} others reposted a note you were tagged in
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts your note received
|
||||||
|
name__and__count__others_reposted_your_note_70a0 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reposted your note
|
||||||
|
*[other] {$name} and {$count} others reposted your note
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"}
|
|||||||
# Column title for adding last notes column
|
# Column title for adding last notes column
|
||||||
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
|
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
|
||||||
|
|
||||||
|
# Tooltip text for adding a new deck button
|
||||||
|
Add_new_deck_f2fc = {"["}Àdd ñéw déçk{"]"}
|
||||||
|
|
||||||
# Column title for adding notifications column
|
# Column title for adding notifications column
|
||||||
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
|
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
|
||||||
|
|
||||||
@@ -64,6 +67,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds
|
|||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = {"["}Àmóúñt{"]"}
|
Amount_70f0 = {"["}Àmóúñt{"]"}
|
||||||
|
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = {"["}Àppéàràñçé{"]"}
|
||||||
|
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = {"["}Àsk{"]"}
|
Ask_b7f4 = {"["}Àsk{"]"}
|
||||||
|
|
||||||
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = {"["}Bróàdçàst Lóçàl{"]"}
|
|||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = {"["}Çàñçél{"]"}
|
Cancel_ed3b = {"["}Çàñçél{"]"}
|
||||||
|
|
||||||
|
# Label for cancel clear cache, Storage settings section
|
||||||
|
Cancel_fd8b = {"["}Çàñçél{"]"}
|
||||||
|
|
||||||
|
# Label for clear cache button, Storage settings section
|
||||||
|
Clear_cache_dccb = {"["}Çléàr çàçhé{"]"}
|
||||||
|
|
||||||
# Hover text for editable zap amount
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"}
|
Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"}
|
||||||
|
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = {"["}Çómpósé Ñóté{"]"}
|
Compose_Note_c094 = {"["}Çómpósé Ñóté{"]"}
|
||||||
|
|
||||||
|
# Label for configure relays, settings section
|
||||||
|
Configure_relays_d156 = {"["}Çóñfígúré rélàys{"]"}
|
||||||
|
|
||||||
|
# Label for confirm clear cache, Storage settings section
|
||||||
|
Confirm_9d9d = {"["}Çóñfírm{"]"}
|
||||||
|
|
||||||
# Button label to confirm an action
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = {"["}Çóñfírm{"]"}
|
Confirm_f8a6 = {"["}Çóñfírm{"]"}
|
||||||
|
|
||||||
@@ -121,6 +139,9 @@ Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
|
|||||||
# Copy the raw note data in JSON format to clipboard
|
# Copy the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
|
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
|
||||||
|
|
||||||
|
# Tooltip text for copying npub to clipboard
|
||||||
|
Copy_npub_to_clipboard_c105 = {"["}Çópy ñpúb tó çlípbóàrd{"]"}
|
||||||
|
|
||||||
# Copy the author's public key to clipboard
|
# Copy the author's public key to clipboard
|
||||||
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
|
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
|
||||||
|
|
||||||
@@ -163,6 +184,9 @@ Customize_Zap_Amount_cfc4 = {"["}Çústómízé Zàp Àmóúñt{"]"}
|
|||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = {"["}Dàmús Súppórt{"]"}
|
Damus_Support_27c0 = {"["}Dàmús Súppórt{"]"}
|
||||||
|
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = {"["}Dàrk{"]"}
|
||||||
|
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = {"["}Déçk ñàmé{"]"}
|
Deck_name_cd32 = {"["}Déçk ñàmé{"]"}
|
||||||
|
|
||||||
@@ -190,6 +214,9 @@ Display_name_f9d9 = {"["}Dísplày ñàmé{"]"}
|
|||||||
# Domain identification message
|
# Domain identification message
|
||||||
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
||||||
|
|
||||||
|
# Button to indicate that the user is done going through the onboarding process.
|
||||||
|
Done_50dd = {"["}Dóñé{"]"}
|
||||||
|
|
||||||
# Column title for editing deck
|
# Column title for editing deck
|
||||||
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
|
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
|
||||||
|
|
||||||
@@ -220,6 +247,9 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
|||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
|
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
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
|
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
|
||||||
|
|
||||||
@@ -229,6 +259,9 @@ Home_8c19 = {"["}Hómé{"]"}
|
|||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = {"["}Íçóñ{"]"}
|
Icon_b0ab = {"["}Íçóñ{"]"}
|
||||||
|
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = {"["}Ímàgé çàçhé sízé:{"]"}
|
||||||
|
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = {"["}Íñdívídúàl{"]"}
|
Individual_b776 = {"["}Íñdívídúàl{"]"}
|
||||||
|
|
||||||
@@ -259,9 +292,18 @@ k_5K_f7e6 = {"["}5K{"]"}
|
|||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
|
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
|
||||||
|
|
||||||
|
# label for keys setting section
|
||||||
|
Keys_435f = {"["}Kéys{"]"}
|
||||||
|
|
||||||
|
# Label for language, Appearance settings section
|
||||||
|
Language_e264 = {"["}Làñgúàgé:{"]"}
|
||||||
|
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = {"["}Làst Ñóté pér Úsér{"]"}
|
Last_Note_per_User_17ad = {"["}Làst Ñóté pér Úsér{"]"}
|
||||||
|
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = {"["}Líght{"]"}
|
||||||
|
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"}
|
Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"}
|
||||||
|
|
||||||
@@ -280,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó
|
|||||||
# Title for the user's deck
|
# Title for the user's deck
|
||||||
My_Deck_4ac5 = {"["}My Déçk{"]"}
|
My_Deck_4ac5 = {"["}My Déçk{"]"}
|
||||||
|
|
||||||
|
# reaction from user to a note you were tagged in
|
||||||
|
name__reacted_to_a_note_you_were_tagged_in_4b62 = {"["}{$name} réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
|
||||||
|
# reaction from user to your note
|
||||||
|
name__reacted_to_your_note_ead9 = {"["}{$name} réàçtéd tó yóúr ñóté{"]"}
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_a_note_you_were_tagged_in_1379 = {"["}{$name} répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_your_note_1379 = {"["}{$name} répóstéd yóúr ñóté{"]"}
|
||||||
|
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||||
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
|
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
|
||||||
|
|
||||||
@@ -319,12 +373,21 @@ Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
|
|||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = {"["}ñów{"]"}
|
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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
|
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
|
||||||
|
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = {"["}Ópéñ yóúr défàúlt émàíl çlíéñt tó gét hélp fróm thé Dàmús téàm{"]"}
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = {"["}Ópéñ yóúr défàúlt émàíl çlíéñt tó gét hélp fróm thé Dàmús téàm{"]"}
|
||||||
|
|
||||||
|
# Label for others settings section
|
||||||
|
Others_7267 = {"["}Óthérs{"]"}
|
||||||
|
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"}
|
Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"}
|
||||||
|
|
||||||
@@ -346,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
|
|||||||
# Profile picture URL field label
|
# Profile picture URL field label
|
||||||
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
|
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
|
||||||
|
|
||||||
|
# label describing public key
|
||||||
|
PUBLIC_ACCOUNT_ID_4394 = {"["}PÚBLÍÇ ÀÇÇÓÚÑT ÍD{"]"}
|
||||||
|
|
||||||
# Column title for quote composition
|
# Column title for quote composition
|
||||||
Quote_475c = {"["}Qúóté{"]"}
|
Quote_475c = {"["}Qúóté{"]"}
|
||||||
|
|
||||||
@@ -394,6 +460,12 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
|
|||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = {"["}Répóstéd{"]"}
|
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{"]"}
|
||||||
|
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
|
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
|
||||||
|
|
||||||
@@ -418,15 +490,24 @@ Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
|
|||||||
# Search in progress message
|
# Search in progress message
|
||||||
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
|
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
|
||||||
|
|
||||||
|
# label describing secret key
|
||||||
|
SECRET_ACCOUNT_LOGIN_KEY_8440 = {"["}SÉÇRÉT ÀÇÇÓÚÑT LÓGÍÑ KÉY{"]"}
|
||||||
|
|
||||||
# Description for Home column
|
# Description for Home column
|
||||||
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
|
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
|
||||||
|
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"}
|
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
|
# Button label to send a zap
|
||||||
Send_1ea4 = {"["}Séñd{"]"}
|
Send_1ea4 = {"["}Séñd{"]"}
|
||||||
|
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = {"["}Séttíñgs{"]"}
|
||||||
|
|
||||||
# Description for last note per user column
|
# 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{"]"}
|
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{"]"}
|
||||||
|
|
||||||
@@ -439,6 +520,9 @@ Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"}
|
|||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
|
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
|
# 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{"]"}
|
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{"]"}
|
||||||
|
|
||||||
@@ -466,12 +550,18 @@ Step_1_8656 = {"["}Stép 1{"]"}
|
|||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = {"["}Stép 2{"]"}
|
Step_2_d08d = {"["}Stép 2{"]"}
|
||||||
|
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = {"["}Stóràgé{"]"}
|
||||||
|
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé élsé's ñótés{"]"}
|
Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé élsé's ñótés{"]"}
|
||||||
|
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
|
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
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
|
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
|
||||||
|
|
||||||
@@ -484,6 +574,9 @@ Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"}
|
|||||||
# Message shown when Dave trial period has ended
|
# 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 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"}
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"}
|
||||||
|
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = {"["}Thémé:{"]"}
|
||||||
|
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = {"["}Thréàd{"]"}
|
Thread_0f20 = {"["}Thréàd{"]"}
|
||||||
|
|
||||||
@@ -505,6 +598,9 @@ username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username
|
|||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = {"["}Úsérñàmé{"]"}
|
Username_daa7 = {"["}Úsérñàmé{"]"}
|
||||||
|
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = {"["}Víéw fóldér{"]"}
|
||||||
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = {"["}Wàllét{"]"}
|
Wallet_5e50 = {"["}Wàllét{"]"}
|
||||||
|
|
||||||
@@ -532,6 +628,9 @@ Zap_16b4 = {"["}Zàp{"]"}
|
|||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = {"["}Zàp thís ñóté{"]"}
|
Zap_this_note_42b2 = {"["}Zàp thís ñóté{"]"}
|
||||||
|
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = {"["}Zóóm Lévél:{"]"}
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
# Search results count
|
# Search results count
|
||||||
@@ -540,3 +639,35 @@ Got__count__results_for___query_85fb =
|
|||||||
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
|
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
|
||||||
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
|
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# amount of reactions a note you were tagged in received
|
||||||
|
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reactions your note received
|
||||||
|
name__and__count__others_reacted_to_your_note_0f6a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó yóúr ñóté{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó yóúr ñóté{"]"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts a note you were tagged in received
|
||||||
|
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts your note received
|
||||||
|
name__and__count__others_reposted_your_note_70a0 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér répóstéd yóúr ñóté{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs répóstéd yóúr ñóté{"]"}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ Add_account_1cfc = Agregar cuenta
|
|||||||
# Column title for adding new account
|
# Column title for adding new account
|
||||||
Add_Account_d06c = Agregar cuenta
|
Add_Account_d06c = Agregar cuenta
|
||||||
# Column title for adding algorithm column
|
# Column title for adding algorithm column
|
||||||
Add_Algo_Column_0d75 = Añadir columna algorítmica
|
Add_Algo_Column_0d75 = Agregar columna algorítmica
|
||||||
# Column title for adding new column
|
# Column title for adding new column
|
||||||
Add_Column_c764 = Agregar columna
|
Add_Column_c764 = Agregar columna
|
||||||
# Column title for adding new deck
|
# Column title for adding new deck
|
||||||
Add_Deck_fabf = Agregar Deck
|
Add_Deck_fabf = Agregar deck
|
||||||
# Column title for adding external notifications column
|
# Column title for adding external notifications column
|
||||||
Add_External_Notifications_Column_41ae = Agregar columna de notificaciones externas
|
Add_External_Notifications_Column_41ae = Agregar columna de notificaciones externas
|
||||||
# Column title for adding hashtag column
|
# Column title for adding hashtag column
|
||||||
Add_Hashtag_Column_ebf4 = Agregar columna de hashtag
|
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
|
||||||
# Column title for adding last notes column
|
# Column title for adding last notes column
|
||||||
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
|
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
|
||||||
# Column title for adding notifications column
|
# Column title for adding notifications column
|
||||||
@@ -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
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Cantidad
|
Amount_70f0 = Cantidad
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = Aspecto
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Preguntar
|
Ask_b7f4 = Preguntar
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Transmitir
|
|||||||
Broadcast_Local_7e50 = Transmitir localmente
|
Broadcast_Local_7e50 = Transmitir localmente
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Cancelar
|
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
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Haz clic para editar
|
Click_to_edit_0414 = Haz clic para editar
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Redactar nota
|
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
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Confirmar
|
Confirm_f8a6 = Confirmar
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -84,7 +94,7 @@ Copy_Note_ID_6b45 = Copiar ID de nota
|
|||||||
# Copy the raw note data in JSON format to clipboard
|
# Copy the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
||||||
# Copy the author's public key to clipboard
|
# Copy the author's public key to clipboard
|
||||||
Copy_Pubkey_9cc4 = Copiar Pubkey
|
Copy_Pubkey_9cc4 = Copiar pubkey
|
||||||
# Copy the text content of the note to clipboard
|
# Copy the text content of the note to clipboard
|
||||||
Copy_Text_f81c = Copiar texto
|
Copy_Text_f81c = Copiar texto
|
||||||
# Relative time in days
|
# Relative time in days
|
||||||
@@ -94,7 +104,7 @@ count_h_3ecb = { $count }h
|
|||||||
# Relative time in minutes
|
# Relative time in minutes
|
||||||
count_m_b41e = { $count }m
|
count_m_b41e = { $count }m
|
||||||
# Relative time in months
|
# Relative time in months
|
||||||
count_mo_7aba = { $count }ms
|
count_mo_7aba = { $count }mes
|
||||||
# Relative time in seconds
|
# Relative time in seconds
|
||||||
count_s_aa26 = { $count }s
|
count_s_aa26 = { $count }s
|
||||||
# Relative time in weeks
|
# Relative time in weeks
|
||||||
@@ -104,23 +114,25 @@ count_y_9408 = { $count }a
|
|||||||
# Button to create a new account
|
# Button to create a new account
|
||||||
Create_Account_6994 = Crear cuenta
|
Create_Account_6994 = Crear cuenta
|
||||||
# Button label to create a new deck
|
# Button label to create a new deck
|
||||||
Create_Deck_16b7 = Crear Deck
|
Create_Deck_16b7 = Crear deck
|
||||||
# Column title for custom timelines
|
# Column title for custom timelines
|
||||||
Custom_a69e = Personalizado
|
Custom_a69e = Personalizado
|
||||||
# Column title for zap amount customization
|
# Column title for zap amount customization
|
||||||
Customize_Zap_Amount_cfc4 = Personalizar monto de zap
|
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Ayuda de Damus
|
Damus_Support_27c0 = Ayuda de Damus
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = Oscuro
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Nombre del Deck
|
Deck_name_cd32 = Nombre del deck
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
DECKS_1fad = DECKS
|
DECKS_1fad = DECKS
|
||||||
# Label for default zap amount input
|
# Label for default zap amount input
|
||||||
Default_amount_per_zap_399d = Monto predeterminado por zap:
|
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
|
||||||
# Name of the default deck feed
|
# Name of the default deck feed
|
||||||
Default_Deck_fcca = Deck predeterminado
|
Default_Deck_fcca = Deck predeterminado
|
||||||
# Button label to delete a deck
|
# Button label to delete a deck
|
||||||
Delete_Deck_bb29 = Eliminar Deck
|
Delete_Deck_bb29 = Eliminar deck
|
||||||
# Tooltip for deleting a column
|
# Tooltip for deleting a column
|
||||||
Delete_this_column_8d5a = Eliminar esta columna
|
Delete_this_column_8d5a = Eliminar esta columna
|
||||||
# Button label to delete a wallet
|
# Button label to delete a wallet
|
||||||
@@ -130,9 +142,9 @@ Display_name_f9d9 = Nombre para mostrar
|
|||||||
# Domain identification message
|
# Domain identification message
|
||||||
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
||||||
# Column title for editing deck
|
# Column title for editing deck
|
||||||
Edit_Deck_4018 = Editar Deck
|
Edit_Deck_4018 = Editar deck
|
||||||
# Button label to edit a deck
|
# Button label to edit a deck
|
||||||
Edit_Deck_fd93 = Editar Deck
|
Edit_Deck_fd93 = Editar deck
|
||||||
# Button label to edit user profile
|
# Button label to edit user profile
|
||||||
Edit_Profile_49e6 = Editar perfil
|
Edit_Profile_49e6 = Editar perfil
|
||||||
# Column title for profile editing
|
# Column title for profile editing
|
||||||
@@ -149,16 +161,20 @@ 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.
|
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
|
# Label for find user button
|
||||||
Find_User_bd12 = Buscar usuario
|
Find_User_bd12 = Buscar usuario
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = Font size:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Inicio
|
Home_8c19 = Inicio
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Ícono
|
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
|
# Title for individual user column
|
||||||
Individual_b776 = Individual
|
Individual_b776 = Individual
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
Invalid_amount_6630 = Importe no válido
|
Invalid_amount_6630 = Cantidad no válida
|
||||||
# Error message for invalid key input
|
# Error message for invalid key input
|
||||||
Invalid_key_4726 = Clave no válida.
|
Invalid_key_4726 = Clave no válida.
|
||||||
# Error message for invalid Nostr Wallet Connect URI
|
# Error message for invalid Nostr Wallet Connect URI
|
||||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 50.000
|
|||||||
k_5K_f7e6 = 5.000
|
k_5K_f7e6 = 5.000
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
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
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Última nota por usuario
|
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
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -184,11 +204,11 @@ Login_9eef = Inicio de sesión
|
|||||||
# Login button text
|
# Login button text
|
||||||
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
|
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
|
||||||
# Text shown on blurred media from unfollowed users
|
# Text shown on blurred media from unfollowed users
|
||||||
Media_from_someone_you_don_t_follow_5611 = Medios de alguien que no sigues
|
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
|
||||||
# Tooltip for moving a column
|
# Tooltip for moving a column
|
||||||
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
||||||
# Title for the user's deck
|
# Title for the user's deck
|
||||||
My_Deck_4ac5 = Mi Deck
|
My_Deck_4ac5 = Mi deck
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||||
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
||||||
# NIP-05 identity field label
|
# NIP-05 identity field label
|
||||||
@@ -215,18 +235,22 @@ Notifications_d673 = Notificaciones
|
|||||||
Notifications_ef56 = Notificaciones
|
Notifications_ef56 = Notificaciones
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = ahora
|
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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Abrir correo electrónico
|
Open_Email_25e9 = Abrir correo electrónico
|
||||||
# Instruction to open email client
|
# 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
|
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
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Pega tu URI NWC aquí...
|
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
|
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el deck.
|
||||||
# Error message for missing deck name and icon
|
# Error message for missing deck name and icon
|
||||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el Deck y selecciona un icono.
|
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un ícono.
|
||||||
# Error message for missing deck icon
|
# Error message for missing deck icon
|
||||||
Please_select_an_icon_655b = Selecciona un icono.
|
Please_select_an_icon_655b = Selecciona un ícono.
|
||||||
# Button label to post a note
|
# Button label to post a note
|
||||||
Post_now_8a49 = Publicar ahora
|
Post_now_8a49 = Publicar ahora
|
||||||
# Instruction for copying logs
|
# Instruction for copying logs
|
||||||
@@ -250,7 +274,7 @@ Reply_to_this_note_f5de = Responder a esta nota
|
|||||||
# Error message when reply note cannot be found
|
# Error message when reply note cannot be found
|
||||||
Reply_to_unknown_note_4401 = Responder a nota desconocida
|
Reply_to_unknown_note_4401 = Responder a nota desconocida
|
||||||
# Fallback template for replying to user
|
# Fallback template for replying to user
|
||||||
replying_to__user_15ab = responder a { $user }
|
replying_to__user_15ab = respondiendo a { $user }
|
||||||
# Template for replying to user in unknown thread
|
# Template for replying to user in unknown thread
|
||||||
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
|
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
|
||||||
# Template for replying to note in different user's thread
|
# Template for replying to note in different user's thread
|
||||||
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = respondiendo a una nota
|
|||||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Publicadas de nuevo
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = ¿Encontraste un error?
|
Running_into_a_bug_1796 = ¿Encontraste un error?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# 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
|
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Enviar
|
Send_1ea4 = Enviar
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = Configuración
|
||||||
# Description for last note per user column
|
# 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
|
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
|
# 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
|
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
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
|
# 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
|
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
|
# 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_1_8656 = Paso 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Paso 2
|
Step_2_d08d = Paso 2
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = Almacenamiento
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
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
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||||
# Hover text for light mode toggle button
|
# 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
|
Tap_to_Load_4b05 = Toca para cargar
|
||||||
# Message shown when Dave trial period has ended
|
# 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!
|
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
|
# Column title for note thread view
|
||||||
Thread_0f20 = Conversación
|
Thread_0f20 = Conversación
|
||||||
# Link text for thread references
|
# 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
|
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Nombre de usuario
|
Username_daa7 = Nombre de usuario
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = Ver carpeta
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Billetera
|
Wallet_5e50 = Billetera
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -356,7 +396,9 @@ Your_Notifications_080d = Tus notificaciones
|
|||||||
# Heading for zap (tip) action
|
# Heading for zap (tip) action
|
||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Enviar zap a esta nota
|
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
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ Add_Algo_Column_0d75 = Añadir columna algorítmica
|
|||||||
# Column title for adding new column
|
# Column title for adding new column
|
||||||
Add_Column_c764 = Añadir columna
|
Add_Column_c764 = Añadir columna
|
||||||
# Column title for adding new deck
|
# Column title for adding new deck
|
||||||
Add_Deck_fabf = Añadir Deck
|
Add_Deck_fabf = Añadir deck
|
||||||
# Column title for adding external notifications column
|
# Column title for adding external notifications column
|
||||||
Add_External_Notifications_Column_41ae = Añadir columna de notificaciones externas
|
Add_External_Notifications_Column_41ae = Añadir columna de notificaciones externas
|
||||||
# Column title for adding hashtag column
|
# Column title for adding hashtag column
|
||||||
Add_Hashtag_Column_ebf4 = Añadir columna de hashtag
|
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
|
||||||
# Column title for adding last notes column
|
# Column title for adding last notes column
|
||||||
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
|
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
|
||||||
# Column title for adding notifications column
|
# Column title for adding notifications column
|
||||||
@@ -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
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Cantidad
|
Amount_70f0 = Cantidad
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = Aspecto
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Preguntar
|
Ask_b7f4 = Preguntar
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Transmitir
|
|||||||
Broadcast_Local_7e50 = Transmitir localmente
|
Broadcast_Local_7e50 = Transmitir localmente
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Cancelar
|
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
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Haz clic para editar
|
Click_to_edit_0414 = Haz clic para editar
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Redactar nota
|
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
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Confirmar
|
Confirm_f8a6 = Confirmar
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -84,7 +94,7 @@ Copy_Note_ID_6b45 = Copiar ID de nota
|
|||||||
# Copy the raw note data in JSON format to clipboard
|
# Copy the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
||||||
# Copy the author's public key to clipboard
|
# Copy the author's public key to clipboard
|
||||||
Copy_Pubkey_9cc4 = Copiar Pubkey
|
Copy_Pubkey_9cc4 = Copiar pubkey
|
||||||
# Copy the text content of the note to clipboard
|
# Copy the text content of the note to clipboard
|
||||||
Copy_Text_f81c = Copiar texto
|
Copy_Text_f81c = Copiar texto
|
||||||
# Relative time in days
|
# Relative time in days
|
||||||
@@ -94,7 +104,7 @@ count_h_3ecb = { $count }h
|
|||||||
# Relative time in minutes
|
# Relative time in minutes
|
||||||
count_m_b41e = { $count }m
|
count_m_b41e = { $count }m
|
||||||
# Relative time in months
|
# Relative time in months
|
||||||
count_mo_7aba = { $count }ms
|
count_mo_7aba = { $count }mes
|
||||||
# Relative time in seconds
|
# Relative time in seconds
|
||||||
count_s_aa26 = { $count }s
|
count_s_aa26 = { $count }s
|
||||||
# Relative time in weeks
|
# Relative time in weeks
|
||||||
@@ -104,23 +114,25 @@ count_y_9408 = { $count }a
|
|||||||
# Button to create a new account
|
# Button to create a new account
|
||||||
Create_Account_6994 = Crear cuenta
|
Create_Account_6994 = Crear cuenta
|
||||||
# Button label to create a new deck
|
# Button label to create a new deck
|
||||||
Create_Deck_16b7 = Crear Deck
|
Create_Deck_16b7 = Crear deck
|
||||||
# Column title for custom timelines
|
# Column title for custom timelines
|
||||||
Custom_a69e = Personalizado
|
Custom_a69e = Personalizado
|
||||||
# Column title for zap amount customization
|
# Column title for zap amount customization
|
||||||
Customize_Zap_Amount_cfc4 = Personalizar monto de zap
|
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Ayuda de Damus
|
Damus_Support_27c0 = Ayuda de Damus
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = Oscuro
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Nombre del Deck
|
Deck_name_cd32 = Nombre del deck
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
DECKS_1fad = DECKS
|
DECKS_1fad = DECKS
|
||||||
# Label for default zap amount input
|
# Label for default zap amount input
|
||||||
Default_amount_per_zap_399d = Monto predeterminado por zap:
|
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
|
||||||
# Name of the default deck feed
|
# Name of the default deck feed
|
||||||
Default_Deck_fcca = Deck predeterminado
|
Default_Deck_fcca = Deck predeterminado
|
||||||
# Button label to delete a deck
|
# Button label to delete a deck
|
||||||
Delete_Deck_bb29 = Eliminar Deck
|
Delete_Deck_bb29 = Eliminar deck
|
||||||
# Tooltip for deleting a column
|
# Tooltip for deleting a column
|
||||||
Delete_this_column_8d5a = Eliminar esta columna
|
Delete_this_column_8d5a = Eliminar esta columna
|
||||||
# Button label to delete a wallet
|
# Button label to delete a wallet
|
||||||
@@ -130,9 +142,9 @@ Display_name_f9d9 = Nombre para mostrar
|
|||||||
# Domain identification message
|
# Domain identification message
|
||||||
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
||||||
# Column title for editing deck
|
# Column title for editing deck
|
||||||
Edit_Deck_4018 = Editar Deck
|
Edit_Deck_4018 = Editar deck
|
||||||
# Button label to edit a deck
|
# Button label to edit a deck
|
||||||
Edit_Deck_fd93 = Editar Deck
|
Edit_Deck_fd93 = Editar deck
|
||||||
# Button label to edit user profile
|
# Button label to edit user profile
|
||||||
Edit_Profile_49e6 = Editar perfil
|
Edit_Profile_49e6 = Editar perfil
|
||||||
# Column title for profile editing
|
# Column title for profile editing
|
||||||
@@ -149,16 +161,20 @@ 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.
|
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
|
# Label for find user button
|
||||||
Find_User_bd12 = Buscar usuario
|
Find_User_bd12 = Buscar usuario
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = Font size:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Inicio
|
Home_8c19 = Inicio
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Ícono
|
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
|
# Title for individual user column
|
||||||
Individual_b776 = Individual
|
Individual_b776 = Individual
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
Invalid_amount_6630 = Importe no válido
|
Invalid_amount_6630 = Cantidad no válida
|
||||||
# Error message for invalid key input
|
# Error message for invalid key input
|
||||||
Invalid_key_4726 = Clave no válida.
|
Invalid_key_4726 = Clave no válida.
|
||||||
# Error message for invalid Nostr Wallet Connect URI
|
# Error message for invalid Nostr Wallet Connect URI
|
||||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 50.000
|
|||||||
k_5K_f7e6 = 5.000
|
k_5K_f7e6 = 5.000
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
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
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Última nota por usuario
|
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
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -184,11 +204,11 @@ Login_9eef = Inicio de sesión
|
|||||||
# Login button text
|
# Login button text
|
||||||
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
|
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
|
||||||
# Text shown on blurred media from unfollowed users
|
# Text shown on blurred media from unfollowed users
|
||||||
Media_from_someone_you_don_t_follow_5611 = Medios de alguien que no sigues
|
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
|
||||||
# Tooltip for moving a column
|
# Tooltip for moving a column
|
||||||
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
||||||
# Title for the user's deck
|
# Title for the user's deck
|
||||||
My_Deck_4ac5 = Mi Deck
|
My_Deck_4ac5 = Mi deck
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||||
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
||||||
# NIP-05 identity field label
|
# NIP-05 identity field label
|
||||||
@@ -215,16 +235,20 @@ Notifications_d673 = Notificaciones
|
|||||||
Notifications_ef56 = Notificaciones
|
Notifications_ef56 = Notificaciones
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = ahora
|
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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Abrir correo electrónico
|
Open_Email_25e9 = Abrir correo electrónico
|
||||||
# Instruction to open email client
|
# 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
|
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
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Pega tu URI NWC aquí...
|
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
|
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
|
||||||
# Error message for missing deck name and icon
|
# Error message for missing deck name and icon
|
||||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el Deck y selecciona un icono.
|
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un icono.
|
||||||
# Error message for missing deck icon
|
# Error message for missing deck icon
|
||||||
Please_select_an_icon_655b = Selecciona un icono.
|
Please_select_an_icon_655b = Selecciona un icono.
|
||||||
# Button label to post a note
|
# Button label to post a note
|
||||||
@@ -250,7 +274,7 @@ Reply_to_this_note_f5de = Responder a esta nota
|
|||||||
# Error message when reply note cannot be found
|
# Error message when reply note cannot be found
|
||||||
Reply_to_unknown_note_4401 = Responder a nota desconocida
|
Reply_to_unknown_note_4401 = Responder a nota desconocida
|
||||||
# Fallback template for replying to user
|
# Fallback template for replying to user
|
||||||
replying_to__user_15ab = responder a { $user }
|
replying_to__user_15ab = respondiendo a { $user }
|
||||||
# Template for replying to user in unknown thread
|
# Template for replying to user in unknown thread
|
||||||
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
|
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
|
||||||
# Template for replying to note in different user's thread
|
# Template for replying to note in different user's thread
|
||||||
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = respondiendo a una nota
|
|||||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Publicadas de nuevo
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = ¿Has encontrado un error?
|
Running_into_a_bug_1796 = ¿Has encontrado un error?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# 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
|
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Enviar
|
Send_1ea4 = Enviar
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = Configuración
|
||||||
# Description for last note per user column
|
# 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
|
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
|
# 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
|
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
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
|
# 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
|
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
|
# 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_1_8656 = Paso 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Paso 2
|
Step_2_d08d = Paso 2
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = Almacenamiento
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
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
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||||
# Hover text for light mode toggle button
|
# 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
|
Tap_to_Load_4b05 = Toca para cargar
|
||||||
# Message shown when Dave trial period has ended
|
# 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!
|
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
|
# Column title for note thread view
|
||||||
Thread_0f20 = Conversación
|
Thread_0f20 = Conversación
|
||||||
# Link text for thread references
|
# 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
|
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Nombre de usuario
|
Username_daa7 = Nombre de usuario
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = Ver carpeta
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Monedero
|
Wallet_5e50 = Monedero
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -356,7 +396,9 @@ Your_Notifications_080d = Tus notificaciones
|
|||||||
# Heading for zap (tip) action
|
# Heading for zap (tip) action
|
||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Enviar zap a esta nota
|
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
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ Algo_2452 = Algo
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Des fils algorithmiques pour faciliter la découverte de notes
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Des fils algorithmiques pour faciliter la découverte de notes
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Montant
|
Amount_70f0 = Montant
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = Apparence
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Demander
|
Ask_b7f4 = Demander
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = Diffusion
|
|||||||
Broadcast_Local_7e50 = Diffusion locale
|
Broadcast_Local_7e50 = Diffusion locale
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Annuler
|
Cancel_ed3b = Annuler
|
||||||
|
# Label for cancel clear cache, Storage settings section
|
||||||
|
Cancel_fd8b = Annuler
|
||||||
|
# Label for clear cache button, Storage settings section
|
||||||
|
Clear_cache_dccb = Vider le cache
|
||||||
# Hover text for editable zap amount
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Cliquer pour modifier
|
Click_to_edit_0414 = Cliquer pour modifier
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Ecrire une note
|
Compose_Note_c094 = Ecrire une note
|
||||||
|
# Label for configure relays, settings section
|
||||||
|
Configure_relays_d156 = Configurer les relais
|
||||||
|
# Label for confirm clear cache, Storage settings section
|
||||||
|
Confirm_9d9d = Confirmer
|
||||||
# Button label to confirm an action
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Confirmer
|
Confirm_f8a6 = Confirmer
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -111,6 +121,8 @@ Custom_a69e = Personnaliser
|
|||||||
Customize_Zap_Amount_cfc4 = Personnaliser le montant du Zap
|
Customize_Zap_Amount_cfc4 = Personnaliser le montant du Zap
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Assistance Damus
|
Damus_Support_27c0 = Assistance Damus
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = Sombre
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Nom du deck
|
Deck_name_cd32 = Nom du deck
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
@@ -149,12 +161,16 @@ 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.
|
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
|
# Label for find user button
|
||||||
Find_User_bd12 = Trouver un utilisateur
|
Find_User_bd12 = Trouver un utilisateur
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = Taille du texte :
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Accueil
|
Home_8c19 = Accueil
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Icone
|
Icon_b0ab = Icone
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = Taille du cache des images :
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = Individuel
|
Individual_b776 = Individuel
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 50K
|
|||||||
k_5K_f7e6 = 5K
|
k_5K_f7e6 = 5K
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
|
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
|
||||||
|
# Label for language, Appearance settings section
|
||||||
|
Language_e264 = Langue :
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Dernière note par utilisateur
|
Last_Note_per_User_17ad = Dernière note par utilisateur
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = Clair
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
|
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -215,10 +235,16 @@ Notifications_d673 = Notifications
|
|||||||
Notifications_ef56 = Notifications
|
Notifications_ef56 = Notifications
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = maintenant
|
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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Ouvrir Email
|
Open_Email_25e9 = Ouvrir Email
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Ouvrez votre service d'email par défaut pour obtenir de l'aide de l'équipe Damus
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Ouvrez votre service d'email par défaut pour obtenir de l'aide de l'équipe Damus
|
||||||
|
# Label for others settings section
|
||||||
|
Others_7267 = Autres
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Collez ici votre NWC URI...
|
Paste_your_NWC_URI_here_b471 = Collez ici votre NWC URI...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -265,6 +291,10 @@ replying_to_a_note_e0bc = répondre à une note
|
|||||||
Repost_this_note_8e56 = Republier cette note
|
Repost_this_note_8e56 = Republier cette note
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Republier
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = Vous rencontrez un problème ?
|
Running_into_a_bug_1796 = Vous rencontrez un problème ?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -285,8 +315,12 @@ Searching_for___query_5d18 = Recherche par '{ $query }'
|
|||||||
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
|
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
|
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
|
# Button label to send a zap
|
||||||
Send_1ea4 = Envoyer
|
Send_1ea4 = Envoyer
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = Paramètres
|
||||||
# Description for last note per user column
|
# 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
|
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
|
# Button label to sign out of account
|
||||||
@@ -295,6 +329,8 @@ Sign_out_337b = Se déconnecter
|
|||||||
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
|
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
|
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
|
# 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
|
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
|
# Description for hashtags column
|
||||||
@@ -313,10 +349,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour
|
|||||||
Step_1_8656 = Etape 1
|
Step_1_8656 = Etape 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Etape 2
|
Step_2_d08d = Etape 2
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = Stockage
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
|
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
|
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
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Passer en mode sombre
|
Switch_to_dark_mode_4dec = Passer en mode sombre
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -325,6 +365,8 @@ Switch_to_light_mode_72ce = Passer en mode clair
|
|||||||
Tap_to_Load_4b05 = Appuyer pour charger
|
Tap_to_Load_4b05 = Appuyer pour charger
|
||||||
# Message shown when Dave trial period has ended
|
# 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 période d'essai de l'assistant IA Dave Nostr est terminée :(. Merci de l'avoir testé ! Un Dave compatible-Zap sera bientôt disponible !
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La période d'essai de l'assistant IA Dave Nostr est terminée :(. Merci de l'avoir testé ! Un Dave compatible-Zap sera bientôt disponible !
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = Thème :
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = Fil
|
Thread_0f20 = Fil
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -339,6 +381,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Utiliser ce portefeuille pou
|
|||||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" à "{ $domain }" sera utilisé pour l'identification
|
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" à "{ $domain }" sera utilisé pour l'identification
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Nom d'utilisateur
|
Username_daa7 = Nom d'utilisateur
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = Voir le dossier
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Portefeuille
|
Wallet_5e50 = Portefeuille
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -357,6 +401,8 @@ Your_Notifications_080d = Vos notifications
|
|||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Zap cette note
|
Zap_this_note_42b2 = Zap cette note
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = Niveau de zoom :
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
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 }' 件取得しました
|
||||||
|
}
|
||||||
414
assets/translations/pt-BR/main.ftl
Normal file
414
assets/translations/pt-BR/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 = Transmitir
|
||||||
|
# Label for add column button
|
||||||
|
Add_47df = Adicionar coluna
|
||||||
|
# Button label to add a different wallet
|
||||||
|
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar outra carteira a ser usada apenas nesta conta
|
||||||
|
# Error message for missing wallet
|
||||||
|
Add_a_wallet_to_continue_d170 = Obrigatório adicionar carteira
|
||||||
|
# Button label to add a new account
|
||||||
|
Add_account_1cfc = Adicionar conta nova aqui
|
||||||
|
# Column title for adding new account
|
||||||
|
Add_Account_d06c = Adicionar nova 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 #
|
||||||
|
# Column title for adding last notes column
|
||||||
|
Add_Last_Notes_Column_bbad = Adicionar última coluna de 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 transmissão
|
||||||
|
# Button label to add a wallet
|
||||||
|
Add_Wallet_d1be = Adicionar carteira
|
||||||
|
# Title for algorithmic feeds column
|
||||||
|
Algo_2452 = Algoritmos
|
||||||
|
# Description for algorithmic feeds column
|
||||||
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algoritmos para pesquisar notas
|
||||||
|
# Label for zap amount input field
|
||||||
|
Amount_70f0 = Valor
|
||||||
|
# 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 ao Dave
|
||||||
|
# Profile banner URL field label
|
||||||
|
Banner_52ef = Destaque
|
||||||
|
# Beta version label
|
||||||
|
BETA_8e5d = Beta
|
||||||
|
# Broadcast the note to all connected relays
|
||||||
|
Broadcast_fe43 = Encaminhar
|
||||||
|
# Broadcast the note only to local network relays
|
||||||
|
Broadcast_Local_7e50 = Encaminhar especificamente
|
||||||
|
# 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 = Editar valor
|
||||||
|
# Column title for note composition
|
||||||
|
Compose_Note_c094 = Compor nota
|
||||||
|
# Label for configure relays, settings section
|
||||||
|
Configure_relays_d156 = Configurar canais
|
||||||
|
# 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 = Conectar
|
||||||
|
# Status label for connecting relay
|
||||||
|
Connecting_6b7e = Conectando...
|
||||||
|
# Title for contact list column
|
||||||
|
Contact_List_f85a = Lista de contatos
|
||||||
|
# Column title for contact lists
|
||||||
|
Contacts_7533 = Contatos
|
||||||
|
# Column title for last notes per contact
|
||||||
|
Contacts__last_notes_3f84 = Contatos (ú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 nota "JSON"
|
||||||
|
# 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 }Mes
|
||||||
|
# Relative time in seconds
|
||||||
|
count_s_aa26 = { $count }S
|
||||||
|
# Relative time in weeks
|
||||||
|
count_w_7468 = { $count }Sem
|
||||||
|
# Relative time in years
|
||||||
|
count_y_9408 = { $count }A
|
||||||
|
# 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 = Personalizar
|
||||||
|
# Column title for zap amount customization
|
||||||
|
Customize_Zap_Amount_cfc4 = Personalizar valor do ZAP
|
||||||
|
# Column title for support page
|
||||||
|
Damus_Support_27c0 = Ajuda
|
||||||
|
# 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 de ZAP
|
||||||
|
# Name of the default deck feed
|
||||||
|
Default_Deck_fcca = Nome padrão de abas
|
||||||
|
# Button label to delete a deck
|
||||||
|
Delete_Deck_bb29 = Deletar aba
|
||||||
|
# Tooltip for deleting a column
|
||||||
|
Delete_this_column_8d5a = Deletar esta coluna
|
||||||
|
# Button label to delete a wallet
|
||||||
|
Delete_Wallet_d1d4 = Deletar carteira
|
||||||
|
# Profile display name field label
|
||||||
|
Display_name_f9d9 = Nome de exibição
|
||||||
|
# Domain identification message
|
||||||
|
domain___will_be_used_for_identification_b67e = "{ $domain }" será utilizado para identificação
|
||||||
|
# Column title for editing deck
|
||||||
|
Edit_Deck_4018 = Editar aba
|
||||||
|
# Button label to edit a deck
|
||||||
|
Edit_Deck_fd93 = Editar
|
||||||
|
# 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 = Digite as # desejadas aqui (para múltiplos espaços separados)
|
||||||
|
# Placeholder for relay input field
|
||||||
|
Enter_the_relay_here_1c8b = Insira a retransmissão aqui
|
||||||
|
# Hint text to prompt entering the user's public key.
|
||||||
|
Enter_the_user_s_key__npub__hex__nip05__here_650c = Digite a chave do usuário (npub, hex, nip05) aqui...
|
||||||
|
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||||
|
Enter_your_key_0fca = Sua chave aqui
|
||||||
|
# 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 = 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 = #
|
||||||
|
# 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 de 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 = 100 mil
|
||||||
|
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||||
|
k_10K_f7e6 = 10 mil
|
||||||
|
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||||
|
k_20K_4977 = 20 mil
|
||||||
|
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||||
|
k_50K_c2dc = 50 mil
|
||||||
|
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||||
|
k_5K_f7e6 = 5 mil
|
||||||
|
# Description for your notes column
|
||||||
|
Keep_track_of_your_notes___replies_a334 = Acompanhe suas 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 Usuário
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = Modo claro
|
||||||
|
# Bitcoin Lightning network address field label
|
||||||
|
Lightning_network_address__lud16_ea51 = Endereço de rede de eletrização (lud16)
|
||||||
|
# Login page title
|
||||||
|
Login_9eef = Entrar
|
||||||
|
# Login button text
|
||||||
|
Login_now___let_s_do_this_5630 = Entrar agora! Vamos nessa!
|
||||||
|
# Text shown on blurred media from unfollowed users
|
||||||
|
Media_from_someone_you_don_t_follow_5611 = Conteúdo de pessoas que você não segue
|
||||||
|
# Tooltip for moving a column
|
||||||
|
Moves_this_column_to_another_position_0d4b = Mover esta coluna
|
||||||
|
# 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 = Novo no Nostr?
|
||||||
|
# NIP-05 identity field label
|
||||||
|
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (Identidade NIP-05)
|
||||||
|
# Default username when profile is not available
|
||||||
|
nostrich_df29 = Nostrich
|
||||||
|
# Status label for disconnected relay
|
||||||
|
Not_Connected_6292 = Desconectado
|
||||||
|
# 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 erros e entre em contato conosco 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 = 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
|
||||||
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abra o seu cliente de e-mail padrão para obter ajuda do time Damus
|
||||||
|
# Label for others settings section
|
||||||
|
Others_7267 = Outros
|
||||||
|
# Placeholder text for NWC URI input
|
||||||
|
Paste_your_NWC_URI_here_b471 = Cole seu URI NWC aqui...
|
||||||
|
# Error message for missing deck name
|
||||||
|
Please_create_a_name_for_the_deck_38e7 = Por favor, crie 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 = Por favor, crie um nome para a aba e selecione um ícone.
|
||||||
|
# Error message for missing deck icon
|
||||||
|
Please_select_an_icon_655b = Favor selecionar um ícone.
|
||||||
|
# Button label to post a note
|
||||||
|
Post_now_8a49 = Postar
|
||||||
|
# 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 = Clique abaixo para copiar seus registros mais recentes para a área de transferência do seu sistema. Em seguida, cole-os no seu 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 = Modo leitura
|
||||||
|
# Column title for relay management
|
||||||
|
Relays_9d89 = Canais
|
||||||
|
# Label for relay list section
|
||||||
|
Relays_ad5e = Canais
|
||||||
|
# Column title for reply composition
|
||||||
|
Reply_3bf1 = Responder
|
||||||
|
# Hover text for reply button
|
||||||
|
Reply_to_this_note_f5de = Responder esta nota
|
||||||
|
# Error message when reply note cannot be found
|
||||||
|
Reply_to_unknown_note_4401 = Responder nota desconhecida
|
||||||
|
# Fallback template for replying to user
|
||||||
|
replying_to__user_15ab = Respondendo { $user }
|
||||||
|
# Template for replying to user in unknown thread
|
||||||
|
replying_to__user__in_someone_s_thread_e148 = Respondendo { $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 = Resposta { $user }de { $note } em { $thread_user }' { $thread }
|
||||||
|
# Template for replying to user's note
|
||||||
|
replying_to__user__s__note_ccba = Respondendo { $user }de { $note }
|
||||||
|
# Template for replying to root thread
|
||||||
|
replying_to__user__s__thread_444d = Respondendo { $user }de { $thread }
|
||||||
|
# Fallback text when reply note is not found
|
||||||
|
replying_to_a_note_e0bc = Respondendo nota
|
||||||
|
# Hover text for repost button
|
||||||
|
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
|
||||||
|
Running_into_a_bug_1796 = Precisa de ajuda?
|
||||||
|
# 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 = Salvar
|
||||||
|
# Button label to save profile changes
|
||||||
|
Save_changes_00db = Salvo
|
||||||
|
# Column title for search page
|
||||||
|
Search_c573 = Pesquisar
|
||||||
|
# Placeholder for search notes input field
|
||||||
|
Search_notes_42a6 = Pesquisar notas...
|
||||||
|
# Search in progress message
|
||||||
|
Searching_for___query_5d18 = Pesquisando por '{ $query }'
|
||||||
|
# Description for Home column
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
Sign_out_337b = Sair
|
||||||
|
# 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 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
|
||||||
|
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantenha-se atualizado com uma certa hashtag
|
||||||
|
# Description for notifications column
|
||||||
|
Stay_up_to_date_with_notifications_and_mentions_6f4e = Ficar atualizado com notificações e menções
|
||||||
|
# Description for someone else's notes column
|
||||||
|
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantenha-se atualizado com as notas e respostas de alguém
|
||||||
|
# Description for someone else's notifications column
|
||||||
|
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantenha-se atualizado com as notificações e menções de alguém
|
||||||
|
# Description for individual user column
|
||||||
|
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantenha-se atualizado com as notas e respostas de alguém
|
||||||
|
# Description for your notifications column
|
||||||
|
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantenha-se atualizado com suas 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 = 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
|
||||||
|
Switch_to_light_mode_72ce = Mudar para modo claro
|
||||||
|
# Button text to load blurred media
|
||||||
|
Tap_to_Load_4b05 = Toque 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 Nostr terminou :(. Obrigado por testar! Em breve teremos Dave habilitado para Zap
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = Tema:
|
||||||
|
# Column title for note thread view
|
||||||
|
Thread_0f20 = Fio
|
||||||
|
# Link text for thread references
|
||||||
|
thread_ad1f = Fio
|
||||||
|
# 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 = Use esta carteira apenas para a conta atual
|
||||||
|
# Username and domain identification message
|
||||||
|
username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username }" em "{ $domain }" será usado para identificação
|
||||||
|
# Profile username field label
|
||||||
|
Username_daa7 = Usuário
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = Visualizar pasta
|
||||||
|
# Column title for wallet management
|
||||||
|
Wallet_5e50 = Carteira
|
||||||
|
# Hint for deck name input field
|
||||||
|
We_recommend_short_names_083e = Recomendamos nomes pequenos
|
||||||
|
# Profile website field label
|
||||||
|
Website_7980 = Site
|
||||||
|
# Placeholder for note input field
|
||||||
|
Write_a_banger_note_here_bad2 = Escreva uma nota criativa aqui.
|
||||||
|
# Placeholder text for key input field
|
||||||
|
Your_key_here_81bd = Sua chave aqui...
|
||||||
|
# Title for your notes column
|
||||||
|
Your_Notes_f6db = Suas notas
|
||||||
|
# Title for your notifications column
|
||||||
|
Your_Notifications_080d = Suas notificações
|
||||||
|
# Heading for zap (tip) action
|
||||||
|
Zap_16b4 = Zap
|
||||||
|
# Hover text for zap button
|
||||||
|
Zap_this_note_42b2 = Zap 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] Obteve um resultado { $count } para '{ $query }'
|
||||||
|
*[other] Obteve { $count } resultados para '{ $query }'
|
||||||
|
}
|
||||||
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 }'
|
||||||
|
}
|
||||||
@@ -45,6 +45,8 @@ Algo_2452 = อัลกอฯ
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลกอริทึมที่ช่วยในการค้นหาโน้ต
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลกอริทึมที่ช่วยในการค้นหาโน้ต
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = จำนวน
|
Amount_70f0 = จำนวน
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = ลักษณะ
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = ถาม
|
Ask_b7f4 = ถาม
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = เผยแพร่
|
|||||||
Broadcast_Local_7e50 = เผยแพร่เฉพาะที่
|
Broadcast_Local_7e50 = เผยแพร่เฉพาะที่
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = ยกเลิก
|
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
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = คลิกเพื่อแก้ไข
|
Click_to_edit_0414 = คลิกเพื่อแก้ไข
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = เขียนโน้ต
|
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
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = ยืนยัน
|
Confirm_f8a6 = ยืนยัน
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -80,11 +90,11 @@ Copy_a688 = คัดลอก
|
|||||||
# Button to copy media link to clipboard
|
# Button to copy media link to clipboard
|
||||||
Copy_Link_dc7c = คัดลอกลิงก์
|
Copy_Link_dc7c = คัดลอกลิงก์
|
||||||
# Copy the unique note identifier to clipboard
|
# 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 the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
|
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
|
||||||
# Copy the author's public key to clipboard
|
# 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 the text content of the note to clipboard
|
||||||
Copy_Text_f81c = คัดลอกข้อความ
|
Copy_Text_f81c = คัดลอกข้อความ
|
||||||
# Relative time in days
|
# Relative time in days
|
||||||
@@ -111,6 +121,8 @@ Custom_a69e = กำหนดเอง
|
|||||||
Customize_Zap_Amount_cfc4 = กำหนดจำนวน Zap
|
Customize_Zap_Amount_cfc4 = กำหนดจำนวน Zap
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = ฝ่ายสนับสนุน Damus
|
Damus_Support_27c0 = ฝ่ายสนับสนุน Damus
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = มืด
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = ชื่อ Deck
|
Deck_name_cd32 = ชื่อ Deck
|
||||||
# Label for decks section in side panel
|
# 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
|
|||||||
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
|
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = ค้นหาผู้ใช้
|
Find_User_bd12 = ค้นหาผู้ใช้
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = ขนาดตัวอักษร:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = แฮชแท็ก
|
Hashtags_f8e0 = แฮชแท็ก
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = หน้าแรก
|
Home_8c19 = หน้าแรก
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = ไอคอน
|
Icon_b0ab = ไอคอน
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = ขนาดแคชรูปภาพ:
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = ปัจเจคบุคคล
|
Individual_b776 = ปัจเจคบุคคล
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -177,8 +193,12 @@ k_50K_c2dc = 50K
|
|||||||
k_5K_f7e6 = 5K
|
k_5K_f7e6 = 5K
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
|
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
|
||||||
|
# Label for language, Appearance settings section
|
||||||
|
Language_e264 = ภาษา:
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = โน้ตล่าสุดของผู้ใช้แต่ละคน
|
Last_Note_per_User_17ad = โน้ตล่าสุดของผู้ใช้แต่ละคน
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = สว่าง
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = ที่อยู่ Lightning Network (lud16)
|
Lightning_network_address__lud16_ea51 = ที่อยู่ Lightning Network (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -217,10 +237,16 @@ Notifications_d673 = การแจ้งเตือน
|
|||||||
Notifications_ef56 = การแจ้งเตือน
|
Notifications_ef56 = การแจ้งเตือน
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = เมื่อสักครู่
|
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
|
# Button label to open email client
|
||||||
Open_Email_25e9 = เปิดอีเมล
|
Open_Email_25e9 = เปิดอีเมล
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = เปิดโปรแกรมอีเมลของคุณเพื่อรับความช่วยเหลือจากทีม Damus
|
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
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = วาง NWC URI ของคุณที่นี่...
|
Paste_your_NWC_URI_here_b471 = วาง NWC URI ของคุณที่นี่...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -230,7 +256,7 @@ Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาต
|
|||||||
# Error message for missing deck icon
|
# Error message for missing deck icon
|
||||||
Please_select_an_icon_655b = กรุณาเลือกไอคอน
|
Please_select_an_icon_655b = กรุณาเลือกไอคอน
|
||||||
# Button label to post a note
|
# Button label to post a note
|
||||||
Post_now_8a49 = โพสต์เลย
|
Post_now_8a49 = โพสต์
|
||||||
# Instruction for copying logs
|
# 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 = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ
|
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 URL field label
|
||||||
@@ -267,6 +293,10 @@ replying_to_a_note_e0bc = ตอบกลับโน้ต
|
|||||||
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
|
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = รีโพสต์แล้ว
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = พบปัญหาในการใช้งานใช่ไหม?
|
Running_into_a_bug_1796 = พบปัญหาในการใช้งานใช่ไหม?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -287,8 +317,12 @@ Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
|
|||||||
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
|
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
|
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = เลือกทั้งหมด
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = ส่ง
|
Send_1ea4 = ส่ง
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = การตั้งค่า
|
||||||
# Description for last note per user column
|
# 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_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -297,6 +331,8 @@ Sign_out_337b = ออกจากระบบ
|
|||||||
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
|
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
|
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
|
||||||
|
# Label for Sort replies newest first, others settings section
|
||||||
|
Sort_replies_newest_first_b6c3 = เรียงการตอบกลับจากใหม่ที่สุดไปเก่าที่สุด
|
||||||
# Description for contact list column
|
# 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_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -315,10 +351,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = ติดตาม
|
|||||||
Step_1_8656 = ขั้นตอนที่ 1
|
Step_1_8656 = ขั้นตอนที่ 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = ขั้นตอนที่ 2
|
Step_2_d08d = ขั้นตอนที่ 2
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = พื้นที่จัดเก็บ
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
|
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
|
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
|
||||||
|
# Support email address
|
||||||
|
Support_email_44d9 = อีเมลฝ่ายสนับสนุน:
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
|
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -327,6 +367,8 @@ Switch_to_light_mode_72ce = เปลี่ยนเป็นโหมดสว
|
|||||||
Tap_to_Load_4b05 = แตะเพื่อโหลด
|
Tap_to_Load_4b05 = แตะเพื่อโหลด
|
||||||
# Message shown when Dave trial period has ended
|
# 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 = ช่วงทดลองใช้ผู้ช่วย AI 'Dave Nostr' ได้สิ้นสุดลงแล้ว :( ขอบคุณที่ร่วมทดสอบ! Dave ที่รองรับการ Zap กำลังจะมาเร็วๆ นี้!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = ช่วงทดลองใช้ผู้ช่วย AI 'Dave Nostr' ได้สิ้นสุดลงแล้ว :( ขอบคุณที่ร่วมทดสอบ! Dave ที่รองรับการ Zap กำลังจะมาเร็วๆ นี้!
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = ธีม:
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = เธรด
|
Thread_0f20 = เธรด
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -338,9 +380,11 @@ Universe_ffaa = จักรวาล
|
|||||||
# Checkbox label for using wallet only for current account
|
# Checkbox label for using wallet only for current account
|
||||||
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
|
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
|
||||||
# Username and domain identification message
|
# 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
|
# Profile username field label
|
||||||
Username_daa7 = ชื่อผู้ใช้
|
Username_daa7 = ชื่อผู้ใช้
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = ดูโฟลเดอร์
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = วอลเล็ต
|
Wallet_5e50 = วอลเล็ต
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -348,7 +392,7 @@ We_recommend_short_names_083e = เราแนะนำให้ใช้ชื
|
|||||||
# Profile website field label
|
# Profile website field label
|
||||||
Website_7980 = เว็บไซต์
|
Website_7980 = เว็บไซต์
|
||||||
# Placeholder for note input field
|
# Placeholder for note input field
|
||||||
Write_a_banger_note_here_bad2 = เขียนโน้ตปังๆ ที่นี่...
|
Write_a_banger_note_here_bad2 = เขียนโน้ตที่นี่...
|
||||||
# Placeholder text for key input field
|
# Placeholder text for key input field
|
||||||
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
|
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
|
||||||
# Title for your notes column
|
# Title for your notes column
|
||||||
@@ -359,6 +403,8 @@ Your_Notifications_080d = การแจ้งเตือนของคุณ
|
|||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Zap โน้ตนี้
|
Zap_this_note_42b2 = Zap โน้ตนี้
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = ระดับการซูม:
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ Algo_2452 = 算法
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用于帮助发现笔记的算法源
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用于帮助发现笔记的算法源
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = 金额
|
Amount_70f0 = 金额
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = 外观
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = 询问
|
Ask_b7f4 = 询问
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = 广播
|
|||||||
Broadcast_Local_7e50 = 仅广播至本地中继
|
Broadcast_Local_7e50 = 仅广播至本地中继
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = 取消
|
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
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = 点击以编辑
|
Click_to_edit_0414 = 点击以编辑
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = 撰写笔记
|
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
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = 确认
|
Confirm_f8a6 = 确认
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -111,6 +121,8 @@ Custom_a69e = 自定义
|
|||||||
Customize_Zap_Amount_cfc4 = 自定义打闪金额
|
Customize_Zap_Amount_cfc4 = 自定义打闪金额
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = 达摩支持
|
Damus_Support_27c0 = 达摩支持
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = 暗色
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = 仪表板名称
|
Deck_name_cd32 = 仪表板名称
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
@@ -149,12 +161,16 @@ 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)。 你必须输入你的私钥才能发帖、回复等等。
|
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
|
# Label for find user button
|
||||||
Find_User_bd12 = 查找用户
|
Find_User_bd12 = 查找用户
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = 字体大小:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = 标签
|
Hashtags_f8e0 = 标签
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = 主页
|
Home_8c19 = 主页
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = 图标
|
Icon_b0ab = 图标
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = 图像缓存大小:
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = 个人
|
Individual_b776 = 个人
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 5万
|
|||||||
k_5K_f7e6 = 5千
|
k_5K_f7e6 = 5千
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = 随时查看你的笔记和回复
|
Keep_track_of_your_notes___replies_a334 = 随时查看你的笔记和回复
|
||||||
|
# Label for language, Appearance settings section
|
||||||
|
Language_e264 = 语言:
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = 每个用户的最新笔记
|
Last_Note_per_User_17ad = 每个用户的最新笔记
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = 亮色
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = 闪电网络地址(lud16)
|
Lightning_network_address__lud16_ea51 = 闪电网络地址(lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -215,10 +235,14 @@ Notifications_d673 = 通知
|
|||||||
Notifications_ef56 = 通知
|
Notifications_ef56 = 通知
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = 刚刚
|
now_2181 = 刚刚
|
||||||
|
# Setting to turn on sorting replies so that the newest are shown first
|
||||||
|
On_f412 = 开启
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = 打开电子邮箱
|
Open_Email_25e9 = 打开电子邮箱
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打开你的默认电子邮件客户端以获得达摩团队的帮助
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打开你的默认电子邮件客户端以获得达摩团队的帮助
|
||||||
|
# Label for others settings section
|
||||||
|
Others_7267 = 其它
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = 在此粘贴你的 NWC URI...
|
Paste_your_NWC_URI_here_b471 = 在此粘贴你的 NWC URI...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = 正在回复笔记
|
|||||||
Repost_this_note_8e56 = 转发此笔记
|
Repost_this_note_8e56 = 转发此笔记
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = 已转发
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = 遇到故障了吗?
|
Running_into_a_bug_1796 = 遇到故障了吗?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = 查看来自你的联系人的笔记
|
|||||||
See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
|
See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = 发送
|
Send_1ea4 = 发送
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = 设置
|
||||||
# Description for last note per user column
|
# 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_50e7 = 显示列表中每个用户的最新一条笔记
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -295,6 +325,8 @@ Sign_out_337b = 登出
|
|||||||
Someone_else_s_Notes_7e5f = 其他人的笔记
|
Someone_else_s_Notes_7e5f = 其他人的笔记
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = 其他人的通知
|
Someone_else_s_Notifications_82e6 = 其他人的通知
|
||||||
|
# Label for Sort replies newest first, others settings section
|
||||||
|
Sort_replies_newest_first_b6c3 = 按最新排序回复:
|
||||||
# Description for contact list column
|
# 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_e157 = 获取你的联系人列表中每个用户的最新一条笔记
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 获取你的通知
|
|||||||
Step_1_8656 = 第一步
|
Step_1_8656 = 第一步
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = 第二步
|
Step_2_d08d = 第二步
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = 存储
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
|
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
|
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
|
||||||
|
# Support email address
|
||||||
|
Support_email_44d9 = 支持电子邮件:
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = 切换到暗色模式
|
Switch_to_dark_mode_4dec = 切换到暗色模式
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = 切换到亮色模式
|
|||||||
Tap_to_Load_4b05 = 点击加载
|
Tap_to_Load_4b05 = 点击加载
|
||||||
# Message shown when Dave trial period has ended
|
# 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 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = 主题:
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = 帖子
|
Thread_0f20 = 帖子
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = 此钱包仅限用于当前
|
|||||||
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
|
# Profile username field label
|
||||||
Username_daa7 = 用户名
|
Username_daa7 = 用户名
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = 查看文件夹
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = 钱包
|
Wallet_5e50 = 钱包
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -357,6 +397,8 @@ Your_Notifications_080d = 你的通知
|
|||||||
Zap_16b4 = 打闪
|
Zap_16b4 = 打闪
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = 打闪此笔记
|
Zap_this_note_42b2 = 打闪此笔记
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = 缩放大小:
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ Algo_2452 = 算法
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用於幫助發現筆記的算法源
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用於幫助發現筆記的算法源
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = 金額
|
Amount_70f0 = 金額
|
||||||
|
# Label for appearance settings section
|
||||||
|
Appearance_4c7f = 外觀
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = 詢問
|
Ask_b7f4 = 詢問
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -59,10 +61,18 @@ Broadcast_fe43 = 廣播
|
|||||||
Broadcast_Local_7e50 = 僅廣播至本地中繼
|
Broadcast_Local_7e50 = 僅廣播至本地中繼
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = 取消
|
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
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = 點擊編輯
|
Click_to_edit_0414 = 點擊編輯
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = 撰寫筆記
|
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
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = 確認
|
Confirm_f8a6 = 確認
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -111,6 +121,8 @@ Custom_a69e = 自訂
|
|||||||
Customize_Zap_Amount_cfc4 = 自訂打閃金額
|
Customize_Zap_Amount_cfc4 = 自訂打閃金額
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = 達摩支持
|
Damus_Support_27c0 = 達摩支持
|
||||||
|
# Label for Theme Dark, Appearance settings section
|
||||||
|
Dark_85fe = 暗色
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = 儀表板名稱
|
Deck_name_cd32 = 儀表板名稱
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
@@ -149,12 +161,16 @@ 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)。你必須輸入你的私鑰才能發貼、回覆等等。
|
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
|
# Label for find user button
|
||||||
Find_User_bd12 = 查找用戶
|
Find_User_bd12 = 查找用戶
|
||||||
|
# Label for font size, Appearance settings section
|
||||||
|
Font_size_dd73 = 字體大小:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = 標籤
|
Hashtags_f8e0 = 標籤
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = 主頁
|
Home_8c19 = 主頁
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = 圖標
|
Icon_b0ab = 圖標
|
||||||
|
# Label for Image cache size, Storage settings section
|
||||||
|
Image_cache_size_3004 = 圖像快取大小:
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = 個人
|
Individual_b776 = 個人
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -175,8 +191,12 @@ k_50K_c2dc = 5萬
|
|||||||
k_5K_f7e6 = 5千
|
k_5K_f7e6 = 5千
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = 隨時查看你的筆記和回覆
|
Keep_track_of_your_notes___replies_a334 = 隨時查看你的筆記和回覆
|
||||||
|
# Label for language, Appearance settings section
|
||||||
|
Language_e264 = 語言:
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = 每個用戶的最新筆記
|
Last_Note_per_User_17ad = 每個用戶的最新筆記
|
||||||
|
# Label for Theme Light, Appearance settings section
|
||||||
|
Light_7475 = 亮色
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = 閃電網絡地址(lud16)
|
Lightning_network_address__lud16_ea51 = 閃電網絡地址(lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -215,10 +235,14 @@ Notifications_d673 = 通知
|
|||||||
Notifications_ef56 = 通知
|
Notifications_ef56 = 通知
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = 剛剛
|
now_2181 = 剛剛
|
||||||
|
# Setting to turn on sorting replies so that the newest are shown first
|
||||||
|
On_f412 = 開啟
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = 打開電子郵箱
|
Open_Email_25e9 = 打開電子郵箱
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
|
||||||
|
# Label for others settings section
|
||||||
|
Others_7267 = 其他
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = 在此貼上你的 NWC URI...
|
Paste_your_NWC_URI_here_b471 = 在此貼上你的 NWC URI...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -265,6 +289,10 @@ replying_to_a_note_e0bc = 正在回覆筆記
|
|||||||
Repost_this_note_8e56 = 轉發此筆記
|
Repost_this_note_8e56 = 轉發此筆記
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = 已轉發
|
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
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = 遇到故障了嗎?
|
Running_into_a_bug_1796 = 遇到故障了嗎?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -287,6 +315,8 @@ See_notes_from_your_contacts_ac16 = 查看來自你的聯繫人的筆記
|
|||||||
See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
|
See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = 發送
|
Send_1ea4 = 發送
|
||||||
|
# Column title for app settings
|
||||||
|
Settings_7a4f = 設置
|
||||||
# Description for last note per user column
|
# 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_50e7 = 顯示列表中每個用戶的最後一條筆記
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -295,6 +325,8 @@ Sign_out_337b = 登出
|
|||||||
Someone_else_s_Notes_7e5f = 其他人的筆記
|
Someone_else_s_Notes_7e5f = 其他人的筆記
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = 其他人的通知
|
Someone_else_s_Notifications_82e6 = 其他人的通知
|
||||||
|
# Label for Sort replies newest first, others settings section
|
||||||
|
Sort_replies_newest_first_b6c3 = 按最新排序回覆:
|
||||||
# Description for contact list column
|
# 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_e157 = 獲取你的聯繫人列表中每個用戶的最新一條筆記
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -313,10 +345,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 獲取你的通知
|
|||||||
Step_1_8656 = 第一步
|
Step_1_8656 = 第一步
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = 第二步
|
Step_2_d08d = 第二步
|
||||||
|
# Label for storage settings section
|
||||||
|
Storage_ed65 = 儲存
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
|
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
|
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
|
||||||
|
# Support email address
|
||||||
|
Support_email_44d9 = 支持電子郵件:
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = 切換到暗色模式
|
Switch_to_dark_mode_4dec = 切換到暗色模式
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -325,6 +361,8 @@ Switch_to_light_mode_72ce = 切換到亮色模式
|
|||||||
Tap_to_Load_4b05 = 點擊加載
|
Tap_to_Load_4b05 = 點擊加載
|
||||||
# Message shown when Dave trial period has ended
|
# 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 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
|
||||||
|
# Label for theme, Appearance settings section
|
||||||
|
Theme_4aac = 主題:
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = 串文
|
Thread_0f20 = 串文
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -339,6 +377,8 @@ Use_this_wallet_for_the_current_account_only_61dc = 此錢包僅限用於當前
|
|||||||
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
|
# Profile username field label
|
||||||
Username_daa7 = 用戶名
|
Username_daa7 = 用戶名
|
||||||
|
# Label for view folder button, Storage settings section
|
||||||
|
View_folder_9742 = 查看文件夾
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = 錢包
|
Wallet_5e50 = 錢包
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -357,6 +397,8 @@ Your_Notifications_080d = 你的通知
|
|||||||
Zap_16b4 = 打閃
|
Zap_16b4 = 打閃
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = 打閃此筆記
|
Zap_this_note_42b2 = 打閃此筆記
|
||||||
|
# Label for zoom level, Appearance settings section
|
||||||
|
Zoom_Level_29a8 = 縮放大小:
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ pub fn setup_multicast_relay(
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut events = Events::with_capacity(1);
|
let mut events = Events::with_capacity(1);
|
||||||
loop {
|
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.");
|
error!("multicast socket poll error: {err}. ending multicast poller.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl From<RelayEvent<'_>> for OwnedRelayEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
pub struct RelaySub {
|
pub struct _RelaySub {
|
||||||
pub(crate) subid: String,
|
pub(crate) subid: String,
|
||||||
pub(crate) filter: String,
|
pub(crate) filter: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ nostrdb = { workspace = true }
|
|||||||
jni = { workspace = true }
|
jni = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
|
blurhash = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
strum_macros = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
enostr = { workspace = true }
|
enostr = { workspace = true }
|
||||||
nostr = { workspace = true }
|
nostr = { workspace = true }
|
||||||
egui = { workspace = true }
|
egui = { workspace = true }
|
||||||
|
egui_extras = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
base32 = { workspace = true }
|
base32 = { workspace = true }
|
||||||
@@ -45,7 +47,11 @@ fluent-langneg = { workspace = true }
|
|||||||
unic-langid = { workspace = true }
|
unic-langid = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
|
bitflags = { workspace = true }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
chrono = { workspace = true }
|
||||||
|
indexmap = {workspace = true}
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
@@ -53,6 +59,8 @@ tokio = { workspace = true }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
jni = { workspace = true }
|
jni = { workspace = true }
|
||||||
|
android-activity = { workspace = true }
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
puffin = ["puffin_egui", "dep:puffin"]
|
puffin = ["puffin_egui", "dep:puffin"]
|
||||||
|
|||||||
@@ -207,6 +207,10 @@ impl Accounts {
|
|||||||
self.cache.selected_mut()
|
self.cache.selected_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_wallet(&self) -> Option<&ZapWallet> {
|
||||||
|
self.cache.selected().wallet.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_selected_wallet_mut(&mut self) -> Option<&mut ZapWallet> {
|
pub fn get_selected_wallet_mut(&mut self) -> Option<&mut ZapWallet> {
|
||||||
self.cache.selected_mut().wallet.as_mut()
|
self.cache.selected_mut().wallet.as_mut()
|
||||||
}
|
}
|
||||||
@@ -263,6 +267,11 @@ impl Accounts {
|
|||||||
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
|
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) {
|
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
|
||||||
let data = &self.get_selected_account().data;
|
let data = &self.get_selected_account().data;
|
||||||
// send the active account's relay list subscription
|
// send the active account's relay list subscription
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub enum ContactState {
|
|||||||
Received {
|
Received {
|
||||||
contacts: HashSet<Pubkey>,
|
contacts: HashSet<Pubkey>,
|
||||||
note_key: NoteKey,
|
note_key: NoteKey,
|
||||||
|
timestamp: u64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ impl Contacts {
|
|||||||
|
|
||||||
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||||
let binding = ndb
|
let binding = ndb
|
||||||
.query(txn, &[self.filter.clone()], 1)
|
.query(txn, std::slice::from_ref(&self.filter), 1)
|
||||||
.expect("query user relays results");
|
.expect("query user relays results");
|
||||||
|
|
||||||
let Some(res) = binding.first() else {
|
let Some(res) = binding.first() else {
|
||||||
@@ -57,6 +58,7 @@ impl Contacts {
|
|||||||
ContactState::Received {
|
ContactState::Received {
|
||||||
contacts,
|
contacts,
|
||||||
note_key: _,
|
note_key: _,
|
||||||
|
timestamp: _,
|
||||||
} => {
|
} => {
|
||||||
if contacts.contains(other_pubkey) {
|
if contacts.contains(other_pubkey) {
|
||||||
IsFollowing::Yes
|
IsFollowing::Yes
|
||||||
@@ -82,6 +84,18 @@ impl Contacts {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let ContactState::Received {
|
||||||
|
contacts: _,
|
||||||
|
note_key: _,
|
||||||
|
timestamp,
|
||||||
|
} = self.get_state()
|
||||||
|
{
|
||||||
|
if *timestamp > note.created_at() {
|
||||||
|
// the current contact list is more up to date than the one we just received. ignore it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update_state(&mut self.state, ¬e, *key);
|
update_state(&mut self.state, ¬e, *key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +110,17 @@ fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
|
|||||||
*state = ContactState::Received {
|
*state = ContactState::Received {
|
||||||
contacts: get_contacts_owned(note),
|
contacts: get_contacts_owned(note),
|
||||||
note_key: key,
|
note_key: key,
|
||||||
|
timestamp: note.created_at(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ContactState::Received { contacts, note_key } => {
|
ContactState::Received {
|
||||||
|
contacts,
|
||||||
|
note_key,
|
||||||
|
timestamp,
|
||||||
|
} => {
|
||||||
update_contacts(contacts, note);
|
update_contacts(contacts, note);
|
||||||
*note_key = key;
|
*note_key = key;
|
||||||
|
*timestamp = note.created_at();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl AccountMutedData {
|
|||||||
.limit()
|
.limit()
|
||||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||||
let nks = ndb
|
let nks = ndb
|
||||||
.query(txn, &[self.filter.clone()], lim)
|
.query(txn, std::slice::from_ref(&self.filter), lim)
|
||||||
.expect("query user muted results")
|
.expect("query user muted results")
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| qr.note_key)
|
.map(|qr| qr.note_key)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use crate::{AccountData, RelaySpec};
|
||||||
use enostr::{Keypair, Pubkey, RelayPool};
|
use enostr::{Keypair, Pubkey, RelayPool};
|
||||||
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
|
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{AccountData, RelaySpec};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct AccountRelayData {
|
pub(crate) struct AccountRelayData {
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
@@ -37,7 +36,7 @@ impl AccountRelayData {
|
|||||||
.limit()
|
.limit()
|
||||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||||
let nks = ndb
|
let nks = ndb
|
||||||
.query(txn, &[self.filter.clone()], lim)
|
.query(txn, std::slice::from_ref(&self.filter), lim)
|
||||||
.expect("query user relays results")
|
.expect("query user relays results")
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| qr.note_key)
|
.map(|qr| qr.note_key)
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use crate::account::FALLBACK_PUBKEY;
|
use crate::account::FALLBACK_PUBKEY;
|
||||||
use crate::i18n::Localization;
|
use crate::i18n::Localization;
|
||||||
use crate::persist::{AppSizeHandler, ZoomHandler};
|
use crate::persist::{AppSizeHandler, SettingsHandler};
|
||||||
use crate::wallet::GlobalWallet;
|
use crate::wallet::GlobalWallet;
|
||||||
use crate::zaps::Zaps;
|
use crate::zaps::Zaps;
|
||||||
|
use crate::Error;
|
||||||
use crate::JobPool;
|
use crate::JobPool;
|
||||||
|
use crate::NotedeckOptions;
|
||||||
use crate::{
|
use crate::{
|
||||||
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
||||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
|
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
|
||||||
UnknownIds,
|
|
||||||
};
|
};
|
||||||
use egui::Margin;
|
use egui::Margin;
|
||||||
use egui::ThemePreference;
|
use egui::ThemePreference;
|
||||||
@@ -19,6 +20,10 @@ use std::collections::BTreeSet;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use android_activity::AndroidApp;
|
||||||
|
|
||||||
pub enum AppAction {
|
pub enum AppAction {
|
||||||
Note(NoteAction),
|
Note(NoteAction),
|
||||||
@@ -40,9 +45,8 @@ pub struct Notedeck {
|
|||||||
global_wallet: GlobalWallet,
|
global_wallet: GlobalWallet,
|
||||||
path: DataPath,
|
path: DataPath,
|
||||||
args: Args,
|
args: Args,
|
||||||
theme: ThemeHandler,
|
settings: SettingsHandler,
|
||||||
app: Option<Rc<RefCell<dyn App>>>,
|
app: Option<Rc<RefCell<dyn App>>>,
|
||||||
zoom: ZoomHandler,
|
|
||||||
app_size: AppSizeHandler,
|
app_size: AppSizeHandler,
|
||||||
unrecognized_args: BTreeSet<String>,
|
unrecognized_args: BTreeSet<String>,
|
||||||
clipboard: Clipboard,
|
clipboard: Clipboard,
|
||||||
@@ -50,6 +54,9 @@ pub struct Notedeck {
|
|||||||
frame_history: FrameHistory,
|
frame_history: FrameHistory,
|
||||||
job_pool: JobPool,
|
job_pool: JobPool,
|
||||||
i18n: Localization,
|
i18n: Localization,
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
android_app: Option<AndroidApp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Our chrome, which is basically nothing
|
/// Our chrome, which is basically nothing
|
||||||
@@ -99,10 +106,18 @@ impl eframe::App for Notedeck {
|
|||||||
|
|
||||||
render_notedeck(self, ctx);
|
render_notedeck(self, ctx);
|
||||||
|
|
||||||
self.zoom.try_save_zoom_factor(ctx);
|
self.settings.update_batch(|settings| {
|
||||||
|
settings.zoom_factor = ctx.zoom_factor();
|
||||||
|
settings.locale = self.i18n.get_current_locale().to_string();
|
||||||
|
settings.theme = if ctx.style().visuals.dark_mode {
|
||||||
|
ThemePreference::Dark
|
||||||
|
} else {
|
||||||
|
ThemePreference::Light
|
||||||
|
};
|
||||||
|
});
|
||||||
self.app_size.try_save_app_size(ctx);
|
self.app_size.try_save_app_size(ctx);
|
||||||
|
|
||||||
if self.args.relay_debug {
|
if self.args.options.contains(NotedeckOptions::RelayDebug) {
|
||||||
if self.pool.debug.is_none() {
|
if self.pool.debug.is_none() {
|
||||||
self.pool.use_debug();
|
self.pool.use_debug();
|
||||||
}
|
}
|
||||||
@@ -129,6 +144,11 @@ fn setup_puffin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Notedeck {
|
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 {
|
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
setup_puffin();
|
setup_puffin();
|
||||||
@@ -159,10 +179,11 @@ impl Notedeck {
|
|||||||
1024usize * 1024usize * 1024usize * 1024usize
|
1024usize * 1024usize * 1024usize * 1024usize
|
||||||
};
|
};
|
||||||
|
|
||||||
let theme = ThemeHandler::new(&path);
|
let settings = SettingsHandler::new(&path).load();
|
||||||
|
|
||||||
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
|
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
|
||||||
|
|
||||||
let keystore = if parsed_args.use_keystore {
|
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
|
||||||
let keys_path = path.path(DataPathType::Keys);
|
let keys_path = path.path(DataPathType::Keys);
|
||||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||||
Some(AccountStorage::new(
|
Some(AccountStorage::new(
|
||||||
@@ -213,12 +234,8 @@ impl Notedeck {
|
|||||||
|
|
||||||
let img_cache = Images::new(img_cache_dir);
|
let img_cache = Images::new(img_cache_dir);
|
||||||
let note_cache = NoteCache::default();
|
let note_cache = NoteCache::default();
|
||||||
let zoom = ZoomHandler::new(&path);
|
|
||||||
let app_size = AppSizeHandler::new(&path);
|
|
||||||
|
|
||||||
if let Some(z) = zoom.get_zoom_factor() {
|
let app_size = AppSizeHandler::new(&path);
|
||||||
ctx.set_zoom_factor(z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrate
|
// migrate
|
||||||
if let Err(e) = img_cache.migrate_v0() {
|
if let Err(e) = img_cache.migrate_v0() {
|
||||||
@@ -231,15 +248,22 @@ impl Notedeck {
|
|||||||
|
|
||||||
// Initialize localization
|
// Initialize localization
|
||||||
let mut i18n = Localization::new();
|
let mut i18n = Localization::new();
|
||||||
|
|
||||||
|
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
|
||||||
|
settings.locale().parse();
|
||||||
|
|
||||||
|
if 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 Some(locale) = &parsed_args.locale {
|
||||||
if let Err(err) = i18n.set_locale(locale.to_owned()) {
|
if let Err(err) = i18n.set_locale(locale.to_owned()) {
|
||||||
error!("{err}");
|
error!("{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize global i18n context
|
|
||||||
//crate::i18n::init_global_i18n(i18n.clone());
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ndb,
|
ndb,
|
||||||
img_cache,
|
img_cache,
|
||||||
@@ -250,9 +274,8 @@ impl Notedeck {
|
|||||||
global_wallet,
|
global_wallet,
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
args: parsed_args,
|
args: parsed_args,
|
||||||
theme,
|
settings,
|
||||||
app: None,
|
app: None,
|
||||||
zoom,
|
|
||||||
app_size,
|
app_size,
|
||||||
unrecognized_args,
|
unrecognized_args,
|
||||||
frame_history: FrameHistory::default(),
|
frame_history: FrameHistory::default(),
|
||||||
@@ -260,9 +283,49 @@ impl Notedeck {
|
|||||||
zaps,
|
zaps,
|
||||||
job_pool,
|
job_pool,
|
||||||
i18n,
|
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 {
|
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
|
||||||
self.set_app(app);
|
self.set_app(app);
|
||||||
self
|
self
|
||||||
@@ -279,12 +342,14 @@ impl Notedeck {
|
|||||||
global_wallet: &mut self.global_wallet,
|
global_wallet: &mut self.global_wallet,
|
||||||
path: &self.path,
|
path: &self.path,
|
||||||
args: &self.args,
|
args: &self.args,
|
||||||
theme: &mut self.theme,
|
settings: &mut self.settings,
|
||||||
clipboard: &mut self.clipboard,
|
clipboard: &mut self.clipboard,
|
||||||
zaps: &mut self.zaps,
|
zaps: &mut self.zaps,
|
||||||
frame_history: &mut self.frame_history,
|
frame_history: &mut self.frame_history,
|
||||||
job_pool: &mut self.job_pool,
|
job_pool: &mut self.job_pool,
|
||||||
i18n: &mut self.i18n,
|
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 {
|
pub fn theme(&self) -> ThemePreference {
|
||||||
self.theme.load()
|
self.settings.theme()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_body_font_size(&self) -> f32 {
|
||||||
|
self.settings.note_body_font_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_factor(&self) -> f32 {
|
||||||
|
self.settings.zoom_factor()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use crate::NotedeckOptions;
|
||||||
use enostr::{Keypair, Pubkey, SecretKey};
|
use enostr::{Keypair, Pubkey, SecretKey};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||||
|
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
pub relays: Vec<String>,
|
pub relays: Vec<String>,
|
||||||
pub is_mobile: Option<bool>,
|
|
||||||
pub locale: Option<LanguageIdentifier>,
|
pub locale: Option<LanguageIdentifier>,
|
||||||
pub show_note_client: bool,
|
|
||||||
pub keys: Vec<Keypair>,
|
pub keys: Vec<Keypair>,
|
||||||
pub light: bool,
|
pub options: NotedeckOptions,
|
||||||
pub debug: bool,
|
|
||||||
pub relay_debug: bool,
|
|
||||||
|
|
||||||
/// Enable when running tests so we don't panic on app startup
|
|
||||||
pub tests: bool,
|
|
||||||
|
|
||||||
pub use_keystore: bool,
|
|
||||||
pub dbpath: Option<String>,
|
pub dbpath: Option<String>,
|
||||||
pub datapath: Option<String>,
|
pub datapath: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -28,14 +20,8 @@ impl Args {
|
|||||||
let mut unrecognized_args = BTreeSet::new();
|
let mut unrecognized_args = BTreeSet::new();
|
||||||
let mut res = Args {
|
let mut res = Args {
|
||||||
relays: vec![],
|
relays: vec![],
|
||||||
is_mobile: None,
|
|
||||||
keys: vec![],
|
keys: vec![],
|
||||||
light: false,
|
options: NotedeckOptions::default(),
|
||||||
show_note_client: false,
|
|
||||||
debug: false,
|
|
||||||
relay_debug: false,
|
|
||||||
tests: false,
|
|
||||||
use_keystore: true,
|
|
||||||
dbpath: None,
|
dbpath: None,
|
||||||
datapath: None,
|
datapath: None,
|
||||||
locale: None,
|
locale: None,
|
||||||
@@ -47,9 +33,9 @@ impl Args {
|
|||||||
let arg = &args[i];
|
let arg = &args[i];
|
||||||
|
|
||||||
if arg == "--mobile" {
|
if arg == "--mobile" {
|
||||||
res.is_mobile = Some(true);
|
res.options.set(NotedeckOptions::Mobile, true);
|
||||||
} else if arg == "--light" {
|
} else if arg == "--light" {
|
||||||
res.light = true;
|
res.options.set(NotedeckOptions::LightTheme, true);
|
||||||
} else if arg == "--locale" {
|
} else if arg == "--locale" {
|
||||||
i += 1;
|
i += 1;
|
||||||
let Some(locale) = args.get(i) else {
|
let Some(locale) = args.get(i) else {
|
||||||
@@ -68,11 +54,11 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if arg == "--dark" {
|
} else if arg == "--dark" {
|
||||||
res.light = false;
|
res.options.set(NotedeckOptions::LightTheme, false);
|
||||||
} else if arg == "--debug" {
|
} else if arg == "--debug" {
|
||||||
res.debug = true;
|
res.options.set(NotedeckOptions::Debug, true);
|
||||||
} else if arg == "--testrunner" {
|
} else if arg == "--testrunner" {
|
||||||
res.tests = true;
|
res.options.set(NotedeckOptions::Tests, true);
|
||||||
} else if arg == "--pub" || arg == "--npub" {
|
} else if arg == "--pub" || arg == "--npub" {
|
||||||
i += 1;
|
i += 1;
|
||||||
let pubstr = if let Some(next_arg) = args.get(i) {
|
let pubstr = if let Some(next_arg) = args.get(i) {
|
||||||
@@ -135,11 +121,13 @@ impl Args {
|
|||||||
};
|
};
|
||||||
res.relays.push(relay.clone());
|
res.relays.push(relay.clone());
|
||||||
} else if arg == "--no-keystore" {
|
} else if arg == "--no-keystore" {
|
||||||
res.use_keystore = false;
|
res.options.set(NotedeckOptions::UseKeystore, true);
|
||||||
} else if arg == "--relay-debug" {
|
} else if arg == "--relay-debug" {
|
||||||
res.relay_debug = true;
|
res.options.set(NotedeckOptions::RelayDebug, true);
|
||||||
} else if arg == "--show-note-client" {
|
} else if arg == "--notebook" {
|
||||||
res.show_note_client = true;
|
res.options.set(NotedeckOptions::FeatureNotebook, true);
|
||||||
|
} else if arg == "--clndash" {
|
||||||
|
res.options.set(NotedeckOptions::FeatureClnDash, true);
|
||||||
} else {
|
} else {
|
||||||
unrecognized_args.insert(arg.clone());
|
unrecognized_args.insert(arg.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
|
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
|
||||||
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
|
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
|
||||||
UnknownIds,
|
UnknownIds,
|
||||||
};
|
};
|
||||||
use egui_winit::clipboard::Clipboard;
|
use egui_winit::clipboard::Clipboard;
|
||||||
@@ -8,6 +8,9 @@ use egui_winit::clipboard::Clipboard;
|
|||||||
use enostr::RelayPool;
|
use enostr::RelayPool;
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use android_activity::AndroidApp;
|
||||||
|
use egui::{Pos2, Rect};
|
||||||
// TODO: make this interface more sandboxed
|
// TODO: make this interface more sandboxed
|
||||||
|
|
||||||
pub struct AppContext<'a> {
|
pub struct AppContext<'a> {
|
||||||
@@ -20,10 +23,68 @@ pub struct AppContext<'a> {
|
|||||||
pub global_wallet: &'a mut GlobalWallet,
|
pub global_wallet: &'a mut GlobalWallet,
|
||||||
pub path: &'a DataPath,
|
pub path: &'a DataPath,
|
||||||
pub args: &'a Args,
|
pub args: &'a Args,
|
||||||
pub theme: &'a mut ThemeHandler,
|
pub settings: &'a mut SettingsHandler,
|
||||||
pub clipboard: &'a mut Clipboard,
|
pub clipboard: &'a mut Clipboard,
|
||||||
pub zaps: &'a mut Zaps,
|
pub zaps: &'a mut Zaps,
|
||||||
pub frame_history: &'a mut FrameHistory,
|
pub frame_history: &'a mut FrameHistory,
|
||||||
pub job_pool: &'a mut JobPool,
|
pub job_pool: &'a mut JobPool,
|
||||||
pub i18n: &'a mut Localization,
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,26 @@ pub enum ZapError {
|
|||||||
#[error("invalid lud16")]
|
#[error("invalid lud16")]
|
||||||
InvalidLud16(String),
|
InvalidLud16(String),
|
||||||
#[error("invalid endpoint response")]
|
#[error("invalid endpoint response")]
|
||||||
EndpointError(String),
|
EndpointError(EndpointError),
|
||||||
#[error("bech encoding/decoding error")]
|
#[error("bech encoding/decoding error")]
|
||||||
Bech(String),
|
Bech(String),
|
||||||
#[error("serialization/deserialization problem")]
|
#[error("serialization/deserialization problem")]
|
||||||
Serialization(String),
|
Serialization(String),
|
||||||
#[error("nwc error")]
|
#[error("nwc error")]
|
||||||
NWC(String),
|
NWC(String),
|
||||||
|
#[error("ndb error")]
|
||||||
|
Ndb(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ZapError {
|
||||||
|
pub fn endpoint_error(error: String) -> ZapError {
|
||||||
|
ZapError::EndpointError(EndpointError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EndpointError(pub String);
|
||||||
|
|
||||||
impl From<String> for Error {
|
impl From<String> for Error {
|
||||||
fn from(s: String) -> Self {
|
fn from(s: String) -> Self {
|
||||||
Error::Generic(s)
|
Error::Generic(s)
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ impl FilterStates {
|
|||||||
}
|
}
|
||||||
self.states.insert(relay, state);
|
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.
|
/// 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
|
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
|
// Get the latest entry in the events
|
||||||
if notes.is_empty() {
|
let Some(latest) = latest_note else {
|
||||||
return filter;
|
return filter;
|
||||||
}
|
};
|
||||||
|
|
||||||
// get the latest note
|
// get the latest note
|
||||||
let latest = notes[0];
|
|
||||||
let since = latest.created_at - since_gap;
|
let since = latest.created_at - since_gap;
|
||||||
|
|
||||||
filter.since_mut(since)
|
filter.since_mut(since)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
|
pub fn since_optimize_filter(filter: Filter, latest: Option<&NoteRef>) -> Filter {
|
||||||
since_optimize_filter_with(filter, notes, 60)
|
since_optimize_filter_with(filter, latest, 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_limit() -> u64 {
|
pub fn default_limit() -> u64 {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
use crate::{ui, NotedeckTextStyle};
|
use crate::{ui, NotedeckTextStyle};
|
||||||
|
use egui::FontData;
|
||||||
|
use egui::FontDefinitions;
|
||||||
|
use egui::FontTweak;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub enum NamedFontFamily {
|
pub enum NamedFontFamily {
|
||||||
Medium,
|
Medium,
|
||||||
@@ -31,6 +36,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
|||||||
NotedeckTextStyle::Button => 13.0,
|
NotedeckTextStyle::Button => 13.0,
|
||||||
NotedeckTextStyle::Small => 12.0,
|
NotedeckTextStyle::Small => 12.0,
|
||||||
NotedeckTextStyle::Tiny => 10.0,
|
NotedeckTextStyle::Tiny => 10.0,
|
||||||
|
NotedeckTextStyle::NoteBody => 16.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +52,7 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
|||||||
NotedeckTextStyle::Button => 13.0,
|
NotedeckTextStyle::Button => 13.0,
|
||||||
NotedeckTextStyle::Small => 12.0,
|
NotedeckTextStyle::Small => 12.0,
|
||||||
NotedeckTextStyle::Tiny => 10.0,
|
NotedeckTextStyle::Tiny => 10.0,
|
||||||
|
NotedeckTextStyle::NoteBody => 13.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,3 +63,148 @@ pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32
|
|||||||
desktop_font_size(text_style)
|
desktop_font_size(text_style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use gossip's approach to font loading. This includes japanese fonts
|
||||||
|
// for rending stuff from japanese users.
|
||||||
|
pub fn setup_fonts(ctx: &egui::Context) {
|
||||||
|
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
|
||||||
|
let mut families = BTreeMap::new();
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"Onest".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"OnestMedium".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSans".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"OnestBold".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSansBold".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSans".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||||
|
);
|
||||||
|
font_data.insert(
|
||||||
|
"DejaVuSansBold".to_owned(),
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"Inconsolata".to_owned(),
|
||||||
|
Arc::new(
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/Inconsolata-Regular.ttf"
|
||||||
|
))
|
||||||
|
.tweak(FontTweak {
|
||||||
|
scale: 1.22, // This font is smaller than DejaVuSans
|
||||||
|
y_offset_factor: -0.18, // and too low
|
||||||
|
y_offset: 0.0,
|
||||||
|
baseline_offset_factor: 0.0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"NotoSansCJK".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
font_data.insert(
|
||||||
|
"NotoSansThai".to_owned(),
|
||||||
|
Arc::new(FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/NotoSansThai-Regular.ttf"
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Some good looking emojis. Use as first priority:
|
||||||
|
font_data.insert(
|
||||||
|
"NotoEmoji".to_owned(),
|
||||||
|
Arc::new(
|
||||||
|
FontData::from_static(include_bytes!(
|
||||||
|
"../../../assets/fonts/NotoEmoji-Regular.ttf"
|
||||||
|
))
|
||||||
|
.tweak(FontTweak {
|
||||||
|
scale: 1.1, // make them a touch larger
|
||||||
|
y_offset_factor: 0.0,
|
||||||
|
y_offset: 0.0,
|
||||||
|
baseline_offset_factor: 0.0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let base_fonts = vec![
|
||||||
|
"DejaVuSans".to_owned(),
|
||||||
|
"NotoEmoji".to_owned(),
|
||||||
|
"NotoSansCJK".to_owned(),
|
||||||
|
"NotoSansThai".to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut proportional = vec!["Onest".to_owned()];
|
||||||
|
proportional.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut medium = vec!["OnestMedium".to_owned()];
|
||||||
|
medium.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut mono = vec!["Inconsolata".to_owned()];
|
||||||
|
mono.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut bold = vec!["OnestBold".to_owned()];
|
||||||
|
bold.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let emoji = vec!["NotoEmoji".to_owned()];
|
||||||
|
|
||||||
|
families.insert(egui::FontFamily::Proportional, proportional);
|
||||||
|
families.insert(egui::FontFamily::Monospace, mono);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||||
|
medium,
|
||||||
|
);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||||
|
bold,
|
||||||
|
);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::debug!("fonts: {:?}", families);
|
||||||
|
|
||||||
|
let defs = FontDefinitions {
|
||||||
|
font_data,
|
||||||
|
families,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.set_fonts(defs);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,16 +5,32 @@ use std::borrow::Cow;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use unic_langid::{langid, LanguageIdentifier};
|
use unic_langid::{langid, LanguageIdentifier};
|
||||||
|
|
||||||
const EN_XA: LanguageIdentifier = langid!("en-XA");
|
|
||||||
const EN_US: LanguageIdentifier = langid!("en-US");
|
const EN_US: LanguageIdentifier = langid!("en-US");
|
||||||
|
const EN_XA: LanguageIdentifier = langid!("en-XA");
|
||||||
const DE: LanguageIdentifier = langid!("de");
|
const DE: LanguageIdentifier = langid!("de");
|
||||||
const ES_419: LanguageIdentifier = langid!("es-419");
|
const ES_419: LanguageIdentifier = langid!("es-419");
|
||||||
const ES_ES: LanguageIdentifier = langid!("es-ES");
|
const ES_ES: LanguageIdentifier = langid!("es-ES");
|
||||||
const FR: LanguageIdentifier = langid!("FR");
|
const FR: LanguageIdentifier = langid!("fr");
|
||||||
const TH: LanguageIdentifier = langid!("TH");
|
const JA: LanguageIdentifier = langid!("ja");
|
||||||
const ZH_CN: LanguageIdentifier = langid!("ZH_CN");
|
const PT_BR: LanguageIdentifier = langid!("pt-BR");
|
||||||
const ZH_TW: LanguageIdentifier = langid!("ZH_TW");
|
const PT_PT: LanguageIdentifier = langid!("pt-PT");
|
||||||
const NUM_FTLS: usize = 9;
|
const TH: LanguageIdentifier = langid!("th");
|
||||||
|
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
|
||||||
|
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
|
||||||
|
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é)";
|
||||||
|
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 = "繁體中文";
|
||||||
|
|
||||||
struct StaticBundle {
|
struct StaticBundle {
|
||||||
identifier: LanguageIdentifier,
|
identifier: LanguageIdentifier,
|
||||||
@@ -46,6 +62,18 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
|
|||||||
identifier: FR,
|
identifier: FR,
|
||||||
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
|
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 {
|
StaticBundle {
|
||||||
identifier: TH,
|
identifier: TH,
|
||||||
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
|
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
|
||||||
@@ -70,6 +98,8 @@ pub struct Localization {
|
|||||||
available_locales: Vec<LanguageIdentifier>,
|
available_locales: Vec<LanguageIdentifier>,
|
||||||
/// Fallback locale
|
/// Fallback locale
|
||||||
fallback_locale: LanguageIdentifier,
|
fallback_locale: LanguageIdentifier,
|
||||||
|
/// Native names for locales
|
||||||
|
locale_native_names: HashMap<LanguageIdentifier, String>,
|
||||||
|
|
||||||
/// Cached string results per locale (only for strings without arguments)
|
/// Cached string results per locale (only for strings without arguments)
|
||||||
string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
|
string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
|
||||||
@@ -95,15 +125,34 @@ impl Default for Localization {
|
|||||||
ES_419.clone(),
|
ES_419.clone(),
|
||||||
ES_ES.clone(),
|
ES_ES.clone(),
|
||||||
FR.clone(),
|
FR.clone(),
|
||||||
|
JA.clone(),
|
||||||
|
PT_BR.clone(),
|
||||||
|
PT_PT.clone(),
|
||||||
TH.clone(),
|
TH.clone(),
|
||||||
ZH_CN.clone(),
|
ZH_CN.clone(),
|
||||||
ZH_TW.clone(),
|
ZH_TW.clone(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let locale_native_names = HashMap::from([
|
||||||
|
(EN_US, EN_US_NATIVE_NAME.to_owned()),
|
||||||
|
(EN_XA, EN_XA_NATIVE_NAME.to_owned()),
|
||||||
|
(DE, DE_NATIVE_NAME.to_owned()),
|
||||||
|
(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()),
|
||||||
|
]);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_locale: default_locale.to_owned(),
|
current_locale: default_locale.to_owned(),
|
||||||
available_locales,
|
available_locales,
|
||||||
fallback_locale,
|
fallback_locale,
|
||||||
|
locale_native_names,
|
||||||
use_isolating: true,
|
use_isolating: true,
|
||||||
normalized_key_cache: HashMap::new(),
|
normalized_key_cache: HashMap::new(),
|
||||||
string_cache: HashMap::new(),
|
string_cache: HashMap::new(),
|
||||||
@@ -391,6 +440,10 @@ impl Localization {
|
|||||||
&self.fallback_locale
|
&self.fallback_locale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_locale_native_name(&self, locale: &LanguageIdentifier) -> Option<&str> {
|
||||||
|
self.locale_native_names.get(locale).map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets cache statistics for monitoring performance
|
/// Gets cache statistics for monitoring performance
|
||||||
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
|
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let mut total_strings = 0;
|
let mut total_strings = 0;
|
||||||
|
|||||||
@@ -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::urls::{UrlCache, UrlMimes};
|
||||||
|
use crate::ImageMetadata;
|
||||||
|
use crate::ObfuscationType;
|
||||||
|
use crate::RenderableMedia;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
use image::{Delay, Frame};
|
use image::{Delay, Frame};
|
||||||
@@ -7,9 +13,11 @@ use poll_promise::Promise;
|
|||||||
use egui::ColorImage;
|
use egui::ColorImage;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{self, create_dir_all, File};
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
use std::{io, thread};
|
||||||
|
|
||||||
use hex::ToHex;
|
use hex::ToHex;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
@@ -19,7 +27,7 @@ use tracing::warn;
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TexturesCache {
|
pub struct TexturesCache {
|
||||||
cache: hashbrown::HashMap<String, TextureStateInternal>,
|
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TexturesCache {
|
impl TexturesCache {
|
||||||
@@ -27,7 +35,7 @@ impl TexturesCache {
|
|||||||
&mut self,
|
&mut self,
|
||||||
url: &str,
|
url: &str,
|
||||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||||
) -> LoadableTextureState {
|
) -> LoadableTextureState<'_> {
|
||||||
let internal = self.handle_and_get_state_internal(url, true, closure);
|
let internal = self.handle_and_get_state_internal(url, true, closure);
|
||||||
|
|
||||||
internal.into()
|
internal.into()
|
||||||
@@ -37,7 +45,7 @@ impl TexturesCache {
|
|||||||
&mut self,
|
&mut self,
|
||||||
url: &str,
|
url: &str,
|
||||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||||
) -> TextureState {
|
) -> TextureState<'_> {
|
||||||
let internal = self.handle_and_get_state_internal(url, false, closure);
|
let internal = self.handle_and_get_state_internal(url, false, closure);
|
||||||
|
|
||||||
internal.into()
|
internal.into()
|
||||||
@@ -88,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| {
|
self.cache.get_mut(url).map(|state| {
|
||||||
handle_occupied(state, true);
|
handle_occupied(state, true);
|
||||||
state.into()
|
state.into()
|
||||||
@@ -139,6 +147,12 @@ pub enum TextureState<'a> {
|
|||||||
Loaded(&'a mut TexturedImage),
|
Loaded(&'a mut TexturedImage),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> TextureState<'a> {
|
||||||
|
pub fn is_loaded(&self) -> bool {
|
||||||
|
matches!(self, Self::Loaded(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
|
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
|
||||||
fn from(value: &'a mut TextureStateInternal) -> Self {
|
fn from(value: &'a mut TextureStateInternal) -> Self {
|
||||||
match value {
|
match value {
|
||||||
@@ -220,6 +234,7 @@ pub struct MediaCache {
|
|||||||
pub cache_dir: path::PathBuf,
|
pub cache_dir: path::PathBuf,
|
||||||
pub textures_cache: TexturesCache,
|
pub textures_cache: TexturesCache,
|
||||||
pub cache_type: MediaCacheType,
|
pub cache_type: MediaCacheType,
|
||||||
|
pub cache_size: Arc<Mutex<Option<u64>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||||
@@ -231,10 +246,29 @@ pub enum MediaCacheType {
|
|||||||
impl MediaCache {
|
impl MediaCache {
|
||||||
pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
|
pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
|
||||||
let cache_dir = parent_dir.join(Self::rel_dir(cache_type));
|
let cache_dir = parent_dir.join(Self::rel_dir(cache_type));
|
||||||
|
|
||||||
|
let cache_dir_clone = cache_dir.clone();
|
||||||
|
let cache_size = Arc::new(Mutex::new(None));
|
||||||
|
let cache_size_clone = Arc::clone(&cache_size);
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut last_checked = Instant::now() - Duration::from_secs(999);
|
||||||
|
loop {
|
||||||
|
// check cache folder size every 60 s
|
||||||
|
if last_checked.elapsed() >= Duration::from_secs(60) {
|
||||||
|
let size = compute_folder_size(&cache_dir_clone);
|
||||||
|
*cache_size_clone.lock().unwrap() = Some(size);
|
||||||
|
last_checked = Instant::now();
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_secs(5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cache_dir,
|
cache_dir,
|
||||||
textures_cache: TexturesCache::default(),
|
textures_cache: TexturesCache::default(),
|
||||||
cache_type,
|
cache_type,
|
||||||
|
cache_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,8 +365,14 @@ impl MediaCache {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.textures_cache.cache.clear();
|
||||||
|
*self.cache_size.try_lock().unwrap() = Some(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
|
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
|
||||||
@@ -349,10 +389,33 @@ fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
|
|||||||
.expect("Failed to create RgbaImage from ColorImage")
|
.expect("Failed to create RgbaImage from ColorImage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compute_folder_size<P: AsRef<Path>>(path: P) -> u64 {
|
||||||
|
fn walk(path: &Path) -> u64 {
|
||||||
|
let mut size = 0;
|
||||||
|
if let Ok(entries) = fs::read_dir(path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
if metadata.is_file() {
|
||||||
|
size += metadata.len();
|
||||||
|
} else if metadata.is_dir() {
|
||||||
|
size += walk(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
|
walk(path.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Images {
|
pub struct Images {
|
||||||
|
pub base_path: path::PathBuf,
|
||||||
pub static_imgs: MediaCache,
|
pub static_imgs: MediaCache,
|
||||||
pub gifs: MediaCache,
|
pub gifs: MediaCache,
|
||||||
pub urls: UrlMimes,
|
pub urls: UrlMimes,
|
||||||
|
/// cached imeta data
|
||||||
|
pub metadata: HashMap<String, ImageMetadata>,
|
||||||
pub gif_states: GifStateMap,
|
pub gif_states: GifStateMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,10 +423,12 @@ impl Images {
|
|||||||
/// path to directory to place [`MediaCache`]s
|
/// path to directory to place [`MediaCache`]s
|
||||||
pub fn new(path: path::PathBuf) -> Self {
|
pub fn new(path: path::PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
base_path: path.clone(),
|
||||||
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
|
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
|
||||||
gifs: MediaCache::new(&path, MediaCacheType::Gif),
|
gifs: MediaCache::new(&path, MediaCacheType::Gif),
|
||||||
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
||||||
gif_states: Default::default(),
|
gif_states: Default::default(),
|
||||||
|
metadata: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +437,65 @@ impl Images {
|
|||||||
self.gifs.migrate_v0()
|
self.gifs.migrate_v0()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
|
||||||
|
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_renderable_media(
|
||||||
|
urls: &mut UrlMimes,
|
||||||
|
imeta: &HashMap<String, ImageMetadata>,
|
||||||
|
url: &str,
|
||||||
|
) -> Option<RenderableMedia> {
|
||||||
|
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
|
||||||
|
|
||||||
|
let obfuscation_type = match imeta.get(url) {
|
||||||
|
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
|
||||||
|
None => ObfuscationType::Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(RenderableMedia {
|
||||||
|
url: url.to_string(),
|
||||||
|
media_type,
|
||||||
|
obfuscation_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_texture(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
url: &str,
|
||||||
|
img_type: ImageType,
|
||||||
|
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 {
|
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||||
match cache_type {
|
match cache_type {
|
||||||
MediaCacheType::Image => &self.static_imgs,
|
MediaCacheType::Image => &self.static_imgs,
|
||||||
@@ -385,6 +509,26 @@ impl Images {
|
|||||||
MediaCacheType::Gif => &mut self.gifs,
|
MediaCacheType::Gif => &mut self.gifs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_folder_contents(&mut self) -> io::Result<()> {
|
||||||
|
for entry in fs::read_dir(self.base_path.clone())? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
fs::remove_dir_all(path)?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.urls.cache.clear();
|
||||||
|
self.static_imgs.clear();
|
||||||
|
self.gifs.clear();
|
||||||
|
self.gif_states.clear();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type GifStateMap = HashMap<String, GifState>;
|
pub type GifStateMap = HashMap<String, GifState>;
|
||||||
@@ -395,3 +539,35 @@ pub struct GifState {
|
|||||||
pub next_frame_time: Option<SystemTime>,
|
pub next_frame_time: Option<SystemTime>,
|
||||||
pub last_frame_index: usize,
|
pub last_frame_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LatestTexture {
|
||||||
|
pub texture: TextureHandle,
|
||||||
|
pub request_next_repaint: Option<SystemTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_render_state<'a>(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
images: &'a mut Images,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
|
url: &str,
|
||||||
|
img_type: ImageType,
|
||||||
|
) -> RenderState<'a> {
|
||||||
|
let cache = match cache_type {
|
||||||
|
MediaCacheType::Image => &mut images.static_imgs,
|
||||||
|
MediaCacheType::Gif => &mut images.gifs,
|
||||||
|
};
|
||||||
|
|
||||||
|
let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
|
||||||
|
crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
|
||||||
|
});
|
||||||
|
|
||||||
|
RenderState {
|
||||||
|
texture_state,
|
||||||
|
gifs: &mut images.gif_states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderState<'a> {
|
||||||
|
pub texture_state: TextureState<'a>,
|
||||||
|
pub gifs: &'a mut GifStateMap,
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ impl JobPool {
|
|||||||
pub fn new(num_threads: usize) -> Self {
|
pub fn new(num_threads: usize) -> Self {
|
||||||
let (tx, rx) = mpsc::channel::<Job>();
|
let (tx, rx) = mpsc::channel::<Job>();
|
||||||
|
|
||||||
|
// TODO(jb55) why not mpmc here !???
|
||||||
let arc_rx = Arc::new(Mutex::new(rx));
|
let arc_rx = Arc::new(Mutex::new(rx));
|
||||||
for _ in 0..num_threads {
|
for _ in 0..num_threads {
|
||||||
let arc_rx_clone = arc_rx.clone();
|
let arc_rx_clone = arc_rx.clone();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::JobPool;
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||||
use notedeck::JobPool;
|
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -12,16 +12,21 @@ mod frame_history;
|
|||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
mod imgcache;
|
mod imgcache;
|
||||||
mod job_pool;
|
mod job_pool;
|
||||||
|
mod jobs;
|
||||||
|
pub mod media;
|
||||||
mod muted;
|
mod muted;
|
||||||
pub mod name;
|
pub mod name;
|
||||||
|
mod nip51_set;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
mod notecache;
|
mod notecache;
|
||||||
|
mod options;
|
||||||
mod persist;
|
mod persist;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay_debug;
|
pub mod relay_debug;
|
||||||
pub mod relayspec;
|
pub mod relayspec;
|
||||||
mod result;
|
mod result;
|
||||||
|
mod setup;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
mod style;
|
mod style;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
@@ -41,23 +46,33 @@ pub use account::relay::RelayAction;
|
|||||||
pub use account::FALLBACK_PUBKEY;
|
pub use account::FALLBACK_PUBKEY;
|
||||||
pub use app::{App, AppAction, Notedeck};
|
pub use app::{App, AppAction, Notedeck};
|
||||||
pub use args::Args;
|
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 error::{show_one_error_message, Error, FilterError, ZapError};
|
||||||
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
||||||
pub use fonts::NamedFontFamily;
|
pub use fonts::NamedFontFamily;
|
||||||
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
|
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
|
||||||
pub use imgcache::{
|
pub use imgcache::{
|
||||||
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
|
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
|
||||||
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
|
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
|
||||||
|
TexturedImage, TexturesCache,
|
||||||
};
|
};
|
||||||
pub use job_pool::JobPool;
|
pub use job_pool::JobPool;
|
||||||
|
pub use jobs::{
|
||||||
|
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
|
||||||
|
};
|
||||||
|
pub use media::{
|
||||||
|
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
|
||||||
|
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
|
||||||
|
};
|
||||||
pub use muted::{MuteFun, Muted};
|
pub use muted::{MuteFun, Muted};
|
||||||
pub use name::NostrName;
|
pub use name::NostrName;
|
||||||
|
pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache};
|
||||||
pub use note::{
|
pub use note::{
|
||||||
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
|
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
|
||||||
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
||||||
};
|
};
|
||||||
pub use notecache::{CachedNote, NoteCache};
|
pub use notecache::{CachedNote, NoteCache};
|
||||||
|
pub use options::NotedeckOptions;
|
||||||
pub use persist::*;
|
pub use persist::*;
|
||||||
pub use profile::get_profile_url;
|
pub use profile::get_profile_url;
|
||||||
pub use relay_debug::RelayDebugView;
|
pub use relay_debug::RelayDebugView;
|
||||||
@@ -67,13 +82,14 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
|
|||||||
pub use style::NotedeckTextStyle;
|
pub use style::NotedeckTextStyle;
|
||||||
pub use theme::ColorTheme;
|
pub use theme::ColorTheme;
|
||||||
pub use time::time_ago_since;
|
pub use time::time_ago_since;
|
||||||
|
pub use time::time_format;
|
||||||
pub use timecache::TimeCached;
|
pub use timecache::TimeCached;
|
||||||
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
||||||
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
||||||
pub use user_account::UserAccount;
|
pub use user_account::UserAccount;
|
||||||
pub use wallet::{
|
pub use wallet::{
|
||||||
get_current_wallet, get_wallet_for, GlobalWallet, Wallet, WalletError, WalletType,
|
get_current_wallet, get_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
|
||||||
WalletUIState, ZapWallet,
|
WalletType, WalletUIState, ZapWallet,
|
||||||
};
|
};
|
||||||
pub use zaps::{
|
pub use zaps::{
|
||||||
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
|
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
|
||||||
|
|||||||
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};
|
use crate::jobs::{Job, JobError, JobParamsOwned};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Blur<'a> {
|
pub struct ImageMetadata {
|
||||||
pub blurhash: &'a str,
|
pub blurhash: String,
|
||||||
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ impl PointDimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Blur<'_> {
|
impl ImageMetadata {
|
||||||
pub fn scaled_pixel_dimensions(
|
pub fn scaled_pixel_dimensions(
|
||||||
&self,
|
&self,
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
@@ -75,9 +75,8 @@ impl Blur<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
/// Find blurhashes in image metadata and update our cache
|
||||||
let mut blurs = HashMap::new();
|
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
|
||||||
|
|
||||||
for tag in note.tags() {
|
for tag in note.tags() {
|
||||||
let mut tag_iter = tag.into_iter();
|
let mut tag_iter = tag.into_iter();
|
||||||
if tag_iter
|
if tag_iter
|
||||||
@@ -93,13 +92,11 @@ pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
blurs.insert(url, blur);
|
blurs.insert(url.to_string(), blur);
|
||||||
}
|
}
|
||||||
|
|
||||||
blurs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
|
||||||
let mut url = None;
|
let mut url = None;
|
||||||
let mut blurhash = None;
|
let mut blurhash = None;
|
||||||
let mut dims = None;
|
let mut dims = None;
|
||||||
@@ -138,21 +135,21 @@ fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Some((
|
Some((
|
||||||
url,
|
url.to_string(),
|
||||||
Blur {
|
ImageMetadata {
|
||||||
blurhash,
|
blurhash: blurhash.to_string(),
|
||||||
dimensions,
|
dimensions,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ObfuscationType<'a> {
|
pub enum ObfuscationType {
|
||||||
Blurhash(Blur<'a>),
|
Blurhash(ImageMetadata),
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn compute_blurhash(
|
pub fn compute_blurhash(
|
||||||
params: Option<JobParamsOwned>,
|
params: Option<JobParamsOwned>,
|
||||||
dims: PixelDimensions,
|
dims: PixelDimensions,
|
||||||
) -> Result<Job, JobError> {
|
) -> Result<Job, JobError> {
|
||||||
@@ -185,9 +182,9 @@ fn generate_blurhash_texturehandle(
|
|||||||
url: &str,
|
url: &str,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
) -> notedeck::Result<egui::TextureHandle> {
|
) -> Result<egui::TextureHandle, crate::Error> {
|
||||||
let bytes = blurhash::decode(blurhash, width, height, 1.0)
|
let bytes = blurhash::decode(blurhash, width, height, 1.0)
|
||||||
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
|
.map_err(|e| crate::Error::Generic(e.to_string()))?;
|
||||||
|
|
||||||
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
|
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
|
||||||
Ok(ctx.load_texture(url, img, Default::default()))
|
Ok(ctx.load_texture(url, img, Default::default()))
|
||||||
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
|
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 super::context::ContextSelection;
|
||||||
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
|
use crate::{zaps::NoteZapTargetOwned, MediaAction};
|
||||||
use egui::Vec2;
|
use egui::Vec2;
|
||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use poll_promise::Promise;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ScrollInfo {
|
pub struct ScrollInfo {
|
||||||
@@ -25,7 +24,11 @@ pub enum NoteAction {
|
|||||||
Profile(Pubkey),
|
Profile(Pubkey),
|
||||||
|
|
||||||
/// User has clicked a note link
|
/// 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
|
/// User has selected some context option
|
||||||
Context(ContextSelection),
|
Context(ContextSelection),
|
||||||
@@ -45,6 +48,7 @@ impl NoteAction {
|
|||||||
NoteAction::Note {
|
NoteAction::Note {
|
||||||
note_id: id,
|
note_id: id,
|
||||||
preview: false,
|
preview: false,
|
||||||
|
scroll_offset: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,62 +65,3 @@ pub struct ZapTargetAmount {
|
|||||||
pub target: NoteZapTargetOwned,
|
pub target: NoteZapTargetOwned,
|
||||||
pub specified_msats: Option<u64>, // if None use default amount
|
pub specified_msats: Option<u64>, // if None use default amount
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum MediaAction {
|
|
||||||
FetchImage {
|
|
||||||
url: String,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
|
|
||||||
},
|
|
||||||
DoneLoading {
|
|
||||||
url: String,
|
|
||||||
cache_type: MediaCacheType,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for MediaAction {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::FetchImage {
|
|
||||||
url,
|
|
||||||
cache_type,
|
|
||||||
no_pfp_promise,
|
|
||||||
} => f
|
|
||||||
.debug_struct("FetchNoPfpImage")
|
|
||||||
.field("url", url)
|
|
||||||
.field("cache_type", cache_type)
|
|
||||||
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
|
|
||||||
.finish(),
|
|
||||||
Self::DoneLoading { url, cache_type } => f
|
|
||||||
.debug_struct("DoneLoading")
|
|
||||||
.field("url", url)
|
|
||||||
.field("cache_type", cache_type)
|
|
||||||
.finish(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MediaAction {
|
|
||||||
pub fn process(self, images: &mut Images) {
|
|
||||||
match self {
|
|
||||||
MediaAction::FetchImage {
|
|
||||||
url,
|
|
||||||
cache_type,
|
|
||||||
no_pfp_promise: promise,
|
|
||||||
} => {
|
|
||||||
images
|
|
||||||
.get_cache_mut(cache_type)
|
|
||||||
.textures_cache
|
|
||||||
.insert_pending(&url, promise);
|
|
||||||
}
|
|
||||||
MediaAction::DoneLoading { url, cache_type } => {
|
|
||||||
let cache = match cache_type {
|
|
||||||
MediaCacheType::Image => &mut images.static_imgs,
|
|
||||||
MediaCacheType::Gif => &mut images.gifs,
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.textures_cache.move_to_loaded(&url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
mod action;
|
mod action;
|
||||||
mod context;
|
mod context;
|
||||||
|
|
||||||
pub use action::{MediaAction, NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||||
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
||||||
|
|
||||||
use crate::Accounts;
|
use crate::Accounts;
|
||||||
|
use crate::GlobalWallet;
|
||||||
use crate::JobPool;
|
use crate::JobPool;
|
||||||
use crate::Localization;
|
use crate::Localization;
|
||||||
use crate::UnknownIds;
|
use crate::UnknownIds;
|
||||||
@@ -20,6 +21,7 @@ use std::fmt;
|
|||||||
pub struct NoteContext<'d> {
|
pub struct NoteContext<'d> {
|
||||||
pub ndb: &'d Ndb,
|
pub ndb: &'d Ndb,
|
||||||
pub accounts: &'d Accounts,
|
pub accounts: &'d Accounts,
|
||||||
|
pub global_wallet: &'d GlobalWallet,
|
||||||
pub i18n: &'d mut Localization,
|
pub i18n: &'d mut Localization,
|
||||||
pub img_cache: &'d mut Images,
|
pub img_cache: &'d mut Images,
|
||||||
pub note_cache: &'d mut NoteCache,
|
pub note_cache: &'d mut NoteCache,
|
||||||
@@ -28,7 +30,6 @@ pub struct NoteContext<'d> {
|
|||||||
pub job_pool: &'d mut JobPool,
|
pub job_pool: &'d mut JobPool,
|
||||||
pub unknown_ids: &'d mut UnknownIds,
|
pub unknown_ids: &'d mut UnknownIds,
|
||||||
pub clipboard: &'d mut egui_winit::clipboard::Clipboard,
|
pub clipboard: &'d mut egui_winit::clipboard::Clipboard,
|
||||||
pub current_account_has_wallet: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
|
||||||
|
|||||||
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 app_size;
|
||||||
mod theme_handler;
|
mod settings_handler;
|
||||||
mod token_handler;
|
mod token_handler;
|
||||||
mod zoom;
|
|
||||||
|
|
||||||
pub use app_size::AppSizeHandler;
|
pub use app_size::AppSizeHandler;
|
||||||
pub use theme_handler::ThemeHandler;
|
pub use settings_handler::Settings;
|
||||||
|
pub use settings_handler::SettingsHandler;
|
||||||
|
pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE;
|
||||||
pub use token_handler::TokenHandler;
|
pub use token_handler::TokenHandler;
|
||||||
pub use zoom::ZoomHandler;
|
|
||||||
|
|||||||
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 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
|
// Thread-safe static global
|
||||||
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
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);
|
debug!("updating virtual keyboard height {}", height);
|
||||||
|
|
||||||
// Convert and store atomically
|
// Convert and store atomically
|
||||||
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
|
KEYBOARD_HEIGHT.store(height.max(0), Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current Android virtual keyboard height. Useful for transforming
|
/// 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 {
|
pub fn virtual_keyboard_height() -> i32 {
|
||||||
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
|
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")]
|
use crate::{platform::file::SelectedMedia, Error};
|
||||||
pub mod android;
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub fn virtual_keyboard_height() -> i32 {
|
pub mod android;
|
||||||
android::virtual_keyboard_height()
|
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"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn virtual_keyboard_height() -> i32 {
|
pub fn virtual_keyboard_height(virt: bool) -> i32 {
|
||||||
0
|
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 {}
|
impl Eq for RelaySpec {}
|
||||||
|
|
||||||
|
#[allow(clippy::non_canonical_partial_ord_impl)]
|
||||||
impl PartialOrd for RelaySpec {
|
impl PartialOrd for RelaySpec {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.url.cmp(&other.url))
|
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,
|
Button,
|
||||||
Small,
|
Small,
|
||||||
Tiny,
|
Tiny,
|
||||||
|
NoteBody,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NotedeckTextStyle {
|
impl NotedeckTextStyle {
|
||||||
@@ -29,6 +30,7 @@ impl NotedeckTextStyle {
|
|||||||
Self::Button => TextStyle::Button,
|
Self::Button => TextStyle::Button,
|
||||||
Self::Small => TextStyle::Small,
|
Self::Small => TextStyle::Small,
|
||||||
Self::Tiny => TextStyle::Name("Tiny".into()),
|
Self::Tiny => TextStyle::Name("Tiny".into()),
|
||||||
|
Self::NoteBody => TextStyle::Name("NoteBody".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ impl NotedeckTextStyle {
|
|||||||
Self::Button => FontFamily::Proportional,
|
Self::Button => FontFamily::Proportional,
|
||||||
Self::Small => FontFamily::Proportional,
|
Self::Small => FontFamily::Proportional,
|
||||||
Self::Tiny => FontFamily::Proportional,
|
Self::Tiny => FontFamily::Proportional,
|
||||||
|
Self::NoteBody => FontFamily::Proportional,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,35 @@
|
|||||||
use egui::{
|
use crate::{fonts, NotedeckTextStyle};
|
||||||
style::{Selection, WidgetVisuals, Widgets},
|
use egui::style::Interaction;
|
||||||
Color32, CornerRadius, Stroke, Visuals,
|
use egui::style::Selection;
|
||||||
};
|
use egui::style::WidgetVisuals;
|
||||||
|
use egui::style::Widgets;
|
||||||
|
use egui::Color32;
|
||||||
|
use egui::CornerRadius;
|
||||||
|
use egui::FontId;
|
||||||
|
use egui::Stroke;
|
||||||
|
use egui::Style;
|
||||||
|
use egui::Visuals;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||||
|
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
||||||
|
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||||
|
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
||||||
|
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
||||||
|
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
||||||
|
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
||||||
|
|
||||||
|
// BACKGROUNDS
|
||||||
|
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
||||||
|
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
||||||
|
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
||||||
|
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
||||||
|
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
||||||
|
|
||||||
|
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
||||||
|
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
||||||
|
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
||||||
|
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
||||||
|
|
||||||
pub struct ColorTheme {
|
pub struct ColorTheme {
|
||||||
// VISUALS
|
// VISUALS
|
||||||
@@ -86,3 +114,131 @@ pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
|
|||||||
..default
|
..default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn desktop_dark_color_theme() -> ColorTheme {
|
||||||
|
ColorTheme {
|
||||||
|
// VISUALS
|
||||||
|
panel_fill: DARKER_BG,
|
||||||
|
extreme_bg_color: DARK_ISH_BG,
|
||||||
|
text_color: Color32::WHITE,
|
||||||
|
err_fg_color: RED_700,
|
||||||
|
warn_fg_color: ORANGE_700,
|
||||||
|
hyperlink_color: PURPLE,
|
||||||
|
selection_color: PURPLE_ALT,
|
||||||
|
|
||||||
|
// WINDOW
|
||||||
|
window_fill: DARK_ISH_BG,
|
||||||
|
window_stroke_color: DARK_BG,
|
||||||
|
|
||||||
|
// NONINTERACTIVE WIDGET
|
||||||
|
noninteractive_bg_fill: DARK_ISH_BG,
|
||||||
|
noninteractive_weak_bg_fill: DARK_BG,
|
||||||
|
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
||||||
|
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||||
|
|
||||||
|
// INACTIVE WIDGET
|
||||||
|
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
||||||
|
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||||
|
inactive_weak_bg_fill: SEMI_DARK_BG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mobile_dark_color_theme() -> ColorTheme {
|
||||||
|
ColorTheme {
|
||||||
|
panel_fill: Color32::BLACK,
|
||||||
|
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||||
|
..desktop_dark_color_theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light_color_theme() -> ColorTheme {
|
||||||
|
ColorTheme {
|
||||||
|
// VISUALS
|
||||||
|
panel_fill: Color32::WHITE,
|
||||||
|
extreme_bg_color: LIGHTER_GRAY,
|
||||||
|
text_color: BLACK,
|
||||||
|
err_fg_color: RED_700,
|
||||||
|
warn_fg_color: ORANGE_700,
|
||||||
|
hyperlink_color: PURPLE,
|
||||||
|
selection_color: PURPLE_ALT,
|
||||||
|
|
||||||
|
// WINDOW
|
||||||
|
window_fill: Color32::WHITE,
|
||||||
|
window_stroke_color: DARKER_GRAY,
|
||||||
|
|
||||||
|
// NONINTERACTIVE WIDGET
|
||||||
|
noninteractive_bg_fill: Color32::WHITE,
|
||||||
|
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
||||||
|
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
||||||
|
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||||
|
|
||||||
|
// INACTIVE WIDGET
|
||||||
|
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
||||||
|
inactive_bg_fill: LIGHTER_GRAY,
|
||||||
|
inactive_weak_bg_fill: LIGHTER_GRAY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create custom text sizes for any FontSizes
|
||||||
|
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
||||||
|
let font_size = if is_mobile {
|
||||||
|
fonts::mobile_font_size
|
||||||
|
} else {
|
||||||
|
fonts::desktop_font_size
|
||||||
|
};
|
||||||
|
|
||||||
|
style.text_styles = NotedeckTextStyle::iter()
|
||||||
|
.map(|text_style| {
|
||||||
|
(
|
||||||
|
text_style.text_style(),
|
||||||
|
FontId::new(font_size(&text_style), text_style.font_family()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
style.interaction = Interaction {
|
||||||
|
tooltip_delay: 0.1,
|
||||||
|
show_tooltips_only_when_still: false,
|
||||||
|
..Interaction::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// debug: show callstack for the current widget on hover if all
|
||||||
|
// modifier keys are pressed down.
|
||||||
|
/*
|
||||||
|
#[cfg(feature = "debug-widget-callstack")]
|
||||||
|
{
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
compile_error!(
|
||||||
|
"The `debug-widget-callstack` feature requires a debug build, \
|
||||||
|
release builds are unsupported."
|
||||||
|
);
|
||||||
|
style.debug.debug_on_hover_with_all_modifiers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug: show an overlay on all interactive widgets
|
||||||
|
#[cfg(feature = "debug-interactive-widgets")]
|
||||||
|
{
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
compile_error!(
|
||||||
|
"The `debug-interactive-widgets` feature requires a debug build, \
|
||||||
|
release builds are unsupported."
|
||||||
|
);
|
||||||
|
style.debug.show_interactive_widgets = true;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light_mode() -> Visuals {
|
||||||
|
create_themed_visuals(crate::theme::light_color_theme(), Visuals::light())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dark_mode(is_oled: bool) -> Visuals {
|
||||||
|
create_themed_visuals(
|
||||||
|
if is_oled {
|
||||||
|
mobile_dark_color_theme()
|
||||||
|
} else {
|
||||||
|
desktop_dark_color_theme()
|
||||||
|
},
|
||||||
|
Visuals::dark(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::{tr, Localization};
|
use crate::{tr, Localization};
|
||||||
|
use chrono::DateTime;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
// Time duration constants in seconds
|
// 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 {
|
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ use crate::debouncer::Debouncer;
|
|||||||
use crate::{storage, DataPath, DataPathType, Directory};
|
use crate::{storage, DataPath, DataPathType, Directory};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::info; // Adjust this import path as needed
|
use tracing::info;
|
||||||
|
|
||||||
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
|
pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> {
|
||||||
directory: Directory,
|
directory: Directory,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
debouncer: Debouncer,
|
debouncer: Debouncer,
|
||||||
saved_item: Option<T>,
|
saved_item: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
||||||
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
|
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
|
||||||
let directory = Directory::new(path.path(path_type));
|
let directory = Directory::new(path.path(path_type));
|
||||||
let delay = Duration::from_millis(1000);
|
let delay = Duration::from_millis(1000);
|
||||||
@@ -30,11 +30,11 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns whether successful
|
/// Returns whether it actually wrote the new value
|
||||||
pub fn try_save(&mut self, cur_item: T) -> bool {
|
pub fn try_save(&mut self, cur_item: T) -> bool {
|
||||||
if self.debouncer.should_act() {
|
if self.debouncer.should_act() {
|
||||||
if let Some(saved_item) = self.saved_item {
|
if let Some(ref saved_item) = self.saved_item {
|
||||||
if saved_item != cur_item {
|
if *saved_item != cur_item {
|
||||||
return self.save(cur_item);
|
return self.save(cur_item);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -45,8 +45,8 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_item(&self) -> Option<T> {
|
pub fn get_item(&self) -> Option<T> {
|
||||||
if self.saved_item.is_some() {
|
if let Some(ref item) = self.saved_item {
|
||||||
return self.saved_item;
|
return Some(item.clone());
|
||||||
}
|
}
|
||||||
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
|
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
|
||||||
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
|
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
|
||||||
|
|||||||
@@ -1,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
|
/// Determine if the screen is narrow. This is useful for detecting mobile
|
||||||
/// contexts, but with the nuance that we may also have a wide android tablet.
|
/// contexts, but with the nuance that we may also have a wide android tablet.
|
||||||
pub fn is_narrow(ctx: &egui::Context) -> bool {
|
pub fn is_narrow(ctx: &egui::Context) -> bool {
|
||||||
let screen_size = ctx.input(|c| c.screen_rect().size());
|
let screen_size = ctx.input(|c| c.screen_rect().size());
|
||||||
screen_size.x < 550.0
|
screen_size.x < NARROW_SCREEN_WIDTH
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_oled() -> bool {
|
pub fn is_oled(is_mobile_override: bool) -> bool {
|
||||||
is_compiled_as_mobile()
|
is_mobile_override || is_compiled_as_mobile()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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
|
// we already have this profile, skip
|
||||||
if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() {
|
if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let unknown_id = UnknownId::Pubkey(*pubkey);
|
let unknown_id = UnknownId::Pubkey(Pubkey::new(*pubkey));
|
||||||
if self.ids.contains_key(&unknown_id) {
|
if self.ids.contains_key(&unknown_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ impl UrlCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
if self.from_disk_promise.is_none() {
|
||||||
|
let cache = self.cache.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut locked_cache) = cache.write() {
|
||||||
|
locked_cache.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_cache(cur_cache: Arc<RwLock<UrlsToMime>>, from_disk: UrlsToMime) {
|
fn merge_cache(cur_cache: Arc<RwLock<UrlsToMime>>, from_disk: UrlsToMime) {
|
||||||
@@ -227,7 +238,9 @@ impl SupportedMimeType {
|
|||||||
{
|
{
|
||||||
Ok(Self { mime })
|
Ok(Self { mime })
|
||||||
} else {
|
} 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 {
|
KeypairUnowned {
|
||||||
pubkey: &self.key.pubkey,
|
pubkey: &self.key.pubkey,
|
||||||
secret_key: self.key.secret_key.as_ref(),
|
secret_key: self.key.secret_key.as_ref(),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub fn get_wallet_for<'a>(
|
|||||||
global_wallet.wallet.as_ref()
|
global_wallet.wallet.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_wallet<'a>(
|
pub fn get_current_wallet_mut<'a>(
|
||||||
accounts: &'a mut Accounts,
|
accounts: &'a mut Accounts,
|
||||||
global_wallet: &'a mut GlobalWallet,
|
global_wallet: &'a mut GlobalWallet,
|
||||||
) -> Option<&'a mut ZapWallet> {
|
) -> Option<&'a mut ZapWallet> {
|
||||||
@@ -35,6 +35,17 @@ pub fn get_current_wallet<'a>(
|
|||||||
Some(wallet)
|
Some(wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_current_wallet<'a>(
|
||||||
|
accounts: &'a Accounts,
|
||||||
|
global_wallet: &'a GlobalWallet,
|
||||||
|
) -> Option<&'a ZapWallet> {
|
||||||
|
let Some(wallet) = accounts.get_selected_wallet() else {
|
||||||
|
return global_wallet.wallet.as_ref();
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||||
pub enum WalletType {
|
pub enum WalletType {
|
||||||
Auto,
|
Auto,
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use nwc::nostr::nips::nip47::PayInvoiceResponse;
|
use nwc::nostr::nips::nip47::PayInvoiceResponse;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{get_wallet_for, Accounts, GlobalWallet, ZapError};
|
use crate::{
|
||||||
|
get_wallet_for,
|
||||||
use super::{
|
zaps::{
|
||||||
networking::{fetch_invoice_lnurl, fetch_invoice_lud16, FetchedInvoice, FetchingInvoice},
|
get_users_zap_address,
|
||||||
zap::Zap,
|
networking::{fetch_invoice_promise, FetchedInvoiceResponse, LNUrlPayResponse, PayEntry},
|
||||||
|
},
|
||||||
|
Accounts, GlobalWallet, ZapError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{networking::FetchingInvoice, zap::Zap};
|
||||||
|
|
||||||
type ZapId = u32;
|
type ZapId = u32;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -23,11 +30,31 @@ pub struct Zaps {
|
|||||||
zaps: std::collections::HashMap<ZapId, ZapState>,
|
zaps: std::collections::HashMap<ZapId, ZapState>,
|
||||||
in_flight: Vec<ZapPromise>,
|
in_flight: Vec<ZapPromise>,
|
||||||
events: Vec<EventResponse>,
|
events: Vec<EventResponse>,
|
||||||
|
|
||||||
|
pay_cache: PayCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache to hold LNURL payRequest responses from the desired LNURL endpoint
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PayCache {
|
||||||
|
// endpoint URL to response
|
||||||
|
pub pay_responses: HashMap<Url, LNUrlPayResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PayCache {
|
||||||
|
pub fn get_response(&self, url: &Url) -> Option<&LNUrlPayResponse> {
|
||||||
|
self.pay_responses.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, entry: PayEntry) {
|
||||||
|
self.pay_responses.insert(entry.url, entry.response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_event(
|
fn process_event(
|
||||||
id: ZapId,
|
id: ZapId,
|
||||||
event: ZapEvent,
|
event: ZapEvent,
|
||||||
|
cache: &PayCache,
|
||||||
accounts: &mut Accounts,
|
accounts: &mut Accounts,
|
||||||
global_wallet: &mut GlobalWallet,
|
global_wallet: &mut GlobalWallet,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
@@ -37,7 +64,7 @@ fn process_event(
|
|||||||
ZapEvent::FetchInvoice {
|
ZapEvent::FetchInvoice {
|
||||||
zap_ctx,
|
zap_ctx,
|
||||||
sender_relays,
|
sender_relays,
|
||||||
} => process_new_zap_event(zap_ctx, accounts, ndb, txn, sender_relays),
|
} => process_new_zap_event(cache, zap_ctx, accounts, ndb, txn, sender_relays),
|
||||||
ZapEvent::SendNWC {
|
ZapEvent::SendNWC {
|
||||||
zap_ctx,
|
zap_ctx,
|
||||||
req_noteid,
|
req_noteid,
|
||||||
@@ -74,6 +101,7 @@ fn process_event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_new_zap_event(
|
fn process_new_zap_event(
|
||||||
|
cache: &PayCache,
|
||||||
zap_ctx: ZapCtx,
|
zap_ctx: ZapCtx,
|
||||||
accounts: &Accounts,
|
accounts: &Accounts,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
@@ -96,7 +124,8 @@ fn process_new_zap_event(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let id = zap_ctx.id;
|
let id = zap_ctx.id;
|
||||||
let promise = send_note_zap(
|
let m_promise = send_note_zap(
|
||||||
|
cache,
|
||||||
ndb,
|
ndb,
|
||||||
txn,
|
txn,
|
||||||
note_target,
|
note_target,
|
||||||
@@ -106,55 +135,41 @@ fn process_new_zap_event(
|
|||||||
)
|
)
|
||||||
.map(|promise| ZapPromise::FetchingInvoice {
|
.map(|promise| ZapPromise::FetchingInvoice {
|
||||||
ctx: zap_ctx,
|
ctx: zap_ctx,
|
||||||
promise,
|
promise: Box::new(promise),
|
||||||
});
|
});
|
||||||
let Some(promise) = promise else {
|
|
||||||
return NextState::Event(EventResponse {
|
let promise = match m_promise {
|
||||||
id,
|
Ok(promise) => promise,
|
||||||
event: Err(ZappingError::InvalidZapAddress),
|
Err(e) => {
|
||||||
});
|
return NextState::Event(EventResponse {
|
||||||
|
id,
|
||||||
|
event: Err(ZappingError::InvoiceFetchFailed(e)),
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
NextState::Transition(promise)
|
NextState::Transition(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_note_zap(
|
fn send_note_zap(
|
||||||
|
cache: &PayCache,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
note_target: NoteZapTargetOwned,
|
note_target: NoteZapTargetOwned,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
nsec: &[u8; 32],
|
nsec: &[u8; 32],
|
||||||
relays: Vec<String>,
|
relays: Vec<String>,
|
||||||
) -> Option<FetchingInvoice> {
|
) -> Result<FetchingInvoice, ZapError> {
|
||||||
let address = get_users_zap_endpoint(txn, ndb, ¬e_target.zap_recipient)?;
|
let address = get_users_zap_address(txn, ndb, ¬e_target.zap_recipient)?;
|
||||||
|
|
||||||
let promise = match address {
|
fetch_invoice_promise(
|
||||||
ZapAddress::Lud16(s) => {
|
cache,
|
||||||
fetch_invoice_lud16(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
|
address,
|
||||||
}
|
msats,
|
||||||
ZapAddress::Lud06(s) => {
|
*nsec,
|
||||||
fetch_invoice_lnurl(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
|
ZapTargetOwned::Note(note_target),
|
||||||
}
|
relays,
|
||||||
};
|
)
|
||||||
Some(promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ZapAddress {
|
|
||||||
Lud16(String),
|
|
||||||
Lud06(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_users_zap_endpoint(txn: &Transaction, ndb: &Ndb, receiver: &Pubkey) -> Option<ZapAddress> {
|
|
||||||
let profile = ndb
|
|
||||||
.get_profile_by_pubkey(txn, receiver.bytes())
|
|
||||||
.ok()?
|
|
||||||
.record()
|
|
||||||
.profile()?;
|
|
||||||
|
|
||||||
profile
|
|
||||||
.lud06()
|
|
||||||
.map(|l| ZapAddress::Lud06(l.to_string()))
|
|
||||||
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_get_promise_response(
|
fn try_get_promise_response(
|
||||||
@@ -169,7 +184,7 @@ fn try_get_promise_response(
|
|||||||
|
|
||||||
match promise {
|
match promise {
|
||||||
ZapPromise::FetchingInvoice { ctx, promise } => {
|
ZapPromise::FetchingInvoice { ctx, promise } => {
|
||||||
let result = promise.block_and_take();
|
let result = Box::new(promise.block_and_take());
|
||||||
|
|
||||||
Some(PromiseResponse::FetchingInvoice { ctx, result })
|
Some(PromiseResponse::FetchingInvoice { ctx, result })
|
||||||
}
|
}
|
||||||
@@ -272,6 +287,16 @@ impl Zaps {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let PromiseResponse::FetchingInvoice { ctx: _, result } = &resp {
|
||||||
|
if let Ok(resp) = &**result {
|
||||||
|
if let Some(entry) = &resp.pay_entry {
|
||||||
|
let url = &entry.url;
|
||||||
|
tracing::info!("inserting {url} in pay cache");
|
||||||
|
self.pay_cache.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.events.push(resp.take_as_event_response());
|
self.events.push(resp.take_as_event_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +311,15 @@ impl Zaps {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let txn = nostrdb::Transaction::new(ndb).expect("txn");
|
let txn = nostrdb::Transaction::new(ndb).expect("txn");
|
||||||
match process_event(event_resp.id, event, accounts, global_wallet, ndb, &txn) {
|
match process_event(
|
||||||
|
event_resp.id,
|
||||||
|
event,
|
||||||
|
&self.pay_cache,
|
||||||
|
accounts,
|
||||||
|
global_wallet,
|
||||||
|
ndb,
|
||||||
|
&txn,
|
||||||
|
) {
|
||||||
NextState::Event(event_resp) => {
|
NextState::Event(event_resp) => {
|
||||||
self.zaps
|
self.zaps
|
||||||
.insert(event_resp.id, ZapState::Pending(event_resp.event));
|
.insert(event_resp.id, ZapState::Pending(event_resp.event));
|
||||||
@@ -483,7 +516,7 @@ impl std::fmt::Display for ZappingError {
|
|||||||
enum ZapPromise {
|
enum ZapPromise {
|
||||||
FetchingInvoice {
|
FetchingInvoice {
|
||||||
ctx: ZapCtx,
|
ctx: ZapCtx,
|
||||||
promise: Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>,
|
promise: Box<Promise<Result<FetchedInvoiceResponse, JoinError>>>,
|
||||||
},
|
},
|
||||||
SendingNWCInvoice {
|
SendingNWCInvoice {
|
||||||
ctx: SendingNWCInvoiceContext,
|
ctx: SendingNWCInvoiceContext,
|
||||||
@@ -494,7 +527,7 @@ enum ZapPromise {
|
|||||||
enum PromiseResponse {
|
enum PromiseResponse {
|
||||||
FetchingInvoice {
|
FetchingInvoice {
|
||||||
ctx: ZapCtx,
|
ctx: ZapCtx,
|
||||||
result: Result<Result<FetchedInvoice, ZapError>, JoinError>,
|
result: Box<Result<FetchedInvoiceResponse, JoinError>>,
|
||||||
},
|
},
|
||||||
SendingNWCInvoice {
|
SendingNWCInvoice {
|
||||||
ctx: SendingNWCInvoiceContext,
|
ctx: SendingNWCInvoiceContext,
|
||||||
@@ -507,8 +540,8 @@ impl PromiseResponse {
|
|||||||
match self {
|
match self {
|
||||||
PromiseResponse::FetchingInvoice { ctx, result } => {
|
PromiseResponse::FetchingInvoice { ctx, result } => {
|
||||||
let id = ctx.id;
|
let id = ctx.id;
|
||||||
let event = match result {
|
let event = match *result {
|
||||||
Ok(r) => match r {
|
Ok(r) => match r.invoice {
|
||||||
Ok(invoice) => Ok(ZapEvent::SendNWC {
|
Ok(invoice) => Ok(ZapEvent::SendNWC {
|
||||||
zap_ctx: ctx,
|
zap_ctx: ctx,
|
||||||
req_noteid: invoice.request_noteid,
|
req_noteid: invoice.request_noteid,
|
||||||
|
|||||||
@@ -11,3 +11,39 @@ pub use default_zap::{
|
|||||||
get_current_default_msats, DefaultZapError, DefaultZapMsats, PendingDefaultZapState,
|
get_current_default_msats, DefaultZapError, DefaultZapMsats, PendingDefaultZapState,
|
||||||
UserZapMsats,
|
UserZapMsats,
|
||||||
};
|
};
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
|
||||||
|
use crate::ZapError;
|
||||||
|
|
||||||
|
pub enum ZapAddress {
|
||||||
|
Lud16(String),
|
||||||
|
Lud06(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_users_zap_address(
|
||||||
|
txn: &Transaction,
|
||||||
|
ndb: &Ndb,
|
||||||
|
receiver: &Pubkey,
|
||||||
|
) -> Result<ZapAddress, ZapError> {
|
||||||
|
let Some(profile) = ndb
|
||||||
|
.get_profile_by_pubkey(txn, receiver.bytes())
|
||||||
|
.map_err(|e| ZapError::Ndb(e.to_string()))?
|
||||||
|
.record()
|
||||||
|
.profile()
|
||||||
|
else {
|
||||||
|
return Err(ZapError::Ndb(format!("No profile for {receiver}")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(address) = profile
|
||||||
|
.lud06()
|
||||||
|
.map(|l| ZapAddress::Lud06(l.to_string()))
|
||||||
|
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
|
||||||
|
else {
|
||||||
|
return Err(ZapError::Ndb(format!(
|
||||||
|
"profile for {receiver} doesn't have lud06 or lud16"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(address)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use crate::{zaps::ZapTargetOwned, ZapError};
|
use crate::{
|
||||||
use enostr::NoteId;
|
error::EndpointError,
|
||||||
|
zaps::{cache::PayCache, ZapAddress, ZapTargetOwned},
|
||||||
|
ZapError,
|
||||||
|
};
|
||||||
|
use enostr::{NoteId, Pubkey};
|
||||||
use nostrdb::NoteBuilder;
|
use nostrdb::NoteBuilder;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -11,15 +15,20 @@ pub struct FetchedInvoice {
|
|||||||
pub request_noteid: NoteId, // note id of kind 9734 request
|
pub request_noteid: NoteId, // note id of kind 9734 request
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FetchingInvoice = Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>;
|
pub struct FetchedInvoiceResponse {
|
||||||
|
pub invoice: Result<FetchedInvoice, ZapError>,
|
||||||
|
pub pay_entry: Option<PayEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
|
pub type FetchingInvoice = Promise<Result<FetchedInvoiceResponse, JoinError>>;
|
||||||
|
|
||||||
|
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> {
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
|
|
||||||
let on_done = move |response: Result<ehttp::Response, String>| {
|
let on_done = move |response: Result<ehttp::Response, String>| {
|
||||||
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
|
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
return Err(ZapError::EndpointError(format!(
|
return Err(ZapError::endpoint_error(format!(
|
||||||
"bad http response: {}",
|
"bad http response: {}",
|
||||||
resp.status_text
|
resp.status_text
|
||||||
)));
|
)));
|
||||||
@@ -36,20 +45,9 @@ async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
|
|||||||
tokio::task::block_in_place(|| promise.block_and_take())
|
tokio::task::block_in_place(|| promise.block_and_take())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_pay_req_from_lud16(lud16: &str) -> Result<LNUrlPayRequest, ZapError> {
|
|
||||||
let url = match generate_endpoint_url(lud16) {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch_pay_req_async(&url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
|
static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
|
||||||
|
|
||||||
fn lud16_to_lnurl(lud16: &str) -> Result<String, ZapError> {
|
fn endpoint_url_to_lnurl(endpoint_url: &Url) -> Result<String, ZapError> {
|
||||||
let endpoint_url = generate_endpoint_url(lud16)?;
|
|
||||||
|
|
||||||
let url_str = endpoint_url.to_string();
|
let url_str = endpoint_url.to_string();
|
||||||
let data = url_str.as_bytes();
|
let data = url_str.as_bytes();
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ fn make_kind_9734<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LNUrlPayRequest {
|
pub struct LNUrlPayResponseRaw {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[serde(rename = "allowsNostr")]
|
#[serde(rename = "allowsNostr")]
|
||||||
allow_nostr: bool,
|
allow_nostr: bool,
|
||||||
@@ -121,57 +119,117 @@ pub struct LNUrlPayRequest {
|
|||||||
max_sendable: u64,
|
max_sendable: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<LNUrlPayResponseRaw> for LNUrlPayResponse {
|
||||||
|
fn from(value: LNUrlPayResponseRaw) -> Self {
|
||||||
|
let nostr_pubkey = Pubkey::from_hex(&value.nostr_pubkey)
|
||||||
|
.map_err(|e: enostr::Error| EndpointError(e.to_string()));
|
||||||
|
|
||||||
|
let callback_url = Url::parse(&value.callback_url)
|
||||||
|
.map_err(|e| EndpointError(format!("invalid callback url: {e}")));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
allow_nostr: value.allow_nostr,
|
||||||
|
nostr_pubkey,
|
||||||
|
callback_url,
|
||||||
|
min_sendable: value.min_sendable,
|
||||||
|
max_sendable: value.max_sendable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LNUrlPayResponse {
|
||||||
|
pub allow_nostr: bool,
|
||||||
|
pub nostr_pubkey: Result<Pubkey, EndpointError>,
|
||||||
|
pub callback_url: Result<Url, EndpointError>,
|
||||||
|
pub min_sendable: u64,
|
||||||
|
pub max_sendable: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PayEntry {
|
||||||
|
pub url: Url,
|
||||||
|
pub response: LNUrlPayResponse,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct LNInvoice {
|
struct LNInvoice {
|
||||||
#[serde(rename = "pr")]
|
#[serde(rename = "pr")]
|
||||||
invoice: String,
|
invoice: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn endpoint_query_for_invoice<'a>(
|
fn endpoint_query_for_invoice(
|
||||||
endpoint_base_url: &'a mut Url,
|
endpoint_base_url: &Url,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
lnurl: &str,
|
lnurl: &str,
|
||||||
note: nostrdb::Note,
|
note: nostrdb::Note,
|
||||||
) -> Result<&'a Url, ZapError> {
|
) -> Result<Url, ZapError> {
|
||||||
|
let mut new_url = endpoint_base_url.clone();
|
||||||
let nostr = note
|
let nostr = note
|
||||||
.json()
|
.json()
|
||||||
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
|
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
|
||||||
|
|
||||||
Ok(endpoint_base_url
|
new_url
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
.append_pair("amount", &msats.to_string())
|
.append_pair("amount", &msats.to_string())
|
||||||
.append_pair("lnurl", lnurl)
|
.append_pair("lnurl", lnurl)
|
||||||
.append_pair("nostr", &nostr)
|
.append_pair("nostr", &nostr)
|
||||||
.finish())
|
.finish();
|
||||||
|
|
||||||
|
Ok(new_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_invoice_lud16(
|
pub fn fetch_invoice_promise(
|
||||||
lud16: String,
|
cache: &PayCache,
|
||||||
|
zap_address: ZapAddress,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
sender_nsec: [u8; 32],
|
sender_nsec: [u8; 32],
|
||||||
target: ZapTargetOwned,
|
target: ZapTargetOwned,
|
||||||
relays: Vec<String>,
|
relays: Vec<String>,
|
||||||
) -> FetchingInvoice {
|
) -> Result<FetchingInvoice, ZapError> {
|
||||||
Promise::spawn_async(tokio::spawn(async move {
|
let (url, lnurl) = match zap_address {
|
||||||
fetch_invoice_lud16_async(&lud16, msats, &sender_nsec, target, relays).await
|
ZapAddress::Lud16(lud16) => {
|
||||||
}))
|
let url = generate_endpoint_url(&lud16)?;
|
||||||
}
|
let lnurl = endpoint_url_to_lnurl(&url)?;
|
||||||
|
(url, lnurl)
|
||||||
|
}
|
||||||
|
ZapAddress::Lud06(lnurl) => (convert_lnurl_to_endpoint_url(&lnurl)?, lnurl),
|
||||||
|
};
|
||||||
|
|
||||||
pub fn fetch_invoice_lnurl(
|
match cache.get_response(&url) {
|
||||||
lnurl: String,
|
Some(endpoint_resp) => {
|
||||||
msats: u64,
|
tracing::info!("Using existing endpoint response for {url}");
|
||||||
sender_nsec: [u8; 32],
|
let response = endpoint_resp.clone();
|
||||||
target: ZapTargetOwned,
|
Ok(Promise::spawn_async(tokio::spawn(async move {
|
||||||
relays: Vec<String>,
|
fetch_invoice_lnurl_async(
|
||||||
) -> FetchingInvoice {
|
&lnurl,
|
||||||
Promise::spawn_async(tokio::spawn(async move {
|
PayEntry { url, response },
|
||||||
let pay_req = match fetch_pay_req_from_lnurl_async(&lnurl).await {
|
msats,
|
||||||
Ok(req) => req,
|
&sender_nsec,
|
||||||
Err(e) => return Err(e),
|
relays,
|
||||||
};
|
target,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
None => Ok(Promise::spawn_async(tokio::spawn(async move {
|
||||||
|
tracing::info!("querying ln endpoint: {url}");
|
||||||
|
let pay_req = match fetch_pay_req_async(&url).await {
|
||||||
|
Ok(p) => PayEntry {
|
||||||
|
url,
|
||||||
|
response: p.into(),
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(e),
|
||||||
|
pay_entry: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, &sender_nsec, relays, target).await
|
fetch_invoice_lnurl_async(&lnurl, pay_req, msats, &sender_nsec, relays, target).await
|
||||||
}))
|
}))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
||||||
@@ -181,68 +239,96 @@ fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
|||||||
String::from_utf8(data).map_err(|e| ZapError::Bech(format!("string conversion: {e}")))?;
|
String::from_utf8(data).map_err(|e| ZapError::Bech(format!("string conversion: {e}")))?;
|
||||||
|
|
||||||
Url::parse(&url_str)
|
Url::parse(&url_str)
|
||||||
.map_err(|e| ZapError::EndpointError(format!("endpoint url from lnurl is invalid: {e}")))
|
.map_err(|e| ZapError::endpoint_error(format!("endpoint url from lnurl is invalid: {e}")))
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_pay_req_from_lnurl_async(lnurl: &str) -> Result<LNUrlPayRequest, ZapError> {
|
|
||||||
let url = match convert_lnurl_to_endpoint_url(lnurl) {
|
|
||||||
Ok(u) => u,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch_pay_req_async(&url).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_invoice_lnurl_async(
|
async fn fetch_invoice_lnurl_async(
|
||||||
lnurl: &str,
|
lnurl: &str,
|
||||||
pay_req: &LNUrlPayRequest,
|
pay_entry: PayEntry,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
sender_nsec: &[u8; 32],
|
sender_nsec: &[u8; 32],
|
||||||
relays: Vec<String>,
|
relays: Vec<String>,
|
||||||
target: ZapTargetOwned,
|
target: ZapTargetOwned,
|
||||||
) -> Result<FetchedInvoice, ZapError> {
|
) -> FetchedInvoiceResponse {
|
||||||
//let recipient = Pubkey::from_hex(&pay_req.nostr_pubkey)
|
if !pay_entry.response.allow_nostr {
|
||||||
//.map_err(|e| ZapError::EndpointError(format!("invalid pubkey hex from endpoint: {e}")))?;
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::endpoint_error(
|
||||||
|
"endpoint does not allow nostr".to_owned(),
|
||||||
|
)),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let mut base_url = Url::parse(&pay_req.callback_url)
|
if let Err(e) = &pay_entry.response.nostr_pubkey {
|
||||||
.map_err(|e| ZapError::EndpointError(format!("invalid callback url from endpoint: {e}")))?;
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::EndpointError(e.clone())),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let min_sendable = pay_entry.response.min_sendable;
|
||||||
|
if msats < min_sendable {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::endpoint_error(format!(
|
||||||
|
"zap amount {msats} is less than minimum sendable: {min_sendable} (in msats)"
|
||||||
|
))),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_sendable = pay_entry.response.max_sendable;
|
||||||
|
if msats > max_sendable {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::endpoint_error(format!(
|
||||||
|
"zap amount {msats} is greater than maximum sendable: {max_sendable} (in msats)"
|
||||||
|
))),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_url = match &pay_entry.response.callback_url {
|
||||||
|
Ok(url) => url.clone(),
|
||||||
|
Err(error) => {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::EndpointError(error.clone())),
|
||||||
|
pay_entry: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let (query, noteid) = {
|
let (query, noteid) = {
|
||||||
let comment: &str = "";
|
let comment: &str = "";
|
||||||
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
|
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
|
||||||
let noteid = NoteId::new(*note.id());
|
let noteid = NoteId::new(*note.id());
|
||||||
let query = endpoint_query_for_invoice(&mut base_url, msats, lnurl, note)?;
|
let query = match endpoint_query_for_invoice(&base_url, msats, lnurl, note) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(e),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
(query, noteid)
|
(query, noteid)
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = fetch_invoice(query).await;
|
let res = fetch_ln_invoice(&query).await;
|
||||||
res.map(|i| FetchedInvoice {
|
FetchedInvoiceResponse {
|
||||||
invoice: i.invoice,
|
invoice: res.map(|r| FetchedInvoice {
|
||||||
request_noteid: noteid,
|
invoice: r.invoice,
|
||||||
})
|
request_noteid: noteid,
|
||||||
|
}),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_invoice_lud16_async(
|
async fn fetch_ln_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
|
||||||
lud16: &str,
|
|
||||||
msats: u64,
|
|
||||||
sender_nsec: &[u8; 32],
|
|
||||||
target: ZapTargetOwned,
|
|
||||||
relays: Vec<String>,
|
|
||||||
) -> Result<FetchedInvoice, ZapError> {
|
|
||||||
let pay_req = fetch_pay_req_from_lud16(lud16).await?;
|
|
||||||
|
|
||||||
let lnurl = lud16_to_lnurl(lud16)?;
|
|
||||||
|
|
||||||
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, sender_nsec, relays, target).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
|
|
||||||
let request = ehttp::Request::get(req);
|
let request = ehttp::Request::get(req);
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
let on_done = move |response: Result<ehttp::Response, String>| {
|
let on_done = move |response: Result<ehttp::Response, String>| {
|
||||||
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
|
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
return Err(ZapError::EndpointError(format!(
|
return Err(ZapError::endpoint_error(format!(
|
||||||
"invalid http response: {}",
|
"invalid http response: {}",
|
||||||
resp.status_text
|
resp.status_text
|
||||||
)));
|
)));
|
||||||
@@ -290,25 +376,32 @@ fn generate_endpoint_url(lud16: &str) -> Result<Url, ZapError> {
|
|||||||
if use_http { "" } else { "s" }
|
if use_http { "" } else { "s" }
|
||||||
);
|
);
|
||||||
|
|
||||||
Url::parse(&url_str).map_err(|e| ZapError::EndpointError(e.to_string()))
|
Url::parse(&url_str).map_err(|e| ZapError::endpoint_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use enostr::{FullKeypair, NoteId};
|
use enostr::{FullKeypair, NoteId};
|
||||||
|
|
||||||
use crate::zaps::networking::convert_lnurl_to_endpoint_url;
|
use crate::zaps::{
|
||||||
|
cache::PayCache,
|
||||||
use super::{
|
networking::{
|
||||||
fetch_invoice_lnurl, fetch_invoice_lud16, fetch_pay_req_from_lud16, lud16_to_lnurl,
|
convert_lnurl_to_endpoint_url, endpoint_url_to_lnurl, fetch_pay_req_async,
|
||||||
|
generate_endpoint_url,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::fetch_invoice_promise;
|
||||||
|
|
||||||
#[ignore] // don't run this test automatically since it sends real http
|
#[ignore] // don't run this test automatically since it sends real http
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_get_pay_req() {
|
async fn test_get_pay_req() {
|
||||||
let lud16 = "jb55@sendsats.lol";
|
let lud16 = "jb55@sendsats.lol";
|
||||||
|
|
||||||
let maybe_res = fetch_pay_req_from_lud16(lud16).await;
|
let url = generate_endpoint_url(lud16);
|
||||||
|
assert!(url.is_ok());
|
||||||
|
|
||||||
|
let maybe_res = fetch_pay_req_async(&url.unwrap()).await;
|
||||||
|
|
||||||
assert!(maybe_res.is_ok());
|
assert!(maybe_res.is_ok());
|
||||||
|
|
||||||
@@ -328,7 +421,10 @@ mod tests {
|
|||||||
fn test_lnurl() {
|
fn test_lnurl() {
|
||||||
let lud16 = "jb55@sendsats.lol";
|
let lud16 = "jb55@sendsats.lol";
|
||||||
|
|
||||||
let maybe_lnurl = lud16_to_lnurl(lud16);
|
let url = generate_endpoint_url(lud16);
|
||||||
|
assert!(url.is_ok());
|
||||||
|
|
||||||
|
let maybe_lnurl = endpoint_url_to_lnurl(&url.unwrap());
|
||||||
assert!(maybe_lnurl.is_ok());
|
assert!(maybe_lnurl.is_ok());
|
||||||
|
|
||||||
let lnurl = maybe_lnurl.unwrap();
|
let lnurl = maybe_lnurl.unwrap();
|
||||||
@@ -344,9 +440,11 @@ mod tests {
|
|||||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
|
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
|
||||||
|
|
||||||
let kp = FullKeypair::generate();
|
let kp = FullKeypair::generate();
|
||||||
|
let mut cache = PayCache::default();
|
||||||
let maybe_invoice = rt.block_on(async {
|
let maybe_invoice = rt.block_on(async {
|
||||||
fetch_invoice_lud16(
|
fetch_invoice_promise(
|
||||||
"jb55@sendsats.lol".to_owned(),
|
&mut cache,
|
||||||
|
crate::zaps::ZapAddress::Lud16("jb55@sendsats.lol".to_owned()),
|
||||||
1000,
|
1000,
|
||||||
FullKeypair::generate().secret_key.to_secret_bytes(),
|
FullKeypair::generate().secret_key.to_secret_bytes(),
|
||||||
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
||||||
@@ -355,14 +453,18 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
vec!["wss://relay.damus.io".to_owned()],
|
vec!["wss://relay.damus.io".to_owned()],
|
||||||
)
|
)
|
||||||
.block_and_take()
|
.map(|p| p.block_and_take())
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(maybe_invoice.is_ok());
|
assert!(maybe_invoice.is_ok());
|
||||||
let inner = maybe_invoice.unwrap();
|
let inner = maybe_invoice.unwrap();
|
||||||
assert!(inner.is_ok());
|
assert!(inner.is_ok());
|
||||||
let invoice = inner.unwrap();
|
let inner = inner.unwrap().invoice;
|
||||||
assert!(invoice.invoice.starts_with("lnbc"));
|
assert!(inner.is_ok());
|
||||||
|
|
||||||
|
let inner = inner.unwrap();
|
||||||
|
|
||||||
|
assert!(inner.invoice.starts_with("lnbc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -385,9 +487,11 @@ mod tests {
|
|||||||
|
|
||||||
let kp = FullKeypair::generate();
|
let kp = FullKeypair::generate();
|
||||||
|
|
||||||
|
let mut cache = PayCache::default();
|
||||||
let maybe_invoice = rt.block_on(async {
|
let maybe_invoice = rt.block_on(async {
|
||||||
fetch_invoice_lnurl(
|
fetch_invoice_promise(
|
||||||
lnurl.to_owned(),
|
&mut cache,
|
||||||
|
crate::zaps::ZapAddress::Lud06(lnurl.to_owned()),
|
||||||
1000,
|
1000,
|
||||||
kp.secret_key.to_secret_bytes(),
|
kp.secret_key.to_secret_bytes(),
|
||||||
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
||||||
@@ -396,11 +500,17 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
[relay.to_owned()].to_vec(),
|
[relay.to_owned()].to_vec(),
|
||||||
)
|
)
|
||||||
.block_and_take()
|
.map(|p| p.block_and_take())
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(maybe_invoice.is_ok());
|
assert!(maybe_invoice.is_ok());
|
||||||
|
let inner = maybe_invoice.unwrap();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
let inner = inner.unwrap().invoice;
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
|
||||||
assert!(maybe_invoice.unwrap().unwrap().invoice.starts_with("lnbc"));
|
let inner = inner.unwrap();
|
||||||
|
|
||||||
|
assert!(inner.invoice.starts_with("lnbc"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ license = "GPLv3"
|
|||||||
description = "The nostr browser"
|
description = "The nostr browser"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bitflags = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
egui_tabs = { workspace = true }
|
egui_tabs = { workspace = true }
|
||||||
egui_extras = { workspace = true }
|
egui_extras = { workspace = true }
|
||||||
@@ -16,6 +17,8 @@ egui = { workspace = true }
|
|||||||
notedeck_columns = { workspace = true }
|
notedeck_columns = { workspace = true }
|
||||||
notedeck_ui = { workspace = true }
|
notedeck_ui = { workspace = true }
|
||||||
notedeck_dave = { workspace = true }
|
notedeck_dave = { workspace = true }
|
||||||
|
notedeck_notebook = { workspace = true }
|
||||||
|
notedeck_clndash = { workspace = true }
|
||||||
notedeck = { workspace = true }
|
notedeck = { workspace = true }
|
||||||
nostrdb = { workspace = true }
|
nostrdb = { workspace = true }
|
||||||
puffin = { workspace = true, optional = true }
|
puffin = { workspace = true, optional = true }
|
||||||
@@ -63,6 +66,12 @@ short_description = "The nostr browser"
|
|||||||
identifier = "com.damus.notedeck"
|
identifier = "com.damus.notedeck"
|
||||||
icon = ["assets/app_icon.icns"]
|
icon = ["assets/app_icon.icns"]
|
||||||
|
|
||||||
|
[package.metadata.android.manifest.queries]
|
||||||
|
intent = [
|
||||||
|
{ action = ["android.intent.action.MAIN"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
[package.metadata.android]
|
[package.metadata.android]
|
||||||
package = "com.damus.app"
|
package = "com.damus.app"
|
||||||
apk_name = "Notedeck"
|
apk_name = "Notedeck"
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
|
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
>
|
>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -23,9 +24,16 @@
|
|||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.vulkan.level"
|
<uses-feature android:name="android.hardware.vulkan.level"
|
||||||
android:required="true"
|
android:required="true"
|
||||||
android:version="1" />
|
android:version="1" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|||||||
@@ -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;
|
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.Bundle;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.DisplayCutoutCompat;
|
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
import androidx.core.view.WindowCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
@@ -15,52 +20,38 @@ import androidx.core.view.WindowInsetsControllerCompat;
|
|||||||
|
|
||||||
import com.google.androidgamesdk.GameActivity;
|
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 {
|
public class MainActivity extends GameActivity {
|
||||||
static {
|
static final int REQUEST_CODE_PICK_FILE = 420;
|
||||||
System.loadLibrary("notedeck_chrome");
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void nativeOnKeyboardHeightChanged(int height);
|
private native void nativeOnFilePickedFailed(String uri, String e);
|
||||||
private KeyboardHeightHelper keyboardHelper;
|
private native void nativeOnFilePickedWithContent(Object[] uri_info, byte[] content);
|
||||||
|
|
||||||
@Override
|
public void openFilePicker() {
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
// Shrink view so it does not get covered by insets.
|
intent.setType("*/*");
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
setupInsets();
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
//setupFullscreen()
|
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
|
||||||
keyboardHelper = new KeyboardHeightHelper(this);
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupFullscreen() {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
||||||
|
|
||||||
WindowInsetsControllerCompat controller =
|
|
||||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
|
||||||
if (controller != null) {
|
|
||||||
controller.setSystemBarsBehavior(
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
);
|
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
|
||||||
}
|
|
||||||
|
|
||||||
//focus(getContent())
|
|
||||||
}
|
|
||||||
|
|
||||||
// not sure if this does anything
|
|
||||||
private void focus(View content) {
|
|
||||||
content.setFocusable(true);
|
|
||||||
content.setFocusableInTouchMode(true);
|
|
||||||
content.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private View getContent() {
|
|
||||||
return getWindow().getDecorView().findViewById(android.R.id.content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupInsets() {
|
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();
|
View content = getContent();
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
@@ -72,38 +63,176 @@ public class MainActivity extends GameActivity {
|
|||||||
mlp.rightMargin = insets.right;
|
mlp.rightMargin = insets.right;
|
||||||
v.setLayoutParams(mlp);
|
v.setLayoutParams(mlp);
|
||||||
|
|
||||||
return WindowInsetsCompat.CONSUMED;
|
return windowInsets;
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void processSelectedFile(Uri uri) {
|
||||||
public void onResume() {
|
try {
|
||||||
super.onResume();
|
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
|
||||||
keyboardHelper.start();
|
} catch (Exception e) {
|
||||||
|
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
|
||||||
|
|
||||||
|
nativeOnFilePickedFailed(uri.toString(), e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Object[] getUriInfo(Uri uri) throws Exception {
|
||||||
public void onPause() {
|
if (!uri.getScheme().equals("content")) {
|
||||||
super.onPause();
|
throw new Exception("uri should start with content://");
|
||||||
keyboardHelper.stop();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
keyboardHelper.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
while (cursor.moveToNext()) {
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
Object[] info = new Object[3];
|
||||||
// Offset the location so it fits the view with margins caused by insets.
|
|
||||||
|
|
||||||
int[] location = new int[2];
|
int col_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
findViewById(android.R.id.content).getLocationOnScreen(location);
|
info[0] = cursor.getString(col_idx);
|
||||||
event.offsetLocation(-location[0], -location[1]);
|
|
||||||
|
|
||||||
return super.onTouchEvent(event);
|
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) {
|
||||||
|
// Shrink view so it does not get covered by insets.
|
||||||
|
|
||||||
|
setupInsets();
|
||||||
|
//setupFullscreen()
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
WindowInsetsControllerCompat controller =
|
||||||
|
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||||
|
if (controller != null) {
|
||||||
|
controller.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
);
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||||
|
}
|
||||||
|
|
||||||
|
//focus(getContent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// not sure if this does anything
|
||||||
|
private void focus(View content) {
|
||||||
|
content.setFocusable(true);
|
||||||
|
content.setFocusableInTouchMode(true);
|
||||||
|
content.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private View getContent() {
|
||||||
|
return getWindow().getDecorView().findViewById(android.R.id.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
// Offset the location so it fits the view with margins caused by insets.
|
||||||
|
|
||||||
|
int[] location = new int[2];
|
||||||
|
findViewById(android.R.id.content).getLocationOnScreen(location);
|
||||||
|
event.offsetLocation(-location[0], -location[1]);
|
||||||
|
|
||||||
|
return super.onTouchEvent(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,21 @@
|
|||||||
//use egui_android::run_android;
|
//use egui_android::run_android;
|
||||||
|
|
||||||
use egui_winit::winit::platform::android::activity::AndroidApp;
|
use egui_winit::winit::platform::android::activity::AndroidApp;
|
||||||
use notedeck_columns::Damus;
|
|
||||||
use notedeck_dave::Dave;
|
|
||||||
|
|
||||||
use crate::{app::NotedeckApp, chrome::Chrome, setup::setup_chrome};
|
use crate::chrome::Chrome;
|
||||||
use notedeck::Notedeck;
|
use notedeck::Notedeck;
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn android_main(app: AndroidApp) {
|
pub async fn android_main(android_app: AndroidApp) {
|
||||||
//use tracing_logcat::{LogcatMakeWriter, LogcatTag};
|
//use tracing_logcat::{LogcatMakeWriter, LogcatTag};
|
||||||
use tracing_subscriber::{prelude::*, EnvFilter};
|
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||||
|
|
||||||
std::env::set_var("RUST_BACKTRACE", "full");
|
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("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
|
||||||
std::env::set_var(
|
std::env::set_var(
|
||||||
"RUST_LOG",
|
"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(
|
//std::env::set_var(
|
||||||
@@ -44,7 +41,7 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
.init();
|
.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 {
|
let mut options = eframe::NativeOptions {
|
||||||
depth_buffer: 24,
|
depth_buffer: 24,
|
||||||
..eframe::NativeOptions::default()
|
..eframe::NativeOptions::default()
|
||||||
@@ -57,41 +54,20 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
// builder.with_android_app(app_clone_for_event_loop);
|
// 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(
|
let _res = eframe::run_native(
|
||||||
"Damus Notedeck",
|
"Damus Notedeck",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
let ctx = &cc.egui_ctx;
|
let ctx = &cc.egui_ctx;
|
||||||
|
|
||||||
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
||||||
setup_chrome(ctx, ¬edeck.args(), notedeck.theme());
|
notedeck.set_android_context(android_app);
|
||||||
|
notedeck.setup(ctx);
|
||||||
let context = &mut notedeck.app_context();
|
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
||||||
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();
|
|
||||||
assert!(
|
|
||||||
completely_unrecognized.is_empty(),
|
|
||||||
"unrecognized args: {:?}",
|
|
||||||
completely_unrecognized
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.add_app(NotedeckApp::Columns(columns));
|
|
||||||
chrome.add_app(NotedeckApp::Dave(dave));
|
|
||||||
|
|
||||||
// test dav
|
|
||||||
chrome.set_active(0);
|
|
||||||
|
|
||||||
notedeck.set_app(chrome);
|
notedeck.set_app(chrome);
|
||||||
|
|
||||||
Ok(Box::new(notedeck))
|
Ok(Box::new(notedeck))
|
||||||
@@ -128,7 +104,7 @@ Using internal storage would be better but it seems hard to get the config file
|
|||||||
the device ...
|
the device ...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fn get_app_args(_app: AndroidApp) -> Vec<String> {
|
fn get_app_args() -> Vec<String> {
|
||||||
vec!["argv0-placeholder".to_string()]
|
vec!["argv0-placeholder".to_string()]
|
||||||
/*
|
/*
|
||||||
use serde_json::value;
|
use serde_json::value;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use notedeck::{AppAction, AppContext};
|
use notedeck::{AppAction, AppContext};
|
||||||
|
use notedeck_clndash::ClnDash;
|
||||||
use notedeck_columns::Damus;
|
use notedeck_columns::Damus;
|
||||||
use notedeck_dave::Dave;
|
use notedeck_dave::Dave;
|
||||||
|
use notedeck_notebook::Notebook;
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum NotedeckApp {
|
pub enum NotedeckApp {
|
||||||
Dave(Dave),
|
Dave(Box<Dave>),
|
||||||
Columns(Damus),
|
Columns(Box<Damus>),
|
||||||
|
Notebook(Box<Notebook>),
|
||||||
|
ClnDash(Box<ClnDash>),
|
||||||
Other(Box<dyn notedeck::App>),
|
Other(Box<dyn notedeck::App>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +18,8 @@ impl notedeck::App for NotedeckApp {
|
|||||||
match self {
|
match self {
|
||||||
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
||||||
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
||||||
|
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
|
||||||
|
NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
|
||||||
NotedeckApp::Other(other) => other.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 setup;
|
||||||
pub mod theme;
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
mod android;
|
mod android;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod chrome;
|
mod chrome;
|
||||||
|
mod options;
|
||||||
|
|
||||||
pub use app::NotedeckApp;
|
pub use app::NotedeckApp;
|
||||||
pub use chrome::Chrome;
|
pub use chrome::Chrome;
|
||||||
|
pub use options::ChromeOptions;
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ static GLOBAL: AccountingAllocator<std::alloc::System> =
|
|||||||
AccountingAllocator::new(std::alloc::System);
|
AccountingAllocator::new(std::alloc::System);
|
||||||
|
|
||||||
use notedeck::{DataPath, DataPathType, Notedeck};
|
use notedeck::{DataPath, DataPathType, Notedeck};
|
||||||
use notedeck_chrome::{
|
use notedeck_chrome::{setup::generate_native_options, Chrome};
|
||||||
setup::{generate_native_options, setup_chrome},
|
|
||||||
Chrome, NotedeckApp,
|
|
||||||
};
|
|
||||||
use notedeck_columns::Damus;
|
|
||||||
use notedeck_dave::Dave;
|
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
@@ -91,29 +86,8 @@ async fn main() {
|
|||||||
let ctx = &cc.egui_ctx;
|
let ctx = &cc.egui_ctx;
|
||||||
|
|
||||||
let mut notedeck = Notedeck::new(ctx, base_path, &args);
|
let mut notedeck = Notedeck::new(ctx, base_path, &args);
|
||||||
|
notedeck.setup(ctx);
|
||||||
let mut chrome = Chrome::new();
|
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
|
||||||
let columns = Damus::new(&mut notedeck.app_context(), &args);
|
|
||||||
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
|
||||||
|
|
||||||
setup_chrome(ctx, notedeck.args(), notedeck.theme());
|
|
||||||
|
|
||||||
// ensure we recognized all the arguments
|
|
||||||
let completely_unrecognized: Vec<String> = notedeck
|
|
||||||
.unrecognized_args()
|
|
||||||
.intersection(columns.unrecognized_args())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
assert!(
|
|
||||||
completely_unrecognized.is_empty(),
|
|
||||||
"unrecognized args: {completely_unrecognized:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.add_app(NotedeckApp::Columns(columns));
|
|
||||||
chrome.add_app(NotedeckApp::Dave(dave));
|
|
||||||
|
|
||||||
chrome.set_active(0);
|
|
||||||
|
|
||||||
notedeck.set_app(chrome);
|
notedeck.set_app(chrome);
|
||||||
|
|
||||||
Ok(Box::new(notedeck))
|
Ok(Box::new(notedeck))
|
||||||
@@ -147,7 +121,8 @@ pub fn main() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Damus, Notedeck};
|
use super::Notedeck;
|
||||||
|
use notedeck_columns::Damus;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn create_tmp_dir() -> PathBuf {
|
fn create_tmp_dir() -> PathBuf {
|
||||||
@@ -208,21 +183,9 @@ mod tests {
|
|||||||
|
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
|
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
|
||||||
let unrecognized_args = notedeck.unrecognized_args().clone();
|
|
||||||
let mut app_ctx = notedeck.app_context();
|
let mut app_ctx = notedeck.app_context();
|
||||||
let app = Damus::new(&mut app_ctx, &args);
|
let app = Damus::new(&mut app_ctx, &args);
|
||||||
|
|
||||||
// ensure we recognized all the arguments
|
|
||||||
let completely_unrecognized: Vec<String> = unrecognized_args
|
|
||||||
.intersection(app.unrecognized_args())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
assert!(
|
|
||||||
completely_unrecognized.is_empty(),
|
|
||||||
"unrecognized args: {:?}",
|
|
||||||
completely_unrecognized
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2);
|
assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2);
|
||||||
|
|
||||||
let tl1 = app
|
let tl1 = app
|
||||||
|
|||||||
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: {:?}",
|
"unrecognized args: {:?}",
|
||||||
notedeck.unrecognized_args()
|
notedeck.unrecognized_args()
|
||||||
);
|
);
|
||||||
setup_chrome(ctx, notedeck.args(), notedeck.theme());
|
setup_chrome(
|
||||||
|
ctx,
|
||||||
|
notedeck.args(),
|
||||||
|
notedeck.theme(),
|
||||||
|
notedeck.note_body_font_size(),
|
||||||
|
notedeck.zoom_factor(),
|
||||||
|
);
|
||||||
|
|
||||||
notedeck.set_app(PreviewApp::new(preview));
|
notedeck.set_app(PreviewApp::new(preview));
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,6 @@
|
|||||||
use crate::{fonts, theme};
|
|
||||||
|
|
||||||
use eframe::NativeOptions;
|
use eframe::NativeOptions;
|
||||||
use egui::ThemePreference;
|
|
||||||
use notedeck::{AppSizeHandler, DataPath};
|
use notedeck::{AppSizeHandler, DataPath};
|
||||||
use notedeck_ui::app_images;
|
use notedeck_ui::app_images;
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
pub fn setup_chrome(ctx: &egui::Context, args: ¬edeck::Args, theme: ThemePreference) {
|
|
||||||
let is_mobile = args
|
|
||||||
.is_mobile
|
|
||||||
.unwrap_or(notedeck::ui::is_compiled_as_mobile());
|
|
||||||
|
|
||||||
let is_oled = notedeck::ui::is_oled();
|
|
||||||
|
|
||||||
// Some people have been running notedeck in debug, let's catch that!
|
|
||||||
if !args.tests && cfg!(debug_assertions) && !args.debug {
|
|
||||||
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
|
||||||
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
|
|
||||||
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
|
||||||
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
|
||||||
println!("---------------------------------");
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.options_mut(|o| {
|
|
||||||
info!("Loaded theme {:?} from disk", theme);
|
|
||||||
o.theme_preference = theme;
|
|
||||||
});
|
|
||||||
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
|
|
||||||
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
|
|
||||||
setup_cc(ctx, is_mobile);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) {
|
|
||||||
fonts::setup_fonts(ctx);
|
|
||||||
|
|
||||||
if notedeck::ui::is_compiled_as_mobile() {
|
|
||||||
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
|
|
||||||
}
|
|
||||||
//ctx.set_pixels_per_point(1.0);
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//ctx.tessellation_options_mut(|to| to.feathering = false);
|
|
||||||
|
|
||||||
egui_extras::install_image_loaders(ctx);
|
|
||||||
|
|
||||||
ctx.options_mut(|o| {
|
|
||||||
o.input_options.max_click_duration = 0.4;
|
|
||||||
});
|
|
||||||
ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
||||||
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
use egui::{style::Interaction, Color32, FontId, Style, Visuals};
|
|
||||||
use notedeck::{ColorTheme, NotedeckTextStyle};
|
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
|
|
||||||
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
|
||||||
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
|
||||||
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
|
||||||
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
|
||||||
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
|
||||||
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
|
||||||
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
|
||||||
|
|
||||||
// BACKGROUNDS
|
|
||||||
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
|
||||||
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
|
||||||
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
|
||||||
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
|
||||||
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
|
||||||
|
|
||||||
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
|
||||||
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
|
||||||
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
|
||||||
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
|
||||||
|
|
||||||
pub fn desktop_dark_color_theme() -> ColorTheme {
|
|
||||||
ColorTheme {
|
|
||||||
// VISUALS
|
|
||||||
panel_fill: DARKER_BG,
|
|
||||||
extreme_bg_color: DARK_ISH_BG,
|
|
||||||
text_color: Color32::WHITE,
|
|
||||||
err_fg_color: RED_700,
|
|
||||||
warn_fg_color: ORANGE_700,
|
|
||||||
hyperlink_color: PURPLE,
|
|
||||||
selection_color: PURPLE_ALT,
|
|
||||||
|
|
||||||
// WINDOW
|
|
||||||
window_fill: DARK_ISH_BG,
|
|
||||||
window_stroke_color: DARK_BG,
|
|
||||||
|
|
||||||
// NONINTERACTIVE WIDGET
|
|
||||||
noninteractive_bg_fill: DARK_ISH_BG,
|
|
||||||
noninteractive_weak_bg_fill: DARK_BG,
|
|
||||||
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
|
||||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
|
||||||
|
|
||||||
// INACTIVE WIDGET
|
|
||||||
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
|
||||||
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
|
||||||
inactive_weak_bg_fill: SEMI_DARK_BG,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mobile_dark_color_theme() -> ColorTheme {
|
|
||||||
ColorTheme {
|
|
||||||
panel_fill: Color32::BLACK,
|
|
||||||
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
|
||||||
..desktop_dark_color_theme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light_color_theme() -> ColorTheme {
|
|
||||||
ColorTheme {
|
|
||||||
// VISUALS
|
|
||||||
panel_fill: Color32::WHITE,
|
|
||||||
extreme_bg_color: LIGHTER_GRAY,
|
|
||||||
text_color: BLACK,
|
|
||||||
err_fg_color: RED_700,
|
|
||||||
warn_fg_color: ORANGE_700,
|
|
||||||
hyperlink_color: PURPLE,
|
|
||||||
selection_color: PURPLE_ALT,
|
|
||||||
|
|
||||||
// WINDOW
|
|
||||||
window_fill: Color32::WHITE,
|
|
||||||
window_stroke_color: DARKER_GRAY,
|
|
||||||
|
|
||||||
// NONINTERACTIVE WIDGET
|
|
||||||
noninteractive_bg_fill: Color32::WHITE,
|
|
||||||
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
|
||||||
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
|
||||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
|
||||||
|
|
||||||
// INACTIVE WIDGET
|
|
||||||
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
|
||||||
inactive_bg_fill: LIGHTER_GRAY,
|
|
||||||
inactive_weak_bg_fill: LIGHTER_GRAY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light_mode() -> Visuals {
|
|
||||||
notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dark_mode(is_oled: bool) -> Visuals {
|
|
||||||
notedeck::theme::create_themed_visuals(
|
|
||||||
if is_oled {
|
|
||||||
mobile_dark_color_theme()
|
|
||||||
} else {
|
|
||||||
desktop_dark_color_theme()
|
|
||||||
},
|
|
||||||
Visuals::dark(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create custom text sizes for any FontSizes
|
|
||||||
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
|
||||||
let font_size = if is_mobile {
|
|
||||||
notedeck::fonts::mobile_font_size
|
|
||||||
} else {
|
|
||||||
notedeck::fonts::desktop_font_size
|
|
||||||
};
|
|
||||||
|
|
||||||
style.text_styles = NotedeckTextStyle::iter()
|
|
||||||
.map(|text_style| {
|
|
||||||
(
|
|
||||||
text_style.text_style(),
|
|
||||||
FontId::new(font_size(&text_style), text_style.font_family()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
style.interaction = Interaction {
|
|
||||||
tooltip_delay: 0.1,
|
|
||||||
show_tooltips_only_when_still: false,
|
|
||||||
..Interaction::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// debug: show callstack for the current widget on hover if all
|
|
||||||
// modifier keys are pressed down.
|
|
||||||
#[cfg(feature = "debug-widget-callstack")]
|
|
||||||
{
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
compile_error!(
|
|
||||||
"The `debug-widget-callstack` feature requires a debug build, \
|
|
||||||
release builds are unsupported."
|
|
||||||
);
|
|
||||||
style.debug.debug_on_hover_with_all_modifiers = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// debug: show an overlay on all interactive widgets
|
|
||||||
#[cfg(feature = "debug-interactive-widgets")]
|
|
||||||
{
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
compile_error!(
|
|
||||||
"The `debug-interactive-widgets` feature requires a debug build, \
|
|
||||||
release builds are unsupported."
|
|
||||||
);
|
|
||||||
style.debug.show_interactive_widgets = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,7 +10,12 @@ description = "A tweetdeck-style notedeck app"
|
|||||||
[lib]
|
[lib]
|
||||||
crate-type = ["lib", "cdylib"]
|
crate-type = ["lib", "cdylib"]
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
jni = { workspace = true }
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
opener = { workspace = true }
|
||||||
rmpv = { workspace = true }
|
rmpv = { workspace = true }
|
||||||
bech32 = { workspace = true }
|
bech32 = { workspace = true }
|
||||||
notedeck = { workspace = true }
|
notedeck = { workspace = true }
|
||||||
@@ -31,7 +36,7 @@ image = { workspace = true }
|
|||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
nostrdb = { workspace = true }
|
nostrdb = { workspace = true }
|
||||||
notedeck_ui = { workspace = true }
|
notedeck_ui = { workspace = true }
|
||||||
open = { workspace = true }
|
robius-open = { workspace = true }
|
||||||
poll-promise = { workspace = true }
|
poll-promise = { workspace = true }
|
||||||
puffin = { workspace = true, optional = true }
|
puffin = { workspace = true, optional = true }
|
||||||
puffin_egui = { workspace = true, optional = true }
|
puffin_egui = { workspace = true, optional = true }
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user