Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c1d3be4c07
|
|||
| 6a08d4b1b2 | |||
| d6d7e4c35e | |||
| c3499729f2 | |||
| dac786e60f | |||
| 41aa2db3c7 | |||
| 10225158e5 | |||
| 557608db9b | |||
| 8697a5cb0a | |||
| 7aca39aae8 | |||
| aa467b9be0 | |||
| 09eeb57bd9 | |||
| b1a5dd6cab | |||
| d12e5b363c | |||
| cc8bafddff | |||
| 3766308ce6 | |||
| 17f72f6127 | |||
| f592015c0c | |||
| 1ab4eeb48c | |||
| a8c6baeacb | |||
| a896a6ecfa | |||
| f282363748 | |||
| ba76b20ad2 | |||
| b04f50a9f6 | |||
| 233be47659 | |||
| 173972f920 | |||
| 31ec21ea02 | |||
| d3d8d7be4b | |||
| 09dc101c1b | |||
| 261477339b | |||
| 9ff5753bca | |||
| b9e2fe5dd1 | |||
| d1a9e0020e | |||
| 1163dd8461 | |||
| 692f4889cf | |||
| f2153f53dc | |||
| 40764d7368 | |||
| be720c0f76 | |||
| 5848f1c355 | |||
| 0dcf70bc15 | |||
| 0fc8e70180 | |||
| 2de6851fbd | |||
| f57d582307 | |||
| 09e608ca75 | |||
| 2bd636ce0a | |||
| 79bf6cf126 | |||
| b8207106d7 | |||
| 5280028a82 | |||
| f4a6e8f9bb | |||
| 83fd6de076 | |||
| b80a0ab0f1 | |||
| e437a0db1c | |||
| 6e81b98d2f | |||
| 217f1e45da | |||
| 96e0366787 | |||
| 2a85ee562c | |||
| 1fabd347ca | |||
| 0087fe7dff | |||
| 51f7744149 | |||
| 6d393c9c37 | |||
|
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 | |||
|
b285be97a1
|
|||
|
7321e82800
|
|||
|
e686afed1c
|
|||
| ed38c75193 | |||
| fdef74c353 | |||
| 030e4226f8 | |||
| 508d8dc0ba | |||
| 34afa755b8 | |||
| 45490c918d | |||
| a31fdd3ed2 |
@@ -1,33 +0,0 @@
|
||||
name: Crowdin Download Translations
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [ crowdin-translation-complete ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
crowdin-download:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Crowdin download translations
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
localization_branch_name: crowdin-translations
|
||||
create_pull_request: true
|
||||
pull_request_title: 'Crowdin Translations'
|
||||
pull_request_body: 'Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||
pull_request_base_branch_name: 'master'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Crowdin Upload & Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
crowdin-upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fetch crowdin-translations branch
|
||||
run: |
|
||||
git fetch origin crowdin-translations:crowdin-translations || true
|
||||
|
||||
- name: Checkout crowdin-translations branch
|
||||
run: git checkout crowdin-translations || git checkout -b crowdin-translations
|
||||
|
||||
- name: Rebase master onto crowdin-translations
|
||||
run: git rebase master
|
||||
|
||||
- name: Fail if rebase conflicts occurred
|
||||
run: |
|
||||
if [ -d .git/rebase-merge ] || [ -d .git/rebase-apply ]; then
|
||||
echo "❌ Rebase conflict detected! Please resolve conflicts in the crowdin-translations branch manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Run export_source_strings.py
|
||||
run: python3 scripts/export_source_strings.py
|
||||
|
||||
- name: Check for changes in main.ftl
|
||||
id: check_diff
|
||||
run: |
|
||||
git diff --quiet assets/translations/en-US/main.ftl assets/translations/en-XA/main.ftl || echo "changed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit changes to crowdin-translations
|
||||
if: steps.check_diff.outputs.changed == 'true'
|
||||
run: |
|
||||
git add assets/translations/en-US/main.ftl assets/translations/en-XA/main.ftl
|
||||
git commit -m "Update source strings from export_source_strings.py" || true
|
||||
git push --force origin crowdin-translations
|
||||
|
||||
- name: Crowdin upload sources
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
localization_branch_name: crowdin-translations
|
||||
create_pull_request: true
|
||||
pull_request_title: 'Crowdin Translations'
|
||||
pull_request_body: 'Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||
pull_request_base_branch_name: 'master'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
@@ -19,3 +19,7 @@ queries/damus-notifs.json
|
||||
.direnv/
|
||||
scripts/macos_build_secrets.sh
|
||||
/tags
|
||||
.zed
|
||||
.lsp
|
||||
.idea
|
||||
local.properties
|
||||
Generated
+298
-66
@@ -765,6 +765,25 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
|
||||
dependencies = [
|
||||
"block-sys",
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -802,6 +821,17 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "built"
|
||||
version = "0.7.7"
|
||||
@@ -978,6 +1008,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1233,6 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1378,20 +1410,26 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eframe"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1427,24 +1465,25 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
"backtrace",
|
||||
"bitflags 2.9.1",
|
||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||
"epaint",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
"serde",
|
||||
"similar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui-wgpu"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1463,7 +1502,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui-winit"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"arboard",
|
||||
@@ -1481,7 +1520,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_extras"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"egui",
|
||||
@@ -1498,7 +1537,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1515,7 +1554,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_nav"
|
||||
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=3c67eb6298edbff36d46546897cfac33df4f04db#3c67eb6298edbff36d46546897cfac33df4f04db"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
@@ -1577,7 +1616,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde",
|
||||
@@ -1595,7 +1634,7 @@ version = "0.3.0"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"ewebsock",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"hex",
|
||||
"mio",
|
||||
"nostr 0.37.0",
|
||||
@@ -1675,13 +1714,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
"ecolor",
|
||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
|
||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||
"epaint_default_fonts",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
@@ -1693,7 +1732,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "epaint_default_fonts"
|
||||
version = "0.31.1"
|
||||
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
|
||||
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||
|
||||
[[package]]
|
||||
name = "equator"
|
||||
@@ -2269,7 +2308,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"gpu-descriptor-types",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2291,6 +2330,12 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
@@ -2335,6 +2380,17 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex_color"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
@@ -2496,6 +2552,16 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icrate"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
|
||||
dependencies = [
|
||||
"block2 0.4.0",
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
@@ -2654,6 +2720,17 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
@@ -2661,7 +2738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2733,25 +2810,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -2868,6 +2926,19 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsoncanvas"
|
||||
version = "0.1.6"
|
||||
source = "git+https://github.com/jb55/jsoncanvas?rev=ae60f96e4d022cf037e086b793cacc3225bc14e5#ae60f96e4d022cf037e086b793cacc3225bc14e5"
|
||||
dependencies = [
|
||||
"hex_color",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -3190,7 +3261,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
@@ -3304,6 +3375,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "nostr"
|
||||
version = "0.37.0"
|
||||
@@ -3383,8 +3463,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostrdb"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/damus-io/nostrdb-rs?rev=a307f5d3863b5319c728b2782959839b8df544cb#a307f5d3863b5319c728b2782959839b8df544cb"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/damus-io/nostrdb-rs?rev=2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3#2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
@@ -3398,21 +3478,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck"
|
||||
version = "0.5.6"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"base32",
|
||||
"bech32",
|
||||
"bincode",
|
||||
"bitflags 2.9.1",
|
||||
"blurhash",
|
||||
"dirs",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui-winit",
|
||||
"egui_extras",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"fluent",
|
||||
"fluent-langneg",
|
||||
"fluent-resmgr",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"hex",
|
||||
"image",
|
||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -3434,6 +3517,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"sys-locale",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tokenator",
|
||||
@@ -3446,7 +3530,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_chrome"
|
||||
version = "0.5.6"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -3457,6 +3541,7 @@ dependencies = [
|
||||
"notedeck",
|
||||
"notedeck_columns",
|
||||
"notedeck_dave",
|
||||
"notedeck_notebook",
|
||||
"notedeck_ui",
|
||||
"profiling",
|
||||
"puffin",
|
||||
@@ -3475,7 +3560,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_columns"
|
||||
version = "0.5.6"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bech32",
|
||||
@@ -3490,16 +3575,16 @@ dependencies = [
|
||||
"egui_virtual_list",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"hex",
|
||||
"human_format",
|
||||
"image",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
"notedeck_ui",
|
||||
"oot_bitset",
|
||||
"open",
|
||||
"opener",
|
||||
"poll-promise",
|
||||
"pretty_assertions",
|
||||
"profiling",
|
||||
@@ -3507,6 +3592,7 @@ dependencies = [
|
||||
"puffin_egui",
|
||||
"rfd",
|
||||
"rmpv",
|
||||
"robius-open",
|
||||
"security-framework 2.11.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3528,7 +3614,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_dave"
|
||||
version = "0.5.6"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"async-openai",
|
||||
"bytemuck",
|
||||
@@ -3550,19 +3636,27 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_notebook"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"jsoncanvas",
|
||||
"notedeck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notedeck_ui"
|
||||
version = "0.5.6"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"blurhash",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui-winit",
|
||||
"egui_extras",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.4",
|
||||
"image",
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
@@ -3992,14 +4086,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.2"
|
||||
name = "opener"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
|
||||
checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
"bstr",
|
||||
"normpath",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4103,12 +4197,6 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -4397,7 +4485,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"natord",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
@@ -4725,6 +4813,26 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@@ -4916,6 +5024,30 @@ dependencies = [
|
||||
"rmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "robius-android-env"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef"
|
||||
dependencies = [
|
||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ndk-context",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "robius-open"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243e2abbc8c1ca8ddc283056d4675b67e452fd527c3741c5318642da37840ff3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"icrate",
|
||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"objc2 0.5.2",
|
||||
"robius-android-env",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.19.0"
|
||||
@@ -5062,6 +5194,30 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -5215,7 +5371,7 @@ version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -5254,6 +5410,38 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.9.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -5315,6 +5503,12 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.2"
|
||||
@@ -5528,6 +5722,15 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.13"
|
||||
@@ -5848,7 +6051,7 @@ version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -6631,7 +6834,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"naga",
|
||||
"once_cell",
|
||||
@@ -6752,6 +6955,16 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core 0.54.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
@@ -6771,6 +6984,16 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
@@ -6847,6 +7070,15 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
|
||||
+16
-10
@@ -1,17 +1,19 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
package.version = "0.5.6"
|
||||
package.version = "0.5.9"
|
||||
members = [
|
||||
"crates/notedeck",
|
||||
"crates/notedeck_chrome",
|
||||
"crates/notedeck_columns",
|
||||
"crates/notedeck_dave",
|
||||
"crates/notedeck_notebook",
|
||||
"crates/notedeck_ui",
|
||||
|
||||
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
opener = "0.8.2"
|
||||
base32 = "0.4.0"
|
||||
base64 = "0.22.1"
|
||||
rmpv = "1.3.0"
|
||||
@@ -23,7 +25,7 @@ egui = { version = "0.31.1", features = ["serde"] }
|
||||
egui-wgpu = "0.31.1"
|
||||
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
||||
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "111de8ac40b5d18df53e9691eb18a50d49cb31d8" }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "3c67eb6298edbff36d46546897cfac33df4f04db" }
|
||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
||||
#egui_virtual_list = "0.6.0"
|
||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
||||
@@ -41,16 +43,17 @@ md5 = "0.7.0"
|
||||
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
|
||||
nwc = "0.39.0"
|
||||
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"
|
||||
notedeck = { path = "crates/notedeck" }
|
||||
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
||||
notedeck_columns = { path = "crates/notedeck_columns" }
|
||||
notedeck_dave = { path = "crates/notedeck_dave" }
|
||||
notedeck_notebook = { path = "crates/notedeck_notebook" }
|
||||
notedeck_ui = { path = "crates/notedeck_ui" }
|
||||
tokenator = { path = "crates/tokenator" }
|
||||
once_cell = "1.19.0"
|
||||
open = "5.3.0"
|
||||
robius-open = "0.1"
|
||||
poll-promise = { version = "0.3.0", features = ["tokio"] }
|
||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||
@@ -66,6 +69,7 @@ tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tempfile = "3.13.0"
|
||||
unic-langid = { version = "0.9.6", features = ["macros"] }
|
||||
sys-locale = "0.3"
|
||||
url = "2.5.2"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = { version = "1.10.0", features = ["v4"] }
|
||||
@@ -81,6 +85,8 @@ hashbrown = "0.15.2"
|
||||
openai-api-rs = "6.0.3"
|
||||
re_memory = "0.23.4"
|
||||
oot_bitset = "0.1.1"
|
||||
blurhash = "0.2.3"
|
||||
|
||||
|
||||
[profile.small]
|
||||
inherits = 'release'
|
||||
@@ -98,12 +104,12 @@ strip = true # Strip symbols from binary*
|
||||
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
||||
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
||||
|
||||
egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
|
||||
egui = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||
eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||
epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -6,9 +6,7 @@
|
||||
# Regular strings
|
||||
|
||||
# Profile about/bio field label
|
||||
About_00c0 = Über
|
||||
# Display name for account management
|
||||
Accounts_e233 = Konten
|
||||
About_00c0 = Über mich
|
||||
# Column title for account management
|
||||
Accounts_f018 = Konten
|
||||
# Button label to add a relay
|
||||
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = Wallet hinzufügen um fortzufahren
|
||||
Add_account_1cfc = Konto hinzufügen
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Konto hinzufügen
|
||||
# Display name for adding account
|
||||
Add_Account_d715 = Konto hinzufügen
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Algorithmus-Spalte hinzufügen
|
||||
# Display name for adding column
|
||||
Add_Column_c6ff = Spalte hinzufügen
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Spalte hinzufügen
|
||||
# Display name for adding deck
|
||||
Add_Deck_6e5f = Deck hinzufügen
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Deck hinzufügen
|
||||
# Column title for adding external notifications column
|
||||
@@ -69,8 +61,6 @@ Broadcast_Local_7e50 = Lokal senden
|
||||
Cancel_ed3b = Abbrechen
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
||||
# Display name for note composition
|
||||
Compose_Note_ad11 = Notiz erstellen
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Notiz erstellen
|
||||
# Button label to confirm an action
|
||||
@@ -83,8 +73,6 @@ Connecting_6b7e = Verbinde...
|
||||
Contact_List_f85a = Kontaktliste
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = Kontakte
|
||||
# Timeline kind label for contact lists
|
||||
Contacts_8b98 = Kontakte
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = Kontakte (letzte Notizen)
|
||||
# Button label to copy logs
|
||||
@@ -100,7 +88,7 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = Text kopieren
|
||||
# Relative time in days
|
||||
count_d_b9be = { $count }T.
|
||||
count_d_b9be = { $count }Tg.
|
||||
# Relative time in hours
|
||||
count_h_3ecb = { $count }Std.
|
||||
# Relative time in minutes
|
||||
@@ -119,12 +107,8 @@ Create_Account_6994 = Konto erstellen
|
||||
Create_Deck_16b7 = Deck erstellen
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Benutzerdefiniert
|
||||
# Display name for custom timelines
|
||||
Custom_cb4f = Benutzerdefiniert
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
||||
# Display name for zap customization
|
||||
Customize_Zap_Amount_ed29 = Zap-Betrag anpassen
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Damus Support
|
||||
# Label for deck name input field
|
||||
@@ -147,14 +131,10 @@ Display_name_f9d9 = Anzeigename
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" wird zur Identifikation verwendet
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Deck bearbeiten
|
||||
# Display name for editing deck
|
||||
Edit_Deck_c9ba = Deck bearbeiten
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Deck bearbeiten
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = Profil bearbeiten
|
||||
# Display name for profile editing
|
||||
Edit_Profile_6699 = Profil bearbeiten
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = Profil bearbeiten
|
||||
# Placeholder for hashtag input field
|
||||
@@ -171,14 +151,8 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Profil finden
|
||||
# Timeline kind label for hashtag feeds
|
||||
Hashtag_a0ab = Hashtag
|
||||
# Display name for hashtag feeds
|
||||
Hashtags_617e = Hashtags
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Display name for home feed
|
||||
Home_3efc = Startseite
|
||||
# Title for Home column
|
||||
Home_8c19 = Startseite
|
||||
# Label for deck icon selection
|
||||
@@ -205,10 +179,6 @@ k_5K_f7e6 = 5K
|
||||
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Letzte Notiz pro Profil
|
||||
# Timeline kind label for last notes per pubkey
|
||||
Last_Notes_aefe = Letzte Notizen
|
||||
# Display name for last notes per contact
|
||||
Last_Per_Pubkey__Contact_33ce = Zuletzt pro Pubkey (Kontakt)
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
||||
# Login page title
|
||||
@@ -241,10 +211,6 @@ Notes_60d2 = Notizen
|
||||
Notes___Replies_1ec2 = Notizen & Antworten
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notizen & Antworten
|
||||
# Timeline kind label for notifications
|
||||
Notifications_6228 = Benachrichtigungen
|
||||
# Display name for notifications
|
||||
Notifications_8029 = Benachrichtigungen
|
||||
# Column title for notifications
|
||||
Notifications_d673 = Benachrichtigungen
|
||||
# Title for notifications column
|
||||
@@ -267,30 +233,20 @@ Please_select_an_icon_655b = Bitte wählen ein Symbol aus.
|
||||
Post_now_8a49 = Jetzt veröffentlichen
|
||||
# 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 = Drücke die Schaltfläche unten, um deine neuesten Protokolle in die Zwischenablage deines Systems zu kopieren. Dann füge sie in deine E-Mail ein.
|
||||
# Display name for user profiles
|
||||
Profile_2478 = Profil
|
||||
# Timeline kind label for user profiles
|
||||
Profile_9027 = Profil
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Profilbild
|
||||
# Column title for quote composition
|
||||
Quote_475c = Zitat
|
||||
# Display name for quote composition
|
||||
Quote_a38e = Zitat
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = Zitat von unbekannter Notiz
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = Nur Lesezugriff
|
||||
# Display name for relay management
|
||||
Relays_7335 = Relays
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relays
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relays
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Antwort
|
||||
# Display name for reply composition
|
||||
Reply_b40f = Antworten
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = Auf diese Notiz antworten
|
||||
# Error message when reply note cannot be found
|
||||
@@ -321,12 +277,6 @@ sats_e5ec = Sats
|
||||
Save_6f7c = Speichern
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = Änderungen speichern
|
||||
# Display name for search results
|
||||
Search_0aa0 = Suche
|
||||
# Display name for search page
|
||||
Search_4503 = Suche
|
||||
# Timeline kind label for search results
|
||||
Search_a0b8 = Suche
|
||||
# Column title for search page
|
||||
Search_c573 = Suche
|
||||
# Placeholder for search notes input field
|
||||
@@ -369,8 +319,6 @@ Step_2_d08d = Schritt 2
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
|
||||
# Display name for support page
|
||||
Support_a4b4 = Support
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
||||
# Hover text for light mode toggle button
|
||||
@@ -380,17 +328,9 @@ Tap_to_Load_4b05 = Zum Laden antippen
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Unterhaltungen
|
||||
# Display name for thread view
|
||||
Thread_9957 = Unterhaltungen
|
||||
Thread_0f20 = Unterhaltung
|
||||
# Link text for thread references
|
||||
thread_ad1f = Unterhaltungen
|
||||
# Generic timeline kind label
|
||||
Timeline_b0fc = Timeline
|
||||
# Timeline kind label for universe feed
|
||||
Universe_0a3e = Weltraum
|
||||
# Display name for universe feed
|
||||
Universe_d47e = Weltraum
|
||||
thread_ad1f = Unterhaltung
|
||||
# Title for universe column
|
||||
Universe_e01e = Weltraum
|
||||
# Column title for universe feed
|
||||
@@ -403,8 +343,6 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
||||
Username_daa7 = Benutzername
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Wallet
|
||||
# Display name for wallet management
|
||||
Wallet_cdca = Wallet
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Wir empfehlen kurze Namen
|
||||
# Profile website field label
|
||||
|
||||
@@ -64,6 +64,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in no
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Amount
|
||||
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = Appearance
|
||||
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Ask
|
||||
|
||||
@@ -76,6 +79,9 @@ Banner_52ef = Banner
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = Bottom
|
||||
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Broadcast
|
||||
|
||||
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = Broadcast Local
|
||||
# Button label to cancel an action
|
||||
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
|
||||
Click_to_edit_0414 = Click to edit
|
||||
|
||||
# Column title for note composition
|
||||
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
|
||||
Confirm_f8a6 = Confirm
|
||||
|
||||
@@ -163,6 +181,9 @@ Customize_Zap_Amount_cfc4 = Customize Zap Amount
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Damus Support
|
||||
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Dark
|
||||
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Deck name
|
||||
|
||||
@@ -223,12 +244,18 @@ Find_User_bd12 = Find User
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = Hide
|
||||
|
||||
# Title for Home column
|
||||
Home_8c19 = Home
|
||||
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Icon
|
||||
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Image cache size:
|
||||
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
|
||||
@@ -259,9 +286,15 @@ k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
|
||||
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Language:
|
||||
|
||||
# Title for last note per user column
|
||||
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
|
||||
Lightning_network_address__lud16_ea51 = Lightning network address (lud16)
|
||||
|
||||
@@ -325,6 +358,9 @@ Open_Email_25e9 = Open Email
|
||||
# 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
|
||||
|
||||
# Label for others settings section
|
||||
Others_7267 = Others
|
||||
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Paste your NWC URI here...
|
||||
|
||||
@@ -394,6 +430,9 @@ Repost_this_note_8e56 = Repost this note
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Reposted
|
||||
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Reset
|
||||
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = Running into a bug?
|
||||
|
||||
@@ -427,6 +466,12 @@ See_the_whole_nostr_universe_7694 = See the whole nostr universe
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Send
|
||||
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Settings
|
||||
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = Show source client
|
||||
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
|
||||
|
||||
@@ -466,6 +511,9 @@ Step_1_8656 = Step 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Step 2
|
||||
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Storage
|
||||
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
|
||||
|
||||
@@ -484,12 +532,18 @@ Tap_to_Load_4b05 = Tap to Load
|
||||
# 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!
|
||||
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Theme:
|
||||
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Thread
|
||||
|
||||
# Link text for thread references
|
||||
thread_ad1f = thread
|
||||
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = Top
|
||||
|
||||
# Title for universe column
|
||||
Universe_e01e = Universe
|
||||
|
||||
@@ -505,6 +559,9 @@ username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at
|
||||
# Profile username field label
|
||||
Username_daa7 = Username
|
||||
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = View folder:
|
||||
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Wallet
|
||||
|
||||
@@ -532,6 +589,9 @@ Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Zap this note
|
||||
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = Zoom Level:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
# Search results count
|
||||
|
||||
@@ -64,6 +64,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = {"["}Àmóúñt{"]"}
|
||||
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = {"["}Àppéàràñçé{"]"}
|
||||
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = {"["}Àsk{"]"}
|
||||
|
||||
@@ -76,6 +79,9 @@ Banner_52ef = {"["}Bàññér{"]"}
|
||||
# Beta version label
|
||||
BETA_8e5d = {"["}BÉTÀ{"]"}
|
||||
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = {"["}Bóttóm{"]"}
|
||||
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
|
||||
|
||||
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = {"["}Bróàdçàst Lóçàl{"]"}
|
||||
# Button label to cancel an action
|
||||
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
|
||||
Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"}
|
||||
|
||||
# Column title for note composition
|
||||
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
|
||||
Confirm_f8a6 = {"["}Çóñfírm{"]"}
|
||||
|
||||
@@ -163,6 +181,9 @@ Customize_Zap_Amount_cfc4 = {"["}Çústómízé Zàp Àmóúñt{"]"}
|
||||
# Column title for support page
|
||||
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
|
||||
Deck_name_cd32 = {"["}Déçk ñàmé{"]"}
|
||||
|
||||
@@ -223,12 +244,18 @@ Find_User_bd12 = {"["}Fíñd Úsér{"]"}
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
|
||||
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = {"["}Hídé{"]"}
|
||||
|
||||
# Title for Home column
|
||||
Home_8c19 = {"["}Hómé{"]"}
|
||||
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = {"["}Íçóñ{"]"}
|
||||
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = {"["}Ímàgé çàçhé sízé:{"]"}
|
||||
|
||||
# Title for individual user column
|
||||
Individual_b776 = {"["}Íñdívídúàl{"]"}
|
||||
|
||||
@@ -259,9 +286,15 @@ k_5K_f7e6 = {"["}5K{"]"}
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
|
||||
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = {"["}Làñgúàgé:{"]"}
|
||||
|
||||
# Title for last note per user column
|
||||
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
|
||||
Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"}
|
||||
|
||||
@@ -325,6 +358,9 @@ Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
|
||||
# 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{"]"}
|
||||
|
||||
# Label for others settings section
|
||||
Others_7267 = {"["}Óthérs{"]"}
|
||||
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"}
|
||||
|
||||
@@ -394,6 +430,9 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = {"["}Répóstéd{"]"}
|
||||
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = {"["}Rését{"]"}
|
||||
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
|
||||
|
||||
@@ -427,6 +466,12 @@ See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérs
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = {"["}Séñd{"]"}
|
||||
|
||||
# Column title for app settings
|
||||
Settings_7a4f = {"["}Séttíñgs{"]"}
|
||||
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = {"["}Shów sóúrçé çlíéñt{"]"}
|
||||
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
|
||||
|
||||
@@ -466,6 +511,9 @@ Step_1_8656 = {"["}Stép 1{"]"}
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = {"["}Stép 2{"]"}
|
||||
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = {"["}Stóràgé{"]"}
|
||||
|
||||
# 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{"]"}
|
||||
|
||||
@@ -484,12 +532,18 @@ Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"}
|
||||
# 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óóñ!{"]"}
|
||||
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = {"["}Thémé:{"]"}
|
||||
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = {"["}Thréàd{"]"}
|
||||
|
||||
# Link text for thread references
|
||||
thread_ad1f = {"["}thréàd{"]"}
|
||||
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = {"["}Tóp{"]"}
|
||||
|
||||
# Title for universe column
|
||||
Universe_e01e = {"["}Úñívérsé{"]"}
|
||||
|
||||
@@ -505,6 +559,9 @@ username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username
|
||||
# Profile username field label
|
||||
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
|
||||
Wallet_5e50 = {"["}Wàllét{"]"}
|
||||
|
||||
@@ -532,6 +589,9 @@ Zap_16b4 = {"["}Zàp{"]"}
|
||||
# Hover text for zap button
|
||||
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
|
||||
|
||||
# Search results count
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
# 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 = Información
|
||||
# Column title for account management
|
||||
Accounts_f018 = Cuentas
|
||||
# Button label to add a relay
|
||||
Add_269d = Agregar
|
||||
# Label for add column button
|
||||
Add_47df = Agregar
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Agregar una billetera diferente que solo se utilizará para esta cuenta
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = Agregar una billetera para continuar
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = Agregar cuenta
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Agregar cuenta
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Agregar columna algorítmica
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Agregar columna
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Agregar deck
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = Agregar columna de notificaciones externas
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Agregar columna de notificaciones
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = Agregar relé
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = Agregar billetera
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = Algo
|
||||
# Description for algorithmic feeds column
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Cantidad
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Preguntar
|
||||
# Placeholder text for Dave AI input field
|
||||
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = Banner
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Transmitir
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = Transmitir localmente
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Cancelar
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Haz clic para editar
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Redactar nota
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Confirmar
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = Conectado
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = Conectando...
|
||||
# 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 enlace
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = Copiar ID de nota
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copiar pubkey
|
||||
# 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 = Crear cuenta
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = Crear deck
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Personalizado
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Ayuda de Damus
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Nombre del deck
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = DECKS
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Deck predeterminado
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = Eliminar deck
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = Eliminar esta columna
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = Eliminar billetera
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = Nombre para mostrar
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Editar deck
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Editar deck
|
||||
# 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 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = Ingresa el relé aquí
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = Ingresa tu clave
|
||||
# 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 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Buscar usuario
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
Home_8c19 = Inicio
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Ícono
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = Cantidad no válida
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = Clave no válida.
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = NWC URI no válido
|
||||
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_100K_686c = 100.000
|
||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_10K_f7e6 = 10.000
|
||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_20K_4977 = 20.000
|
||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_50K_c2dc = 50.000
|
||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_5K_f7e6 = 5.000
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Última nota por usuario
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||
# Login page title
|
||||
Login_9eef = Inicio de sesión
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
||||
# Title for the user's 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.
|
||||
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = nostrich
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = No 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 es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
|
||||
# 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 y respuestas
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notas y respuestas
|
||||
# Column title for notifications
|
||||
Notifications_d673 = Notificaciones
|
||||
# Title for notifications column
|
||||
Notifications_ef56 = Notificaciones
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = ahora
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir correo electrónico
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||
# Error message for missing deck name
|
||||
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el deck.
|
||||
# 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 ícono.
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = Selecciona un ícono.
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = Publicar ahora
|
||||
# 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 = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Imagen de perfil
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citar
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = Solo lectura
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relés
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relés
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Respuesta
|
||||
# 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 desconocida
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = respondiendo a { $user }
|
||||
# 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
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = respondiendo a una nota
|
||||
# Hover text for repost button
|
||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicadas de nuevo
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = ¿Encontraste un error?
|
||||
# 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 cambios
|
||||
# Column title for search page
|
||||
Search_c573 = Búsqueda
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = Buscar notas...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Buscando '{ $query }'
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
||||
# Button label to sign out of account
|
||||
Sign_out_337b = Cerrar sesión
|
||||
# Title for someone else's notes column
|
||||
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||
# Description for hashtags column
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
|
||||
# Step 1 label in support instructions
|
||||
Step_1_8656 = Paso 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Paso 2
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = Cambiar a modo claro
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = Toca para cargar
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Conversación
|
||||
# Link text for thread references
|
||||
thread_ad1f = conversación
|
||||
# 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 billetera solo para la cuenta actual
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||
# Profile username field label
|
||||
Username_daa7 = Nombre de usuario
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Billetera
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Recomendamos nombres cortos
|
||||
# Profile website field label
|
||||
Website_7980 = Sitio web
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = Tu clave aquí...
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = Tus notas
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = Tus notificaciones
|
||||
# Heading for zap (tip) action
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Enviar un zap a esta nota
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[uno] Obtuvo { $count } resultado para '{ $query }'
|
||||
*[otro] Obtuvo { $count } resultados para '{ $query }'
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
# 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 = Información
|
||||
# Column title for account management
|
||||
Accounts_f018 = Cuentas
|
||||
# Button label to add a relay
|
||||
Add_269d = Añadir
|
||||
# Label for add column button
|
||||
Add_47df = Añadir
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Añadir un monedero diferente que solo se utilizará para esta cuenta
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = Añadir un monedero para continuar
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = Añadir cuenta
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Añadir cuenta
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Añadir columna algorítmica
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Añadir columna
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Añadir deck
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = Añadir columna de notificaciones externas
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Añadir columna de notificaciones
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = Añadir relé
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = Añadir monedero
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = Algo
|
||||
# Description for algorithmic feeds column
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Cantidad
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Preguntar
|
||||
# Placeholder text for Dave AI input field
|
||||
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = Banner
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Transmitir
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = Transmitir localmente
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Cancelar
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Haz clic para editar
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Redactar nota
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Confirmar
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = Conectado
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = Conectando...
|
||||
# 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 enlace
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = Copiar ID de nota
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copiar pubkey
|
||||
# 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 = Crear cuenta
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = Crear deck
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Personalizado
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Ayuda de Damus
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Nombre del deck
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = DECKS
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Deck predeterminado
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = Eliminar deck
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = Eliminar esta columna
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = Eliminar monedero
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = Nombre para mostrar
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Editar deck
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Editar deck
|
||||
# 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 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = Ingresa el relé aquí
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = Ingresa tu clave
|
||||
# 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 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Buscar usuario
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
Home_8c19 = Inicio
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Icono
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = Cantidad no válida
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = Clave no válida.
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = NWC URI no válido
|
||||
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_100K_686c = 100.000
|
||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_10K_f7e6 = 10.000
|
||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_20K_4977 = 20.000
|
||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_50K_c2dc = 50.000
|
||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_5K_f7e6 = 5.000
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Última nota por usuario
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||
# Login page title
|
||||
Login_9eef = Inicio de sesión
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
||||
# Title for the user's 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.
|
||||
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = nostrich
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = No 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 es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
|
||||
# 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 y respuestas
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notas y respuestas
|
||||
# Column title for notifications
|
||||
Notifications_d673 = Notificaciones
|
||||
# Title for notifications column
|
||||
Notifications_ef56 = Notificaciones
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = ahora
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir correo electrónico
|
||||
# Instruction to open email client
|
||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||
# Error message for missing deck name
|
||||
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
|
||||
# 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.
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = Selecciona un icono.
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = Publicar ahora
|
||||
# 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 = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Imagen de perfil
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citar
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = Solo lectura
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relés
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relés
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Respuesta
|
||||
# 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 desconocida
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = respondiendo a { $user }
|
||||
# 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
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = respondiendo a una nota
|
||||
# Hover text for repost button
|
||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicadas de nuevo
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = ¿Has encontrado un error?
|
||||
# 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 cambios
|
||||
# Column title for search page
|
||||
Search_c573 = Búsqueda
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = Buscar notas...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Buscando '{ $query }'
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
||||
# Button label to sign out of account
|
||||
Sign_out_337b = Cerrar sesión
|
||||
# Title for someone else's notes column
|
||||
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||
# Description for hashtags column
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
|
||||
# Step 1 label in support instructions
|
||||
Step_1_8656 = Paso 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Paso 2
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = Cambiar a modo claro
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = Toca para cargar
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Conversación
|
||||
# Link text for thread references
|
||||
thread_ad1f = conversación
|
||||
# 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 este monedero solo para la cuenta actual
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||
# Profile username field label
|
||||
Username_daa7 = Nombre de usuario
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Monedero
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Recomendamos nombres cortos
|
||||
# Profile website field label
|
||||
Website_7980 = Sitio web
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = Tu clave aquí...
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = Tus notas
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = Tus notificaciones
|
||||
# Heading for zap (tip) action
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Enviar un zap a esta nota
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[uno] Obtuvo { $count } resultado para '{ $query }'
|
||||
*[otro] Obtuvo { $count } resultados para '{ $query }'
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
# Profile about/bio field label
|
||||
About_00c0 = A propos
|
||||
# Display name for account management
|
||||
Accounts_e233 = Comptes
|
||||
# Column title for account management
|
||||
Accounts_f018 = Comptes
|
||||
# Button label to add a relay
|
||||
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = Ajouter un portefeuille pour continuer
|
||||
Add_account_1cfc = Ajouter un compte
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Ajouter un compte
|
||||
# Display name for adding account
|
||||
Add_Account_d715 = Ajouter un compte
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Ajouter une colonne Algo
|
||||
# Display name for adding column
|
||||
Add_Column_c6ff = Ajouter une colonne
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Ajouter une colonne
|
||||
# Display name for adding deck
|
||||
Add_Deck_6e5f = Ajouter un deck
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Ajouter un deck
|
||||
# Column title for adding external notifications column
|
||||
@@ -53,6 +45,8 @@ Algo_2452 = Algo
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Des fils algorithmiques pour faciliter la découverte de notes
|
||||
# Label for zap amount input field
|
||||
Amount_70f0 = Montant
|
||||
# Label for appearance settings section
|
||||
Appearance_4c7f = Apparence
|
||||
# Button to send message to Dave AI assistant
|
||||
Ask_b7f4 = Demander
|
||||
# Placeholder text for Dave AI input field
|
||||
@@ -61,18 +55,26 @@ Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
|
||||
Banner_52ef = Bannière
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = En bas
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Diffusion
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = Diffusion locale
|
||||
# Button label to cancel an action
|
||||
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
|
||||
Click_to_edit_0414 = Cliquer pour modifier
|
||||
# Display name for note composition
|
||||
Compose_Note_ad11 = Ecrire une note
|
||||
# Column title for note composition
|
||||
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
|
||||
Confirm_f8a6 = Confirmer
|
||||
# Status label for connected relay
|
||||
@@ -83,8 +85,6 @@ Connecting_6b7e = Connexion...
|
||||
Contact_List_f85a = Liste de contacts
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = Contacts
|
||||
# Timeline kind label for contact lists
|
||||
Contacts_8b98 = Contacts
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = Contacts (dernières notes)
|
||||
# Button label to copy logs
|
||||
@@ -119,14 +119,12 @@ Create_Account_6994 = Créer un compte
|
||||
Create_Deck_16b7 = Créer un deck
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Personnaliser
|
||||
# Display name for custom timelines
|
||||
Custom_cb4f = Personnaliser
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Personnaliser le montant du Zap
|
||||
# Display name for zap customization
|
||||
Customize_Zap_Amount_ed29 = Personnaliser le montant du Zap
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Assistance Damus
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Sombre
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Nom du deck
|
||||
# Label for decks section in side panel
|
||||
@@ -147,14 +145,10 @@ Display_name_f9d9 = Nom d'utilisateur
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Modifier le deck
|
||||
# Display name for editing deck
|
||||
Edit_Deck_c9ba = Modifier le deck
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Modifier le deck
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = Modifier le profil
|
||||
# Display name for profile editing
|
||||
Edit_Profile_6699 = Modifier le profil
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = Modifier le profil
|
||||
# Placeholder for hashtag input field
|
||||
@@ -169,18 +163,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.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Trouver un utilisateur
|
||||
# Timeline kind label for hashtag feeds
|
||||
Hashtag_a0ab = Hashtag
|
||||
# Display name for hashtag feeds
|
||||
Hashtags_617e = Hashtags
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Display name for home feed
|
||||
Home_3efc = Accueil
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = Masquer
|
||||
# Title for Home column
|
||||
Home_8c19 = Accueil
|
||||
# Label for deck icon selection
|
||||
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
|
||||
Individual_b776 = Individuel
|
||||
# Error message for invalid zap amount
|
||||
@@ -201,12 +193,12 @@ k_50K_c2dc = 50K
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
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
|
||||
Last_Note_per_User_17ad = Dernière note par utilisateur
|
||||
# Timeline kind label for last notes per pubkey
|
||||
Last_Notes_aefe = Dernières notes
|
||||
# Display name for last notes per contact
|
||||
Last_Per_Pubkey__Contact_33ce = Dernière par Pubkey (Contact)
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = Clair
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
|
||||
# Login page title
|
||||
@@ -239,10 +231,6 @@ Notes_60d2 = Notes
|
||||
Notes___Replies_1ec2 = Notes & Réponses
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notes & Réponses
|
||||
# Timeline kind label for notifications
|
||||
Notifications_6228 = Notifications
|
||||
# Display name for notifications
|
||||
Notifications_8029 = Notifications
|
||||
# Column title for notifications
|
||||
Notifications_d673 = Notifications
|
||||
# Title for notifications column
|
||||
@@ -253,6 +241,8 @@ now_2181 = maintenant
|
||||
Open_Email_25e9 = Ouvrir Email
|
||||
# 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
|
||||
# Label for others settings section
|
||||
Others_7267 = Autres
|
||||
# Placeholder text for NWC URI input
|
||||
Paste_your_NWC_URI_here_b471 = Collez ici votre NWC URI...
|
||||
# Error message for missing deck name
|
||||
@@ -265,30 +255,20 @@ Please_select_an_icon_655b = Veuillez choisir une icône.
|
||||
Post_now_8a49 = Publier maintenant
|
||||
# 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 = Cliquez sur le bouton ci-dessous pour copier vos données les plus récentes dans le presse-papiers de votre système. Collez-les ensuite dans votre courrier électronique.
|
||||
# Display name for user profiles
|
||||
Profile_2478 = Profil
|
||||
# Timeline kind label for user profiles
|
||||
Profile_9027 = Profil
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Photo de profil
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citation
|
||||
# Display name for quote composition
|
||||
Quote_a38e = Citation
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = Citation d'une note inconnue
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = En lecture seule
|
||||
# Display name for relay management
|
||||
Relays_7335 = Relais
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relais
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relais
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Répondre
|
||||
# Display name for reply composition
|
||||
Reply_b40f = Répondre
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = Répondre à cette note
|
||||
# Error message when reply note cannot be found
|
||||
@@ -309,6 +289,8 @@ replying_to_a_note_e0bc = répondre à une note
|
||||
Repost_this_note_8e56 = Republier cette note
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Republier
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Réinitialiser
|
||||
# Heading for support section
|
||||
Running_into_a_bug_1796 = Vous rencontrez un problème ?
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
@@ -319,12 +301,6 @@ sats_e5ec = sats
|
||||
Save_6f7c = Enregistrer
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = Enregistrer les modifications
|
||||
# Display name for search results
|
||||
Search_0aa0 = Recherche
|
||||
# Display name for search page
|
||||
Search_4503 = Rechercher
|
||||
# Timeline kind label for search results
|
||||
Search_a0b8 = Recherche
|
||||
# Column title for search page
|
||||
Search_c573 = Rechercher
|
||||
# Placeholder for search notes input field
|
||||
@@ -337,6 +313,10 @@ See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
|
||||
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Envoyer
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Paramètres
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = Afficher le client source
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
|
||||
# Button label to sign out of account
|
||||
@@ -363,12 +343,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour
|
||||
Step_1_8656 = Etape 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Etape 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Stockage
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
|
||||
# Display name for support page
|
||||
Support_a4b4 = Assistance
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Passer en mode sombre
|
||||
# Hover text for light mode toggle button
|
||||
@@ -377,18 +357,14 @@ Switch_to_light_mode_72ce = Passer en mode clair
|
||||
Tap_to_Load_4b05 = Appuyer pour charger
|
||||
# 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 !
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Thème :
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Fil
|
||||
# Display name for thread view
|
||||
Thread_9957 = Fil
|
||||
# Link text for thread references
|
||||
thread_ad1f = fil
|
||||
# Generic timeline kind label
|
||||
Timeline_b0fc = Chronologie
|
||||
# Timeline kind label for universe feed
|
||||
Universe_0a3e = Universel
|
||||
# Display name for universe feed
|
||||
Universe_d47e = Universel
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = En haut
|
||||
# Title for universe column
|
||||
Universe_e01e = Universel
|
||||
# Column title for universe feed
|
||||
@@ -399,10 +375,10 @@ 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
|
||||
# Profile username field label
|
||||
Username_daa7 = Nom d'utilisateur
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Voir le dossier :
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Portefeuille
|
||||
# Display name for wallet management
|
||||
Wallet_cdca = Portefeuille
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Nous recommandons des noms courts
|
||||
# Profile website field label
|
||||
@@ -419,6 +395,8 @@ Your_Notifications_080d = Vos notifications
|
||||
Zap_16b4 = Zap
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = Zap cette note
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = Niveau de zoom :
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
# 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
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = Abaixo
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Encaminhar
|
||||
# Broadcast the note only to local network relays
|
||||
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
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = #
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = Ocultar
|
||||
# Title for Home column
|
||||
Home_8c19 = Início
|
||||
# Label for deck icon selection
|
||||
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
|
||||
# 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 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 label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
Settings_7a4f = Configurações
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = Mostrar cliente de origem
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
|
||||
# Button label to sign out of account
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = Topo
|
||||
# Title for universe column
|
||||
Universe_e01e = Universo
|
||||
# Column title for universe feed
|
||||
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 }'
|
||||
}
|
||||
@@ -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 = เพิ่ม Deck
|
||||
# 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 = ถามเดฟได้ทุกเรื่อง...
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = ภาพปก
|
||||
# Beta version label
|
||||
BETA_8e5d = เบต้า
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = ด้านล่าง
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = เผยแพร่
|
||||
# Broadcast the note only to local network relays
|
||||
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 = คัดลอก Pubkey
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = คัดลอกข้อความ
|
||||
# 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 }mo
|
||||
# Relative time in seconds
|
||||
count_s_aa26 = { $count }s
|
||||
# Relative time in weeks
|
||||
count_w_7468 = { $count }w
|
||||
# Relative time in years
|
||||
count_y_9408 = { $count }y
|
||||
# Button to create a new account
|
||||
Create_Account_6994 = สร้างบัญชี
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = สร้าง Deck
|
||||
# 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 = ชื่อ Deck
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = DECKS
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = ยอด Zap เริ่มต้น
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Deck หลัก
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = ลบ Deck
|
||||
# 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 = แก้ไข Deck
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = แก้ไข Deck
|
||||
# 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 = ค้นหาผู้ใช้
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = แฮชแท็ก
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = ซ่อน
|
||||
# Title for Home column
|
||||
Home_8c19 = หน้าแรก
|
||||
# Label for deck icon selection
|
||||
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 = ที่อยู่ Lightning Network (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 = Deck ของฉัน
|
||||
# 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 = เมื่อสักครู่
|
||||
# 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 = กรุณาตั้งชื่อ Deck
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาตั้งชื่อ Deck และเลือกไอคอน
|
||||
# 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 = ตอบกลับโน้ต { $note } ของ { $user } ในเธรด { $thread } ของ { $thread_user }
|
||||
# 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 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 = ท่องจักรวาล Nostr ทั้งหมด
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = ส่ง
|
||||
# Column title for app settings
|
||||
Settings_7a4f = การตั้งค่า
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = แสดงไคลเอนต์ต้นทาง
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
|
||||
# Button label to sign out of account
|
||||
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 = การแจ้งเตือนของผู้อื่น
|
||||
# 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 = ติดตามโน้ตของผู้อื่น
|
||||
# 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 = ช่วงทดลองใช้ผู้ช่วย AI 'Dave Nostr' ได้สิ้นสุดลงแล้ว :( ขอบคุณที่ร่วมทดสอบ! Dave ที่รองรับการ Zap กำลังจะมาเร็วๆ นี้!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = ธีม:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = เธรด
|
||||
# Link text for thread references
|
||||
thread_ad1f = เธรด
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = ด้านบน
|
||||
# Title for universe column
|
||||
Universe_e01e = จักรวาล
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = จักรวาล
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" ที่ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
|
||||
# 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 = เว็บไซต์
|
||||
# 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 } รายการ
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
# Profile about/bio field label
|
||||
About_00c0 = 关于
|
||||
# Display name for account management
|
||||
Accounts_e233 = 帐户
|
||||
# Column title for account management
|
||||
Accounts_f018 = 帐户
|
||||
# Button label to add a relay
|
||||
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = 添加钱包以继续
|
||||
Add_account_1cfc = 添加帐户
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = 添加帐户
|
||||
# Display name for adding account
|
||||
Add_Account_d715 = 添加帐户
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = 添加算法列
|
||||
# Display name for adding column
|
||||
Add_Column_c6ff = 添加列
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = 添加列
|
||||
# Display name for adding deck
|
||||
Add_Deck_6e5f = 添加仪表板
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = 添加仪表板
|
||||
# Column title for adding external notifications column
|
||||
@@ -53,6 +45,8 @@ Algo_2452 = 算法
|
||||
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
|
||||
@@ -61,18 +55,26 @@ Ask_dave_anything_33d1 = 向 Dave 提问任何问题…
|
||||
Banner_52ef = 横幅
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = 底部
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = 广播
|
||||
# Broadcast the note only to local network relays
|
||||
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 = 点击以编辑
|
||||
# Display name for note composition
|
||||
Compose_Note_ad11 = 撰写笔记
|
||||
# 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
|
||||
@@ -83,8 +85,6 @@ Connecting_6b7e = 正在连接...
|
||||
Contact_List_f85a = 联系人列表
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = 联系人
|
||||
# Timeline kind label for contact lists
|
||||
Contacts_8b98 = 联系人
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = 联系人(最新笔记)
|
||||
# Button label to copy logs
|
||||
@@ -119,14 +119,12 @@ Create_Account_6994 = 创建帐户
|
||||
Create_Deck_16b7 = 创建仪表板
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = 自定义
|
||||
# Display name for custom timelines
|
||||
Custom_cb4f = 自定义
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = 自定义打闪金额
|
||||
# Display name for zap customization
|
||||
Customize_Zap_Amount_ed29 = 自定义打闪金额
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = 达摩支持
|
||||
# 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
|
||||
@@ -147,14 +145,10 @@ Display_name_f9d9 = 显示名称
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" 将用于身份识别
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = 编辑仪表板
|
||||
# Display name for editing deck
|
||||
Edit_Deck_c9ba = 编辑仪表板
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = 编辑仪表板
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = 编辑个人档案
|
||||
# Display name for profile editing
|
||||
Edit_Profile_6699 = 编辑个人档案
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = 编辑个人档案
|
||||
# Placeholder for hashtag input field
|
||||
@@ -169,18 +163,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)。 你必须输入你的私钥才能发帖、回复等等。
|
||||
# Label for find user button
|
||||
Find_User_bd12 = 查找用户
|
||||
# Timeline kind label for hashtag feeds
|
||||
Hashtag_a0ab = 标签
|
||||
# Display name for hashtag feeds
|
||||
Hashtags_617e = 标签
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = 标签
|
||||
# Display name for home feed
|
||||
Home_3efc = 主页
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = 隐藏
|
||||
# Title for Home column
|
||||
Home_8c19 = 主页
|
||||
# Label for deck icon selection
|
||||
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
|
||||
@@ -201,12 +193,12 @@ k_50K_c2dc = 5万
|
||||
k_5K_f7e6 = 5千
|
||||
# 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 = 每个用户的最新笔记
|
||||
# Timeline kind label for last notes per pubkey
|
||||
Last_Notes_aefe = 最新笔记
|
||||
# Display name for last notes per contact
|
||||
Last_Per_Pubkey__Contact_33ce = 每个公钥(联系人)的最新笔记
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = 亮色
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = 闪电网络地址(lud16)
|
||||
# Login page title
|
||||
@@ -239,10 +231,6 @@ Notes_60d2 = 笔记
|
||||
Notes___Replies_1ec2 = 笔记和回复
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = 笔记和回复
|
||||
# Timeline kind label for notifications
|
||||
Notifications_6228 = 通知
|
||||
# Display name for notifications
|
||||
Notifications_8029 = 通知
|
||||
# Column title for notifications
|
||||
Notifications_d673 = 通知
|
||||
# Title for notifications column
|
||||
@@ -253,6 +241,8 @@ now_2181 = 刚刚
|
||||
Open_Email_25e9 = 打开电子邮箱
|
||||
# Instruction to open email client
|
||||
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
|
||||
Paste_your_NWC_URI_here_b471 = 在此粘贴你的 NWC URI...
|
||||
# Error message for missing deck name
|
||||
@@ -265,30 +255,20 @@ Please_select_an_icon_655b = 请选择一个图标。
|
||||
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 = 请按下面的按钮将你最近的日志复制到系统剪贴板,然后将其粘贴到你的电子邮件。
|
||||
# Display name for user profiles
|
||||
Profile_2478 = 个人资料
|
||||
# Timeline kind label for user profiles
|
||||
Profile_9027 = 个人资料
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = 头像图片
|
||||
# Column title for quote composition
|
||||
Quote_475c = 引用
|
||||
# Display name for quote composition
|
||||
Quote_a38e = 引用
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = 引用未知笔记
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = 只读
|
||||
# Display name for relay management
|
||||
Relays_7335 = 中继器
|
||||
# Column title for relay management
|
||||
Relays_9d89 = 中继器
|
||||
# Label for relay list section
|
||||
Relays_ad5e = 中继器
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = 回复
|
||||
# Display name for reply composition
|
||||
Reply_b40f = 回复
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = 回复此笔记
|
||||
# Error message when reply note cannot be found
|
||||
@@ -309,6 +289,8 @@ replying_to_a_note_e0bc = 正在回复笔记
|
||||
Repost_this_note_8e56 = 转发此笔记
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = 已转发
|
||||
# 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
|
||||
@@ -319,12 +301,6 @@ sats_e5ec = 聪
|
||||
Save_6f7c = 保存
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = 保存变更
|
||||
# Display name for search results
|
||||
Search_0aa0 = 搜索
|
||||
# Display name for search page
|
||||
Search_4503 = 搜索
|
||||
# Timeline kind label for search results
|
||||
Search_a0b8 = 搜索
|
||||
# Column title for search page
|
||||
Search_c573 = 搜索
|
||||
# Placeholder for search notes input field
|
||||
@@ -337,6 +313,10 @@ See_notes_from_your_contacts_ac16 = 查看来自你的联系人的笔记
|
||||
See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = 发送
|
||||
# Column title for app settings
|
||||
Settings_7a4f = 设置
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = 显示来源客户端
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
|
||||
# Button label to sign out of account
|
||||
@@ -363,12 +343,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 获取你的通知
|
||||
Step_1_8656 = 第一步
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = 第二步
|
||||
# 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 = 订阅某人的笔记
|
||||
# Display name for support page
|
||||
Support_a4b4 = 获取帮助
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = 切换到暗色模式
|
||||
# Hover text for light mode toggle button
|
||||
@@ -377,18 +357,14 @@ Switch_to_light_mode_72ce = 切换到亮色模式
|
||||
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 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = 主题:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = 帖子
|
||||
# Display name for thread view
|
||||
Thread_9957 = 帖子
|
||||
# Link text for thread references
|
||||
thread_ad1f = 帖子
|
||||
# Generic timeline kind label
|
||||
Timeline_b0fc = 时间线
|
||||
# Timeline kind label for universe feed
|
||||
Universe_0a3e = 宇宙
|
||||
# Display name for universe feed
|
||||
Universe_d47e = 宇宙
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = 顶部
|
||||
# Title for universe column
|
||||
Universe_e01e = 宇宙
|
||||
# Column title for universe feed
|
||||
@@ -399,10 +375,10 @@ Use_this_wallet_for_the_current_account_only_61dc = 此钱包仅限用于当前
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 于 "{ $domain }" 将被用于身份识别
|
||||
# Profile username field label
|
||||
Username_daa7 = 用户名
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = 查看文件夹:
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = 钱包
|
||||
# Display name for wallet management
|
||||
Wallet_cdca = 钱包
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = 我们推荐使用简短的名称
|
||||
# Profile website field label
|
||||
@@ -419,6 +395,8 @@ Your_Notifications_080d = 你的通知
|
||||
Zap_16b4 = 打闪
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = 打闪此笔记
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = 缩放大小:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
# Profile about/bio field label
|
||||
About_00c0 = 關於
|
||||
# Display name for account management
|
||||
Accounts_e233 = 帳戶
|
||||
# Column title for account management
|
||||
Accounts_f018 = 帳戶
|
||||
# Button label to add a relay
|
||||
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = 添加錢包以繼續
|
||||
Add_account_1cfc = 新增帳戶
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = 新增帳戶
|
||||
# Display name for adding account
|
||||
Add_Account_d715 = 新增帳戶
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = 添加算法列
|
||||
# Display name for adding column
|
||||
Add_Column_c6ff = 添加列
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = 添加列
|
||||
# Display name for adding deck
|
||||
Add_Deck_6e5f = 添加儀表板
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = 添加儀表板
|
||||
# Column title for adding external notifications column
|
||||
@@ -53,6 +45,8 @@ Algo_2452 = 算法
|
||||
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
|
||||
@@ -61,18 +55,26 @@ Ask_dave_anything_33d1 = 向 Dave 提問任何問題...
|
||||
Banner_52ef = 橫幅
|
||||
# Beta version label
|
||||
BETA_8e5d = 測試版
|
||||
# Option in settings section to show the source client label at the bottom of the note
|
||||
Bottom_33c8 = 底部
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = 廣播
|
||||
# Broadcast the note only to local network relays
|
||||
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 = 點擊編輯
|
||||
# Display name for note composition
|
||||
Compose_Note_ad11 = 撰寫筆記
|
||||
# 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
|
||||
@@ -83,8 +85,6 @@ Connecting_6b7e = 正在連接 ...
|
||||
Contact_List_f85a = 聯絡人列表
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = 聯絡人
|
||||
# Timeline kind label for contact lists
|
||||
Contacts_8b98 = 聯絡人
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = 聯絡人(最新筆記)
|
||||
# Button label to copy logs
|
||||
@@ -119,14 +119,12 @@ Create_Account_6994 = 創建帳戶
|
||||
Create_Deck_16b7 = 創建儀表板
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = 自訂
|
||||
# Display name for custom timelines
|
||||
Custom_cb4f = 自訂
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = 自訂打閃金額
|
||||
# Display name for zap customization
|
||||
Customize_Zap_Amount_ed29 = 自訂打閃金額
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = 達摩支持
|
||||
# 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
|
||||
@@ -147,14 +145,10 @@ Display_name_f9d9 = 顯示名稱
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" 將用於身份識別
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = 編輯儀表板
|
||||
# Display name for editing deck
|
||||
Edit_Deck_c9ba = 編輯儀表板
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = 編輯儀表板
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = 編輯個人檔案
|
||||
# Display name for profile editing
|
||||
Edit_Profile_6699 = 編輯個人檔案
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = 編輯個人檔案
|
||||
# Placeholder for hashtag input field
|
||||
@@ -169,18 +163,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)。你必須輸入你的私鑰才能發貼、回覆等等。
|
||||
# Label for find user button
|
||||
Find_User_bd12 = 查找用戶
|
||||
# Timeline kind label for hashtag feeds
|
||||
Hashtag_a0ab = 標籤
|
||||
# Display name for hashtag feeds
|
||||
Hashtags_617e = 標籤
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = 標籤
|
||||
# Display name for home feed
|
||||
Home_3efc = 主頁
|
||||
# Option in settings section to hide the source client label in note display
|
||||
Hide_281d = 隱藏
|
||||
# Title for Home column
|
||||
Home_8c19 = 主頁
|
||||
# Label for deck icon selection
|
||||
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
|
||||
@@ -201,12 +193,12 @@ k_50K_c2dc = 5萬
|
||||
k_5K_f7e6 = 5千
|
||||
# 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 = 每個用戶的最新筆記
|
||||
# Timeline kind label for last notes per pubkey
|
||||
Last_Notes_aefe = 最新筆記
|
||||
# Display name for last notes per contact
|
||||
Last_Per_Pubkey__Contact_33ce = 每個公鑰(聯繫人)的最新筆記
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = 亮色
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = 閃電網絡地址(lud16)
|
||||
# Login page title
|
||||
@@ -239,10 +231,6 @@ Notes_60d2 = 筆記
|
||||
Notes___Replies_1ec2 = 筆記和回覆
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = 筆記和回覆
|
||||
# Timeline kind label for notifications
|
||||
Notifications_6228 = 通知
|
||||
# Display name for notifications
|
||||
Notifications_8029 = 通知
|
||||
# Column title for notifications
|
||||
Notifications_d673 = 通知
|
||||
# Title for notifications column
|
||||
@@ -253,6 +241,8 @@ now_2181 = 剛剛
|
||||
Open_Email_25e9 = 打開電子郵箱
|
||||
# Instruction to open email client
|
||||
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
|
||||
Paste_your_NWC_URI_here_b471 = 在此貼上你的 NWC URI...
|
||||
# Error message for missing deck name
|
||||
@@ -265,30 +255,20 @@ Please_select_an_icon_655b = 請選擇一個圖標。
|
||||
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 = 請按下面的按鈕將你最近的日誌複製到剪貼板,然後將其粘貼到你的電子郵件。
|
||||
# Display name for user profiles
|
||||
Profile_2478 = 個人檔案
|
||||
# Timeline kind label for user profiles
|
||||
Profile_9027 = 個人檔案
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = 頭像圖片
|
||||
# Column title for quote composition
|
||||
Quote_475c = 引用
|
||||
# Display name for quote composition
|
||||
Quote_a38e = 引用
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = 引用未知筆記
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = 只讀
|
||||
# Display name for relay management
|
||||
Relays_7335 = 中繼器
|
||||
# Column title for relay management
|
||||
Relays_9d89 = 中繼器
|
||||
# Label for relay list section
|
||||
Relays_ad5e = 中繼器
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = 回覆
|
||||
# Display name for reply composition
|
||||
Reply_b40f = 回覆
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = 回覆此筆記
|
||||
# Error message when reply note cannot be found
|
||||
@@ -309,6 +289,8 @@ replying_to_a_note_e0bc = 正在回覆筆記
|
||||
Repost_this_note_8e56 = 轉發此筆記
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = 已轉發
|
||||
# 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
|
||||
@@ -319,12 +301,6 @@ sats_e5ec = 聰
|
||||
Save_6f7c = 保存
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = 保存變更
|
||||
# Display name for search results
|
||||
Search_0aa0 = 搜索
|
||||
# Display name for search page
|
||||
Search_4503 = 搜索
|
||||
# Timeline kind label for search results
|
||||
Search_a0b8 = 搜索
|
||||
# Column title for search page
|
||||
Search_c573 = 搜索
|
||||
# Placeholder for search notes input field
|
||||
@@ -337,6 +313,10 @@ See_notes_from_your_contacts_ac16 = 查看來自你的聯繫人的筆記
|
||||
See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = 發送
|
||||
# Column title for app settings
|
||||
Settings_7a4f = 設置
|
||||
# Label for Show source client, others settings section
|
||||
Show_source_client_9e31 = 顯示來源客戶端
|
||||
# Description for last note per user column
|
||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
|
||||
# Button label to sign out of account
|
||||
@@ -363,12 +343,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 獲取你的通知
|
||||
Step_1_8656 = 第一步
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = 第二步
|
||||
# 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 = 訂閱某人的筆記
|
||||
# Display name for support page
|
||||
Support_a4b4 = 獲取幫助
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = 切換到暗色模式
|
||||
# Hover text for light mode toggle button
|
||||
@@ -377,18 +357,14 @@ Switch_to_light_mode_72ce = 切換到亮色模式
|
||||
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 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = 主題:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = 串文
|
||||
# Display name for thread view
|
||||
Thread_9957 = 串文
|
||||
# Link text for thread references
|
||||
thread_ad1f = 串文
|
||||
# Generic timeline kind label
|
||||
Timeline_b0fc = 時間線
|
||||
# Timeline kind label for universe feed
|
||||
Universe_0a3e = 宇宙
|
||||
# Display name for universe feed
|
||||
Universe_d47e = 宇宙
|
||||
# Option in settings section to show the source client label at the top of the note
|
||||
Top_6aeb = 頂部
|
||||
# Title for universe column
|
||||
Universe_e01e = 宇宙
|
||||
# Column title for universe feed
|
||||
@@ -399,10 +375,10 @@ Use_this_wallet_for_the_current_account_only_61dc = 此錢包僅限用於當前
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 於 "{ $domain }" 將被用於身份識別
|
||||
# Profile username field label
|
||||
Username_daa7 = 用戶名
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = 查看文件夾:
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = 錢包
|
||||
# Display name for wallet management
|
||||
Wallet_cdca = 錢包
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = 我們推薦使用簡短的名稱
|
||||
# Profile website field label
|
||||
@@ -419,6 +395,8 @@ Your_Notifications_080d = 你的通知
|
||||
Zap_16b4 = 打閃
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = 打閃此筆記
|
||||
# Label for zoom level, Appearance settings section
|
||||
Zoom_Level_29a8 = 縮放大小:
|
||||
|
||||
# Pluralized strings
|
||||
|
||||
|
||||
@@ -9,11 +9,13 @@ nostrdb = { workspace = true }
|
||||
jni = { workspace = true }
|
||||
url = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
blurhash = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
enostr = { workspace = true }
|
||||
nostr = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
image = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
@@ -43,8 +45,10 @@ fluent = { workspace = true }
|
||||
fluent-resmgr = { workspace = true }
|
||||
fluent-langneg = { workspace = true }
|
||||
unic-langid = { workspace = true }
|
||||
sys-locale = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
md5 = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
regex = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -207,6 +207,10 @@ impl Accounts {
|
||||
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> {
|
||||
self.cache.selected_mut().wallet.as_mut()
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub enum ContactState {
|
||||
Received {
|
||||
contacts: HashSet<Pubkey>,
|
||||
note_key: NoteKey,
|
||||
timestamp: u64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ impl Contacts {
|
||||
ContactState::Received {
|
||||
contacts,
|
||||
note_key: _,
|
||||
timestamp: _,
|
||||
} => {
|
||||
if contacts.contains(other_pubkey) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -96,11 +110,17 @@ fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
|
||||
*state = ContactState::Received {
|
||||
contacts: get_contacts_owned(note),
|
||||
note_key: key,
|
||||
timestamp: note.created_at(),
|
||||
};
|
||||
}
|
||||
ContactState::Received { contacts, note_key } => {
|
||||
ContactState::Received {
|
||||
contacts,
|
||||
note_key,
|
||||
timestamp,
|
||||
} => {
|
||||
update_contacts(contacts, note);
|
||||
*note_key = key;
|
||||
*timestamp = note.created_at();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::{AccountData, RelaySpec};
|
||||
use enostr::{Keypair, Pubkey, RelayPool};
|
||||
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
|
||||
use tracing::{debug, error, info};
|
||||
use url::Url;
|
||||
|
||||
use crate::{AccountData, RelaySpec};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AccountRelayData {
|
||||
pub filter: Filter,
|
||||
|
||||
+79
-21
@@ -1,13 +1,14 @@
|
||||
use crate::account::FALLBACK_PUBKEY;
|
||||
use crate::i18n::Localization;
|
||||
use crate::persist::{AppSizeHandler, ZoomHandler};
|
||||
use crate::persist::{AppSizeHandler, SettingsHandler};
|
||||
use crate::wallet::GlobalWallet;
|
||||
use crate::zaps::Zaps;
|
||||
use crate::Error;
|
||||
use crate::JobPool;
|
||||
use crate::NotedeckOptions;
|
||||
use crate::{
|
||||
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
|
||||
UnknownIds,
|
||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
|
||||
};
|
||||
use egui::Margin;
|
||||
use egui::ThemePreference;
|
||||
@@ -19,6 +20,7 @@ use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use tracing::{error, info};
|
||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||
|
||||
pub enum AppAction {
|
||||
Note(NoteAction),
|
||||
@@ -40,9 +42,8 @@ pub struct Notedeck {
|
||||
global_wallet: GlobalWallet,
|
||||
path: DataPath,
|
||||
args: Args,
|
||||
theme: ThemeHandler,
|
||||
settings: SettingsHandler,
|
||||
app: Option<Rc<RefCell<dyn App>>>,
|
||||
zoom: ZoomHandler,
|
||||
app_size: AppSizeHandler,
|
||||
unrecognized_args: BTreeSet<String>,
|
||||
clipboard: Clipboard,
|
||||
@@ -99,10 +100,18 @@ impl eframe::App for Notedeck {
|
||||
|
||||
render_notedeck(self, ctx);
|
||||
|
||||
self.zoom.try_save_zoom_factor(ctx);
|
||||
self.settings.update_batch(|settings| {
|
||||
settings.zoom_factor = ctx.zoom_factor();
|
||||
settings.locale = self.i18n.get_current_locale().to_string();
|
||||
settings.theme = if ctx.style().visuals.dark_mode {
|
||||
ThemePreference::Dark
|
||||
} else {
|
||||
ThemePreference::Light
|
||||
};
|
||||
});
|
||||
self.app_size.try_save_app_size(ctx);
|
||||
|
||||
if self.args.relay_debug {
|
||||
if self.args.options.contains(NotedeckOptions::RelayDebug) {
|
||||
if self.pool.debug.is_none() {
|
||||
self.pool.use_debug();
|
||||
}
|
||||
@@ -159,10 +168,11 @@ impl Notedeck {
|
||||
1024usize * 1024usize * 1024usize * 1024usize
|
||||
};
|
||||
|
||||
let theme = ThemeHandler::new(&path);
|
||||
let settings = SettingsHandler::new(&path).load();
|
||||
|
||||
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
|
||||
|
||||
let keystore = if parsed_args.use_keystore {
|
||||
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
|
||||
let keys_path = path.path(DataPathType::Keys);
|
||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||
Some(AccountStorage::new(
|
||||
@@ -213,12 +223,8 @@ impl Notedeck {
|
||||
|
||||
let img_cache = Images::new(img_cache_dir);
|
||||
let note_cache = NoteCache::default();
|
||||
let zoom = ZoomHandler::new(&path);
|
||||
let app_size = AppSizeHandler::new(&path);
|
||||
|
||||
if let Some(z) = zoom.get_zoom_factor() {
|
||||
ctx.set_zoom_factor(z);
|
||||
}
|
||||
let app_size = AppSizeHandler::new(&path);
|
||||
|
||||
// migrate
|
||||
if let Err(e) = img_cache.migrate_v0() {
|
||||
@@ -231,15 +237,22 @@ impl Notedeck {
|
||||
|
||||
// Initialize localization
|
||||
let mut i18n = Localization::new();
|
||||
|
||||
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
|
||||
settings.locale().parse();
|
||||
|
||||
if setting_locale.is_ok() {
|
||||
if let Err(err) = i18n.set_locale(setting_locale.unwrap()) {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(locale) = &parsed_args.locale {
|
||||
if let Err(err) = i18n.set_locale(locale.to_owned()) {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize global i18n context
|
||||
//crate::i18n::init_global_i18n(i18n.clone());
|
||||
|
||||
Self {
|
||||
ndb,
|
||||
img_cache,
|
||||
@@ -250,9 +263,8 @@ impl Notedeck {
|
||||
global_wallet,
|
||||
path: path.clone(),
|
||||
args: parsed_args,
|
||||
theme,
|
||||
settings,
|
||||
app: None,
|
||||
zoom,
|
||||
app_size,
|
||||
unrecognized_args,
|
||||
frame_history: FrameHistory::default(),
|
||||
@@ -263,6 +275,44 @@ impl Notedeck {
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup egui context
|
||||
pub fn setup(&self, ctx: &egui::Context) {
|
||||
// Initialize global i18n context
|
||||
//crate::i18n::init_global_i18n(i18n.clone());
|
||||
crate::setup::setup_egui_context(
|
||||
ctx,
|
||||
self.args.options,
|
||||
self.theme(),
|
||||
self.note_body_font_size(),
|
||||
self.zoom_factor(),
|
||||
);
|
||||
}
|
||||
|
||||
/// ensure we recognized all the arguments
|
||||
pub fn check_args(&self, other_app_args: &BTreeSet<String>) -> Result<(), Error> {
|
||||
let completely_unrecognized: Vec<String> = self
|
||||
.unrecognized_args()
|
||||
.intersection(other_app_args)
|
||||
.cloned()
|
||||
.collect();
|
||||
if !completely_unrecognized.is_empty() {
|
||||
let err = format!("Unrecognized arguments: {:?}", completely_unrecognized);
|
||||
tracing::error!("{}", &err);
|
||||
return Err(Error::Generic(err));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn options(&self) -> NotedeckOptions {
|
||||
self.args.options
|
||||
}
|
||||
|
||||
pub fn has_option(&self, option: NotedeckOptions) -> bool {
|
||||
self.options().contains(option)
|
||||
}
|
||||
|
||||
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
|
||||
self.set_app(app);
|
||||
self
|
||||
@@ -279,7 +329,7 @@ impl Notedeck {
|
||||
global_wallet: &mut self.global_wallet,
|
||||
path: &self.path,
|
||||
args: &self.args,
|
||||
theme: &mut self.theme,
|
||||
settings: &mut self.settings,
|
||||
clipboard: &mut self.clipboard,
|
||||
zaps: &mut self.zaps,
|
||||
frame_history: &mut self.frame_history,
|
||||
@@ -297,7 +347,15 @@ impl Notedeck {
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> ThemePreference {
|
||||
self.theme.load()
|
||||
self.settings.theme()
|
||||
}
|
||||
|
||||
pub fn note_body_font_size(&self) -> f32 {
|
||||
self.settings.note_body_font_size()
|
||||
}
|
||||
|
||||
pub fn zoom_factor(&self) -> f32 {
|
||||
self.settings.zoom_factor()
|
||||
}
|
||||
|
||||
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
||||
|
||||
+14
-26
@@ -1,23 +1,15 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::NotedeckOptions;
|
||||
use enostr::{Keypair, Pubkey, SecretKey};
|
||||
use tracing::error;
|
||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||
|
||||
pub struct Args {
|
||||
pub relays: Vec<String>,
|
||||
pub is_mobile: Option<bool>,
|
||||
pub locale: Option<LanguageIdentifier>,
|
||||
pub show_note_client: bool,
|
||||
pub keys: Vec<Keypair>,
|
||||
pub light: bool,
|
||||
pub debug: bool,
|
||||
pub relay_debug: bool,
|
||||
|
||||
/// Enable when running tests so we don't panic on app startup
|
||||
pub tests: bool,
|
||||
|
||||
pub use_keystore: bool,
|
||||
pub options: NotedeckOptions,
|
||||
pub dbpath: Option<String>,
|
||||
pub datapath: Option<String>,
|
||||
}
|
||||
@@ -28,14 +20,8 @@ impl Args {
|
||||
let mut unrecognized_args = BTreeSet::new();
|
||||
let mut res = Args {
|
||||
relays: vec![],
|
||||
is_mobile: None,
|
||||
keys: vec![],
|
||||
light: false,
|
||||
show_note_client: false,
|
||||
debug: false,
|
||||
relay_debug: false,
|
||||
tests: false,
|
||||
use_keystore: true,
|
||||
options: NotedeckOptions::default(),
|
||||
dbpath: None,
|
||||
datapath: None,
|
||||
locale: None,
|
||||
@@ -47,9 +33,9 @@ impl Args {
|
||||
let arg = &args[i];
|
||||
|
||||
if arg == "--mobile" {
|
||||
res.is_mobile = Some(true);
|
||||
res.options.set(NotedeckOptions::Mobile, true);
|
||||
} else if arg == "--light" {
|
||||
res.light = true;
|
||||
res.options.set(NotedeckOptions::LightTheme, true);
|
||||
} else if arg == "--locale" {
|
||||
i += 1;
|
||||
let Some(locale) = args.get(i) else {
|
||||
@@ -68,11 +54,11 @@ impl Args {
|
||||
}
|
||||
}
|
||||
} else if arg == "--dark" {
|
||||
res.light = false;
|
||||
res.options.set(NotedeckOptions::LightTheme, false);
|
||||
} else if arg == "--debug" {
|
||||
res.debug = true;
|
||||
res.options.set(NotedeckOptions::Debug, true);
|
||||
} else if arg == "--testrunner" {
|
||||
res.tests = true;
|
||||
res.options.set(NotedeckOptions::Tests, true);
|
||||
} else if arg == "--pub" || arg == "--npub" {
|
||||
i += 1;
|
||||
let pubstr = if let Some(next_arg) = args.get(i) {
|
||||
@@ -135,11 +121,13 @@ impl Args {
|
||||
};
|
||||
res.relays.push(relay.clone());
|
||||
} else if arg == "--no-keystore" {
|
||||
res.use_keystore = false;
|
||||
res.options.set(NotedeckOptions::UseKeystore, true);
|
||||
} else if arg == "--relay-debug" {
|
||||
res.relay_debug = true;
|
||||
} else if arg == "--show-note-client" {
|
||||
res.show_note_client = true;
|
||||
res.options.set(NotedeckOptions::RelayDebug, true);
|
||||
} else if arg == "--show-client" {
|
||||
res.options.set(NotedeckOptions::ShowClient, true);
|
||||
} else if arg == "--notebook" {
|
||||
res.options.set(NotedeckOptions::FeatureNotebook, true);
|
||||
} else {
|
||||
unrecognized_args.insert(arg.clone());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
|
||||
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
|
||||
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
|
||||
UnknownIds,
|
||||
};
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
@@ -20,7 +20,7 @@ pub struct AppContext<'a> {
|
||||
pub global_wallet: &'a mut GlobalWallet,
|
||||
pub path: &'a DataPath,
|
||||
pub args: &'a Args,
|
||||
pub theme: &'a mut ThemeHandler,
|
||||
pub settings: &'a mut SettingsHandler,
|
||||
pub clipboard: &'a mut Clipboard,
|
||||
pub zaps: &'a mut Zaps,
|
||||
pub frame_history: &'a mut FrameHistory,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use crate::{ui, NotedeckTextStyle};
|
||||
use egui::FontData;
|
||||
use egui::FontDefinitions;
|
||||
use egui::FontTweak;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum NamedFontFamily {
|
||||
Medium,
|
||||
@@ -31,6 +36,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
||||
NotedeckTextStyle::Button => 13.0,
|
||||
NotedeckTextStyle::Small => 12.0,
|
||||
NotedeckTextStyle::Tiny => 10.0,
|
||||
NotedeckTextStyle::NoteBody => 16.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +52,7 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
||||
NotedeckTextStyle::Button => 13.0,
|
||||
NotedeckTextStyle::Small => 12.0,
|
||||
NotedeckTextStyle::Tiny => 10.0,
|
||||
NotedeckTextStyle::NoteBody => 13.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,3 +63,148 @@ pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32
|
||||
desktop_font_size(text_style)
|
||||
}
|
||||
}
|
||||
|
||||
// Use gossip's approach to font loading. This includes japanese fonts
|
||||
// for rending stuff from japanese users.
|
||||
pub fn setup_fonts(ctx: &egui::Context) {
|
||||
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
|
||||
let mut families = BTreeMap::new();
|
||||
|
||||
font_data.insert(
|
||||
"Onest".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestMedium".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestBold".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
/*
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||
);
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
*/
|
||||
|
||||
font_data.insert(
|
||||
"Inconsolata".to_owned(),
|
||||
Arc::new(
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/Inconsolata-Regular.ttf"
|
||||
))
|
||||
.tweak(FontTweak {
|
||||
scale: 1.22, // This font is smaller than DejaVuSans
|
||||
y_offset_factor: -0.18, // and too low
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansCJK".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansThai".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoSansThai-Regular.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
// Some good looking emojis. Use as first priority:
|
||||
font_data.insert(
|
||||
"NotoEmoji".to_owned(),
|
||||
Arc::new(
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoEmoji-Regular.ttf"
|
||||
))
|
||||
.tweak(FontTweak {
|
||||
scale: 1.1, // make them a touch larger
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let base_fonts = vec![
|
||||
"DejaVuSans".to_owned(),
|
||||
"NotoEmoji".to_owned(),
|
||||
"NotoSansCJK".to_owned(),
|
||||
"NotoSansThai".to_owned(),
|
||||
];
|
||||
|
||||
let mut proportional = vec!["Onest".to_owned()];
|
||||
proportional.extend(base_fonts.clone());
|
||||
|
||||
let mut medium = vec!["OnestMedium".to_owned()];
|
||||
medium.extend(base_fonts.clone());
|
||||
|
||||
let mut mono = vec!["Inconsolata".to_owned()];
|
||||
mono.extend(base_fonts.clone());
|
||||
|
||||
let mut bold = vec!["OnestBold".to_owned()];
|
||||
bold.extend(base_fonts.clone());
|
||||
|
||||
let emoji = vec!["NotoEmoji".to_owned()];
|
||||
|
||||
families.insert(egui::FontFamily::Proportional, proportional);
|
||||
families.insert(egui::FontFamily::Monospace, mono);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||
medium,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
bold,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
||||
emoji,
|
||||
);
|
||||
|
||||
tracing::debug!("fonts: {:?}", families);
|
||||
|
||||
let defs = FontDefinitions {
|
||||
font_data,
|
||||
families,
|
||||
};
|
||||
|
||||
ctx.set_fonts(defs);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,31 @@ use fluent::{FluentArgs, FluentBundle, FluentResource};
|
||||
use fluent_langneg::negotiate_languages;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use sys_locale;
|
||||
use unic_langid::{langid, LanguageIdentifier};
|
||||
|
||||
const EN_XA: LanguageIdentifier = langid!("en-XA");
|
||||
const EN_US: LanguageIdentifier = langid!("en-US");
|
||||
const EN_XA: LanguageIdentifier = langid!("en-XA");
|
||||
const DE: LanguageIdentifier = langid!("de");
|
||||
const FR: LanguageIdentifier = langid!("FR");
|
||||
const ZH_CN: LanguageIdentifier = langid!("ZH_CN");
|
||||
const ZH_TW: LanguageIdentifier = langid!("ZH_TW");
|
||||
const NUM_FTLS: usize = 6;
|
||||
const ES_419: LanguageIdentifier = langid!("es-419");
|
||||
const ES_ES: LanguageIdentifier = langid!("es-ES");
|
||||
const FR: LanguageIdentifier = langid!("fr");
|
||||
const PT_BR: LanguageIdentifier = langid!("pt-BR");
|
||||
const TH: LanguageIdentifier = langid!("th");
|
||||
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
|
||||
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
|
||||
const NUM_FTLS: usize = 10;
|
||||
|
||||
const 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 PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
|
||||
const TH_NATIVE_NAME: &str = "ภาษาไทย";
|
||||
const ZH_CN_NATIVE_NAME: &str = "简体中文";
|
||||
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
|
||||
|
||||
struct StaticBundle {
|
||||
identifier: LanguageIdentifier,
|
||||
@@ -31,10 +47,26 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
|
||||
identifier: DE,
|
||||
ftl: include_str!("../../../../assets/translations/de/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: ES_419,
|
||||
ftl: include_str!("../../../../assets/translations/es-419/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: ES_ES,
|
||||
ftl: include_str!("../../../../assets/translations/es-ES/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: FR,
|
||||
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: PT_BR,
|
||||
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: TH,
|
||||
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: ZH_CN,
|
||||
ftl: include_str!("../../../../assets/translations/zh-CN/main.ftl"),
|
||||
@@ -55,6 +87,8 @@ pub struct Localization {
|
||||
available_locales: Vec<LanguageIdentifier>,
|
||||
/// Fallback locale
|
||||
fallback_locale: LanguageIdentifier,
|
||||
/// Native names for locales
|
||||
locale_native_names: HashMap<LanguageIdentifier, String>,
|
||||
|
||||
/// Cached string results per locale (only for strings without arguments)
|
||||
string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
|
||||
@@ -68,24 +102,50 @@ pub struct Localization {
|
||||
|
||||
impl Default for Localization {
|
||||
fn default() -> Self {
|
||||
// Default to English (US)
|
||||
let default_locale = &EN_US;
|
||||
let fallback_locale = default_locale.to_owned();
|
||||
|
||||
// Build available locales list
|
||||
let available_locales = vec![
|
||||
EN_US.clone(),
|
||||
EN_XA.clone(),
|
||||
DE.clone(),
|
||||
ES_419.clone(),
|
||||
ES_ES.clone(),
|
||||
FR.clone(),
|
||||
PT_BR.clone(),
|
||||
TH.clone(),
|
||||
ZH_CN.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()),
|
||||
(PT_BR, PT_BR_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()),
|
||||
]);
|
||||
|
||||
// Detect system locale and find best match
|
||||
let current_locale = Self::negotiate_system_locale_with_preferences(&available_locales);
|
||||
|
||||
// Fallback locale is always EN_US
|
||||
let fallback_locale = EN_US.clone();
|
||||
|
||||
tracing::info!(
|
||||
"Localization initialized - Selected locale: {}, Fallback: {}",
|
||||
current_locale,
|
||||
fallback_locale
|
||||
);
|
||||
|
||||
Self {
|
||||
current_locale: default_locale.to_owned(),
|
||||
current_locale,
|
||||
available_locales,
|
||||
fallback_locale,
|
||||
locale_native_names,
|
||||
use_isolating: true,
|
||||
normalized_key_cache: HashMap::new(),
|
||||
string_cache: HashMap::new(),
|
||||
@@ -108,6 +168,150 @@ impl Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract just the language and region from locale string (e.g., "fr-FR-u-mu-celsius" -> "fr-FR")
|
||||
fn extract_language_region(locale_str: &str) -> String {
|
||||
// Split by '-' and analyze the parts
|
||||
let parts: Vec<&str> = locale_str.split('-').collect();
|
||||
|
||||
if parts.len() >= 2 {
|
||||
// Check if the second part looks like a region
|
||||
let second_part = parts[1];
|
||||
if (second_part.len() >= 2) {
|
||||
format!("{}-{}", parts[0], parts[1])
|
||||
} else {
|
||||
// Second part is not a region, probably an extension (e.g., "u", "t", "x")
|
||||
// Just return the language part
|
||||
parts[0].to_string()
|
||||
}
|
||||
} else {
|
||||
// Only one part, return as is
|
||||
locale_str.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Negotiate the best locale from all system preferences against available locales
|
||||
fn negotiate_system_locale_with_preferences(
|
||||
available_locales: &[LanguageIdentifier],
|
||||
) -> LanguageIdentifier {
|
||||
// Get all system preferred locales in descending order
|
||||
let mut system_locales: Vec<String> = sys_locale::get_locales().collect();
|
||||
if system_locales.is_empty() {
|
||||
tracing::info!("No system locales detected, using fallback: en-US");
|
||||
return EN_US.clone();
|
||||
}
|
||||
|
||||
tracing::info!("System preferred locales: {:?}", system_locales);
|
||||
|
||||
// If we only got one locale, it might be that the system only returns the primary locale
|
||||
// In this case, we can try to add common fallbacks based on the detected locale
|
||||
if system_locales.len() == 1 {
|
||||
let primary = &system_locales[0];
|
||||
|
||||
// Try to parse the primary locale, handling extensions
|
||||
let primary_lang = if let Ok(locale) = primary.parse::<LanguageIdentifier>() {
|
||||
locale.language.as_str().to_string()
|
||||
} else {
|
||||
// If parsing fails, try extracting language-region
|
||||
// let stripped = Self::extract_language_region(primary);
|
||||
// if let Ok(locale) = stripped.parse::<LanguageIdentifier>() {
|
||||
// locale.language.as_str().to_string()
|
||||
// } else {
|
||||
tracing::info!("Could not parse primary locale: {}", primary);
|
||||
"unknown".to_string()
|
||||
// }
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"Only one system locale detected: {} (language: {})",
|
||||
primary,
|
||||
primary_lang
|
||||
);
|
||||
|
||||
// Add common fallbacks for the detected language
|
||||
match primary_lang.as_str() {
|
||||
"uk" => {
|
||||
// For Ukrainian, add common fallbacks
|
||||
system_locales.push("es-ES".to_string());
|
||||
system_locales.push("en-US".to_string());
|
||||
tracing::info!("Added fallbacks for Ukrainian: {:?}", system_locales);
|
||||
}
|
||||
"es" => {
|
||||
// For Spanish, add English fallback
|
||||
system_locales.push("en-US".to_string());
|
||||
tracing::info!("Added fallback for Spanish: {:?}", system_locales);
|
||||
}
|
||||
_ => {
|
||||
// For other languages, add English fallback
|
||||
system_locales.push("en-US".to_string());
|
||||
tracing::info!("Added fallback for {}: {:?}", primary_lang, system_locales);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert system locale strings to LanguageIdentifiers, handling extensions
|
||||
let mut parsed_system_locales = Vec::new();
|
||||
for locale_str in system_locales {
|
||||
// Try to parse the locale string directly first
|
||||
if let Ok(locale) = locale_str.parse::<LanguageIdentifier>() {
|
||||
parsed_system_locales.push(locale);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If parsing fails, try extracting just language-region
|
||||
// let stripped_locale = Self::extract_language_region(&locale_str);
|
||||
// if let Ok(locale) = stripped_locale.parse::<LanguageIdentifier>() {
|
||||
// parsed_system_locales.push(locale);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
tracing::info!("Failed to parse locale string: {}", locale_str);
|
||||
}
|
||||
|
||||
if parsed_system_locales.is_empty() {
|
||||
tracing::info!("No valid system locales parsed, using fallback: en-US");
|
||||
return EN_US.clone();
|
||||
}
|
||||
|
||||
// First try exact matches with fluent_langneg
|
||||
let fallback = &EN_US;
|
||||
let negotiated = negotiate_languages(
|
||||
&parsed_system_locales,
|
||||
available_locales,
|
||||
Some(fallback),
|
||||
fluent_langneg::NegotiationStrategy::Filtering,
|
||||
);
|
||||
|
||||
if let Some(result) = negotiated.first() {
|
||||
tracing::info!(
|
||||
"Exact match found: {} from preferences: {:?}",
|
||||
result,
|
||||
parsed_system_locales
|
||||
);
|
||||
return (*result).clone();
|
||||
}
|
||||
|
||||
// If no exact match, try language-only fallbacks
|
||||
tracing::info!("No exact matches found, trying language-only fallbacks");
|
||||
for system_locale in &parsed_system_locales {
|
||||
let system_lang = system_locale.language.as_str();
|
||||
|
||||
// Look for any available locale with the same language
|
||||
for available_locale in available_locales {
|
||||
if available_locale.language.as_str() == system_lang {
|
||||
tracing::debug!(
|
||||
"Language match found: {} (system: {})",
|
||||
available_locale,
|
||||
system_locale
|
||||
);
|
||||
return available_locale.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("No language matches found, using fallback: en-US");
|
||||
EN_US.clone()
|
||||
}
|
||||
|
||||
/// Gets a localized string by its ID
|
||||
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
|
||||
self.get_cached_string(id, None)
|
||||
@@ -373,6 +577,10 @@ impl Localization {
|
||||
&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
|
||||
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut total_strings = 0;
|
||||
@@ -403,20 +611,6 @@ impl Localization {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Negotiates the best locale from a list of preferred locales
|
||||
pub fn negotiate_locale(&self, preferred: &[LanguageIdentifier]) -> LanguageIdentifier {
|
||||
let available = self.available_locales.clone();
|
||||
let negotiated = negotiate_languages(
|
||||
preferred,
|
||||
&available,
|
||||
Some(&self.fallback_locale),
|
||||
fluent_langneg::NegotiationStrategy::Filtering,
|
||||
);
|
||||
negotiated
|
||||
.first()
|
||||
.map_or(self.fallback_locale.clone(), |v| (*v).clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about cache usage
|
||||
@@ -429,6 +623,80 @@ pub struct CacheStats {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_language_region() {
|
||||
// Test that we extract just language and region from various locale formats
|
||||
|
||||
// Test locales with extensions
|
||||
let unicode_locale = "fr-FR-u-mu-celsius";
|
||||
let extracted = Localization::extract_language_region(unicode_locale);
|
||||
assert_eq!(extracted, "fr-FR");
|
||||
|
||||
let transformed_locale = "en-US-t-0-abc123";
|
||||
let extracted = Localization::extract_language_region(transformed_locale);
|
||||
assert_eq!(extracted, "en-US");
|
||||
|
||||
let private_locale = "de-DE-x-phonebk";
|
||||
let extracted = Localization::extract_language_region(private_locale);
|
||||
assert_eq!(extracted, "de-DE");
|
||||
|
||||
// Test simple locale (no extensions)
|
||||
let simple_locale = "en-US";
|
||||
let extracted = Localization::extract_language_region(simple_locale);
|
||||
assert_eq!(extracted, "en-US");
|
||||
|
||||
// Test language-only locale
|
||||
let lang_only = "en";
|
||||
let extracted = Localization::extract_language_region(lang_only);
|
||||
assert_eq!(extracted, "en");
|
||||
|
||||
// Test language with extensions (no region)
|
||||
let lang_with_extensions = "fr-u-mu-celsius";
|
||||
let extracted = Localization::extract_language_region(lang_with_extensions);
|
||||
assert_eq!(extracted, "fr");
|
||||
|
||||
// Test language with other extension types (no region)
|
||||
let lang_with_t_ext = "en-t-0-abc123";
|
||||
let extracted = Localization::extract_language_region(lang_with_t_ext);
|
||||
assert_eq!(extracted, "en");
|
||||
|
||||
let lang_with_x_ext = "de-x-phonebk";
|
||||
let extracted = Localization::extract_language_region(lang_with_x_ext);
|
||||
assert_eq!(extracted, "de");
|
||||
|
||||
// Test locale with numeric region code
|
||||
let numeric_region = "es-419-u-mu-celsius";
|
||||
let extracted = Localization::extract_language_region(numeric_region);
|
||||
assert_eq!(extracted, "es-419");
|
||||
|
||||
// Test locale with 3-letter region code
|
||||
let three_letter_region = "en-USA-t-0-abc123";
|
||||
let extracted = Localization::extract_language_region(three_letter_region);
|
||||
assert_eq!(extracted, "en-USA");
|
||||
|
||||
// Test locale with 2-letter region code
|
||||
let two_letter_region = "fr-FR-u-mu-celsius";
|
||||
let extracted = Localization::extract_language_region(two_letter_region);
|
||||
assert_eq!(extracted, "fr-FR");
|
||||
|
||||
// Test complex locale with multiple parts
|
||||
let complex_locale = "zh-CN-u-ca-chinese-x-private";
|
||||
let extracted = Localization::extract_language_region(complex_locale);
|
||||
assert_eq!(extracted, "zh-CN");
|
||||
|
||||
// Verify that extracted locales can be parsed
|
||||
let test_cases = ["fr-FR", "en-US", "de-DE", "en", "zh-CN"];
|
||||
for extracted in test_cases {
|
||||
if let Ok(locale) = extracted.parse::<LanguageIdentifier>() {
|
||||
tracing::info!("Successfully parsed extracted locale: {}", locale);
|
||||
} else {
|
||||
tracing::error!("Failed to parse extracted locale: {}", extracted);
|
||||
panic!("Should parse locale after extraction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// TODO(jb55): write tests that work, i broke all these during the refacto
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use crate::media::gif::ensure_latest_texture_from_cache;
|
||||
use crate::media::images::ImageType;
|
||||
use crate::urls::{UrlCache, UrlMimes};
|
||||
use crate::ImageMetadata;
|
||||
use crate::ObfuscationType;
|
||||
use crate::RenderableMedia;
|
||||
use crate::Result;
|
||||
use egui::TextureHandle;
|
||||
use image::{Delay, Frame};
|
||||
@@ -7,9 +12,11 @@ use poll_promise::Promise;
|
||||
use egui::ColorImage;
|
||||
|
||||
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::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::{io, thread};
|
||||
|
||||
use hex::ToHex;
|
||||
use sha2::Digest;
|
||||
@@ -19,7 +26,7 @@ use tracing::warn;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TexturesCache {
|
||||
cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||
}
|
||||
|
||||
impl TexturesCache {
|
||||
@@ -139,6 +146,12 @@ pub enum TextureState<'a> {
|
||||
Loaded(&'a mut TexturedImage),
|
||||
}
|
||||
|
||||
impl<'a> TextureState<'a> {
|
||||
pub fn is_loaded(&self) -> bool {
|
||||
matches!(self, Self::Loaded(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
|
||||
fn from(value: &'a mut TextureStateInternal) -> Self {
|
||||
match value {
|
||||
@@ -220,6 +233,7 @@ pub struct MediaCache {
|
||||
pub cache_dir: path::PathBuf,
|
||||
pub textures_cache: TexturesCache,
|
||||
pub cache_type: MediaCacheType,
|
||||
pub cache_size: Arc<Mutex<Option<u64>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
@@ -231,10 +245,29 @@ pub enum MediaCacheType {
|
||||
impl MediaCache {
|
||||
pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
|
||||
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 {
|
||||
cache_dir,
|
||||
textures_cache: TexturesCache::default(),
|
||||
cache_type,
|
||||
cache_size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,8 +364,14 @@ impl MediaCache {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -349,10 +388,33 @@ fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
|
||||
.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 base_path: path::PathBuf,
|
||||
pub static_imgs: MediaCache,
|
||||
pub gifs: MediaCache,
|
||||
pub urls: UrlMimes,
|
||||
/// cached imeta data
|
||||
pub metadata: HashMap<String, ImageMetadata>,
|
||||
pub gif_states: GifStateMap,
|
||||
}
|
||||
|
||||
@@ -360,10 +422,12 @@ impl Images {
|
||||
/// path to directory to place [`MediaCache`]s
|
||||
pub fn new(path: path::PathBuf) -> Self {
|
||||
Self {
|
||||
base_path: path.clone(),
|
||||
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
|
||||
gifs: MediaCache::new(&path, MediaCacheType::Gif),
|
||||
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
||||
gif_states: Default::default(),
|
||||
metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +436,58 @@ impl Images {
|
||||
self.gifs.migrate_v0()
|
||||
}
|
||||
|
||||
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
|
||||
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
|
||||
}
|
||||
|
||||
pub fn find_renderable_media(
|
||||
urls: &mut UrlMimes,
|
||||
imeta: &HashMap<String, ImageMetadata>,
|
||||
url: &str,
|
||||
) -> Option<RenderableMedia> {
|
||||
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
|
||||
|
||||
let obfuscation_type = match imeta.get(url) {
|
||||
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
|
||||
None => ObfuscationType::Default,
|
||||
};
|
||||
|
||||
Some(RenderableMedia {
|
||||
url: url.to_string(),
|
||||
media_type,
|
||||
obfuscation_type,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn latest_texture(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
url: &str,
|
||||
img_type: ImageType,
|
||||
) -> Option<TextureHandle> {
|
||||
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
||||
|
||||
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
|
||||
let is_loaded = self
|
||||
.get_cache_mut(cache_type)
|
||||
.textures_cache
|
||||
.handle_and_get_or_insert(url, || {
|
||||
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
|
||||
})
|
||||
.is_loaded();
|
||||
|
||||
if !is_loaded {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cache = match cache_type {
|
||||
MediaCacheType::Image => &mut self.static_imgs,
|
||||
MediaCacheType::Gif => &mut self.gifs,
|
||||
};
|
||||
|
||||
ensure_latest_texture_from_cache(ui, url, &mut self.gif_states, &mut cache.textures_cache)
|
||||
}
|
||||
|
||||
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => &self.static_imgs,
|
||||
@@ -385,6 +501,26 @@ impl Images {
|
||||
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>;
|
||||
@@ -395,3 +531,35 @@ pub struct GifState {
|
||||
pub next_frame_time: Option<SystemTime>,
|
||||
pub last_frame_index: usize,
|
||||
}
|
||||
|
||||
pub struct LatestTexture {
|
||||
pub texture: TextureHandle,
|
||||
pub request_next_repaint: Option<SystemTime>,
|
||||
}
|
||||
|
||||
pub fn get_render_state<'a>(
|
||||
ctx: &egui::Context,
|
||||
images: &'a mut Images,
|
||||
cache_type: MediaCacheType,
|
||||
url: &str,
|
||||
img_type: ImageType,
|
||||
) -> RenderState<'a> {
|
||||
let cache = match cache_type {
|
||||
MediaCacheType::Image => &mut images.static_imgs,
|
||||
MediaCacheType::Gif => &mut images.gifs,
|
||||
};
|
||||
|
||||
let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
|
||||
crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
|
||||
});
|
||||
|
||||
RenderState {
|
||||
texture_state,
|
||||
gifs: &mut images.gif_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderState<'a> {
|
||||
pub texture_state: TextureState<'a>,
|
||||
pub gifs: &'a mut GifStateMap,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ impl JobPool {
|
||||
pub fn new(num_threads: usize) -> Self {
|
||||
let (tx, rx) = mpsc::channel::<Job>();
|
||||
|
||||
// TODO(jb55) why not mpmc here !???
|
||||
let arc_rx = Arc::new(Mutex::new(rx));
|
||||
for _ in 0..num_threads {
|
||||
let arc_rx_clone = arc_rx.clone();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::JobPool;
|
||||
use egui::TextureHandle;
|
||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||
use notedeck::JobPool;
|
||||
use poll_promise::Promise;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -12,16 +12,20 @@ mod frame_history;
|
||||
pub mod i18n;
|
||||
mod imgcache;
|
||||
mod job_pool;
|
||||
mod jobs;
|
||||
pub mod media;
|
||||
mod muted;
|
||||
pub mod name;
|
||||
pub mod note;
|
||||
mod notecache;
|
||||
mod options;
|
||||
mod persist;
|
||||
pub mod platform;
|
||||
pub mod profile;
|
||||
pub mod relay_debug;
|
||||
pub mod relayspec;
|
||||
mod result;
|
||||
mod setup;
|
||||
pub mod storage;
|
||||
mod style;
|
||||
pub mod theme;
|
||||
@@ -47,10 +51,18 @@ pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
||||
pub use fonts::NamedFontFamily;
|
||||
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
|
||||
pub use imgcache::{
|
||||
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
|
||||
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
|
||||
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
|
||||
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
|
||||
TexturedImage, TexturesCache,
|
||||
};
|
||||
pub use job_pool::JobPool;
|
||||
pub use jobs::{
|
||||
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
|
||||
};
|
||||
pub use media::{
|
||||
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
|
||||
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
|
||||
};
|
||||
pub use muted::{MuteFun, Muted};
|
||||
pub use name::NostrName;
|
||||
pub use note::{
|
||||
@@ -58,6 +70,7 @@ pub use note::{
|
||||
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
||||
};
|
||||
pub use notecache::{CachedNote, NoteCache};
|
||||
pub use options::NotedeckOptions;
|
||||
pub use persist::*;
|
||||
pub use profile::get_profile_url;
|
||||
pub use relay_debug::RelayDebugView;
|
||||
@@ -72,8 +85,8 @@ pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction,
|
||||
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
||||
pub use user_account::UserAccount;
|
||||
pub use wallet::{
|
||||
get_current_wallet, get_wallet_for, GlobalWallet, Wallet, WalletError, WalletType,
|
||||
WalletUIState, ZapWallet,
|
||||
get_current_wallet, get_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
|
||||
WalletType, WalletUIState, ZapWallet,
|
||||
};
|
||||
pub use zaps::{
|
||||
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::{Images, MediaCacheType, TexturedImage};
|
||||
use poll_promise::Promise;
|
||||
|
||||
/// Tracks where media was on the screen so that
|
||||
/// we can do fun animations when opening the
|
||||
/// Media Viewer
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaInfo {
|
||||
/// The original screen position where it
|
||||
/// was rendered from. This is not where
|
||||
/// it should be rendered in the scene.
|
||||
pub original_position: egui::Rect,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Contains various information for when a user
|
||||
/// clicks a piece of media. It contains the current
|
||||
/// location on screen for each piece of media.
|
||||
///
|
||||
/// Viewers can use this to smoothly transition from
|
||||
/// the timeline to the viewer
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ViewMediaInfo {
|
||||
pub clicked_index: usize,
|
||||
pub medias: Vec<MediaInfo>,
|
||||
}
|
||||
|
||||
impl ViewMediaInfo {
|
||||
pub fn clicked_media(&self) -> &MediaInfo {
|
||||
&self.medias[self.clicked_index]
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions generated by media ui interactions
|
||||
pub enum MediaAction {
|
||||
/// An image was clicked on in a carousel, we have
|
||||
/// the opportunity to open into a fullscreen media viewer
|
||||
/// with a list of url values
|
||||
ViewMedias(ViewMediaInfo),
|
||||
|
||||
FetchImage {
|
||||
url: String,
|
||||
cache_type: MediaCacheType,
|
||||
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
|
||||
},
|
||||
DoneLoading {
|
||||
url: String,
|
||||
cache_type: MediaCacheType,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MediaAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ViewMedias(ViewMediaInfo {
|
||||
clicked_index,
|
||||
medias,
|
||||
}) => f
|
||||
.debug_struct("ViewMedias")
|
||||
.field("clicked_index", clicked_index)
|
||||
.field("media", medias)
|
||||
.finish(),
|
||||
Self::FetchImage {
|
||||
url,
|
||||
cache_type,
|
||||
no_pfp_promise,
|
||||
} => f
|
||||
.debug_struct("FetchNoPfpImage")
|
||||
.field("url", url)
|
||||
.field("cache_type", cache_type)
|
||||
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
|
||||
.finish(),
|
||||
Self::DoneLoading { url, cache_type } => f
|
||||
.debug_struct("DoneLoading")
|
||||
.field("url", url)
|
||||
.field("cache_type", cache_type)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaAction {
|
||||
/// Handle view media actions
|
||||
pub fn on_view_media(&self, handler: impl FnOnce(&ViewMediaInfo)) {
|
||||
if let MediaAction::ViewMedias(view_medias) = self {
|
||||
handler(view_medias)
|
||||
}
|
||||
}
|
||||
|
||||
/// Default processing logic for Media Actions. We don't handle ViewMedias here since
|
||||
/// this may be app specific ?
|
||||
pub fn process_default_media_actions(self, images: &mut Images) {
|
||||
match self {
|
||||
MediaAction::ViewMedias(_urls) => {
|
||||
// NOTE(jb55): don't assume we want to show a fullscreen
|
||||
// media viewer we can use on_view_media for that. We
|
||||
// also don't want to have a notedeck_ui dependency in
|
||||
// the notedeck lib (MediaViewerState)
|
||||
//
|
||||
// In general our notedeck crate should be pretty
|
||||
// agnostic to functionallity in general unless it low
|
||||
// level like image rendering.
|
||||
//
|
||||
//mview_state.set_urls(urls);
|
||||
}
|
||||
|
||||
MediaAction::FetchImage {
|
||||
url,
|
||||
cache_type,
|
||||
no_pfp_promise: promise,
|
||||
} => {
|
||||
images
|
||||
.get_cache_mut(cache_type)
|
||||
.textures_cache
|
||||
.insert_pending(&url, promise);
|
||||
}
|
||||
MediaAction::DoneLoading { url, cache_type } => {
|
||||
let cache = match cache_type {
|
||||
MediaCacheType::Image => &mut images.static_imgs,
|
||||
MediaCacheType::Gif => &mut images.gifs,
|
||||
};
|
||||
|
||||
cache.textures_cache.move_to_loaded(&url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ use nostrdb::Note;
|
||||
use crate::jobs::{Job, JobError, JobParamsOwned};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Blur<'a> {
|
||||
pub blurhash: &'a str,
|
||||
pub struct ImageMetadata {
|
||||
pub blurhash: String,
|
||||
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl PointDimensions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Blur<'_> {
|
||||
impl ImageMetadata {
|
||||
pub fn scaled_pixel_dimensions(
|
||||
&self,
|
||||
ui: &egui::Ui,
|
||||
@@ -75,9 +75,8 @@ impl Blur<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
||||
let mut blurs = HashMap::new();
|
||||
|
||||
/// Find blurhashes in image metadata and update our cache
|
||||
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
|
||||
for tag in note.tags() {
|
||||
let mut tag_iter = tag.into_iter();
|
||||
if tag_iter
|
||||
@@ -93,13 +92,11 @@ pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
|
||||
continue;
|
||||
};
|
||||
|
||||
blurs.insert(url, blur);
|
||||
blurs.insert(url.to_string(), blur);
|
||||
}
|
||||
}
|
||||
|
||||
blurs
|
||||
}
|
||||
|
||||
fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
||||
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
|
||||
let mut url = None;
|
||||
let mut blurhash = None;
|
||||
let mut dims = None;
|
||||
@@ -138,21 +135,21 @@ fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
|
||||
});
|
||||
|
||||
Some((
|
||||
url,
|
||||
Blur {
|
||||
blurhash,
|
||||
url.to_string(),
|
||||
ImageMetadata {
|
||||
blurhash: blurhash.to_string(),
|
||||
dimensions,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ObfuscationType<'a> {
|
||||
Blurhash(Blur<'a>),
|
||||
pub enum ObfuscationType {
|
||||
Blurhash(ImageMetadata),
|
||||
Default,
|
||||
}
|
||||
|
||||
pub(crate) fn compute_blurhash(
|
||||
pub fn compute_blurhash(
|
||||
params: Option<JobParamsOwned>,
|
||||
dims: PixelDimensions,
|
||||
) -> Result<Job, JobError> {
|
||||
@@ -185,9 +182,9 @@ fn generate_blurhash_texturehandle(
|
||||
url: &str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> notedeck::Result<egui::TextureHandle> {
|
||||
) -> Result<egui::TextureHandle, crate::Error> {
|
||||
let bytes = blurhash::decode(blurhash, width, height, 1.0)
|
||||
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
|
||||
.map_err(|e| crate::Error::Generic(e.to_string()))?;
|
||||
|
||||
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
|
||||
Ok(ctx.load_texture(url, img, Default::default()))
|
||||
@@ -3,37 +3,32 @@ use std::{
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
|
||||
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
||||
use egui::TextureHandle;
|
||||
use notedeck::{GifState, GifStateMap, TexturedImage};
|
||||
|
||||
pub struct LatextTexture<'a> {
|
||||
pub texture: &'a TextureHandle,
|
||||
pub request_next_repaint: Option<SystemTime>,
|
||||
}
|
||||
|
||||
/// This is necessary because other repaint calls can effectively steal our repaint request.
|
||||
/// So we must keep on requesting to repaint at our desired time to ensure our repaint goes through.
|
||||
/// See [`egui::Context::request_repaint_after`]
|
||||
pub fn handle_repaint<'a>(ui: &egui::Ui, latest: LatextTexture<'a>) -> &'a TextureHandle {
|
||||
if let Some(_repaint) = latest.request_next_repaint {
|
||||
// 24fps for gif is fine
|
||||
ui.ctx()
|
||||
.request_repaint_after(std::time::Duration::from_millis(41));
|
||||
}
|
||||
latest.texture
|
||||
}
|
||||
|
||||
#[must_use = "caller should pass the return value to `gif::handle_repaint`"]
|
||||
pub fn retrieve_latest_texture<'a>(
|
||||
pub fn ensure_latest_texture_from_cache(
|
||||
ui: &egui::Ui,
|
||||
url: &str,
|
||||
gifs: &'a mut GifStateMap,
|
||||
cached_image: &'a mut TexturedImage,
|
||||
) -> LatextTexture<'a> {
|
||||
match cached_image {
|
||||
TexturedImage::Static(texture) => LatextTexture {
|
||||
texture,
|
||||
request_next_repaint: None,
|
||||
},
|
||||
gifs: &mut GifStateMap,
|
||||
textures: &mut TexturesCache,
|
||||
) -> 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))
|
||||
}
|
||||
|
||||
pub fn ensure_latest_texture(
|
||||
ui: &egui::Ui,
|
||||
url: &str,
|
||||
gifs: &mut GifStateMap,
|
||||
img: &mut TexturedImage,
|
||||
) -> TextureHandle {
|
||||
match img {
|
||||
TexturedImage::Static(handle) => handle.clone(),
|
||||
TexturedImage::Animated(animation) => {
|
||||
if let Some(receiver) = &animation.receiver {
|
||||
loop {
|
||||
@@ -115,12 +110,12 @@ pub fn retrieve_latest_texture<'a>(
|
||||
|
||||
if let Some(req) = request_next_repaint {
|
||||
tracing::trace!("requesting repaint for {url} after {req:?}");
|
||||
// 24fps for gif is fine
|
||||
ui.ctx()
|
||||
.request_repaint_after(std::time::Duration::from_millis(41));
|
||||
}
|
||||
|
||||
LatextTexture {
|
||||
texture,
|
||||
request_next_repaint,
|
||||
}
|
||||
texture.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
use crate::{Animation, ImageFrame, MediaCache, MediaCacheType, TextureFrame, TexturedImage};
|
||||
use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
|
||||
use image::codecs::gif::GifDecoder;
|
||||
use image::imageops::FilterType;
|
||||
use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
|
||||
use poll_promise::Promise;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{self, Path};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
|
||||
// NOTE(jb55): chatgpt wrote this because I was too dumb to
|
||||
pub fn aspect_fill(
|
||||
ui: &mut egui::Ui,
|
||||
sense: Sense,
|
||||
texture_id: egui::TextureId,
|
||||
aspect_ratio: f32,
|
||||
) -> egui::Response {
|
||||
let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
|
||||
let frame_ratio = frame.width() / frame.height();
|
||||
|
||||
let (width, height) = if frame_ratio > aspect_ratio {
|
||||
// Frame is wider than the content
|
||||
(frame.width(), frame.width() / aspect_ratio)
|
||||
} else {
|
||||
// Frame is taller than the content
|
||||
(frame.height() * aspect_ratio, frame.height())
|
||||
};
|
||||
|
||||
let content_rect = Rect::from_min_size(
|
||||
frame.min
|
||||
+ egui::vec2(
|
||||
(frame.width() - width) / 2.0,
|
||||
(frame.height() - height) / 2.0,
|
||||
),
|
||||
egui::vec2(width, height),
|
||||
);
|
||||
|
||||
// Set the clipping rectangle to the frame
|
||||
//let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
|
||||
//ui.set_clip_rect(frame);
|
||||
|
||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||
|
||||
let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
|
||||
|
||||
// Draw the texture within the calculated rect, potentially clipping it
|
||||
painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
|
||||
painter.image(texture_id, content_rect, uv, Color32::WHITE);
|
||||
|
||||
// Restore the original clipping rectangle
|
||||
//ui.set_clip_rect(clip_rect);
|
||||
response
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn round_image(image: &mut ColorImage) {
|
||||
// The radius to the edge of of the avatar circle
|
||||
let edge_radius = image.size[0] as f32 / 2.0;
|
||||
let edge_radius_squared = edge_radius * edge_radius;
|
||||
|
||||
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
|
||||
// y coordinate
|
||||
let uy = pixnum / image.size[0];
|
||||
let y = uy as f32;
|
||||
let y_offset = edge_radius - y;
|
||||
|
||||
// x coordinate
|
||||
let ux = pixnum % image.size[0];
|
||||
let x = ux as f32;
|
||||
let x_offset = edge_radius - x;
|
||||
|
||||
// The radius to this pixel (may be inside or outside the circle)
|
||||
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
|
||||
|
||||
// If inside of the avatar circle
|
||||
if pixel_radius_squared <= edge_radius_squared {
|
||||
// squareroot to find how many pixels we are from the edge
|
||||
let pixel_radius: f32 = pixel_radius_squared.sqrt();
|
||||
let distance = edge_radius - pixel_radius;
|
||||
|
||||
// If we are within 1 pixel of the edge, we should fade, to
|
||||
// antialias the edge of the circle. 1 pixel from the edge should
|
||||
// be 100% of the original color, and right on the edge should be
|
||||
// 0% of the original color.
|
||||
if distance <= 1.0 {
|
||||
*pixel = Color32::from_rgba_premultiplied(
|
||||
(pixel.r() as f32 * distance) as u8,
|
||||
(pixel.g() as f32 * distance) as u8,
|
||||
(pixel.b() as f32 * distance) as u8,
|
||||
(pixel.a() as f32 * distance) as u8,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Outside of the avatar circle
|
||||
*pixel = Color32::TRANSPARENT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the image's longest dimension is greater than max_edge, downscale
|
||||
fn resize_image_if_too_big(
|
||||
image: image::DynamicImage,
|
||||
max_edge: u32,
|
||||
filter: FilterType,
|
||||
) -> image::DynamicImage {
|
||||
// if we have no size hint, resize to something reasonable
|
||||
let w = image.width();
|
||||
let h = image.height();
|
||||
let long = w.max(h);
|
||||
|
||||
if long > max_edge {
|
||||
let scale = max_edge as f32 / long as f32;
|
||||
let new_w = (w as f32 * scale).round() as u32;
|
||||
let new_h = (h as f32 * scale).round() as u32;
|
||||
|
||||
image.resize(new_w, new_h, filter)
|
||||
} else {
|
||||
image
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Process an image, resizing so we don't blow up video memory or even crash
|
||||
///
|
||||
/// For profile pictures, make them round and small to fit the size hint
|
||||
/// For everything else, either:
|
||||
///
|
||||
/// - resize to the size hint
|
||||
/// - keep the size if the longest dimension is less than MAX_IMG_LENGTH
|
||||
/// - resize if any larger, using [`resize_image_if_too_big`]
|
||||
///
|
||||
#[profiling::function]
|
||||
fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
|
||||
const MAX_IMG_LENGTH: u32 = 2048;
|
||||
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
|
||||
|
||||
match imgtyp {
|
||||
ImageType::Content(size_hint) => {
|
||||
let image = match size_hint {
|
||||
None => resize_image_if_too_big(image, MAX_IMG_LENGTH, FILTER_TYPE),
|
||||
Some((w, h)) => image.resize(w, h, FILTER_TYPE),
|
||||
};
|
||||
|
||||
let image_buffer = image.into_rgba8();
|
||||
ColorImage::from_rgba_unmultiplied(
|
||||
[
|
||||
image_buffer.width() as usize,
|
||||
image_buffer.height() as usize,
|
||||
],
|
||||
image_buffer.as_flat_samples().as_slice(),
|
||||
)
|
||||
}
|
||||
ImageType::Profile(size) => {
|
||||
// Crop square
|
||||
let smaller = image.width().min(image.height());
|
||||
|
||||
if image.width() > smaller {
|
||||
let excess = image.width() - smaller;
|
||||
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
|
||||
} else if image.height() > smaller {
|
||||
let excess = image.height() - smaller;
|
||||
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
|
||||
}
|
||||
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
|
||||
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
|
||||
let mut color_image = ColorImage::from_rgba_unmultiplied(
|
||||
[
|
||||
image_buffer.width() as usize,
|
||||
image_buffer.height() as usize,
|
||||
],
|
||||
image_buffer.as_flat_samples().as_slice(),
|
||||
);
|
||||
round_image(&mut color_image);
|
||||
color_image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn parse_img_response(
|
||||
response: ehttp::Response,
|
||||
imgtyp: ImageType,
|
||||
) -> Result<ColorImage, crate::Error> {
|
||||
let content_type = response.content_type().unwrap_or_default();
|
||||
let size_hint = match imgtyp {
|
||||
ImageType::Profile(size) => SizeHint::Size(size, size),
|
||||
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
|
||||
ImageType::Content(None) => SizeHint::default(),
|
||||
};
|
||||
|
||||
if content_type.starts_with("image/svg") {
|
||||
profiling::scope!("load_svg");
|
||||
|
||||
let mut color_image =
|
||||
egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
|
||||
round_image(&mut color_image);
|
||||
Ok(color_image)
|
||||
} else if content_type.starts_with("image/") {
|
||||
profiling::scope!("load_from_memory");
|
||||
let dyn_image = image::load_from_memory(&response.bytes)?;
|
||||
Ok(process_image(imgtyp, dyn_image))
|
||||
} else {
|
||||
Err(format!("Expected image, found content-type {content_type:?}").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_img_from_disk(
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
path: &path::Path,
|
||||
cache_type: MediaCacheType,
|
||||
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||
let ctx = ctx.clone();
|
||||
let url = url.to_owned();
|
||||
let path = path.to_owned();
|
||||
|
||||
Promise::spawn_async(async move {
|
||||
Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
|
||||
})
|
||||
}
|
||||
|
||||
async fn async_fetch_img_from_disk(
|
||||
ctx: egui::Context,
|
||||
url: String,
|
||||
path: &path::Path,
|
||||
cache_type: MediaCacheType,
|
||||
) -> Result<TexturedImage, crate::Error> {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => {
|
||||
let data = fs::read(path).await?;
|
||||
let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image)?;
|
||||
|
||||
let img = buffer_to_color_image(
|
||||
image_buffer.as_flat_samples_u8(),
|
||||
image_buffer.width(),
|
||||
image_buffer.height(),
|
||||
);
|
||||
Ok(TexturedImage::Static(ctx.load_texture(
|
||||
&url,
|
||||
img,
|
||||
Default::default(),
|
||||
)))
|
||||
}
|
||||
MediaCacheType::Gif => {
|
||||
let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
|
||||
generate_gif(ctx, url, path, gif_bytes, false, |i| {
|
||||
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_gif(
|
||||
ctx: egui::Context,
|
||||
url: String,
|
||||
path: &path::Path,
|
||||
data: Vec<u8>,
|
||||
write_to_disk: bool,
|
||||
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
|
||||
) -> Result<TexturedImage, crate::Error> {
|
||||
let decoder = {
|
||||
let reader = Cursor::new(data.as_slice());
|
||||
GifDecoder::new(reader)?
|
||||
};
|
||||
let (tex_input, tex_output) = mpsc::sync_channel(4);
|
||||
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
|
||||
let (inp, out) = mpsc::sync_channel(4);
|
||||
(Some(inp), Some(out))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let mut frames: VecDeque<Frame> = decoder
|
||||
.into_frames()
|
||||
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
|
||||
.map_err(|e| crate::Error::Generic(e.to_string()))?;
|
||||
|
||||
let first_frame = frames.pop_front().map(|frame| {
|
||||
generate_animation_frame(
|
||||
&ctx,
|
||||
&url,
|
||||
0,
|
||||
frame,
|
||||
maybe_encoder_input.as_ref(),
|
||||
process_to_egui,
|
||||
)
|
||||
});
|
||||
|
||||
let cur_url = url.clone();
|
||||
thread::spawn(move || {
|
||||
for (index, frame) in frames.into_iter().enumerate() {
|
||||
let texture_frame = generate_animation_frame(
|
||||
&ctx,
|
||||
&cur_url,
|
||||
index,
|
||||
frame,
|
||||
maybe_encoder_input.as_ref(),
|
||||
process_to_egui,
|
||||
);
|
||||
|
||||
if tex_input.send(texture_frame).is_err() {
|
||||
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(encoder_output) = maybe_encoder_output {
|
||||
let path = path.to_owned();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut imgs = Vec::new();
|
||||
while let Ok(img) = encoder_output.recv() {
|
||||
imgs.push(img);
|
||||
}
|
||||
|
||||
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
|
||||
tracing::error!("Could not write gif to disk: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
first_frame.map_or_else(
|
||||
|| {
|
||||
Err(crate::Error::Generic(
|
||||
"first frame not found for gif".to_owned(),
|
||||
))
|
||||
},
|
||||
|first_frame| {
|
||||
Ok(TexturedImage::Animated(Animation {
|
||||
other_frames: Default::default(),
|
||||
receiver: Some(tex_output),
|
||||
first_frame,
|
||||
}))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_animation_frame(
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
index: usize,
|
||||
frame: image::Frame,
|
||||
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
|
||||
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
|
||||
) -> TextureFrame {
|
||||
let delay = Duration::from(frame.delay());
|
||||
let img = DynamicImage::ImageRgba8(frame.into_buffer());
|
||||
let color_img = process_to_egui(img);
|
||||
|
||||
if let Some(sender) = maybe_encoder_input {
|
||||
if let Err(e) = sender.send(ImageFrame {
|
||||
delay,
|
||||
image: color_img.clone(),
|
||||
}) {
|
||||
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
TextureFrame {
|
||||
delay,
|
||||
texture: ctx.load_texture(format!("{url}{index}"), color_img, Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer_to_color_image(
|
||||
samples: Option<FlatSamples<&[u8]>>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> ColorImage {
|
||||
// TODO(jb55): remove unwrap here
|
||||
let flat_samples = samples.unwrap();
|
||||
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
|
||||
}
|
||||
|
||||
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>, crate::Error> {
|
||||
std::fs::read(path).map_err(|e| crate::Error::Generic(e.to_string()))
|
||||
}
|
||||
|
||||
/// Controls type-specific handling
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ImageType {
|
||||
/// Profile Image (size)
|
||||
Profile(u32),
|
||||
/// Content Image with optional size hint
|
||||
Content(Option<(u32, u32)>),
|
||||
}
|
||||
|
||||
pub fn fetch_img(
|
||||
img_cache_path: &Path,
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
imgtyp: ImageType,
|
||||
cache_type: MediaCacheType,
|
||||
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||
let key = MediaCache::key(url);
|
||||
let path = img_cache_path.join(key);
|
||||
|
||||
if path.exists() {
|
||||
fetch_img_from_disk(ctx, url, &path, cache_type)
|
||||
} else {
|
||||
fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
|
||||
}
|
||||
|
||||
// TODO: fetch image from local cache
|
||||
}
|
||||
|
||||
fn fetch_img_from_net(
|
||||
cache_path: &path::Path,
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
imgtyp: ImageType,
|
||||
cache_type: MediaCacheType,
|
||||
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(url);
|
||||
let ctx = ctx.clone();
|
||||
let cloned_url = url.to_owned();
|
||||
let cache_path = cache_path.to_owned();
|
||||
ehttp::fetch(request, move |response| {
|
||||
let handle = response.map_err(crate::Error::Generic).and_then(|resp| {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => {
|
||||
let img = parse_img_response(resp, imgtyp);
|
||||
img.map(|img| {
|
||||
let texture_handle =
|
||||
ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
||||
|
||||
// write to disk
|
||||
std::thread::spawn(move || {
|
||||
MediaCache::write(&cache_path, &cloned_url, img)
|
||||
});
|
||||
|
||||
TexturedImage::Static(texture_handle)
|
||||
})
|
||||
}
|
||||
MediaCacheType::Gif => {
|
||||
let gif_bytes = resp.bytes;
|
||||
generate_gif(
|
||||
ctx.clone(),
|
||||
cloned_url,
|
||||
&cache_path,
|
||||
gif_bytes,
|
||||
true,
|
||||
move |img| process_image(imgtyp, img),
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sender.send(Some(handle)); // send the results back to the UI thread.
|
||||
ctx.request_repaint();
|
||||
});
|
||||
|
||||
promise
|
||||
}
|
||||
|
||||
pub fn fetch_no_pfp_promise(
|
||||
ctx: &Context,
|
||||
cache: &MediaCache,
|
||||
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
|
||||
crate::media::images::fetch_img(
|
||||
&cache.cache_dir,
|
||||
ctx,
|
||||
crate::profile::no_pfp_url(),
|
||||
ImageType::Profile(128),
|
||||
MediaCacheType::Image,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
pub mod action;
|
||||
pub mod blur;
|
||||
pub mod gif;
|
||||
pub mod images;
|
||||
pub mod imeta;
|
||||
pub mod renderable;
|
||||
|
||||
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
|
||||
pub use blur::{
|
||||
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
|
||||
PointDimensions,
|
||||
};
|
||||
pub use images::ImageType;
|
||||
pub use renderable::RenderableMedia;
|
||||
@@ -0,0 +1,9 @@
|
||||
use super::ObfuscationType;
|
||||
use crate::MediaCacheType;
|
||||
|
||||
/// Media that is prepared for rendering. Use [`Images::get_renderable_media`] to get these
|
||||
pub struct RenderableMedia {
|
||||
pub url: String,
|
||||
pub media_type: MediaCacheType,
|
||||
pub obfuscation_type: ObfuscationType,
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::context::ContextSelection;
|
||||
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
|
||||
use crate::{zaps::NoteZapTargetOwned, MediaAction};
|
||||
use egui::Vec2;
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use poll_promise::Promise;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScrollInfo {
|
||||
@@ -61,62 +60,3 @@ pub struct ZapTargetAmount {
|
||||
pub target: NoteZapTargetOwned,
|
||||
pub specified_msats: Option<u64>, // if None use default amount
|
||||
}
|
||||
|
||||
pub enum MediaAction {
|
||||
FetchImage {
|
||||
url: String,
|
||||
cache_type: MediaCacheType,
|
||||
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
|
||||
},
|
||||
DoneLoading {
|
||||
url: String,
|
||||
cache_type: MediaCacheType,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MediaAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::FetchImage {
|
||||
url,
|
||||
cache_type,
|
||||
no_pfp_promise,
|
||||
} => f
|
||||
.debug_struct("FetchNoPfpImage")
|
||||
.field("url", url)
|
||||
.field("cache_type", cache_type)
|
||||
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
|
||||
.finish(),
|
||||
Self::DoneLoading { url, cache_type } => f
|
||||
.debug_struct("DoneLoading")
|
||||
.field("url", url)
|
||||
.field("cache_type", cache_type)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaAction {
|
||||
pub fn process(self, images: &mut Images) {
|
||||
match self {
|
||||
MediaAction::FetchImage {
|
||||
url,
|
||||
cache_type,
|
||||
no_pfp_promise: promise,
|
||||
} => {
|
||||
images
|
||||
.get_cache_mut(cache_type)
|
||||
.textures_cache
|
||||
.insert_pending(&url, promise);
|
||||
}
|
||||
MediaAction::DoneLoading { url, cache_type } => {
|
||||
let cache = match cache_type {
|
||||
MediaCacheType::Image => &mut images.static_imgs,
|
||||
MediaCacheType::Gif => &mut images.gifs,
|
||||
};
|
||||
|
||||
cache.textures_cache.move_to_loaded(&url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
mod action;
|
||||
mod context;
|
||||
|
||||
pub use action::{MediaAction, NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
||||
|
||||
use crate::Accounts;
|
||||
use crate::GlobalWallet;
|
||||
use crate::JobPool;
|
||||
use crate::Localization;
|
||||
use crate::UnknownIds;
|
||||
@@ -20,6 +21,7 @@ use std::fmt;
|
||||
pub struct NoteContext<'d> {
|
||||
pub ndb: &'d Ndb,
|
||||
pub accounts: &'d Accounts,
|
||||
pub global_wallet: &'d GlobalWallet,
|
||||
pub i18n: &'d mut Localization,
|
||||
pub img_cache: &'d mut Images,
|
||||
pub note_cache: &'d mut NoteCache,
|
||||
@@ -28,7 +30,6 @@ pub struct NoteContext<'d> {
|
||||
pub job_pool: &'d mut JobPool,
|
||||
pub unknown_ids: &'d mut UnknownIds,
|
||||
pub clipboard: &'d mut egui_winit::clipboard::Clipboard,
|
||||
pub current_account_has_wallet: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct NotedeckOptions: u64 {
|
||||
// ===== Settings ======
|
||||
/// Are we on light theme?
|
||||
const LightTheme = 1 << 0;
|
||||
|
||||
/// Debug controls, fps stats
|
||||
const Debug = 1 << 1;
|
||||
|
||||
/// Show relay debug window?
|
||||
const RelayDebug = 1 << 2;
|
||||
|
||||
/// Are we running as tests?
|
||||
const Tests = 1 << 3;
|
||||
|
||||
/// Use keystore?
|
||||
const UseKeystore = 1 << 4;
|
||||
|
||||
/// Show client on notes?
|
||||
const ShowClient = 1 << 5;
|
||||
|
||||
/// Simulate is_compiled_as_mobile ?
|
||||
const Mobile = 1 << 6;
|
||||
|
||||
// ===== Feature Flags ======
|
||||
/// Is notebook enabled?
|
||||
const FeatureNotebook = 1 << 32;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NotedeckOptions {
|
||||
fn default() -> Self {
|
||||
NotedeckOptions::UseKeystore
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
mod app_size;
|
||||
mod theme_handler;
|
||||
mod settings_handler;
|
||||
mod token_handler;
|
||||
mod zoom;
|
||||
|
||||
pub use app_size::AppSizeHandler;
|
||||
pub use theme_handler::ThemeHandler;
|
||||
pub use settings_handler::Settings;
|
||||
pub use settings_handler::SettingsHandler;
|
||||
pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE;
|
||||
pub use token_handler::TokenHandler;
|
||||
pub use zoom::ZoomHandler;
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
use crate::{
|
||||
storage::delete_file, timed_serializer::TimedSerializer, DataPath, DataPathType, Directory,
|
||||
};
|
||||
use egui::ThemePreference;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
|
||||
const THEME_FILE: &str = "theme.txt";
|
||||
const ZOOM_FACTOR_FILE: &str = "zoom_level.json";
|
||||
const SETTINGS_FILE: &str = "settings.json";
|
||||
|
||||
const DEFAULT_THEME: ThemePreference = ThemePreference::Dark;
|
||||
const DEFAULT_LOCALE: &str = "en-US";
|
||||
const DEFAULT_ZOOM_FACTOR: f32 = 1.0;
|
||||
const DEFAULT_SHOW_SOURCE_CLIENT: &str = "hide";
|
||||
const DEFAULT_SHOW_REPLIES_NEWEST_FIRST: bool = false;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 13.0;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 16.0;
|
||||
|
||||
fn deserialize_theme(serialized_theme: &str) -> Option<ThemePreference> {
|
||||
match serialized_theme {
|
||||
"dark" => Some(ThemePreference::Dark),
|
||||
"light" => Some(ThemePreference::Light),
|
||||
"system" => Some(ThemePreference::System),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Settings {
|
||||
pub theme: ThemePreference,
|
||||
pub locale: String,
|
||||
pub zoom_factor: f32,
|
||||
pub show_source_client: String,
|
||||
pub show_replies_newest_first: bool,
|
||||
pub note_body_font_size: f32,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: DEFAULT_THEME,
|
||||
locale: DEFAULT_LOCALE.to_string(),
|
||||
zoom_factor: DEFAULT_ZOOM_FACTOR,
|
||||
show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(),
|
||||
show_replies_newest_first: DEFAULT_SHOW_REPLIES_NEWEST_FIRST,
|
||||
note_body_font_size: DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsHandler {
|
||||
directory: Directory,
|
||||
serializer: TimedSerializer<Settings>,
|
||||
current_settings: Option<Settings>,
|
||||
}
|
||||
|
||||
impl SettingsHandler {
|
||||
fn read_from_theme_file(&self) -> Option<ThemePreference> {
|
||||
match self.directory.get_file(THEME_FILE.to_string()) {
|
||||
Ok(contents) => deserialize_theme(contents.trim()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_zomfactor_file(&self) -> Option<f32> {
|
||||
match self.directory.get_file(ZOOM_FACTOR_FILE.to_string()) {
|
||||
Ok(contents) => serde_json::from_str::<f32>(&contents).ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_settings_file(&mut self) -> bool {
|
||||
let mut settings = Settings::default();
|
||||
let mut migrated = false;
|
||||
// if theme.txt exists migrate
|
||||
if let Some(theme_from_file) = self.read_from_theme_file() {
|
||||
info!("migrating theme preference from theme.txt file");
|
||||
_ = delete_file(&self.directory.file_path, THEME_FILE.to_string());
|
||||
|
||||
settings.theme = theme_from_file;
|
||||
migrated = true;
|
||||
} else {
|
||||
info!("theme.txt file not found, using default theme");
|
||||
};
|
||||
|
||||
// if zoom_factor.txt exists migrate
|
||||
if let Some(zom_factor) = self.read_from_zomfactor_file() {
|
||||
info!("migrating theme preference from zom_factor file");
|
||||
_ = delete_file(&self.directory.file_path, ZOOM_FACTOR_FILE.to_string());
|
||||
|
||||
settings.zoom_factor = zom_factor;
|
||||
migrated = true;
|
||||
} else {
|
||||
info!("zoom_factor.txt exists migrate file not found, using default zoom factor");
|
||||
};
|
||||
|
||||
if migrated {
|
||||
self.current_settings = Some(settings);
|
||||
self.try_save_settings();
|
||||
}
|
||||
migrated
|
||||
}
|
||||
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let directory = Directory::new(path.path(DataPathType::Setting));
|
||||
let serializer =
|
||||
TimedSerializer::new(path, DataPathType::Setting, "settings.json".to_owned());
|
||||
|
||||
Self {
|
||||
directory,
|
||||
serializer,
|
||||
current_settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(mut self) -> Self {
|
||||
if self.migrate_to_settings_file() {
|
||||
return self;
|
||||
}
|
||||
|
||||
match self.directory.get_file(SETTINGS_FILE.to_string()) {
|
||||
Ok(contents_str) => {
|
||||
// Parse JSON content
|
||||
match serde_json::from_str::<Settings>(&contents_str) {
|
||||
Ok(settings) => {
|
||||
self.current_settings = Some(settings);
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Invalid settings format. Using defaults");
|
||||
self.current_settings = Some(Settings::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Could not read settings. Using defaults");
|
||||
self.current_settings = Some(Settings::default());
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn try_save_settings(&mut self) {
|
||||
let settings = self.get_settings_mut().clone();
|
||||
self.serializer.try_save(settings);
|
||||
}
|
||||
|
||||
pub fn get_settings_mut(&mut self) -> &mut Settings {
|
||||
if self.current_settings.is_none() {
|
||||
self.current_settings = Some(Settings::default());
|
||||
}
|
||||
self.current_settings.as_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, theme: ThemePreference) {
|
||||
self.get_settings_mut().theme = theme;
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn set_locale<S>(&mut self, locale: S)
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.get_settings_mut().locale = locale.into();
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn set_zoom_factor(&mut self, zoom_factor: f32) {
|
||||
self.get_settings_mut().zoom_factor = zoom_factor;
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn set_show_source_client<S>(&mut self, option: S)
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.get_settings_mut().show_source_client = option.into();
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn set_show_replies_newest_first(&mut self, value: bool) {
|
||||
self.get_settings_mut().show_replies_newest_first = value;
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn set_note_body_font_size(&mut self, value: f32) {
|
||||
self.get_settings_mut().note_body_font_size = value;
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn update_batch<F>(&mut self, update_fn: F)
|
||||
where
|
||||
F: FnOnce(&mut Settings),
|
||||
{
|
||||
let settings = self.get_settings_mut();
|
||||
update_fn(settings);
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn update_settings(&mut self, new_settings: Settings) {
|
||||
self.current_settings = Some(new_settings);
|
||||
self.try_save_settings();
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> ThemePreference {
|
||||
self.current_settings
|
||||
.as_ref()
|
||||
.map(|s| s.theme)
|
||||
.unwrap_or(DEFAULT_THEME)
|
||||
}
|
||||
|
||||
pub fn locale(&self) -> String {
|
||||
self.current_settings
|
||||
.as_ref()
|
||||
.map(|s| s.locale.clone())
|
||||
.unwrap_or_else(|| DEFAULT_LOCALE.to_string())
|
||||
}
|
||||
|
||||
pub fn zoom_factor(&self) -> f32 {
|
||||
self.current_settings
|
||||
.as_ref()
|
||||
.map(|s| s.zoom_factor)
|
||||
.unwrap_or(DEFAULT_ZOOM_FACTOR)
|
||||
}
|
||||
|
||||
pub fn show_source_client(&self) -> String {
|
||||
self.current_settings
|
||||
.as_ref()
|
||||
.map(|s| s.show_source_client.to_string())
|
||||
.unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string())
|
||||
}
|
||||
|
||||
pub fn show_replies_newest_first(&self) -> bool {
|
||||
self.current_settings
|
||||
.as_ref()
|
||||
.map(|s| s.show_replies_newest_first)
|
||||
.unwrap_or(DEFAULT_SHOW_REPLIES_NEWEST_FIRST)
|
||||
}
|
||||
|
||||
pub fn is_loaded(&self) -> bool {
|
||||
self.current_settings.is_some()
|
||||
}
|
||||
|
||||
pub fn note_body_font_size(&self) -> f32 {
|
||||
self.current_settings
|
||||
.as_ref()
|
||||
.map(|s| s.note_body_font_size)
|
||||
.unwrap_or(DEFAULT_NOTE_BODY_FONT_SIZE)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use egui::ThemePreference;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{storage, DataPath, DataPathType, Directory};
|
||||
|
||||
pub struct ThemeHandler {
|
||||
directory: Directory,
|
||||
fallback_theme: ThemePreference,
|
||||
}
|
||||
|
||||
const THEME_FILE: &str = "theme.txt";
|
||||
|
||||
impl ThemeHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let directory = Directory::new(path.path(DataPathType::Setting));
|
||||
let fallback_theme = ThemePreference::Dark;
|
||||
Self {
|
||||
directory,
|
||||
fallback_theme,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&self) -> ThemePreference {
|
||||
match self.directory.get_file(THEME_FILE.to_owned()) {
|
||||
Ok(contents) => match deserialize_theme(contents) {
|
||||
Some(theme) => theme,
|
||||
None => {
|
||||
error!(
|
||||
"Could not deserialize theme. Using fallback {:?} instead",
|
||||
self.fallback_theme
|
||||
);
|
||||
self.fallback_theme
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Could not read {} file: {:?}\nUsing fallback {:?} instead",
|
||||
THEME_FILE, e, self.fallback_theme
|
||||
);
|
||||
self.fallback_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self, theme: ThemePreference) {
|
||||
match storage::write_file(
|
||||
&self.directory.file_path,
|
||||
THEME_FILE.to_owned(),
|
||||
&theme_to_serialized(&theme),
|
||||
) {
|
||||
Ok(_) => info!(
|
||||
"Successfully saved {:?} theme change to {}",
|
||||
theme, THEME_FILE
|
||||
),
|
||||
Err(_) => error!("Could not save {:?} theme change to {}", theme, THEME_FILE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_to_serialized(theme: &ThemePreference) -> String {
|
||||
match theme {
|
||||
ThemePreference::Dark => "dark",
|
||||
ThemePreference::Light => "light",
|
||||
ThemePreference::System => "system",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn deserialize_theme(serialized_theme: String) -> Option<ThemePreference> {
|
||||
match serialized_theme.as_str() {
|
||||
"dark" => Some(ThemePreference::Dark),
|
||||
"light" => Some(ThemePreference::Light),
|
||||
"system" => Some(ThemePreference::System),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use crate::{DataPath, DataPathType};
|
||||
use egui::Context;
|
||||
|
||||
use crate::timed_serializer::TimedSerializer;
|
||||
|
||||
pub struct ZoomHandler {
|
||||
serializer: TimedSerializer<f32>,
|
||||
}
|
||||
|
||||
impl ZoomHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let serializer =
|
||||
TimedSerializer::new(path, DataPathType::Setting, "zoom_level.json".to_owned());
|
||||
|
||||
Self { serializer }
|
||||
}
|
||||
|
||||
pub fn try_save_zoom_factor(&mut self, ctx: &Context) {
|
||||
let cur_zoom_level = ctx.zoom_factor();
|
||||
self.serializer.try_save(cur_zoom_level);
|
||||
}
|
||||
|
||||
pub fn get_zoom_factor(&self) -> Option<f32> {
|
||||
self.serializer.get_item()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
|
||||
debug!("updating virtual keyboard height {}", height);
|
||||
|
||||
// Convert and store atomically
|
||||
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
|
||||
KEYBOARD_HEIGHT.store(height, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Gets the current Android virtual keyboard height. Useful for transforming
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use crate::fonts;
|
||||
use crate::theme;
|
||||
use crate::NotedeckOptions;
|
||||
use crate::NotedeckTextStyle;
|
||||
use egui::FontId;
|
||||
use egui::ThemePreference;
|
||||
|
||||
pub fn setup_egui_context(
|
||||
ctx: &egui::Context,
|
||||
options: NotedeckOptions,
|
||||
theme: ThemePreference,
|
||||
note_body_font_size: f32,
|
||||
zoom_factor: f32,
|
||||
) {
|
||||
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
|
||||
|
||||
let is_oled = crate::ui::is_oled();
|
||||
|
||||
ctx.options_mut(|o| {
|
||||
tracing::info!("Loaded theme {:?} from disk", theme);
|
||||
o.theme_preference = theme;
|
||||
});
|
||||
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
|
||||
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
|
||||
|
||||
fonts::setup_fonts(ctx);
|
||||
|
||||
if crate::ui::is_compiled_as_mobile() {
|
||||
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
|
||||
}
|
||||
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
|
||||
ctx.options_mut(|o| {
|
||||
o.input_options.max_click_duration = 0.4;
|
||||
});
|
||||
ctx.all_styles_mut(|style| crate::theme::add_custom_style(is_mobile, style));
|
||||
|
||||
ctx.set_zoom_factor(zoom_factor);
|
||||
|
||||
let mut style = (*ctx.style()).clone();
|
||||
style.text_styles.insert(
|
||||
NotedeckTextStyle::NoteBody.text_style(),
|
||||
FontId::proportional(note_body_font_size),
|
||||
);
|
||||
ctx.set_style(style);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub enum NotedeckTextStyle {
|
||||
Button,
|
||||
Small,
|
||||
Tiny,
|
||||
NoteBody,
|
||||
}
|
||||
|
||||
impl NotedeckTextStyle {
|
||||
@@ -29,6 +30,7 @@ impl NotedeckTextStyle {
|
||||
Self::Button => TextStyle::Button,
|
||||
Self::Small => TextStyle::Small,
|
||||
Self::Tiny => TextStyle::Name("Tiny".into()),
|
||||
Self::NoteBody => TextStyle::Name("NoteBody".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ impl NotedeckTextStyle {
|
||||
Self::Button => FontFamily::Proportional,
|
||||
Self::Small => FontFamily::Proportional,
|
||||
Self::Tiny => FontFamily::Proportional,
|
||||
Self::NoteBody => FontFamily::Proportional,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
use egui::{
|
||||
style::{Selection, WidgetVisuals, Widgets},
|
||||
Color32, CornerRadius, Stroke, Visuals,
|
||||
};
|
||||
use crate::{fonts, NotedeckTextStyle};
|
||||
use egui::style::Interaction;
|
||||
use egui::style::Selection;
|
||||
use egui::style::WidgetVisuals;
|
||||
use egui::style::Widgets;
|
||||
use egui::Color32;
|
||||
use egui::CornerRadius;
|
||||
use egui::FontId;
|
||||
use egui::Stroke;
|
||||
use egui::Style;
|
||||
use egui::Visuals;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
||||
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
||||
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
||||
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
||||
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
||||
|
||||
// BACKGROUNDS
|
||||
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
||||
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
||||
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
||||
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
||||
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
||||
|
||||
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
||||
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
||||
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
||||
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
||||
|
||||
pub struct ColorTheme {
|
||||
// VISUALS
|
||||
@@ -86,3 +114,131 @@ pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
|
||||
..default
|
||||
}
|
||||
}
|
||||
|
||||
pub fn desktop_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: DARKER_BG,
|
||||
extreme_bg_color: DARK_ISH_BG,
|
||||
text_color: Color32::WHITE,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: DARK_ISH_BG,
|
||||
window_stroke_color: DARK_BG,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: DARK_ISH_BG,
|
||||
noninteractive_weak_bg_fill: DARK_BG,
|
||||
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||
inactive_weak_bg_fill: SEMI_DARK_BG,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mobile_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
panel_fill: Color32::BLACK,
|
||||
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||
..desktop_dark_color_theme()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: Color32::WHITE,
|
||||
extreme_bg_color: LIGHTER_GRAY,
|
||||
text_color: BLACK,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: Color32::WHITE,
|
||||
window_stroke_color: DARKER_GRAY,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: Color32::WHITE,
|
||||
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
||||
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
||||
inactive_bg_fill: LIGHTER_GRAY,
|
||||
inactive_weak_bg_fill: LIGHTER_GRAY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create custom text sizes for any FontSizes
|
||||
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
||||
let font_size = if is_mobile {
|
||||
fonts::mobile_font_size
|
||||
} else {
|
||||
fonts::desktop_font_size
|
||||
};
|
||||
|
||||
style.text_styles = NotedeckTextStyle::iter()
|
||||
.map(|text_style| {
|
||||
(
|
||||
text_style.text_style(),
|
||||
FontId::new(font_size(&text_style), text_style.font_family()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
style.interaction = Interaction {
|
||||
tooltip_delay: 0.1,
|
||||
show_tooltips_only_when_still: false,
|
||||
..Interaction::default()
|
||||
};
|
||||
|
||||
// debug: show callstack for the current widget on hover if all
|
||||
// modifier keys are pressed down.
|
||||
/*
|
||||
#[cfg(feature = "debug-widget-callstack")]
|
||||
{
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_error!(
|
||||
"The `debug-widget-callstack` feature requires a debug build, \
|
||||
release builds are unsupported."
|
||||
);
|
||||
style.debug.debug_on_hover_with_all_modifiers = true;
|
||||
}
|
||||
|
||||
// debug: show an overlay on all interactive widgets
|
||||
#[cfg(feature = "debug-interactive-widgets")]
|
||||
{
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_error!(
|
||||
"The `debug-interactive-widgets` feature requires a debug build, \
|
||||
release builds are unsupported."
|
||||
);
|
||||
style.debug.show_interactive_widgets = true;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
pub fn light_mode() -> Visuals {
|
||||
create_themed_visuals(crate::theme::light_color_theme(), Visuals::light())
|
||||
}
|
||||
|
||||
pub fn dark_mode(is_oled: bool) -> Visuals {
|
||||
create_themed_visuals(
|
||||
if is_oled {
|
||||
mobile_dark_color_theme()
|
||||
} else {
|
||||
desktop_dark_color_theme()
|
||||
},
|
||||
Visuals::dark(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ use crate::debouncer::Debouncer;
|
||||
use crate::{storage, DataPath, DataPathType, Directory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use tracing::info; // Adjust this import path as needed
|
||||
use tracing::info;
|
||||
|
||||
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
|
||||
pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> {
|
||||
directory: Directory,
|
||||
file_name: String,
|
||||
debouncer: Debouncer,
|
||||
saved_item: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
||||
impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
||||
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
|
||||
let directory = Directory::new(path.path(path_type));
|
||||
let delay = Duration::from_millis(1000);
|
||||
@@ -30,11 +30,11 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
||||
self
|
||||
}
|
||||
|
||||
// returns whether successful
|
||||
/// Returns whether it actually wrote the new value
|
||||
pub fn try_save(&mut self, cur_item: T) -> bool {
|
||||
if self.debouncer.should_act() {
|
||||
if let Some(saved_item) = self.saved_item {
|
||||
if saved_item != cur_item {
|
||||
if let Some(ref saved_item) = self.saved_item {
|
||||
if *saved_item != cur_item {
|
||||
return self.save(cur_item);
|
||||
}
|
||||
} else {
|
||||
@@ -45,8 +45,8 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
|
||||
}
|
||||
|
||||
pub fn get_item(&self) -> Option<T> {
|
||||
if self.saved_item.is_some() {
|
||||
return self.saved_item;
|
||||
if let Some(ref item) = self.saved_item {
|
||||
return Some(item.clone());
|
||||
}
|
||||
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
|
||||
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
use crate::NotedeckTextStyle;
|
||||
|
||||
pub const NARROW_SCREEN_WIDTH: f32 = 550.0;
|
||||
|
||||
pub fn richtext_small<S>(text: S) -> egui::RichText
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
egui::RichText::new(text).text_style(NotedeckTextStyle::Small.text_style())
|
||||
}
|
||||
|
||||
/// Determine if the screen is narrow. This is useful for detecting mobile
|
||||
/// contexts, but with the nuance that we may also have a wide android tablet.
|
||||
pub fn is_narrow(ctx: &egui::Context) -> bool {
|
||||
let screen_size = ctx.input(|c| c.screen_rect().size());
|
||||
screen_size.x < 550.0
|
||||
screen_size.x < NARROW_SCREEN_WIDTH
|
||||
}
|
||||
|
||||
pub fn is_oled() -> bool {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn get_wallet_for<'a>(
|
||||
global_wallet.wallet.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_current_wallet<'a>(
|
||||
pub fn get_current_wallet_mut<'a>(
|
||||
accounts: &'a mut Accounts,
|
||||
global_wallet: &'a mut GlobalWallet,
|
||||
) -> Option<&'a mut ZapWallet> {
|
||||
@@ -35,6 +35,17 @@ pub fn get_current_wallet<'a>(
|
||||
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)]
|
||||
pub enum WalletType {
|
||||
Auto,
|
||||
|
||||
@@ -16,6 +16,7 @@ egui = { workspace = true }
|
||||
notedeck_columns = { workspace = true }
|
||||
notedeck_ui = { workspace = true }
|
||||
notedeck_dave = { workspace = true }
|
||||
notedeck_notebook = { workspace = true }
|
||||
notedeck = { workspace = true }
|
||||
nostrdb = { workspace = true }
|
||||
puffin = { workspace = true, optional = true }
|
||||
@@ -63,6 +64,12 @@ short_description = "The nostr browser"
|
||||
identifier = "com.damus.notedeck"
|
||||
icon = ["assets/app_icon.icns"]
|
||||
|
||||
[package.metadata.android.manifest.queries]
|
||||
intent = [
|
||||
{ action = ["android.intent.action.MAIN"] },
|
||||
]
|
||||
|
||||
|
||||
[package.metadata.android]
|
||||
package = "com.damus.app"
|
||||
apk_name = "Notedeck"
|
||||
|
||||
@@ -23,9 +23,16 @@
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.level"
|
||||
android:required="true"
|
||||
android:version="1" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
//use egui_android::run_android;
|
||||
|
||||
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;
|
||||
|
||||
#[no_mangle]
|
||||
@@ -67,31 +65,8 @@ pub async fn android_main(app: AndroidApp) {
|
||||
Box::new(move |cc| {
|
||||
let ctx = &cc.egui_ctx;
|
||||
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
||||
setup_chrome(ctx, ¬edeck.args(), notedeck.theme());
|
||||
|
||||
let context = &mut notedeck.app_context();
|
||||
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
||||
let columns = Damus::new(context, &app_args);
|
||||
let mut chrome = Chrome::new();
|
||||
|
||||
// ensure we recognized all the arguments
|
||||
let completely_unrecognized: Vec<String> = notedeck
|
||||
.unrecognized_args()
|
||||
.intersection(columns.unrecognized_args())
|
||||
.cloned()
|
||||
.collect();
|
||||
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.setup(ctx);
|
||||
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
||||
notedeck.set_app(chrome);
|
||||
|
||||
Ok(Box::new(notedeck))
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use notedeck::{AppAction, AppContext};
|
||||
use notedeck_columns::Damus;
|
||||
use notedeck_dave::Dave;
|
||||
use notedeck_notebook::Notebook;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum NotedeckApp {
|
||||
Dave(Dave),
|
||||
Columns(Damus),
|
||||
Dave(Box<Dave>),
|
||||
Columns(Box<Damus>),
|
||||
Notebook(Box<Notebook>),
|
||||
Other(Box<dyn notedeck::App>),
|
||||
}
|
||||
|
||||
@@ -14,6 +16,7 @@ impl notedeck::App for NotedeckApp {
|
||||
match self {
|
||||
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
||||
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
||||
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
|
||||
NotedeckApp::Other(other) => other.update(ctx, ui),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,22 @@
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
//use wasm_bindgen::prelude::*;
|
||||
use crate::app::NotedeckApp;
|
||||
use eframe::CreationContext;
|
||||
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
use notedeck::Error;
|
||||
use notedeck::{
|
||||
tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
|
||||
tr, App, AppAction, AppContext, Localization, Notedeck, NotedeckOptions, NotedeckTextStyle,
|
||||
UserAccount, WalletType,
|
||||
};
|
||||
use notedeck_columns::{
|
||||
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
|
||||
column::SelectionResult,
|
||||
timeline::{kind::ListKind, TimelineKind},
|
||||
Damus,
|
||||
};
|
||||
use notedeck_dave::{Dave, DaveAvatar};
|
||||
use notedeck_notebook::Notebook;
|
||||
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
|
||||
|
||||
static ICON_WIDTH: f32 = 40.0;
|
||||
@@ -58,6 +64,7 @@ pub enum ChromePanelAction {
|
||||
Wallet,
|
||||
Toolbar(ToolbarAction),
|
||||
SaveTheme(ThemePreference),
|
||||
Profile(notedeck::enostr::Pubkey),
|
||||
}
|
||||
|
||||
impl ChromePanelAction {
|
||||
@@ -111,10 +118,8 @@ impl ChromePanelAction {
|
||||
fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
|
||||
match self {
|
||||
Self::SaveTheme(theme) => {
|
||||
ui.ctx().options_mut(|o| {
|
||||
o.theme_preference = *theme;
|
||||
});
|
||||
ctx.theme.save(*theme);
|
||||
ui.ctx().set_theme(*theme);
|
||||
ctx.settings.set_theme(*theme);
|
||||
}
|
||||
|
||||
Self::Toolbar(toolbar_action) => match toolbar_action {
|
||||
@@ -150,7 +155,7 @@ impl ChromePanelAction {
|
||||
}
|
||||
|
||||
Self::Settings => {
|
||||
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Relays);
|
||||
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Settings);
|
||||
}
|
||||
|
||||
Self::Wallet => {
|
||||
@@ -160,13 +165,56 @@ impl ChromePanelAction {
|
||||
notedeck_columns::Route::Wallet(WalletType::Auto),
|
||||
);
|
||||
}
|
||||
Self::Profile(pk) => {
|
||||
columns_route_to_profile(pk, chrome, ctx, ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Some people have been running notedeck in debug, let's catch that!
|
||||
fn stop_debug_mode(options: NotedeckOptions) {
|
||||
if !options.contains(NotedeckOptions::Tests)
|
||||
&& cfg!(debug_assertions)
|
||||
&& !options.contains(NotedeckOptions::Debug)
|
||||
{
|
||||
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
||||
println!(
|
||||
"It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want."
|
||||
);
|
||||
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
||||
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
||||
println!("---------------------------------");
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
impl Chrome {
|
||||
pub fn new() -> Self {
|
||||
Chrome::default()
|
||||
/// Create a new chrome with the default app setup
|
||||
pub fn new_with_apps(
|
||||
cc: &CreationContext,
|
||||
app_args: &[String],
|
||||
notedeck: &mut Notedeck,
|
||||
) -> Result<Self, Error> {
|
||||
stop_debug_mode(notedeck.options());
|
||||
|
||||
let context = &mut notedeck.app_context();
|
||||
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
||||
let columns = Damus::new(context, app_args);
|
||||
let mut chrome = Chrome::default();
|
||||
|
||||
notedeck.check_args(columns.unrecognized_args())?;
|
||||
|
||||
chrome.add_app(NotedeckApp::Columns(Box::new(columns)));
|
||||
chrome.add_app(NotedeckApp::Dave(Box::new(dave)));
|
||||
|
||||
if notedeck.has_option(NotedeckOptions::FeatureNotebook) {
|
||||
chrome.add_app(NotedeckApp::Notebook(Box::default()));
|
||||
}
|
||||
|
||||
chrome.set_active(0);
|
||||
|
||||
Ok(chrome)
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self) {
|
||||
@@ -197,6 +245,16 @@ impl Chrome {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_notebook(&mut self) -> Option<&mut Notebook> {
|
||||
for app in &mut self.apps {
|
||||
if let NotedeckApp::Notebook(notebook) = app {
|
||||
return Some(notebook);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn switch_to_dave(&mut self) {
|
||||
for (i, app) in self.apps.iter().enumerate() {
|
||||
if let NotedeckApp::Dave(_) = app {
|
||||
@@ -205,6 +263,14 @@ impl Chrome {
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_to_notebook(&mut self) {
|
||||
for (i, app) in self.apps.iter().enumerate() {
|
||||
if let NotedeckApp::Notebook(_) = app {
|
||||
self.active = i as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_to_columns(&mut self) {
|
||||
for (i, app) in self.apps.iter().enumerate() {
|
||||
if let NotedeckApp::Columns(_) = app {
|
||||
@@ -321,7 +387,12 @@ impl Chrome {
|
||||
});
|
||||
|
||||
strip.cell(|ui| {
|
||||
if let Some(action) = self.toolbar(ui) {
|
||||
let pk = ctx.accounts.get_selected_account().key.pubkey;
|
||||
|
||||
let unseen_notification =
|
||||
unseen_notification(self.get_columns_app(), ctx.ndb, pk);
|
||||
|
||||
if let Some(action) = self.toolbar(ui, unseen_notification) {
|
||||
got_action = Some(ChromePanelAction::Toolbar(action))
|
||||
}
|
||||
});
|
||||
@@ -330,7 +401,7 @@ impl Chrome {
|
||||
got_action
|
||||
}
|
||||
|
||||
fn toolbar(&mut self, ui: &mut egui::Ui) -> Option<ToolbarAction> {
|
||||
fn toolbar(&mut self, ui: &mut egui::Ui, unseen_notification: bool) -> Option<ToolbarAction> {
|
||||
use egui_tabs::{TabColor, Tabs};
|
||||
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
@@ -374,7 +445,9 @@ impl Chrome {
|
||||
action = Some(ToolbarAction::Dave);
|
||||
}
|
||||
}
|
||||
} else if index == 2 && notifications_button(ui, btn_size).clicked() {
|
||||
} else if index == 2
|
||||
&& notifications_button(ui, btn_size, unseen_notification).clicked()
|
||||
{
|
||||
action = Some(ToolbarAction::Notifications);
|
||||
}
|
||||
|
||||
@@ -426,14 +499,12 @@ impl Chrome {
|
||||
ui.add(milestone_name(i18n));
|
||||
ui.add_space(16.0);
|
||||
//let dark_mode = ui.ctx().style().visuals.dark_mode;
|
||||
{
|
||||
if columns_button(ui)
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.clicked()
|
||||
{
|
||||
self.active = 0;
|
||||
}
|
||||
}
|
||||
ui.add_space(32.0);
|
||||
|
||||
if let Some(dave) = self.get_dave() {
|
||||
@@ -444,8 +515,50 @@ impl Chrome {
|
||||
self.switch_to_dave();
|
||||
}
|
||||
}
|
||||
//ui.add_space(32.0);
|
||||
|
||||
if let Some(_notebook) = self.get_notebook() {
|
||||
if notebook_button(ui)
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.clicked()
|
||||
{
|
||||
self.switch_to_notebook();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unseen_notification(
|
||||
columns: Option<&mut Damus>,
|
||||
ndb: &nostrdb::Ndb,
|
||||
current_pk: notedeck::enostr::Pubkey,
|
||||
) -> bool {
|
||||
let Some(columns) = columns else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(tl) = columns
|
||||
.timeline_cache
|
||||
.get_mut(&TimelineKind::Notifications(current_pk))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let freshness = &mut tl.current_view_mut().freshness;
|
||||
freshness.update(|timestamp_last_viewed| {
|
||||
let filter = notedeck_columns::timeline::kind::notifications_filter(¤t_pk)
|
||||
.since_mut(timestamp_last_viewed);
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
|
||||
let Some(res) = ndb.query(&txn, &[filter], 1).ok() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
!res.is_empty()
|
||||
});
|
||||
|
||||
freshness.has_unseen()
|
||||
}
|
||||
|
||||
impl notedeck::App for Chrome {
|
||||
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
|
||||
@@ -500,6 +613,7 @@ fn expanding_button(
|
||||
light_img: egui::Image,
|
||||
dark_img: egui::Image,
|
||||
ui: &mut egui::Ui,
|
||||
unseen_indicator: bool,
|
||||
) -> egui::Response {
|
||||
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
||||
let img = if ui.visuals().dark_mode {
|
||||
@@ -511,16 +625,34 @@ fn expanding_button(
|
||||
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
|
||||
|
||||
let cur_img_size = helper.scale_1d_pos(img_size);
|
||||
img.paint_at(
|
||||
ui,
|
||||
helper
|
||||
|
||||
let paint_rect = helper
|
||||
.get_animation_rect()
|
||||
.shrink((max_size - cur_img_size) / 2.0),
|
||||
);
|
||||
.shrink((max_size - cur_img_size) / 2.0);
|
||||
img.paint_at(ui, paint_rect);
|
||||
|
||||
if unseen_indicator {
|
||||
paint_unseen_indicator(ui, paint_rect, helper.scale_1d_pos(3.0));
|
||||
}
|
||||
|
||||
helper.take_animation_response()
|
||||
}
|
||||
|
||||
fn paint_unseen_indicator(ui: &mut egui::Ui, rect: egui::Rect, radius: f32) {
|
||||
let center = rect.center();
|
||||
let top_right = rect.right_top();
|
||||
let distance = center.distance(top_right);
|
||||
let midpoint = {
|
||||
let mut cur = center;
|
||||
cur.x += distance / 2.0;
|
||||
cur.y -= distance / 2.0;
|
||||
cur
|
||||
};
|
||||
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.circle_filled(midpoint, radius, notedeck_ui::colors::PINK);
|
||||
}
|
||||
|
||||
fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
||||
expanding_button(
|
||||
"help-button",
|
||||
@@ -528,6 +660,7 @@ fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
||||
app_images::help_light_image(),
|
||||
app_images::help_dark_image(),
|
||||
ui,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -538,16 +671,18 @@ fn settings_button(ui: &mut egui::Ui) -> egui::Response {
|
||||
app_images::settings_light_image(),
|
||||
app_images::settings_dark_image(),
|
||||
ui,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn notifications_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
||||
fn notifications_button(ui: &mut egui::Ui, size: f32, unseen_indicator: bool) -> egui::Response {
|
||||
expanding_button(
|
||||
"notifications-button",
|
||||
size,
|
||||
app_images::notifications_light_image(),
|
||||
app_images::notifications_dark_image(),
|
||||
ui,
|
||||
unseen_indicator,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -558,6 +693,7 @@ fn home_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
||||
app_images::home_light_image(),
|
||||
app_images::home_dark_image(),
|
||||
ui,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -568,6 +704,29 @@ fn columns_button(ui: &mut egui::Ui) -> egui::Response {
|
||||
app_images::columns_image(),
|
||||
app_images::columns_image(),
|
||||
ui,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
|
||||
expanding_button(
|
||||
"accounts-button",
|
||||
24.0,
|
||||
app_images::accounts_image().tint(ui.visuals().text_color()),
|
||||
app_images::accounts_image(),
|
||||
ui,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
|
||||
expanding_button(
|
||||
"notebook-button",
|
||||
40.0,
|
||||
app_images::algo_image(),
|
||||
app_images::algo_image(),
|
||||
ui,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -681,11 +840,12 @@ fn chrome_handle_app_action(
|
||||
ctx.global_wallet,
|
||||
ctx.zaps,
|
||||
ctx.img_cache,
|
||||
&mut columns.view_state,
|
||||
ui,
|
||||
);
|
||||
|
||||
if let Some(action) = m_action {
|
||||
let col = cols.column_mut(0);
|
||||
let col = cols.selected_mut();
|
||||
|
||||
action.process(&mut col.router, &mut col.sheet_router);
|
||||
}
|
||||
@@ -693,6 +853,60 @@ fn chrome_handle_app_action(
|
||||
}
|
||||
}
|
||||
|
||||
fn columns_route_to_profile(
|
||||
pk: ¬edeck::enostr::Pubkey,
|
||||
chrome: &mut Chrome,
|
||||
ctx: &mut AppContext,
|
||||
ui: &mut egui::Ui,
|
||||
) {
|
||||
chrome.switch_to_columns();
|
||||
let Some(columns) = chrome.get_columns_app() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let cols = columns
|
||||
.decks_cache
|
||||
.active_columns_mut(ctx.i18n, ctx.accounts)
|
||||
.unwrap();
|
||||
|
||||
let router = cols.get_selected_router();
|
||||
if router.routes().iter().any(|r| {
|
||||
matches!(
|
||||
r,
|
||||
notedeck_columns::Route::Timeline(TimelineKind::Profile(_))
|
||||
)
|
||||
}) {
|
||||
router.go_back();
|
||||
return;
|
||||
}
|
||||
|
||||
let txn = Transaction::new(ctx.ndb).unwrap();
|
||||
let m_action = notedeck_columns::actionbar::execute_and_process_note_action(
|
||||
notedeck::NoteAction::Profile(*pk),
|
||||
ctx.ndb,
|
||||
cols,
|
||||
0,
|
||||
&mut columns.timeline_cache,
|
||||
&mut columns.threads,
|
||||
ctx.note_cache,
|
||||
ctx.pool,
|
||||
&txn,
|
||||
ctx.unknown_ids,
|
||||
ctx.accounts,
|
||||
ctx.global_wallet,
|
||||
ctx.zaps,
|
||||
ctx.img_cache,
|
||||
&mut columns.view_state,
|
||||
ui,
|
||||
);
|
||||
|
||||
if let Some(action) = m_action {
|
||||
let col = cols.selected_mut();
|
||||
|
||||
action.process(&mut col.router, &mut col.sheet_router);
|
||||
}
|
||||
}
|
||||
|
||||
fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
|
||||
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
||||
let helper = AnimationHelper::new(ui, "pfp-button", egui::vec2(max_size, max_size));
|
||||
@@ -708,6 +922,38 @@ fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.put(helper.get_animation_rect(), &mut widget);
|
||||
|
||||
helper.take_animation_response()
|
||||
|
||||
// let selected = ctx.accounts.cache.selected();
|
||||
|
||||
// pfp_resp.context_menu(|ui| {
|
||||
// for (pk, account) in &ctx.accounts.cache {
|
||||
// let profile = ctx.ndb.get_profile_by_pubkey(&txn, pk).ok();
|
||||
// let is_selected = *pk == selected.key.pubkey;
|
||||
// let has_nsec = account.key.secret_key.is_some();
|
||||
|
||||
// let profile_peview_view = {
|
||||
// let max_size = egui::vec2(ui.available_width(), 77.0);
|
||||
// let resp = ui.allocate_response(max_size, egui::Sense::click());
|
||||
// ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| {
|
||||
// ui.add(
|
||||
// &mut ProfilePic::new(ctx.img_cache, get_profile_url(profile.as_ref()))
|
||||
// .size(24.0),
|
||||
// )
|
||||
// })
|
||||
// };
|
||||
|
||||
// // if let Some(op) = profile_peview_view {
|
||||
// // return_op = Some(match op {
|
||||
// // ProfilePreviewAction::SwitchTo => AccountsViewResponse::SelectAccount(*pk),
|
||||
// // ProfilePreviewAction::RemoveAccount => AccountsViewResponse::RemoveAccount(*pk),
|
||||
// // });
|
||||
// // }
|
||||
// }
|
||||
// // if ui.menu_image_button(image, add_contents).clicked() {
|
||||
// // // ui.ctx().copy_text(url.to_owned());
|
||||
// // ui.close_menu();
|
||||
// // }
|
||||
// });
|
||||
}
|
||||
|
||||
/// The section of the chrome sidebar that starts at the
|
||||
@@ -720,6 +966,7 @@ fn bottomup_sidebar(
|
||||
ui.add_space(8.0);
|
||||
|
||||
let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
let accounts_resp = accounts_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
let theme_action = match ui.ctx().theme() {
|
||||
@@ -761,7 +1008,7 @@ fn bottomup_sidebar(
|
||||
.add(wallet_button())
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
if ctx.args.debug {
|
||||
if ctx.args.options.contains(NotedeckOptions::Debug) {
|
||||
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
|
||||
ui.weak(format!(
|
||||
"{:10.1}",
|
||||
@@ -791,6 +1038,9 @@ fn bottomup_sidebar(
|
||||
}
|
||||
|
||||
if pfp_resp.clicked() {
|
||||
let pk = ctx.accounts.get_selected_account().key.pubkey;
|
||||
Some(ChromePanelAction::Profile(pk))
|
||||
} else if accounts_resp.clicked() {
|
||||
Some(ChromePanelAction::Account)
|
||||
} else if settings_resp.clicked() {
|
||||
Some(ChromePanelAction::Settings)
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
use egui::{FontData, FontDefinitions, FontTweak};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::debug;
|
||||
|
||||
use notedeck::fonts::NamedFontFamily;
|
||||
|
||||
// Use gossip's approach to font loading. This includes japanese fonts
|
||||
// for rending stuff from japanese users.
|
||||
pub fn setup_fonts(ctx: &egui::Context) {
|
||||
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
|
||||
let mut families = BTreeMap::new();
|
||||
|
||||
font_data.insert(
|
||||
"Onest".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestMedium".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestBold".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
/*
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||
);
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
*/
|
||||
|
||||
font_data.insert(
|
||||
"Inconsolata".to_owned(),
|
||||
Arc::new(
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/Inconsolata-Regular.ttf"
|
||||
))
|
||||
.tweak(FontTweak {
|
||||
scale: 1.22, // This font is smaller than DejaVuSans
|
||||
y_offset_factor: -0.18, // and too low
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansCJK".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
|
||||
))),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansThai".to_owned(),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoSansThai-Regular.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
// Some good looking emojis. Use as first priority:
|
||||
font_data.insert(
|
||||
"NotoEmoji".to_owned(),
|
||||
Arc::new(
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoEmoji-Regular.ttf"
|
||||
))
|
||||
.tweak(FontTweak {
|
||||
scale: 1.1, // make them a touch larger
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let base_fonts = vec![
|
||||
"DejaVuSans".to_owned(),
|
||||
"NotoEmoji".to_owned(),
|
||||
"NotoSansCJK".to_owned(),
|
||||
"NotoSansThai".to_owned(),
|
||||
];
|
||||
|
||||
let mut proportional = vec!["Onest".to_owned()];
|
||||
proportional.extend(base_fonts.clone());
|
||||
|
||||
let mut medium = vec!["OnestMedium".to_owned()];
|
||||
medium.extend(base_fonts.clone());
|
||||
|
||||
let mut mono = vec!["Inconsolata".to_owned()];
|
||||
mono.extend(base_fonts.clone());
|
||||
|
||||
let mut bold = vec!["OnestBold".to_owned()];
|
||||
bold.extend(base_fonts.clone());
|
||||
|
||||
let emoji = vec!["NotoEmoji".to_owned()];
|
||||
|
||||
families.insert(egui::FontFamily::Proportional, proportional);
|
||||
families.insert(egui::FontFamily::Monospace, mono);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||
medium,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
bold,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
||||
emoji,
|
||||
);
|
||||
|
||||
debug!("fonts: {:?}", families);
|
||||
|
||||
let defs = FontDefinitions {
|
||||
font_data,
|
||||
families,
|
||||
};
|
||||
|
||||
ctx.set_fonts(defs);
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
pub mod fonts;
|
||||
pub mod setup;
|
||||
pub mod theme;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
|
||||
@@ -10,12 +10,7 @@ static GLOBAL: AccountingAllocator<std::alloc::System> =
|
||||
AccountingAllocator::new(std::alloc::System);
|
||||
|
||||
use notedeck::{DataPath, DataPathType, Notedeck};
|
||||
use notedeck_chrome::{
|
||||
setup::{generate_native_options, setup_chrome},
|
||||
Chrome, NotedeckApp,
|
||||
};
|
||||
use notedeck_columns::Damus;
|
||||
use notedeck_dave::Dave;
|
||||
use notedeck_chrome::{setup::generate_native_options, Chrome};
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -91,29 +86,8 @@ async fn main() {
|
||||
let ctx = &cc.egui_ctx;
|
||||
|
||||
let mut notedeck = Notedeck::new(ctx, base_path, &args);
|
||||
|
||||
let mut chrome = Chrome::new();
|
||||
let columns = Damus::new(&mut notedeck.app_context(), &args);
|
||||
let dave = Dave::new(cc.wgpu_render_state.as_ref());
|
||||
|
||||
setup_chrome(ctx, notedeck.args(), notedeck.theme());
|
||||
|
||||
// ensure we recognized all the arguments
|
||||
let completely_unrecognized: Vec<String> = notedeck
|
||||
.unrecognized_args()
|
||||
.intersection(columns.unrecognized_args())
|
||||
.cloned()
|
||||
.collect();
|
||||
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.setup(ctx);
|
||||
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
|
||||
notedeck.set_app(chrome);
|
||||
|
||||
Ok(Box::new(notedeck))
|
||||
@@ -147,7 +121,8 @@ pub fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Damus, Notedeck};
|
||||
use super::Notedeck;
|
||||
use notedeck_columns::Damus;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn create_tmp_dir() -> PathBuf {
|
||||
@@ -212,17 +187,6 @@ mod tests {
|
||||
let mut app_ctx = notedeck.app_context();
|
||||
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);
|
||||
|
||||
let tl1 = app
|
||||
|
||||
@@ -38,7 +38,13 @@ impl PreviewRunner {
|
||||
"unrecognized args: {:?}",
|
||||
notedeck.unrecognized_args()
|
||||
);
|
||||
setup_chrome(ctx, notedeck.args(), notedeck.theme());
|
||||
setup_chrome(
|
||||
ctx,
|
||||
notedeck.args(),
|
||||
notedeck.theme(),
|
||||
notedeck.note_body_font_size(),
|
||||
notedeck.zoom_factor(),
|
||||
);
|
||||
|
||||
notedeck.set_app(PreviewApp::new(preview));
|
||||
|
||||
|
||||
@@ -1,55 +1,6 @@
|
||||
use crate::{fonts, theme};
|
||||
|
||||
use eframe::NativeOptions;
|
||||
use egui::ThemePreference;
|
||||
use notedeck::{AppSizeHandler, DataPath};
|
||||
use notedeck_ui::app_images;
|
||||
use tracing::info;
|
||||
|
||||
pub fn setup_chrome(ctx: &egui::Context, args: ¬edeck::Args, theme: ThemePreference) {
|
||||
let is_mobile = args
|
||||
.is_mobile
|
||||
.unwrap_or(notedeck::ui::is_compiled_as_mobile());
|
||||
|
||||
let is_oled = notedeck::ui::is_oled();
|
||||
|
||||
// Some people have been running notedeck in debug, let's catch that!
|
||||
if !args.tests && cfg!(debug_assertions) && !args.debug {
|
||||
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
||||
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
|
||||
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
||||
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
||||
println!("---------------------------------");
|
||||
panic!();
|
||||
}
|
||||
|
||||
ctx.options_mut(|o| {
|
||||
info!("Loaded theme {:?} from disk", theme);
|
||||
o.theme_preference = theme;
|
||||
});
|
||||
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
|
||||
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
|
||||
setup_cc(ctx, is_mobile);
|
||||
}
|
||||
|
||||
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) {
|
||||
fonts::setup_fonts(ctx);
|
||||
|
||||
if notedeck::ui::is_compiled_as_mobile() {
|
||||
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
|
||||
}
|
||||
//ctx.set_pixels_per_point(1.0);
|
||||
//
|
||||
//
|
||||
//ctx.tessellation_options_mut(|to| to.feathering = false);
|
||||
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
|
||||
ctx.options_mut(|o| {
|
||||
o.input_options.max_click_duration = 0.4;
|
||||
});
|
||||
ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style));
|
||||
}
|
||||
|
||||
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
||||
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
use egui::{style::Interaction, Color32, FontId, Style, Visuals};
|
||||
use notedeck::{ColorTheme, NotedeckTextStyle};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
||||
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
||||
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
||||
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
||||
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
||||
|
||||
// BACKGROUNDS
|
||||
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
||||
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
||||
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
||||
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
||||
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
||||
|
||||
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
||||
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
||||
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
||||
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
||||
|
||||
pub fn desktop_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: DARKER_BG,
|
||||
extreme_bg_color: DARK_ISH_BG,
|
||||
text_color: Color32::WHITE,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: DARK_ISH_BG,
|
||||
window_stroke_color: DARK_BG,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: DARK_ISH_BG,
|
||||
noninteractive_weak_bg_fill: DARK_BG,
|
||||
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||
inactive_weak_bg_fill: SEMI_DARK_BG,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mobile_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
panel_fill: Color32::BLACK,
|
||||
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||
..desktop_dark_color_theme()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: Color32::WHITE,
|
||||
extreme_bg_color: LIGHTER_GRAY,
|
||||
text_color: BLACK,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: Color32::WHITE,
|
||||
window_stroke_color: DARKER_GRAY,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: Color32::WHITE,
|
||||
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
||||
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
||||
inactive_bg_fill: LIGHTER_GRAY,
|
||||
inactive_weak_bg_fill: LIGHTER_GRAY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light_mode() -> Visuals {
|
||||
notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light())
|
||||
}
|
||||
|
||||
pub fn dark_mode(is_oled: bool) -> Visuals {
|
||||
notedeck::theme::create_themed_visuals(
|
||||
if is_oled {
|
||||
mobile_dark_color_theme()
|
||||
} else {
|
||||
desktop_dark_color_theme()
|
||||
},
|
||||
Visuals::dark(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create custom text sizes for any FontSizes
|
||||
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
||||
let font_size = if is_mobile {
|
||||
notedeck::fonts::mobile_font_size
|
||||
} else {
|
||||
notedeck::fonts::desktop_font_size
|
||||
};
|
||||
|
||||
style.text_styles = NotedeckTextStyle::iter()
|
||||
.map(|text_style| {
|
||||
(
|
||||
text_style.text_style(),
|
||||
FontId::new(font_size(&text_style), text_style.font_family()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
style.interaction = Interaction {
|
||||
tooltip_delay: 0.1,
|
||||
show_tooltips_only_when_still: false,
|
||||
..Interaction::default()
|
||||
};
|
||||
|
||||
// debug: show callstack for the current widget on hover if all
|
||||
// modifier keys are pressed down.
|
||||
#[cfg(feature = "debug-widget-callstack")]
|
||||
{
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_error!(
|
||||
"The `debug-widget-callstack` feature requires a debug build, \
|
||||
release builds are unsupported."
|
||||
);
|
||||
style.debug.debug_on_hover_with_all_modifiers = true;
|
||||
}
|
||||
|
||||
// debug: show an overlay on all interactive widgets
|
||||
#[cfg(feature = "debug-interactive-widgets")]
|
||||
{
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_error!(
|
||||
"The `debug-interactive-widgets` feature requires a debug build, \
|
||||
release builds are unsupported."
|
||||
);
|
||||
style.debug.show_interactive_widgets = true;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ description = "A tweetdeck-style notedeck app"
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
opener = { workspace = true }
|
||||
rmpv = { workspace = true }
|
||||
bech32 = { workspace = true }
|
||||
notedeck = { workspace = true }
|
||||
@@ -31,7 +32,7 @@ image = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
nostrdb = { workspace = true }
|
||||
notedeck_ui = { workspace = true }
|
||||
open = { workspace = true }
|
||||
robius-open = { workspace = true }
|
||||
poll-promise = { workspace = true }
|
||||
puffin = { workspace = true, optional = true }
|
||||
puffin_egui = { workspace = true, optional = true }
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
},
|
||||
ThreadSelection, TimelineCache, TimelineKind,
|
||||
},
|
||||
view_state::ViewState,
|
||||
};
|
||||
|
||||
use enostr::{NoteId, Pubkey, RelayPool};
|
||||
@@ -16,6 +17,7 @@ use notedeck::{
|
||||
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
||||
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
||||
};
|
||||
use notedeck_ui::media::MediaViewerFlags;
|
||||
use tracing::error;
|
||||
|
||||
pub struct NewNotes {
|
||||
@@ -51,6 +53,7 @@ fn execute_note_action(
|
||||
global_wallet: &mut GlobalWallet,
|
||||
zaps: &mut Zaps,
|
||||
images: &mut Images,
|
||||
view_state: &mut ViewState,
|
||||
router_type: RouterType,
|
||||
ui: &mut egui::Ui,
|
||||
col: usize,
|
||||
@@ -153,7 +156,16 @@ fn execute_note_action(
|
||||
}
|
||||
},
|
||||
NoteAction::Media(media_action) => {
|
||||
media_action.process(images);
|
||||
media_action.on_view_media(|medias| {
|
||||
view_state.media_viewer.media_info = medias.clone();
|
||||
tracing::debug!("on_view_media {:?}", &medias);
|
||||
view_state
|
||||
.media_viewer
|
||||
.flags
|
||||
.set(MediaViewerFlags::Open, true);
|
||||
});
|
||||
|
||||
media_action.process_default_media_actions(images)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +192,7 @@ pub fn execute_and_process_note_action(
|
||||
global_wallet: &mut GlobalWallet,
|
||||
zaps: &mut Zaps,
|
||||
images: &mut Images,
|
||||
view_state: &mut ViewState,
|
||||
ui: &mut egui::Ui,
|
||||
) -> Option<RouterAction> {
|
||||
let router_type = {
|
||||
@@ -204,6 +217,7 @@ pub fn execute_and_process_note_action(
|
||||
global_wallet,
|
||||
zaps,
|
||||
images,
|
||||
view_state,
|
||||
router_type,
|
||||
ui,
|
||||
col,
|
||||
|
||||
@@ -10,19 +10,21 @@ use crate::{
|
||||
subscriptions::{SubKind, Subscriptions},
|
||||
support::Support,
|
||||
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
||||
ui::{self, DesktopSidePanel, SidePanelAction},
|
||||
ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction},
|
||||
view_state::ViewState,
|
||||
Result,
|
||||
};
|
||||
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{
|
||||
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
|
||||
Localization, UnknownIds,
|
||||
Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds,
|
||||
};
|
||||
use notedeck_ui::{
|
||||
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
|
||||
NoteOptions,
|
||||
};
|
||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
@@ -38,6 +40,7 @@ pub enum DamusState {
|
||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||
pub struct Damus {
|
||||
state: DamusState,
|
||||
|
||||
pub decks_cache: DecksCache,
|
||||
pub view_state: ViewState,
|
||||
pub drafts: Drafts,
|
||||
@@ -77,9 +80,7 @@ fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
|
||||
columns.select_left();
|
||||
}
|
||||
egui::Key::BrowserBack | egui::Key::Escape => {
|
||||
if let Some(column) = columns.selected_mut() {
|
||||
column.router_mut().go_back();
|
||||
}
|
||||
columns.get_selected_router().go_back();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -360,18 +361,54 @@ fn render_damus(
|
||||
app_ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
) -> Option<AppAction> {
|
||||
damus
|
||||
.note_options
|
||||
.set(NoteOptions::Wide, is_narrow(ui.ctx()));
|
||||
|
||||
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
|
||||
render_damus_mobile(damus, app_ctx, ui)
|
||||
} else {
|
||||
render_damus_desktop(damus, app_ctx, ui)
|
||||
};
|
||||
|
||||
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
|
||||
|
||||
// We use this for keeping timestamps and things up to date
|
||||
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
||||
|
||||
app_action
|
||||
}
|
||||
|
||||
/// Present a fullscreen media viewer if the FullscreenMedia AppOptions flag is set. This is
|
||||
/// typically set by image carousels using a MediaAction's on_view_media callback when
|
||||
/// an image is clicked
|
||||
fn fullscreen_media_viewer_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut MediaViewerState,
|
||||
img_cache: &mut Images,
|
||||
) {
|
||||
if !state.should_show(ui) {
|
||||
if state.scene_rect.is_some() {
|
||||
// if we shouldn't show yet we will have a scene
|
||||
// rect, then we should clear it for next time
|
||||
tracing::debug!("fullscreen_media_viewer_ui: resetting scene rect");
|
||||
state.scene_rect = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = MediaViewer::new(state).fullscreen(true).ui(img_cache, ui);
|
||||
|
||||
if resp.clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
fullscreen_media_close(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the fullscreen media player. This also resets the scene_rect state
|
||||
fn fullscreen_media_close(state: &mut MediaViewerState) {
|
||||
state.flags.set(MediaViewerFlags::Open, false);
|
||||
}
|
||||
|
||||
/*
|
||||
fn determine_key_storage_type() -> KeyStorageType {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -393,47 +430,58 @@ fn determine_key_storage_type() -> KeyStorageType {
|
||||
|
||||
impl Damus {
|
||||
/// Called once before the first frame.
|
||||
pub fn new(ctx: &mut AppContext<'_>, args: &[String]) -> Self {
|
||||
pub fn new(app_context: &mut AppContext<'_>, args: &[String]) -> Self {
|
||||
// arg parsing
|
||||
|
||||
let (parsed_args, unrecognized_args) =
|
||||
ColumnsArgs::parse(args, Some(ctx.accounts.selected_account_pubkey()));
|
||||
ColumnsArgs::parse(args, Some(app_context.accounts.selected_account_pubkey()));
|
||||
|
||||
let account = ctx.accounts.selected_account_pubkey_bytes();
|
||||
let account = app_context.accounts.selected_account_pubkey_bytes();
|
||||
|
||||
let mut timeline_cache = TimelineCache::default();
|
||||
let mut options = AppOptions::default();
|
||||
let tmp_columns = !parsed_args.columns.is_empty();
|
||||
options.set(AppOptions::TmpColumns, tmp_columns);
|
||||
options.set(
|
||||
AppOptions::Debug,
|
||||
app_context.args.options.contains(NotedeckOptions::Debug),
|
||||
);
|
||||
options.set(
|
||||
AppOptions::SinceOptimize,
|
||||
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
|
||||
);
|
||||
|
||||
let decks_cache = if tmp_columns {
|
||||
info!("DecksCache: loading from command line arguments");
|
||||
let mut columns: Columns = Columns::new();
|
||||
let txn = Transaction::new(ctx.ndb).unwrap();
|
||||
let txn = Transaction::new(app_context.ndb).unwrap();
|
||||
for col in &parsed_args.columns {
|
||||
let timeline_kind = col.clone().into_timeline_kind();
|
||||
if let Some(add_result) = columns.add_new_timeline_column(
|
||||
&mut timeline_cache,
|
||||
&txn,
|
||||
ctx.ndb,
|
||||
ctx.note_cache,
|
||||
ctx.pool,
|
||||
app_context.ndb,
|
||||
app_context.note_cache,
|
||||
app_context.pool,
|
||||
&timeline_kind,
|
||||
) {
|
||||
add_result.process(
|
||||
ctx.ndb,
|
||||
ctx.note_cache,
|
||||
app_context.ndb,
|
||||
app_context.note_cache,
|
||||
&txn,
|
||||
&mut timeline_cache,
|
||||
ctx.unknown_ids,
|
||||
app_context.unknown_ids,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
columns_to_decks_cache(ctx.i18n, columns, account)
|
||||
} else if let Some(decks_cache) =
|
||||
crate::storage::load_decks_cache(ctx.path, ctx.ndb, &mut timeline_cache, ctx.i18n)
|
||||
{
|
||||
columns_to_decks_cache(app_context.i18n, columns, account)
|
||||
} else if let Some(decks_cache) = crate::storage::load_decks_cache(
|
||||
app_context.path,
|
||||
app_context.ndb,
|
||||
&mut timeline_cache,
|
||||
app_context.i18n,
|
||||
) {
|
||||
info!(
|
||||
"DecksCache: loading from disk {}",
|
||||
crate::storage::DECKS_CACHE_FILE
|
||||
@@ -441,35 +489,16 @@ impl Damus {
|
||||
decks_cache
|
||||
} else {
|
||||
info!("DecksCache: creating new with demo configuration");
|
||||
DecksCache::new_with_demo_config(&mut timeline_cache, ctx)
|
||||
//for (pk, _) in &ctx.accounts.cache {
|
||||
DecksCache::new_with_demo_config(&mut timeline_cache, app_context)
|
||||
//for (pk, _) in &app_context.accounts.cache {
|
||||
// cache.add_deck_default(*pk);
|
||||
//}
|
||||
};
|
||||
let settings = &app_context.settings;
|
||||
|
||||
let support = Support::new(ctx.path);
|
||||
let mut note_options = NoteOptions::default();
|
||||
note_options.set(
|
||||
NoteOptions::Textmode,
|
||||
parsed_args.is_flag_set(ColumnsFlag::Textmode),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::ScrambleText,
|
||||
parsed_args.is_flag_set(ColumnsFlag::Scramble),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::HideMedia,
|
||||
parsed_args.is_flag_set(ColumnsFlag::NoMedia),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::ShowNoteClient,
|
||||
parsed_args.is_flag_set(ColumnsFlag::ShowNoteClient),
|
||||
);
|
||||
options.set(AppOptions::Debug, ctx.args.debug);
|
||||
options.set(
|
||||
AppOptions::SinceOptimize,
|
||||
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
|
||||
);
|
||||
let support = Support::new(app_context.path);
|
||||
|
||||
let note_options = get_note_options(parsed_args, settings);
|
||||
|
||||
let jobs = JobsCache::default();
|
||||
|
||||
@@ -551,6 +580,39 @@ impl Damus {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions {
|
||||
let mut note_options = NoteOptions::default();
|
||||
|
||||
note_options.set(
|
||||
NoteOptions::Textmode,
|
||||
args.is_flag_set(ColumnsFlag::Textmode),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::ScrambleText,
|
||||
args.is_flag_set(ColumnsFlag::Scramble),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::HideMedia,
|
||||
args.is_flag_set(ColumnsFlag::NoMedia),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::ShowNoteClientTop,
|
||||
ShowSourceClientOption::Top == settings_handler.show_source_client().into()
|
||||
|| args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
|
||||
);
|
||||
note_options.set(
|
||||
NoteOptions::ShowNoteClientBottom,
|
||||
ShowSourceClientOption::Bottom == settings_handler.show_source_client().into()
|
||||
|| args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
|
||||
);
|
||||
|
||||
note_options.set(
|
||||
NoteOptions::RepliesNewestFirst,
|
||||
settings_handler.show_replies_newest_first(),
|
||||
);
|
||||
note_options
|
||||
}
|
||||
|
||||
/*
|
||||
fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
||||
let stroke = ui.style().interact(&response).fg_stroke;
|
||||
@@ -572,6 +634,7 @@ fn render_damus_mobile(
|
||||
let mut app_action: Option<AppAction> = None;
|
||||
|
||||
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
||||
|
||||
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
||||
let r = nav::render_nav(
|
||||
active_col,
|
||||
@@ -662,6 +725,7 @@ fn should_show_compose_button(decks: &DecksCache, accounts: &Accounts) -> bool {
|
||||
Route::Reply(_) => false,
|
||||
Route::Quote(_) => false,
|
||||
Route::Relays => false,
|
||||
Route::Settings => false,
|
||||
Route::ComposeNote => false,
|
||||
Route::AddColumn(_) => false,
|
||||
Route::EditProfile(_) => false,
|
||||
|
||||
@@ -11,7 +11,8 @@ pub enum ColumnsFlag {
|
||||
Textmode,
|
||||
Scramble,
|
||||
NoMedia,
|
||||
ShowNoteClient,
|
||||
ShowNoteClientTop,
|
||||
ShowNoteClientBottom,
|
||||
}
|
||||
|
||||
pub struct ColumnsArgs {
|
||||
@@ -53,8 +54,10 @@ impl ColumnsArgs {
|
||||
res.clear_flag(ColumnsFlag::SinceOptimize);
|
||||
} else if arg == "--scramble" {
|
||||
res.set_flag(ColumnsFlag::Scramble);
|
||||
} else if arg == "--show-note-client" {
|
||||
res.set_flag(ColumnsFlag::ShowNoteClient);
|
||||
} else if arg == "--show-note-client=top" {
|
||||
res.set_flag(ColumnsFlag::ShowNoteClientTop);
|
||||
} else if arg == "--show-note-client=bottom" {
|
||||
res.set_flag(ColumnsFlag::ShowNoteClientBottom);
|
||||
} else if arg == "--no-media" {
|
||||
res.set_flag(ColumnsFlag::NoMedia);
|
||||
} else if arg == "--filter" {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
actionbar::TimelineOpenResult,
|
||||
drag::DragSwitch,
|
||||
route::{Route, Router, SingletonRouter},
|
||||
timeline::{Timeline, TimelineCache, TimelineKind},
|
||||
};
|
||||
@@ -13,6 +14,7 @@ use tracing::warn;
|
||||
pub struct Column {
|
||||
pub router: Router<Route>,
|
||||
pub sheet_router: SingletonRouter<Route>,
|
||||
pub drag: DragSwitch,
|
||||
}
|
||||
|
||||
impl Column {
|
||||
@@ -21,6 +23,7 @@ impl Column {
|
||||
Column {
|
||||
router,
|
||||
sheet_router: SingletonRouter::default(),
|
||||
drag: DragSwitch::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,11 +159,9 @@ impl Columns {
|
||||
|
||||
// Get the first router in the columns if there are columns present.
|
||||
// Otherwise, create a new column picker and return the router
|
||||
pub fn get_first_router(&mut self) -> &mut Router<Route> {
|
||||
if self.columns.is_empty() {
|
||||
self.new_column_picker();
|
||||
}
|
||||
self.columns[0].router_mut()
|
||||
pub fn get_selected_router(&mut self) -> &mut Router<Route> {
|
||||
self.ensure_column();
|
||||
self.selected_mut().router_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -181,16 +182,25 @@ impl Columns {
|
||||
Some(&self.columns[self.selected as usize])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn selected_mut(&mut self) -> Option<&mut Column> {
|
||||
// TODO(jb55): switch to non-empty container for columns?
|
||||
fn ensure_column(&mut self) {
|
||||
if self.columns.is_empty() {
|
||||
return None;
|
||||
self.new_column_picker();
|
||||
}
|
||||
Some(&mut self.columns[self.selected as usize])
|
||||
}
|
||||
|
||||
/// Get the selected column. If you're looking to route something
|
||||
/// and you're not sure which one to choose, use this one
|
||||
#[inline]
|
||||
pub fn selected_mut(&mut self) -> &mut Column {
|
||||
self.ensure_column();
|
||||
assert!(self.selected < self.columns.len() as i32);
|
||||
&mut self.columns[self.selected as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn column_mut(&mut self, ind: usize) -> &mut Column {
|
||||
self.ensure_column();
|
||||
&mut self.columns[ind]
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ impl DecksCache {
|
||||
accounts: ¬edeck::Accounts,
|
||||
) -> Option<&mut Column> {
|
||||
self.active_columns_mut(i18n, accounts)
|
||||
.and_then(|ad| ad.selected_mut())
|
||||
.map(|ad| ad.selected_mut())
|
||||
}
|
||||
|
||||
pub fn selected_column(&self, accounts: ¬edeck::Accounts) -> Option<&Column> {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct DragSwitch {
|
||||
state: Option<DragState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct DragState {
|
||||
start_pos: egui::Pos2,
|
||||
cur_direction: DragDirection,
|
||||
}
|
||||
|
||||
impl DragSwitch {
|
||||
/// should call BEFORE both drag directions get rendered
|
||||
pub fn update(&mut self, horizontal: egui::Id, vertical: egui::Id, ctx: &egui::Context) {
|
||||
let horiz_being_dragged = ctx.is_being_dragged(horizontal);
|
||||
let vert_being_dragged = ctx.is_being_dragged(vertical);
|
||||
|
||||
if !horiz_being_dragged && !vert_being_dragged {
|
||||
self.state = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(state) = &mut self.state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(cur_pos) = ctx.pointer_interact_pos() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dx = (state.start_pos.x - cur_pos.x).abs();
|
||||
let dy = (state.start_pos.y - cur_pos.y).abs();
|
||||
|
||||
let new_direction = if dx > dy {
|
||||
DragDirection::Horizontal
|
||||
} else {
|
||||
DragDirection::Vertical
|
||||
};
|
||||
|
||||
if new_direction == DragDirection::Horizontal
|
||||
&& state.cur_direction == DragDirection::Vertical
|
||||
{
|
||||
// drag is occuring mostly in the horizontal direction
|
||||
ctx.set_dragged_id(horizontal);
|
||||
let new_dir = DragDirection::Horizontal;
|
||||
state.cur_direction = new_dir;
|
||||
} else if new_direction == DragDirection::Vertical
|
||||
&& state.cur_direction == DragDirection::Horizontal
|
||||
{
|
||||
// drag is occuring mostly in the vertical direction
|
||||
let new_dir = DragDirection::Vertical;
|
||||
state.cur_direction = new_dir;
|
||||
ctx.set_dragged_id(vertical);
|
||||
}
|
||||
}
|
||||
|
||||
/// should call AFTER both drag directions rendered
|
||||
pub fn check_for_drag_start(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
horizontal: egui::Id,
|
||||
vertical: egui::Id,
|
||||
) {
|
||||
let Some(drag_id) = ctx.drag_started_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let cur_direction = if drag_id == horizontal {
|
||||
DragDirection::Horizontal
|
||||
} else if drag_id == vertical {
|
||||
DragDirection::Vertical
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(cur_pos) = ctx.pointer_interact_pos() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.state = Some(DragState {
|
||||
start_pos: cur_pos,
|
||||
cur_direction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum DragDirection {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
pub fn get_drag_id(ui: &egui::Ui, scroll_id: egui::Id) -> egui::Id {
|
||||
ui.id().with(egui::Id::new(scroll_id)).with("area")
|
||||
}
|
||||
|
||||
// unfortunately a Frame makes a new id for the Ui
|
||||
pub fn get_drag_id_through_frame(ui: &egui::Ui, scroll_id: egui::Id) -> egui::Id {
|
||||
ui.id()
|
||||
.with(egui::Id::new("child"))
|
||||
.with(egui::Id::new(scroll_id))
|
||||
.with("area")
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod column;
|
||||
mod deck_state;
|
||||
mod decks;
|
||||
mod draft;
|
||||
mod drag;
|
||||
mod key_parsing;
|
||||
pub mod login_manager;
|
||||
mod media_upload;
|
||||
|
||||
@@ -11,7 +11,7 @@ use sha2::{Digest, Sha256};
|
||||
use url::Url;
|
||||
|
||||
use crate::Error;
|
||||
use notedeck_ui::images::fetch_binary_from_disk;
|
||||
use notedeck::media::images::fetch_binary_from_disk;
|
||||
|
||||
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
||||
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
||||
@@ -143,7 +143,7 @@ pub fn nip96_upload(
|
||||
Err(e) => {
|
||||
return Promise::from_ready(Err(Error::Generic(format!(
|
||||
"could not read contents of file to upload: {e}"
|
||||
))))
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -474,12 +474,10 @@ impl TimelineSub {
|
||||
let before = self.state.clone();
|
||||
's: {
|
||||
match &mut self.state {
|
||||
SubState::NoSub { dependers } => {
|
||||
*dependers -= 1;
|
||||
}
|
||||
SubState::NoSub { dependers } => *dependers = dependers.saturating_sub(1),
|
||||
SubState::LocalOnly { local, dependers } => {
|
||||
if *dependers > 1 {
|
||||
*dependers -= 1;
|
||||
*dependers = dependers.saturating_sub(1);
|
||||
break 's;
|
||||
}
|
||||
|
||||
@@ -492,7 +490,7 @@ impl TimelineSub {
|
||||
}
|
||||
SubState::RemoteOnly { remote, dependers } => {
|
||||
if *dependers > 1 {
|
||||
*dependers -= 1;
|
||||
*dependers = dependers.saturating_sub(1);
|
||||
break 's;
|
||||
}
|
||||
|
||||
@@ -502,7 +500,7 @@ impl TimelineSub {
|
||||
}
|
||||
SubState::Unified { unified, dependers } => {
|
||||
if *dependers > 1 {
|
||||
*dependers -= 1;
|
||||
*dependers = dependers.saturating_sub(1);
|
||||
break 's;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,28 @@ use crate::{
|
||||
column::ColumnsAction,
|
||||
deck_state::DeckState,
|
||||
decks::{Deck, DecksAction, DecksCache},
|
||||
drag::{get_drag_id, get_drag_id_through_frame},
|
||||
options::AppOptions,
|
||||
profile::{ProfileAction, SaveProfileChanges},
|
||||
route::{Route, Router, SingletonRouter},
|
||||
timeline::{
|
||||
route::{render_thread_route, render_timeline_route},
|
||||
TimelineCache,
|
||||
TimelineCache, TimelineKind,
|
||||
},
|
||||
ui::{
|
||||
self,
|
||||
add_column::render_add_column_routes,
|
||||
add_column::{render_add_column_routes, AddColumnView},
|
||||
column::NavTitle,
|
||||
configure_deck::ConfigureDeckView,
|
||||
edit_deck::{EditDeckResponse, EditDeckView},
|
||||
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType},
|
||||
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
|
||||
profile::EditProfileView,
|
||||
search::{FocusState, SearchView},
|
||||
settings::SettingsAction,
|
||||
support::SupportView,
|
||||
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
|
||||
RelayView,
|
||||
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
|
||||
TimelineView,
|
||||
},
|
||||
Damus,
|
||||
};
|
||||
@@ -31,8 +34,8 @@ use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, P
|
||||
use enostr::ProfileState;
|
||||
use nostrdb::{Filter, Ndb, Transaction};
|
||||
use notedeck::{
|
||||
get_current_default_msats, get_current_wallet, tr, ui::is_narrow, Accounts, AppContext,
|
||||
NoteAction, NoteContext, RelayAction,
|
||||
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
|
||||
RelayAction,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
@@ -60,6 +63,7 @@ pub enum RenderNavAction {
|
||||
SwitchingAction(SwitchingAction),
|
||||
WalletAction(WalletAction),
|
||||
RelayAction(RelayAction),
|
||||
SettingsAction(SettingsAction),
|
||||
}
|
||||
|
||||
pub enum SwitchingAction {
|
||||
@@ -454,6 +458,7 @@ fn process_render_nav_action(
|
||||
ctx.global_wallet,
|
||||
ctx.zaps,
|
||||
ctx.img_cache,
|
||||
&mut app.view_state,
|
||||
ui,
|
||||
)
|
||||
}
|
||||
@@ -480,6 +485,9 @@ fn process_render_nav_action(
|
||||
.process_relay_action(ui.ctx(), ctx.pool, action);
|
||||
None
|
||||
}
|
||||
RenderNavAction::SettingsAction(action) => {
|
||||
action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx())
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(action) = router_action {
|
||||
@@ -497,13 +505,12 @@ fn process_render_nav_action(
|
||||
fn render_nav_body(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ctx: &mut AppContext,
|
||||
top: &Route,
|
||||
depth: usize,
|
||||
col: usize,
|
||||
inner_rect: egui::Rect,
|
||||
) -> Option<RenderNavAction> {
|
||||
let current_account_has_wallet = get_current_wallet(ctx.accounts, ctx.global_wallet).is_some();
|
||||
let mut note_context = NoteContext {
|
||||
ndb: ctx.ndb,
|
||||
accounts: ctx.accounts,
|
||||
@@ -514,8 +521,8 @@ fn render_nav_body(
|
||||
job_pool: ctx.job_pool,
|
||||
unknown_ids: ctx.unknown_ids,
|
||||
clipboard: ctx.clipboard,
|
||||
current_account_has_wallet,
|
||||
i18n: ctx.i18n,
|
||||
global_wallet: ctx.global_wallet,
|
||||
};
|
||||
match top {
|
||||
Route::Timeline(kind) => {
|
||||
@@ -538,6 +545,8 @@ fn render_nav_body(
|
||||
scroll_to_top,
|
||||
);
|
||||
|
||||
app.timeline_cache.set_fresh(kind);
|
||||
|
||||
// always clear the scroll_to_top request
|
||||
if scroll_to_top {
|
||||
app.options.remove(AppOptions::ScrollToTop);
|
||||
@@ -573,6 +582,15 @@ fn render_nav_body(
|
||||
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
|
||||
.ui(ui)
|
||||
.map(RenderNavAction::RelayAction),
|
||||
|
||||
Route::Settings => SettingsView::new(
|
||||
ctx.settings.get_settings_mut(),
|
||||
&mut note_context,
|
||||
&mut app.note_options,
|
||||
&mut app.jobs,
|
||||
)
|
||||
.ui(ui)
|
||||
.map(RenderNavAction::SettingsAction),
|
||||
Route::Reply(id) => {
|
||||
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
||||
txn
|
||||
@@ -596,15 +614,12 @@ fn render_nav_body(
|
||||
return None;
|
||||
};
|
||||
|
||||
let id = egui::Id::new(("post", col, note.key().unwrap()));
|
||||
let poster = ctx.accounts.selected_filled()?;
|
||||
|
||||
let action = {
|
||||
let draft = app.drafts.reply_mut(note.id());
|
||||
|
||||
let response = egui::ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
ui::PostReplyView::new(
|
||||
let response = ui::PostReplyView::new(
|
||||
&mut note_context,
|
||||
poster,
|
||||
draft,
|
||||
@@ -612,11 +627,9 @@ fn render_nav_body(
|
||||
inner_rect,
|
||||
app.note_options,
|
||||
&mut app.jobs,
|
||||
col,
|
||||
)
|
||||
.id_source(id)
|
||||
.show(ui)
|
||||
})
|
||||
.inner;
|
||||
.show(ui);
|
||||
|
||||
response.action
|
||||
};
|
||||
@@ -637,14 +650,10 @@ fn render_nav_body(
|
||||
return None;
|
||||
};
|
||||
|
||||
let id = egui::Id::new(("post", col, note.key().unwrap()));
|
||||
|
||||
let poster = ctx.accounts.selected_filled()?;
|
||||
let draft = app.drafts.quote_mut(note.id());
|
||||
|
||||
let response = egui::ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
crate::ui::note::QuoteRepostView::new(
|
||||
let response = crate::ui::note::QuoteRepostView::new(
|
||||
&mut note_context,
|
||||
poster,
|
||||
draft,
|
||||
@@ -652,11 +661,9 @@ fn render_nav_body(
|
||||
inner_rect,
|
||||
app.note_options,
|
||||
&mut app.jobs,
|
||||
col,
|
||||
)
|
||||
.id_source(id)
|
||||
.show(ui)
|
||||
})
|
||||
.inner;
|
||||
.show(ui);
|
||||
|
||||
response.action.map(Into::into)
|
||||
}
|
||||
@@ -736,7 +743,7 @@ fn render_nav_body(
|
||||
|
||||
new_deck_state.clear();
|
||||
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
||||
.get_first_router()
|
||||
.get_selected_router()
|
||||
.go_back();
|
||||
}
|
||||
resp
|
||||
@@ -767,7 +774,7 @@ fn render_nav_body(
|
||||
}
|
||||
}
|
||||
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
||||
.get_first_router()
|
||||
.get_selected_router()
|
||||
.go_back();
|
||||
}
|
||||
|
||||
@@ -842,7 +849,7 @@ fn render_nav_body(
|
||||
}
|
||||
};
|
||||
|
||||
WalletView::new(state, ctx.i18n)
|
||||
WalletView::new(state, ctx.i18n, ctx.clipboard)
|
||||
.ui(ui)
|
||||
.map(RenderNavAction::WalletAction)
|
||||
}
|
||||
@@ -929,13 +936,50 @@ pub fn render_nav(
|
||||
}
|
||||
};
|
||||
|
||||
let nav_response = Nav::new(
|
||||
&app.columns(ctx.accounts)
|
||||
let routes = app
|
||||
.columns(ctx.accounts)
|
||||
.column(col)
|
||||
.router()
|
||||
.routes()
|
||||
.clone(),
|
||||
)
|
||||
.clone();
|
||||
let nav = Nav::new(&routes).id_source(egui::Id::new(("nav", col)));
|
||||
|
||||
let drag_ids = 's: {
|
||||
let Some(top_route) = &routes.last().cloned() else {
|
||||
break 's None;
|
||||
};
|
||||
|
||||
let Some(scroll_id) = get_scroll_id(
|
||||
top_route,
|
||||
app.columns(ctx.accounts)
|
||||
.column(col)
|
||||
.router()
|
||||
.routes()
|
||||
.len(),
|
||||
&app.timeline_cache,
|
||||
col,
|
||||
) else {
|
||||
break 's None;
|
||||
};
|
||||
|
||||
let vertical_drag_id = if route_uses_frame(top_route) {
|
||||
get_drag_id_through_frame(ui, scroll_id)
|
||||
} else {
|
||||
get_drag_id(ui, scroll_id)
|
||||
};
|
||||
|
||||
let horizontal_drag_id = nav.drag_id(ui);
|
||||
|
||||
let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.drag;
|
||||
|
||||
drag.update(horizontal_drag_id, vertical_drag_id, ui.ctx());
|
||||
|
||||
Some((horizontal_drag_id, vertical_drag_id))
|
||||
};
|
||||
|
||||
let nav_response = nav
|
||||
.navigating(
|
||||
app.columns_mut(ctx.i18n, ctx.accounts)
|
||||
.column_mut(col)
|
||||
@@ -948,7 +992,6 @@ pub fn render_nav(
|
||||
.router_mut()
|
||||
.returning,
|
||||
)
|
||||
.id_source(egui::Id::new(("nav", col)))
|
||||
.show_mut(ui, |ui, render_type, nav| match render_type {
|
||||
NavUiType::Title => NavTitle::new(
|
||||
ctx.ndb,
|
||||
@@ -971,5 +1014,87 @@ pub fn render_nav(
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((horizontal_drag_id, vertical_drag_id)) = drag_ids {
|
||||
let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.drag;
|
||||
drag.check_for_drag_start(ui.ctx(), horizontal_drag_id, vertical_drag_id);
|
||||
}
|
||||
|
||||
RenderNavResponse::new(col, NotedeckNavResponse::Nav(Box::new(nav_response)))
|
||||
}
|
||||
|
||||
fn get_scroll_id(
|
||||
top: &Route,
|
||||
depth: usize,
|
||||
timeline_cache: &TimelineCache,
|
||||
col: usize,
|
||||
) -> Option<egui::Id> {
|
||||
match top {
|
||||
Route::Timeline(timeline_kind) => match timeline_kind {
|
||||
TimelineKind::List(_)
|
||||
| TimelineKind::Search(_)
|
||||
| TimelineKind::Algo(_)
|
||||
| TimelineKind::Notifications(_)
|
||||
| TimelineKind::Universe
|
||||
| TimelineKind::Hashtag(_)
|
||||
| TimelineKind::Generic(_) => {
|
||||
TimelineView::scroll_id(timeline_cache, timeline_kind, col)
|
||||
}
|
||||
TimelineKind::Profile(pubkey) => {
|
||||
if depth > 1 {
|
||||
Some(ProfileView::scroll_id(col, pubkey))
|
||||
} else {
|
||||
TimelineView::scroll_id(timeline_cache, timeline_kind, col)
|
||||
}
|
||||
}
|
||||
},
|
||||
Route::Thread(thread_selection) => Some(ThreadView::scroll_id(
|
||||
thread_selection.selected_or_root(),
|
||||
col,
|
||||
)),
|
||||
Route::Accounts(accounts_route) => match accounts_route {
|
||||
crate::accounts::AccountsRoute::Accounts => Some(AccountsView::scroll_id()),
|
||||
crate::accounts::AccountsRoute::AddAccount => None,
|
||||
},
|
||||
Route::Reply(note_id) => Some(PostReplyView::scroll_id(col, note_id.bytes())),
|
||||
Route::Quote(note_id) => Some(QuoteRepostView::scroll_id(col, note_id.bytes())),
|
||||
Route::Relays => Some(RelayView::scroll_id()),
|
||||
Route::ComposeNote => Some(PostView::scroll_id()),
|
||||
Route::AddColumn(add_column_route) => Some(AddColumnView::scroll_id(add_column_route)),
|
||||
Route::EditProfile(_) => Some(EditProfileView::scroll_id()),
|
||||
Route::Support => None,
|
||||
Route::NewDeck => Some(ConfigureDeckView::scroll_id()),
|
||||
Route::Search => Some(SearchView::scroll_id()),
|
||||
Route::EditDeck(_) => None,
|
||||
Route::Wallet(_) => None,
|
||||
Route::CustomizeZapAmount(_) => None,
|
||||
Route::Settings => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the corresponding View for the route use a egui::Frame to wrap the ScrollArea?
|
||||
/// TODO(kernelkind): this is quite hacky...
|
||||
fn route_uses_frame(route: &Route) -> bool {
|
||||
match route {
|
||||
Route::Accounts(accounts_route) => match accounts_route {
|
||||
crate::accounts::AccountsRoute::Accounts => true,
|
||||
crate::accounts::AccountsRoute::AddAccount => false,
|
||||
},
|
||||
Route::Relays => true,
|
||||
Route::Timeline(_) => false,
|
||||
Route::Thread(_) => false,
|
||||
Route::Reply(_) => false,
|
||||
Route::Quote(_) => false,
|
||||
Route::Settings => false,
|
||||
Route::ComposeNote => false,
|
||||
Route::AddColumn(_) => false,
|
||||
Route::EditProfile(_) => false,
|
||||
Route::Support => false,
|
||||
Route::NewDeck => false,
|
||||
Route::Search => false,
|
||||
Route::EditDeck(_) => false,
|
||||
Route::Wallet(_) => false,
|
||||
Route::CustomizeZapAmount(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use egui::{text::LayoutJob, TextBuffer, TextFormat};
|
||||
use egui::{
|
||||
text::{CCursor, CCursorRange, LayoutJob},
|
||||
text_edit::TextEditOutput,
|
||||
TextBuffer, TextEdit, TextFormat,
|
||||
};
|
||||
use enostr::{FullKeypair, Pubkey};
|
||||
use nostrdb::{Note, NoteBuilder, NoteReply};
|
||||
use std::{
|
||||
@@ -270,6 +274,36 @@ impl Default for PostBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// New cursor index (indexed by characters) after operation is performed
|
||||
#[must_use = "must call MentionSelectedResponse::process"]
|
||||
pub struct MentionSelectedResponse {
|
||||
pub next_cursor_index: usize,
|
||||
}
|
||||
|
||||
impl MentionSelectedResponse {
|
||||
pub fn process(&self, ctx: &egui::Context, text_edit_output: &TextEditOutput) {
|
||||
let text_edit_id = text_edit_output.response.id;
|
||||
let Some(mut before_state) = TextEdit::load_state(ctx, text_edit_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_cursor = text_edit_output
|
||||
.galley
|
||||
.from_ccursor(CCursor::new(self.next_cursor_index));
|
||||
new_cursor.ccursor.prefer_next_row = true;
|
||||
|
||||
before_state
|
||||
.cursor
|
||||
.set_char_range(Some(CCursorRange::one(CCursor::new(
|
||||
self.next_cursor_index,
|
||||
))));
|
||||
|
||||
ctx.memory_mut(|mem| mem.request_focus(text_edit_id));
|
||||
|
||||
TextEdit::store_state(ctx, text_edit_id, before_state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PostBuffer {
|
||||
pub fn get_new_mentions_key(&mut self) -> usize {
|
||||
let prev = self.mentions_key;
|
||||
@@ -319,15 +353,21 @@ impl PostBuffer {
|
||||
mention_key: usize,
|
||||
full_name: &str,
|
||||
pk: Pubkey,
|
||||
) {
|
||||
if let Some(info) = self.mentions.get(&mention_key) {
|
||||
let text_start_index = info.start_index + 1;
|
||||
self.delete_char_range(text_start_index..info.end_index);
|
||||
self.insert_text(full_name, text_start_index);
|
||||
self.select_full_mention(mention_key, pk);
|
||||
} else {
|
||||
) -> Option<MentionSelectedResponse> {
|
||||
let Some(info) = self.mentions.get(&mention_key) else {
|
||||
error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
let text_start_index = info.start_index + 1; // increment by one to exclude the mention indicator, '@'
|
||||
self.delete_char_range(text_start_index..info.end_index);
|
||||
let text_chars_inserted = self.insert_text(full_name, text_start_index);
|
||||
self.select_full_mention(mention_key, pk);
|
||||
|
||||
let space_chars_inserted = self.insert_text(" ", text_start_index + text_chars_inserted);
|
||||
|
||||
Some(MentionSelectedResponse {
|
||||
next_cursor_index: text_start_index + text_chars_inserted + space_chars_inserted,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_mention(&mut self, mention_key: usize) {
|
||||
@@ -919,7 +959,7 @@ mod tests {
|
||||
buf.select_mention_and_replace_name(0, "jb55", JB55());
|
||||
assert_eq!(buf.as_str(), "@jb55 ");
|
||||
|
||||
buf.insert_text(" test", 5);
|
||||
buf.insert_text("test", 6);
|
||||
assert_eq!(buf.as_str(), "@jb55 test");
|
||||
|
||||
assert_eq!(buf.mentions.len(), 1);
|
||||
@@ -1201,16 +1241,20 @@ mod tests {
|
||||
|
||||
buf.insert_text("@jb", 0);
|
||||
buf.select_mention_and_replace_name(0, "jb55", JB55());
|
||||
buf.insert_text(" test ", 5);
|
||||
buf.insert_text("test ", 6);
|
||||
assert_eq!(buf.as_str(), "@jb55 test ");
|
||||
buf.insert_text("@kernel", 11);
|
||||
buf.select_mention_and_replace_name(1, "KernelKind", KK());
|
||||
buf.insert_text(" test", 22);
|
||||
assert_eq!(buf.as_str(), "@jb55 test @KernelKind ");
|
||||
|
||||
buf.insert_text("test", 23);
|
||||
assert_eq!(buf.as_str(), "@jb55 test @KernelKind test");
|
||||
|
||||
assert_eq!(buf.mentions.len(), 2);
|
||||
|
||||
buf.insert_text(" ", 5);
|
||||
buf.insert_text("@els", 6);
|
||||
assert_eq!(buf.as_str(), "@jb55 @elstest @KernelKind test");
|
||||
|
||||
assert_eq!(buf.mentions.len(), 3);
|
||||
assert_eq!(buf.mentions.get(&2).unwrap().bounds(), 6..10);
|
||||
buf.select_mention_and_replace_name(2, "elsat", JB55());
|
||||
|
||||
@@ -144,6 +144,7 @@ fn send_kind_3_event(ndb: &Ndb, pool: &mut RelayPool, accounts: &Accounts, actio
|
||||
let ContactState::Received {
|
||||
contacts: _,
|
||||
note_key,
|
||||
timestamp: _,
|
||||
} = accounts.get_selected_account().data.contacts.get_state()
|
||||
else {
|
||||
return;
|
||||
|
||||
@@ -19,6 +19,7 @@ pub enum Route {
|
||||
Reply(NoteId),
|
||||
Quote(NoteId),
|
||||
Relays,
|
||||
Settings,
|
||||
ComposeNote,
|
||||
AddColumn(AddColumnRoute),
|
||||
EditProfile(Pubkey),
|
||||
@@ -47,6 +48,10 @@ impl Route {
|
||||
Route::Relays
|
||||
}
|
||||
|
||||
pub fn settings() -> Self {
|
||||
Route::Settings
|
||||
}
|
||||
|
||||
pub fn thread(thread_selection: ThreadSelection) -> Self {
|
||||
Route::Thread(thread_selection)
|
||||
}
|
||||
@@ -110,6 +115,9 @@ impl Route {
|
||||
Route::Relays => {
|
||||
writer.write_token("relay");
|
||||
}
|
||||
Route::Settings => {
|
||||
writer.write_token("settings");
|
||||
}
|
||||
Route::ComposeNote => {
|
||||
writer.write_token("compose");
|
||||
}
|
||||
@@ -169,6 +177,12 @@ impl Route {
|
||||
Ok(Route::Relays)
|
||||
})
|
||||
},
|
||||
|p| {
|
||||
p.parse_all(|p| {
|
||||
p.parse_token("settings")?;
|
||||
Ok(Route::Settings)
|
||||
})
|
||||
},
|
||||
|p| {
|
||||
p.parse_all(|p| {
|
||||
p.parse_token("quote")?;
|
||||
@@ -250,6 +264,9 @@ impl Route {
|
||||
Route::Relays => {
|
||||
ColumnTitle::formatted(tr!(i18n, "Relays", "Column title for relay management"))
|
||||
}
|
||||
Route::Settings => {
|
||||
ColumnTitle::formatted(tr!(i18n, "Settings", "Column title for app settings"))
|
||||
}
|
||||
Route::Accounts(amr) => match amr {
|
||||
AccountsRoute::Accounts => ColumnTitle::formatted(tr!(
|
||||
i18n,
|
||||
@@ -555,6 +572,7 @@ impl fmt::Display for Route {
|
||||
write!(f, "{}", tr!("Quote", "Display name for quote composition"))
|
||||
}
|
||||
Route::Relays => write!(f, "{}", tr!("Relays", "Display name for relay management")),
|
||||
Route::Settings => write!(f, "{}", tr!("Settings", "Display name for settings management")),
|
||||
Route::Accounts(amr) => match amr {
|
||||
AccountsRoute::Accounts => write!(
|
||||
f,
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Support {
|
||||
}
|
||||
|
||||
static MAX_LOG_LINES: usize = 500;
|
||||
static SUPPORT_EMAIL: &str = "support@damus.io";
|
||||
pub static SUPPORT_EMAIL: &str = "support+notedeck@damus.io";
|
||||
static EMAIL_TEMPLATE: &str = concat!("version ", env!("CARGO_PKG_VERSION"), "\nCommit hash: ", env!("GIT_COMMIT_HASH"), "\n\nDescribe the bug you have encountered:\n<-- your statement here -->\n\n===== Paste your log below =====\n\n");
|
||||
|
||||
impl Support {
|
||||
|
||||
@@ -221,6 +221,14 @@ impl TimelineCache {
|
||||
pub fn num_timelines(&self) -> usize {
|
||||
self.timelines.len()
|
||||
}
|
||||
|
||||
pub fn set_fresh(&mut self, kind: &TimelineKind) {
|
||||
let Some(tl) = self.get_mut(kind) else {
|
||||
return;
|
||||
};
|
||||
|
||||
tl.current_view_mut().freshness.set_fresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// Look for new thread notes since our last fetch
|
||||
|
||||
@@ -471,11 +471,9 @@ impl TimelineKind {
|
||||
},
|
||||
|
||||
// TODO: still need to update this to fetch likes, zaps, etc
|
||||
TimelineKind::Notifications(pubkey) => FilterState::ready(vec![Filter::new()
|
||||
.pubkeys([pubkey.bytes()])
|
||||
.kinds([1])
|
||||
.limit(default_limit())
|
||||
.build()]),
|
||||
TimelineKind::Notifications(pubkey) => {
|
||||
FilterState::ready(vec![notifications_filter(pubkey)])
|
||||
}
|
||||
|
||||
TimelineKind::Hashtag(hashtag) => {
|
||||
let filters = hashtag
|
||||
@@ -573,11 +571,7 @@ impl TimelineKind {
|
||||
)),
|
||||
|
||||
TimelineKind::Notifications(pk) => {
|
||||
let notifications_filter = Filter::new()
|
||||
.pubkeys([pk.bytes()])
|
||||
.kinds([1])
|
||||
.limit(default_limit())
|
||||
.build();
|
||||
let notifications_filter = notifications_filter(&pk);
|
||||
|
||||
Some(Timeline::new(
|
||||
TimelineKind::notifications(pk),
|
||||
@@ -628,6 +622,14 @@ impl TimelineKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notifications_filter(pk: &Pubkey) -> Filter {
|
||||
Filter::new()
|
||||
.pubkeys([pk.bytes()])
|
||||
.kinds([1])
|
||||
.limit(default_limit())
|
||||
.build()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TitleNeedsDb<'a> {
|
||||
kind: &'a TimelineKind,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
|
||||
use notedeck::{
|
||||
contacts::hybrid_contacts_filter,
|
||||
debouncer::Debouncer,
|
||||
filter::{self, HybridFilter},
|
||||
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
|
||||
NoteCache, NoteRef, UnknownIds,
|
||||
@@ -16,8 +17,11 @@ use notedeck::{
|
||||
use egui_virtual_list::VirtualList;
|
||||
use enostr::{PoolRelay, Pubkey, RelayPool};
|
||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
};
|
||||
use std::{rc::Rc, time::SystemTime};
|
||||
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
@@ -103,6 +107,7 @@ pub struct TimelineTab {
|
||||
pub selection: i32,
|
||||
pub filter: ViewFilter,
|
||||
pub list: Rc<RefCell<VirtualList>>,
|
||||
pub freshness: NotesFreshness,
|
||||
}
|
||||
|
||||
impl TimelineTab {
|
||||
@@ -138,6 +143,7 @@ impl TimelineTab {
|
||||
selection,
|
||||
filter,
|
||||
list,
|
||||
freshness: NotesFreshness::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +613,7 @@ pub fn fetch_contact_list(
|
||||
ContactState::Received {
|
||||
contacts: _,
|
||||
note_key: _,
|
||||
timestamp: _,
|
||||
} => FilterState::GotRemote(filter::GotRemoteType::Contact),
|
||||
};
|
||||
|
||||
@@ -726,6 +733,7 @@ pub fn is_timeline_ready(
|
||||
let ContactState::Received {
|
||||
contacts: _,
|
||||
note_key,
|
||||
timestamp: _,
|
||||
} = accounts.get_selected_account().data.contacts.get_state()
|
||||
else {
|
||||
return false;
|
||||
@@ -778,3 +786,101 @@ pub fn is_timeline_ready(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotesFreshness {
|
||||
debouncer: Debouncer,
|
||||
state: NotesFreshnessState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NotesFreshnessState {
|
||||
Fresh {
|
||||
timestamp_viewed: u64,
|
||||
},
|
||||
Stale {
|
||||
have_unseen: bool,
|
||||
timestamp_last_viewed: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for NotesFreshness {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
debouncer: Debouncer::new(Duration::from_secs(2)),
|
||||
state: NotesFreshnessState::Stale {
|
||||
have_unseen: true,
|
||||
timestamp_last_viewed: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NotesFreshness {
|
||||
pub fn set_fresh(&mut self) {
|
||||
if !self.debouncer.should_act() {
|
||||
return;
|
||||
}
|
||||
self.state = NotesFreshnessState::Fresh {
|
||||
timestamp_viewed: timestamp_now(),
|
||||
};
|
||||
self.debouncer.bounce();
|
||||
}
|
||||
|
||||
pub fn update(&mut self, check_have_unseen: impl FnOnce(u64) -> bool) {
|
||||
if !self.debouncer.should_act() {
|
||||
return;
|
||||
}
|
||||
|
||||
match &self.state {
|
||||
NotesFreshnessState::Fresh { timestamp_viewed } => {
|
||||
let Ok(dur) = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH + Duration::from_secs(*timestamp_viewed))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if dur > Duration::from_secs(2) {
|
||||
self.state = NotesFreshnessState::Stale {
|
||||
have_unseen: check_have_unseen(*timestamp_viewed),
|
||||
timestamp_last_viewed: *timestamp_viewed,
|
||||
};
|
||||
}
|
||||
}
|
||||
NotesFreshnessState::Stale {
|
||||
have_unseen,
|
||||
timestamp_last_viewed,
|
||||
} => {
|
||||
if *have_unseen {
|
||||
return;
|
||||
}
|
||||
|
||||
self.state = NotesFreshnessState::Stale {
|
||||
have_unseen: check_have_unseen(*timestamp_last_viewed),
|
||||
timestamp_last_viewed: *timestamp_last_viewed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.debouncer.bounce();
|
||||
}
|
||||
|
||||
pub fn has_unseen(&self) -> bool {
|
||||
match &self.state {
|
||||
NotesFreshnessState::Fresh {
|
||||
timestamp_viewed: _,
|
||||
} => false,
|
||||
NotesFreshnessState::Stale {
|
||||
have_unseen,
|
||||
timestamp_last_viewed: _,
|
||||
} => *have_unseen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp_now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or(Duration::ZERO)
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use enostr::Pubkey;
|
||||
use notedeck::NoteContext;
|
||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
||||
use notedeck::{JobsCache, NoteContext};
|
||||
use notedeck_ui::NoteOptions;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_timeline_route(
|
||||
@@ -81,14 +81,17 @@ pub fn render_thread_route(
|
||||
// default truncated everywher eelse
|
||||
note_options.set(NoteOptions::Truncate, false);
|
||||
|
||||
// We need the reply lines in threads
|
||||
note_options.set(NoteOptions::Wide, false);
|
||||
|
||||
ui::ThreadView::new(
|
||||
threads,
|
||||
selection.selected_or_root(),
|
||||
note_options,
|
||||
note_context,
|
||||
jobs,
|
||||
col,
|
||||
)
|
||||
.id_source(col)
|
||||
.ui(ui)
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
@@ -405,32 +405,13 @@ fn direct_replies_filter_non_root(
|
||||
let tmp_selected = *selected_note_id;
|
||||
nostrdb::Filter::new()
|
||||
.kinds([1])
|
||||
.custom(move |n: nostrdb::Note<'_>| {
|
||||
for tag in n.tags() {
|
||||
if tag.count() < 4 {
|
||||
continue;
|
||||
.custom(move |note: nostrdb::Note<'_>| {
|
||||
let reply = nostrdb::NoteReply::new(note.tags());
|
||||
if reply.is_reply_to_root() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some("e") = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(tagged_id) = tag.get_id(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if *tagged_id != tmp_selected {
|
||||
// NOTE: if these aren't dereferenced a segfault occurs...
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(data) = tag.get_str(3) {
|
||||
if data == "reply" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
reply.reply().is_some_and(|r| r.id == &tmp_selected)
|
||||
})
|
||||
.event(root_id)
|
||||
.build()
|
||||
@@ -448,42 +429,13 @@ fn direct_replies_filter_non_root(
|
||||
/// let tmp = *root_id;
|
||||
/// .custom(move |_| { tmp }) // ✅
|
||||
fn direct_replies_filter_root(root_id: &[u8; 32]) -> nostrdb::Filter {
|
||||
let tmp_root = *root_id;
|
||||
let moved_root_id = *root_id;
|
||||
nostrdb::Filter::new()
|
||||
.kinds([1])
|
||||
.custom(move |n: nostrdb::Note<'_>| {
|
||||
let mut contains_root = false;
|
||||
for tag in n.tags() {
|
||||
if tag.count() < 4 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some("e") = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(s) = tag.get_str(3) {
|
||||
if s == "reply" {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(tagged_id) = tag.get_id(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if *tagged_id != tmp_root {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(s) = tag.get_str(3) {
|
||||
if s == "root" {
|
||||
contains_root = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contains_root
|
||||
.custom(move |note: nostrdb::Note<'_>| {
|
||||
nostrdb::NoteReply::new(note.tags())
|
||||
.reply_to_root()
|
||||
.is_some_and(|r| r.id == &moved_root_id)
|
||||
})
|
||||
.event(root_id)
|
||||
.build()
|
||||
|
||||
@@ -52,6 +52,7 @@ impl<'a> AccountsView<'a> {
|
||||
|
||||
ui.add_space(8.0);
|
||||
scroll_area()
|
||||
.id_salt(AccountsView::scroll_id())
|
||||
.show(ui, |ui| {
|
||||
Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n)
|
||||
})
|
||||
@@ -59,6 +60,10 @@ impl<'a> AccountsView<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn scroll_id() -> egui::Id {
|
||||
egui::Id::new("accounts")
|
||||
}
|
||||
|
||||
fn show_accounts(
|
||||
ui: &mut Ui,
|
||||
accounts: &Accounts,
|
||||
|
||||
@@ -64,14 +64,14 @@ enum AddColumnOption {
|
||||
Individual(PubkeySource),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default, Hash)]
|
||||
pub enum AddAlgoRoute {
|
||||
#[default]
|
||||
Base,
|
||||
LastPerPubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
pub enum AddColumnRoute {
|
||||
Base,
|
||||
UndecidedNotification,
|
||||
@@ -187,8 +187,13 @@ impl<'a> AddColumnView<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_id(route: &AddColumnRoute) -> egui::Id {
|
||||
egui::Id::new(("add_column", route))
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
|
||||
ScrollArea::vertical()
|
||||
.id_salt(AddColumnView::scroll_id(&AddColumnRoute::Base))
|
||||
.show(ui, |ui| {
|
||||
let mut selected_option: Option<AddColumnResponse> = None;
|
||||
for column_option_data in self.get_base_options(ui) {
|
||||
|
||||
@@ -481,6 +481,7 @@ impl<'a> NavTitle<'a> {
|
||||
Route::AddColumn(_add_col_route) => None,
|
||||
Route::Support => None,
|
||||
Route::Relays => None,
|
||||
Route::Settings => None,
|
||||
Route::NewDeck => None,
|
||||
Route::EditDeck(_) => None,
|
||||
Route::EditProfile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
||||
|
||||
@@ -33,6 +33,10 @@ impl<'a> ConfigureDeckView<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll_id() -> egui::Id {
|
||||
egui::Id::new("configure-deck")
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) -> Option<ConfigureDeckResponse> {
|
||||
let title_font = egui::FontId::new(
|
||||
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4),
|
||||
@@ -261,6 +265,7 @@ fn glyph_options_ui(
|
||||
) -> Option<char> {
|
||||
let mut selected_glyph = None;
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(ConfigureDeckView::scroll_id())
|
||||
.max_height(max_height)
|
||||
.show(ui, |ui| {
|
||||
let max_width = ui.available_width();
|
||||
|
||||
+27
-14
@@ -11,19 +11,21 @@ use notedeck_ui::{
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
pub struct SearchResultsView<'a> {
|
||||
/// Displays user profiles for the user to pick from.
|
||||
/// Useful for manually typing a username and selecting the profile desired
|
||||
pub struct MentionPickerView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
txn: &'a Transaction,
|
||||
img_cache: &'a mut Images,
|
||||
results: &'a Vec<&'a [u8; 32]>,
|
||||
}
|
||||
|
||||
pub enum SearchResultsResponse {
|
||||
pub enum MentionPickerResponse {
|
||||
SelectResult(Option<usize>),
|
||||
DeleteMention,
|
||||
}
|
||||
|
||||
impl<'a> SearchResultsView<'a> {
|
||||
impl<'a> MentionPickerView<'a> {
|
||||
pub fn new(
|
||||
img_cache: &'a mut Images,
|
||||
ndb: &'a Ndb,
|
||||
@@ -38,8 +40,8 @@ impl<'a> SearchResultsView<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, width: f32) -> SearchResultsResponse {
|
||||
let mut search_results_selection = None;
|
||||
fn show(&mut self, ui: &mut egui::Ui, width: f32) -> MentionPickerResponse {
|
||||
let mut selection = None;
|
||||
ui.vertical(|ui| {
|
||||
for (i, res) in self.results.iter().enumerate() {
|
||||
let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) {
|
||||
@@ -54,16 +56,16 @@ impl<'a> SearchResultsView<'a> {
|
||||
.add(user_result(&profile, self.img_cache, i, width))
|
||||
.clicked()
|
||||
{
|
||||
search_results_selection = Some(i)
|
||||
selection = Some(i)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SearchResultsResponse::SelectResult(search_results_selection)
|
||||
MentionPickerResponse::SelectResult(selection)
|
||||
}
|
||||
|
||||
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> SearchResultsResponse {
|
||||
let widget_id = ui.id().with("search_results");
|
||||
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> MentionPickerResponse {
|
||||
let widget_id = ui.id().with("mention_results");
|
||||
let area_resp = egui::Area::new(widget_id)
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(rect.left_top())
|
||||
@@ -72,10 +74,10 @@ impl<'a> SearchResultsView<'a> {
|
||||
let inner_margin_size = 8.0;
|
||||
egui::Frame::NONE
|
||||
.fill(ui.visuals().panel_fill)
|
||||
.inner_margin(inner_margin_size)
|
||||
.show(ui, |ui| {
|
||||
let width = rect.width() - (2.0 * inner_margin_size);
|
||||
|
||||
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
|
||||
let close_button_resp = {
|
||||
let close_button_size = 16.0;
|
||||
let (close_section_rect, _) = ui.allocate_exact_size(
|
||||
@@ -95,16 +97,16 @@ impl<'a> SearchResultsView<'a> {
|
||||
.inner
|
||||
};
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
|
||||
|
||||
let scroll_resp = ScrollArea::vertical()
|
||||
.max_width(width)
|
||||
.max_width(rect.width())
|
||||
.auto_shrink(Vec2b::FALSE)
|
||||
.show(ui, |ui| self.show(ui, width));
|
||||
ui.advance_cursor_after_rect(rect);
|
||||
|
||||
if close_button_resp {
|
||||
SearchResultsResponse::DeleteMention
|
||||
MentionPickerResponse::DeleteMention
|
||||
} else {
|
||||
scroll_resp.inner
|
||||
}
|
||||
@@ -128,7 +130,18 @@ fn user_result<'a>(
|
||||
let spacing = 8.0;
|
||||
let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
||||
|
||||
let helper = AnimationHelper::new(ui, ("user_result", index), vec2(width, max_image));
|
||||
let animation_rect = {
|
||||
let max_width = ui.available_width();
|
||||
let extra_width = (max_width - width) / 2.0;
|
||||
let left = ui.cursor().left();
|
||||
let (rect, _) =
|
||||
ui.allocate_exact_size(vec2(width + extra_width, max_image), egui::Sense::click());
|
||||
|
||||
let (_, right) = rect.split_left_right_at_x(left + extra_width);
|
||||
right
|
||||
};
|
||||
|
||||
let helper = AnimationHelper::new_from_rect(ui, ("user_result", index), animation_rect);
|
||||
|
||||
let icon_rect = {
|
||||
let r = helper.get_animation_rect();
|
||||
@@ -5,13 +5,14 @@ pub mod column;
|
||||
pub mod configure_deck;
|
||||
pub mod edit_deck;
|
||||
pub mod images;
|
||||
pub mod mentions_picker;
|
||||
pub mod note;
|
||||
pub mod post;
|
||||
pub mod preview;
|
||||
pub mod profile;
|
||||
pub mod relay;
|
||||
pub mod search;
|
||||
pub mod search_results;
|
||||
pub mod settings;
|
||||
pub mod side_panel;
|
||||
pub mod support;
|
||||
pub mod thread;
|
||||
@@ -24,6 +25,8 @@ pub use note::{PostReplyView, PostView};
|
||||
pub use preview::{Preview, PreviewApp, PreviewConfig};
|
||||
pub use profile::ProfileView;
|
||||
pub use relay::RelayView;
|
||||
pub use settings::SettingsView;
|
||||
pub use settings::ShowSourceClientOption;
|
||||
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
||||
pub use thread::ThreadView;
|
||||
pub use timeline::TimelineView;
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::draft::{Draft, Drafts, MentionHint};
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
||||
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
||||
use crate::ui::search_results::SearchResultsView;
|
||||
use crate::ui::mentions_picker::MentionPickerView;
|
||||
use crate::ui::{self, Preview, PreviewConfig};
|
||||
use crate::Result;
|
||||
|
||||
@@ -14,13 +14,12 @@ use egui::{
|
||||
};
|
||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::media::gif::ensure_latest_texture;
|
||||
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
|
||||
|
||||
use notedeck_ui::{
|
||||
app_images,
|
||||
blur::PixelDimensions,
|
||||
context_menu::{input_context, PasteBehavior},
|
||||
gif::{handle_repaint, retrieve_latest_texture},
|
||||
images::{get_render_state, RenderState},
|
||||
jobs::JobsCache,
|
||||
note::render_note_preview,
|
||||
NoteOptions, ProfilePic,
|
||||
};
|
||||
@@ -35,7 +34,6 @@ pub struct PostView<'a, 'd> {
|
||||
draft: &'a mut Draft,
|
||||
post_type: PostType,
|
||||
poster: FilledKeypair<'a>,
|
||||
id_source: Option<egui::Id>,
|
||||
inner_rect: egui::Rect,
|
||||
note_options: NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
@@ -112,12 +110,10 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
note_options: NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
) -> Self {
|
||||
let id_source: Option<egui::Id> = None;
|
||||
PostView {
|
||||
note_context,
|
||||
draft,
|
||||
poster,
|
||||
id_source,
|
||||
post_type,
|
||||
inner_rect,
|
||||
note_options,
|
||||
@@ -125,9 +121,12 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
|
||||
self.id_source = Some(egui::Id::new(id_source));
|
||||
self
|
||||
fn id() -> egui::Id {
|
||||
egui::Id::new("post")
|
||||
}
|
||||
|
||||
pub fn scroll_id() -> egui::Id {
|
||||
PostView::id().with("scroll")
|
||||
}
|
||||
|
||||
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
|
||||
@@ -213,11 +212,13 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
|
||||
let focused = out.response.has_focus();
|
||||
|
||||
ui.ctx().data_mut(|d| d.insert_temp(self.id(), focused));
|
||||
ui.ctx()
|
||||
.data_mut(|d| d.insert_temp(PostView::id(), focused));
|
||||
|
||||
out.response
|
||||
}
|
||||
|
||||
// Displays the mention picker and handles when one is selected.
|
||||
fn show_mention_hints(
|
||||
&mut self,
|
||||
txn: &nostrdb::Transaction,
|
||||
@@ -273,7 +274,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
return;
|
||||
};
|
||||
|
||||
let resp = SearchResultsView::new(
|
||||
let resp = MentionPickerView::new(
|
||||
self.note_context.img_cache,
|
||||
self.note_context.ndb,
|
||||
txn,
|
||||
@@ -281,35 +282,40 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
)
|
||||
.show_in_rect(hint_rect, ui);
|
||||
|
||||
let mut selection_made = None;
|
||||
match resp {
|
||||
ui::search_results::SearchResultsResponse::SelectResult(selection) => {
|
||||
ui::mentions_picker::MentionPickerResponse::SelectResult(selection) => {
|
||||
if let Some(hint_index) = selection {
|
||||
if let Some(pk) = res.get(hint_index) {
|
||||
let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk);
|
||||
|
||||
if let Some(made_selection) =
|
||||
self.draft.buffer.select_mention_and_replace_name(
|
||||
mention.index,
|
||||
get_display_name(record.ok().as_ref()).name(),
|
||||
Pubkey::new(**pk),
|
||||
);
|
||||
)
|
||||
{
|
||||
selection_made = Some(made_selection);
|
||||
}
|
||||
self.draft.cur_mention_hint = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui::search_results::SearchResultsResponse::DeleteMention => {
|
||||
ui::mentions_picker::MentionPickerResponse::DeleteMention => {
|
||||
self.draft.buffer.delete_mention(mention.index)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(selection) = selection_made {
|
||||
selection.process(ui.ctx(), textedit_output);
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self, ui: &egui::Ui) -> bool {
|
||||
ui.ctx()
|
||||
.data(|d| d.get_temp::<bool>(self.id()).unwrap_or(false))
|
||||
}
|
||||
|
||||
fn id(&self) -> egui::Id {
|
||||
self.id_source.unwrap_or_else(|| egui::Id::new("post"))
|
||||
.data(|d| d.get_temp::<bool>(PostView::id()).unwrap_or(false))
|
||||
}
|
||||
|
||||
pub fn outer_margin() -> i8 {
|
||||
@@ -321,6 +327,13 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
||||
ScrollArea::vertical()
|
||||
.id_salt(PostView::scroll_id())
|
||||
.show(ui, |ui| self.ui_no_scroll(txn, ui))
|
||||
.inner
|
||||
}
|
||||
|
||||
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
||||
let focused = self.focused(ui);
|
||||
let stroke = if focused {
|
||||
ui.visuals().selection.stroke
|
||||
@@ -467,7 +480,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
self.note_context.img_cache,
|
||||
cache_type,
|
||||
url,
|
||||
notedeck_ui::images::ImageType::Content,
|
||||
notedeck::ImageType::Content(Some((width, height))),
|
||||
);
|
||||
|
||||
render_post_view_media(
|
||||
@@ -591,12 +604,10 @@ fn render_post_view_media(
|
||||
.to_points(ui.pixels_per_point())
|
||||
.to_vec();
|
||||
|
||||
let texture_handle = handle_repaint(
|
||||
ui,
|
||||
retrieve_latest_texture(url, render_state.gifs, renderable_media),
|
||||
);
|
||||
let texture_handle =
|
||||
ensure_latest_texture(ui, url, render_state.gifs, renderable_media);
|
||||
let img_resp = ui.add(
|
||||
egui::Image::new(texture_handle)
|
||||
egui::Image::new(&texture_handle)
|
||||
.max_size(size)
|
||||
.corner_radius(12.0),
|
||||
);
|
||||
@@ -802,13 +813,13 @@ mod preview {
|
||||
let mut note_context = NoteContext {
|
||||
ndb: app.ndb,
|
||||
accounts: app.accounts,
|
||||
global_wallet: app.global_wallet,
|
||||
img_cache: app.img_cache,
|
||||
note_cache: app.note_cache,
|
||||
zaps: app.zaps,
|
||||
pool: app.pool,
|
||||
job_pool: app.job_pool,
|
||||
unknown_ids: app.unknown_ids,
|
||||
current_account_has_wallet: false,
|
||||
clipboard: app.clipboard,
|
||||
i18n: app.i18n,
|
||||
};
|
||||
|
||||
@@ -4,16 +4,17 @@ use crate::{
|
||||
ui::{self},
|
||||
};
|
||||
|
||||
use egui::ScrollArea;
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use notedeck::NoteContext;
|
||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
||||
use notedeck::{JobsCache, NoteContext};
|
||||
use notedeck_ui::NoteOptions;
|
||||
|
||||
pub struct QuoteRepostView<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
poster: FilledKeypair<'a>,
|
||||
draft: &'a mut Draft,
|
||||
quoting_note: &'a nostrdb::Note<'a>,
|
||||
id_source: Option<egui::Id>,
|
||||
scroll_id: egui::Id,
|
||||
inner_rect: egui::Rect,
|
||||
note_options: NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
@@ -29,22 +30,36 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
|
||||
inner_rect: egui::Rect,
|
||||
note_options: NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
col: usize,
|
||||
) -> Self {
|
||||
let id_source: Option<egui::Id> = None;
|
||||
QuoteRepostView {
|
||||
note_context,
|
||||
poster,
|
||||
draft,
|
||||
quoting_note,
|
||||
id_source,
|
||||
scroll_id: QuoteRepostView::scroll_id(col, quoting_note.id()),
|
||||
inner_rect,
|
||||
note_options,
|
||||
jobs,
|
||||
}
|
||||
}
|
||||
|
||||
fn id(col: usize, note_id: &[u8; 32]) -> egui::Id {
|
||||
egui::Id::new(("quote_repost", col, note_id))
|
||||
}
|
||||
|
||||
pub fn scroll_id(col: usize, note_id: &[u8; 32]) -> egui::Id {
|
||||
QuoteRepostView::id(col, note_id).with("scroll")
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
||||
let id = self.id();
|
||||
ScrollArea::vertical()
|
||||
.id_salt(self.scroll_id)
|
||||
.show(ui, |ui| self.show_internal(ui))
|
||||
.inner
|
||||
}
|
||||
|
||||
fn show_internal(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
||||
let quoting_note_id = self.quoting_note.id();
|
||||
|
||||
let post_resp = ui::PostView::new(
|
||||
@@ -56,18 +71,7 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
|
||||
self.note_options,
|
||||
self.jobs,
|
||||
)
|
||||
.id_source(id)
|
||||
.ui(self.quoting_note.txn().unwrap(), ui);
|
||||
.ui_no_scroll(self.quoting_note.txn().unwrap(), ui);
|
||||
post_resp
|
||||
}
|
||||
|
||||
pub fn id_source(mut self, id: egui::Id) -> Self {
|
||||
self.id_source = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id(&self) -> egui::Id {
|
||||
self.id_source
|
||||
.unwrap_or_else(|| egui::Id::new("quote-repost-view"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ use crate::ui::{
|
||||
note::{PostAction, PostResponse, PostType},
|
||||
};
|
||||
|
||||
use egui::{Rect, Response, Ui};
|
||||
use egui::{Rect, Response, ScrollArea, Ui};
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use notedeck::NoteContext;
|
||||
use notedeck_ui::jobs::JobsCache;
|
||||
use notedeck::{JobsCache, NoteContext};
|
||||
use notedeck_ui::{NoteOptions, NoteView, ProfilePic};
|
||||
|
||||
pub struct PostReplyView<'a, 'd> {
|
||||
@@ -15,7 +14,7 @@ pub struct PostReplyView<'a, 'd> {
|
||||
poster: FilledKeypair<'a>,
|
||||
draft: &'a mut Draft,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
id_source: Option<egui::Id>,
|
||||
scroll_id: egui::Id,
|
||||
inner_rect: egui::Rect,
|
||||
note_options: NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
@@ -31,31 +30,37 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
||||
inner_rect: egui::Rect,
|
||||
note_options: NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
col: usize,
|
||||
) -> Self {
|
||||
let id_source: Option<egui::Id> = None;
|
||||
PostReplyView {
|
||||
note_context,
|
||||
poster,
|
||||
draft,
|
||||
note,
|
||||
id_source,
|
||||
scroll_id: PostReplyView::scroll_id(col, note.id()),
|
||||
inner_rect,
|
||||
note_options,
|
||||
jobs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id_source(mut self, id: egui::Id) -> Self {
|
||||
self.id_source = Some(id);
|
||||
self
|
||||
fn id(col: usize, note_id: &[u8; 32]) -> egui::Id {
|
||||
egui::Id::new(("reply_view", col, note_id))
|
||||
}
|
||||
|
||||
pub fn id(&self) -> egui::Id {
|
||||
self.id_source
|
||||
.unwrap_or_else(|| egui::Id::new("post-reply-view"))
|
||||
pub fn scroll_id(col: usize, note_id: &[u8; 32]) -> egui::Id {
|
||||
PostReplyView::id(col, note_id).with("scroll")
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
||||
ScrollArea::vertical()
|
||||
.id_salt(self.scroll_id)
|
||||
.show(ui, |ui| self.show_internal(ui))
|
||||
.inner
|
||||
}
|
||||
|
||||
// no scroll
|
||||
fn show_internal(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
||||
ui.vertical(|ui| {
|
||||
let avail_rect = ui.available_rect_before_wrap();
|
||||
|
||||
@@ -81,7 +86,6 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
||||
})
|
||||
.inner;
|
||||
|
||||
let id = self.id();
|
||||
let replying_to = self.note.id();
|
||||
let rect_before_post = ui.min_rect();
|
||||
|
||||
@@ -95,8 +99,7 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
||||
self.note_options,
|
||||
self.jobs,
|
||||
)
|
||||
.id_source(id)
|
||||
.ui(self.note.txn().unwrap(), ui)
|
||||
.ui_no_scroll(self.note.txn().unwrap(), ui)
|
||||
};
|
||||
|
||||
post_response.action = post_response
|
||||
|
||||
@@ -24,9 +24,14 @@ impl<'a> EditProfileView<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_id() -> egui::Id {
|
||||
egui::Id::new("edit_profile")
|
||||
}
|
||||
|
||||
// return true to save
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
|
||||
ScrollArea::vertical()
|
||||
.id_salt(EditProfileView::scroll_id())
|
||||
.show(ui, |ui| {
|
||||
banner(ui, self.state.banner(), 188.0);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use enostr::Pubkey;
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
use notedeck::{tr, Localization};
|
||||
use notedeck_ui::profile::follow_button;
|
||||
use robius_open::Uri;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
@@ -13,12 +14,11 @@ use crate::{
|
||||
ui::timeline::{tabs_ui, TimelineTabView},
|
||||
};
|
||||
use notedeck::{
|
||||
name::get_display_name, profile::get_profile_url, IsFollowing, NoteAction, NoteContext,
|
||||
NotedeckTextStyle,
|
||||
name::get_display_name, profile::get_profile_url, IsFollowing, JobsCache, NoteAction,
|
||||
NoteContext, NotedeckTextStyle,
|
||||
};
|
||||
use notedeck_ui::{
|
||||
app_images,
|
||||
jobs::JobsCache,
|
||||
profile::{about_section_widget, banner, display_name_widget},
|
||||
NoteOptions, ProfilePic,
|
||||
};
|
||||
@@ -59,8 +59,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_id(col_id: usize, profile_pubkey: &Pubkey) -> egui::Id {
|
||||
egui::Id::new(("profile_scroll", col_id, profile_pubkey))
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> {
|
||||
let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey));
|
||||
let scroll_id = ProfileView::scroll_id(self.col_id, self.pubkey);
|
||||
let offset_id = scroll_id.with("scroll_offset");
|
||||
|
||||
let mut scroll_area = ScrollArea::vertical().id_salt(scroll_id);
|
||||
@@ -282,8 +286,8 @@ fn handle_link(ui: &mut egui::Ui, website_url: &str) {
|
||||
.interact(Sense::click())
|
||||
.clicked()
|
||||
{
|
||||
if let Err(e) = open::that(website_url) {
|
||||
error!("Failed to open URL {} because: {}", website_url, e);
|
||||
if let Err(e) = Uri::new(website_url).open() {
|
||||
error!("Failed to open URL {} because: {:?}", website_url, e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ impl RelayView<'_> {
|
||||
ui.add_space(8.0);
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(RelayView::scroll_id())
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
@@ -51,6 +52,10 @@ impl RelayView<'_> {
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
pub fn scroll_id() -> egui::Id {
|
||||
egui::Id::new("relay_scroll")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RelayView<'a> {
|
||||
|
||||
@@ -5,11 +5,11 @@ use state::TypingType;
|
||||
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
use nostrdb::{Filter, Ndb, Transaction};
|
||||
use notedeck::{tr, tr_plural, Localization, NoteAction, NoteContext, NoteRef};
|
||||
use notedeck::{tr, tr_plural, JobsCache, Localization, NoteAction, NoteContext, NoteRef};
|
||||
|
||||
use notedeck_ui::{
|
||||
context_menu::{input_context, PasteBehavior},
|
||||
icons::search_icon,
|
||||
jobs::JobsCache,
|
||||
padding, NoteOptions,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -19,7 +19,7 @@ mod state;
|
||||
|
||||
pub use state::{FocusState, SearchQueryState, SearchState};
|
||||
|
||||
use super::search_results::{SearchResultsResponse, SearchResultsView};
|
||||
use super::mentions_picker::{MentionPickerResponse, MentionPickerView};
|
||||
|
||||
pub struct SearchView<'a, 'd> {
|
||||
query: &'a mut SearchQueryState,
|
||||
@@ -76,7 +76,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
break 's;
|
||||
};
|
||||
|
||||
let search_res = SearchResultsView::new(
|
||||
let search_res = MentionPickerView::new(
|
||||
self.note_context.img_cache,
|
||||
self.note_context.ndb,
|
||||
self.txn,
|
||||
@@ -85,7 +85,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
.show_in_rect(ui.available_rect_before_wrap(), ui);
|
||||
|
||||
search_action = match search_res {
|
||||
SearchResultsResponse::SelectResult(Some(index)) => {
|
||||
MentionPickerResponse::SelectResult(Some(index)) => {
|
||||
let Some(pk_bytes) = results.get(index) else {
|
||||
break 's;
|
||||
};
|
||||
@@ -103,8 +103,8 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
new_search_text: format!("@{username}"),
|
||||
})
|
||||
}
|
||||
SearchResultsResponse::DeleteMention => Some(SearchAction::CloseMention),
|
||||
SearchResultsResponse::SelectResult(None) => break 's,
|
||||
MentionPickerResponse::DeleteMention => Some(SearchAction::CloseMention),
|
||||
MentionPickerResponse::SelectResult(None) => break 's,
|
||||
};
|
||||
}
|
||||
SearchState::PerformSearch(search_type) => {
|
||||
@@ -151,6 +151,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
|
||||
fn show_search_results(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(SearchView::scroll_id())
|
||||
.show(ui, |ui| {
|
||||
let reversed = false;
|
||||
TimelineTabView::new(
|
||||
@@ -165,6 +166,10 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
pub fn scroll_id() -> egui::Id {
|
||||
egui::Id::new("search_results")
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_search(
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
use egui::{
|
||||
vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference,
|
||||
};
|
||||
use enostr::NoteId;
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{
|
||||
tr,
|
||||
ui::{is_narrow, richtext_small},
|
||||
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
|
||||
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||
};
|
||||
use notedeck_ui::{NoteOptions, NoteView};
|
||||
use strum::Display;
|
||||
|
||||
use crate::{nav::RouterAction, Damus, Route};
|
||||
|
||||
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
|
||||
|
||||
const THEME_LIGHT: &str = "Light";
|
||||
const THEME_DARK: &str = "Dark";
|
||||
|
||||
const MIN_ZOOM: f32 = 0.5;
|
||||
const MAX_ZOOM: f32 = 3.0;
|
||||
const ZOOM_STEP: f32 = 0.1;
|
||||
const RESET_ZOOM: f32 = 1.0;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Display)]
|
||||
pub enum ShowSourceClientOption {
|
||||
Hide,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl From<ShowSourceClientOption> for String {
|
||||
fn from(show_option: ShowSourceClientOption) -> Self {
|
||||
match show_option {
|
||||
ShowSourceClientOption::Hide => "hide".to_string(),
|
||||
ShowSourceClientOption::Top => "top".to_string(),
|
||||
ShowSourceClientOption::Bottom => "bottom".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NoteOptions> for ShowSourceClientOption {
|
||||
fn from(note_options: NoteOptions) -> Self {
|
||||
if note_options.contains(NoteOptions::ShowNoteClientTop) {
|
||||
ShowSourceClientOption::Top
|
||||
} else if note_options.contains(NoteOptions::ShowNoteClientBottom) {
|
||||
ShowSourceClientOption::Bottom
|
||||
} else {
|
||||
ShowSourceClientOption::Hide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ShowSourceClientOption {
|
||||
fn from(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"hide" => Self::Hide,
|
||||
"top" => Self::Top,
|
||||
"bottom" => Self::Bottom,
|
||||
_ => Self::Hide, // default fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShowSourceClientOption {
|
||||
pub fn set_note_options(self, note_options: &mut NoteOptions) {
|
||||
match self {
|
||||
Self::Hide => {
|
||||
note_options.set(NoteOptions::ShowNoteClientTop, false);
|
||||
note_options.set(NoteOptions::ShowNoteClientBottom, false);
|
||||
}
|
||||
Self::Bottom => {
|
||||
note_options.set(NoteOptions::ShowNoteClientTop, false);
|
||||
note_options.set(NoteOptions::ShowNoteClientBottom, true);
|
||||
}
|
||||
Self::Top => {
|
||||
note_options.set(NoteOptions::ShowNoteClientTop, true);
|
||||
note_options.set(NoteOptions::ShowNoteClientBottom, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, i18n: &mut Localization) -> String {
|
||||
match self {
|
||||
Self::Hide => tr!(
|
||||
i18n,
|
||||
"Hide",
|
||||
"Option in settings section to hide the source client label in note display"
|
||||
),
|
||||
Self::Top => tr!(
|
||||
i18n,
|
||||
"Top",
|
||||
"Option in settings section to show the source client label at the top of the note"
|
||||
),
|
||||
Self::Bottom => tr!(
|
||||
i18n,
|
||||
"Bottom",
|
||||
"Option in settings section to show the source client label at the bottom of the note"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SettingsAction {
|
||||
SetZoomFactor(f32),
|
||||
SetTheme(ThemePreference),
|
||||
SetShowSourceClient(ShowSourceClientOption),
|
||||
SetLocale(LanguageIdentifier),
|
||||
SetRepliestNewestFirst(bool),
|
||||
SetNoteBodyFontSize(f32),
|
||||
OpenRelays,
|
||||
OpenCacheFolder,
|
||||
ClearCacheFolder,
|
||||
}
|
||||
|
||||
impl SettingsAction {
|
||||
pub fn process_settings_action<'a>(
|
||||
self,
|
||||
app: &mut Damus,
|
||||
settings: &'a mut SettingsHandler,
|
||||
i18n: &'a mut Localization,
|
||||
img_cache: &mut Images,
|
||||
ctx: &egui::Context,
|
||||
) -> Option<RouterAction> {
|
||||
let mut route_action: Option<RouterAction> = None;
|
||||
|
||||
match self {
|
||||
Self::OpenRelays => {
|
||||
route_action = Some(RouterAction::route_to(Route::Relays));
|
||||
}
|
||||
Self::SetZoomFactor(zoom_factor) => {
|
||||
ctx.set_zoom_factor(zoom_factor);
|
||||
settings.set_zoom_factor(zoom_factor);
|
||||
}
|
||||
Self::SetShowSourceClient(option) => {
|
||||
option.set_note_options(&mut app.note_options);
|
||||
|
||||
settings.set_show_source_client(option);
|
||||
}
|
||||
Self::SetTheme(theme) => {
|
||||
ctx.set_theme(theme);
|
||||
settings.set_theme(theme);
|
||||
}
|
||||
Self::SetLocale(language) => {
|
||||
if i18n.set_locale(language.clone()).is_ok() {
|
||||
settings.set_locale(language.to_string());
|
||||
}
|
||||
}
|
||||
Self::SetRepliestNewestFirst(value) => {
|
||||
app.note_options.set(NoteOptions::RepliesNewestFirst, value);
|
||||
settings.set_show_replies_newest_first(value);
|
||||
}
|
||||
Self::OpenCacheFolder => {
|
||||
use opener;
|
||||
let _ = opener::open(img_cache.base_path.clone());
|
||||
}
|
||||
Self::ClearCacheFolder => {
|
||||
let _ = img_cache.clear_folder_contents();
|
||||
}
|
||||
Self::SetNoteBodyFontSize(size) => {
|
||||
let mut style = (*ctx.style()).clone();
|
||||
style.text_styles.insert(
|
||||
NotedeckTextStyle::NoteBody.text_style(),
|
||||
FontId::proportional(size),
|
||||
);
|
||||
ctx.set_style(style);
|
||||
|
||||
settings.set_note_body_font_size(size);
|
||||
}
|
||||
}
|
||||
route_action
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsView<'a> {
|
||||
settings: &'a mut Settings,
|
||||
note_context: &'a mut NoteContext<'a>,
|
||||
note_options: &'a mut NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
}
|
||||
|
||||
fn settings_group<S>(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui))
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Frame::group(ui.style())
|
||||
.fill(ui.style().visuals.widgets.open.bg_fill)
|
||||
.inner_margin(10.0)
|
||||
.show(ui, |ui| {
|
||||
ui.label(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()));
|
||||
ui.separator();
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
|
||||
|
||||
contents(ui)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl<'a> SettingsView<'a> {
|
||||
pub fn new(
|
||||
settings: &'a mut Settings,
|
||||
note_context: &'a mut NoteContext<'a>,
|
||||
note_options: &'a mut NoteOptions,
|
||||
jobs: &'a mut JobsCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
settings,
|
||||
note_context,
|
||||
note_options,
|
||||
jobs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the localized name for a language identifier
|
||||
fn get_selected_language_name(&mut self) -> String {
|
||||
if let Ok(lang_id) = self.settings.locale.parse::<LanguageIdentifier>() {
|
||||
self.note_context
|
||||
.i18n
|
||||
.get_locale_native_name(&lang_id)
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| lang_id.to_string())
|
||||
} else {
|
||||
self.settings.locale.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||
let mut action = None;
|
||||
let title = tr!(
|
||||
self.note_context.i18n,
|
||||
"Appearance",
|
||||
"Label for appearance settings section",
|
||||
);
|
||||
settings_group(ui, title, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Font size:",
|
||||
"Label for font size, Appearance settings section",
|
||||
)));
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(&mut self.settings.note_body_font_size, 8.0..=32.0)
|
||||
.text(""),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
action = Some(SettingsAction::SetNoteBodyFontSize(
|
||||
self.settings.note_body_font_size,
|
||||
));
|
||||
};
|
||||
|
||||
if ui
|
||||
.button(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Reset",
|
||||
"Label for reset note body font size, Appearance settings section",
|
||||
)))
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::SetNoteBodyFontSize(
|
||||
DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
let txn = Transaction::new(self.note_context.ndb).unwrap();
|
||||
if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) {
|
||||
if let Ok(preview_note) =
|
||||
self.note_context.ndb.get_note_by_id(&txn, note_id.bytes())
|
||||
{
|
||||
notedeck_ui::padding(8.0, ui, |ui| {
|
||||
if is_narrow(ui.ctx()) {
|
||||
ui.set_max_width(ui.available_width());
|
||||
}
|
||||
|
||||
NoteView::new(
|
||||
self.note_context,
|
||||
&preview_note,
|
||||
*self.note_options,
|
||||
self.jobs,
|
||||
)
|
||||
.actionbar(false)
|
||||
.options_button(false)
|
||||
.show(ui);
|
||||
});
|
||||
ui.separator();
|
||||
}
|
||||
}
|
||||
|
||||
let current_zoom = ui.ctx().zoom_factor();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Zoom Level:",
|
||||
"Label for zoom level, Appearance settings section",
|
||||
)));
|
||||
|
||||
let min_reached = current_zoom <= MIN_ZOOM;
|
||||
let max_reached = current_zoom >= MAX_ZOOM;
|
||||
|
||||
if ui
|
||||
.add_enabled(
|
||||
!min_reached,
|
||||
Button::new(
|
||||
RichText::new("-").text_style(NotedeckTextStyle::Small.text_style()),
|
||||
),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let new_zoom = (current_zoom - ZOOM_STEP).max(MIN_ZOOM);
|
||||
action = Some(SettingsAction::SetZoomFactor(new_zoom));
|
||||
};
|
||||
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", current_zoom * 100.0))
|
||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add_enabled(
|
||||
!max_reached,
|
||||
Button::new(
|
||||
RichText::new("+").text_style(NotedeckTextStyle::Small.text_style()),
|
||||
),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let new_zoom = (current_zoom + ZOOM_STEP).min(MAX_ZOOM);
|
||||
action = Some(SettingsAction::SetZoomFactor(new_zoom));
|
||||
};
|
||||
|
||||
if ui
|
||||
.button(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Reset",
|
||||
"Label for reset zoom level, Appearance settings section",
|
||||
)))
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Language:",
|
||||
"Label for language, Appearance settings section",
|
||||
)));
|
||||
|
||||
//
|
||||
ComboBox::from_label("")
|
||||
.selected_text(self.get_selected_language_name())
|
||||
.show_ui(ui, |ui| {
|
||||
for lang in self.note_context.i18n.get_available_locales() {
|
||||
let name = self
|
||||
.note_context
|
||||
.i18n
|
||||
.get_locale_native_name(lang)
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| lang.to_string());
|
||||
if ui
|
||||
.selectable_value(&mut self.settings.locale, lang.to_string(), name)
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::SetLocale(lang.to_owned()))
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Theme:",
|
||||
"Label for theme, Appearance settings section",
|
||||
)));
|
||||
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut self.settings.theme,
|
||||
ThemePreference::Light,
|
||||
richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
THEME_LIGHT,
|
||||
"Label for Theme Light, Appearance settings section",
|
||||
)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::SetTheme(ThemePreference::Light));
|
||||
}
|
||||
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut self.settings.theme,
|
||||
ThemePreference::Dark,
|
||||
richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
THEME_DARK,
|
||||
"Label for Theme Dark, Appearance settings section",
|
||||
)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::SetTheme(ThemePreference::Dark));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||
let id = ui.id();
|
||||
let mut action: Option<SettingsAction> = None;
|
||||
let title = tr!(
|
||||
self.note_context.i18n,
|
||||
"Storage",
|
||||
"Label for storage settings section"
|
||||
);
|
||||
settings_group(ui, title, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let static_imgs_size = self
|
||||
.note_context
|
||||
.img_cache
|
||||
.static_imgs
|
||||
.cache_size
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
let gifs_size = self.note_context.img_cache.gifs.cache_size.lock().unwrap();
|
||||
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"{} {}",
|
||||
tr!(
|
||||
self.note_context.i18n,
|
||||
"Image cache size:",
|
||||
"Label for Image cache size, Storage settings section"
|
||||
),
|
||||
format_size(
|
||||
[static_imgs_size, gifs_size]
|
||||
.iter()
|
||||
.fold(0_u64, |acc, cur| acc + cur.unwrap_or_default())
|
||||
)
|
||||
))
|
||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||
);
|
||||
|
||||
ui.end_row();
|
||||
|
||||
if !notedeck::ui::is_compiled_as_mobile()
|
||||
&& ui
|
||||
.button(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"View folder",
|
||||
"Label for view folder button, Storage settings section",
|
||||
)))
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::OpenCacheFolder);
|
||||
}
|
||||
|
||||
let clearcache_resp = ui.button(
|
||||
richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Clear cache",
|
||||
"Label for clear cache button, Storage settings section",
|
||||
))
|
||||
.color(Color32::LIGHT_RED),
|
||||
);
|
||||
|
||||
let id_clearcache = id.with("clear_cache");
|
||||
if clearcache_resp.clicked() {
|
||||
ui.data_mut(|d| d.insert_temp(id_clearcache, true));
|
||||
}
|
||||
|
||||
if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) {
|
||||
let mut confirm_pressed = false;
|
||||
clearcache_resp.show_tooltip_ui(|ui| {
|
||||
let confirm_resp = ui.button(tr!(
|
||||
self.note_context.i18n,
|
||||
"Confirm",
|
||||
"Label for confirm clear cache, Storage settings section"
|
||||
));
|
||||
if confirm_resp.clicked() {
|
||||
confirm_pressed = true;
|
||||
}
|
||||
|
||||
if confirm_resp.clicked()
|
||||
|| ui
|
||||
.button(tr!(
|
||||
self.note_context.i18n,
|
||||
"Cancel",
|
||||
"Label for cancel clear cache, Storage settings section"
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
|
||||
}
|
||||
});
|
||||
|
||||
if confirm_pressed {
|
||||
action = Some(SettingsAction::ClearCacheFolder);
|
||||
} else if !confirm_pressed && clearcache_resp.clicked_elsewhere() {
|
||||
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||
let mut action = None;
|
||||
|
||||
let title = tr!(
|
||||
self.note_context.i18n,
|
||||
"Others",
|
||||
"Label for others settings section"
|
||||
);
|
||||
settings_group(ui, title, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Sort replies newest first",
|
||||
"Label for Sort replies newest first, others settings section",
|
||||
)));
|
||||
|
||||
if ui
|
||||
.toggle_value(
|
||||
&mut self.settings.show_replies_newest_first,
|
||||
RichText::new(tr!(self.note_context.i18n, "ON", "ON"))
|
||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
action = Some(SettingsAction::SetRepliestNewestFirst(
|
||||
self.settings.show_replies_newest_first,
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Source client",
|
||||
"Label for Source client, others settings section",
|
||||
)));
|
||||
|
||||
for option in [
|
||||
ShowSourceClientOption::Hide,
|
||||
ShowSourceClientOption::Top,
|
||||
ShowSourceClientOption::Bottom,
|
||||
] {
|
||||
let mut current: ShowSourceClientOption =
|
||||
self.settings.show_source_client.clone().into();
|
||||
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut current,
|
||||
option,
|
||||
RichText::new(option.label(self.note_context.i18n))
|
||||
.text_style(NotedeckTextStyle::Small.text_style()),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
action = Some(SettingsAction::SetShowSourceClient(option));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||
let mut action = None;
|
||||
|
||||
if ui
|
||||
.add_sized(
|
||||
[ui.available_width(), 30.0],
|
||||
Button::new(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Configure relays",
|
||||
"Label for configure relays, settings section",
|
||||
))),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
action = Some(SettingsAction::OpenRelays);
|
||||
}
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||
let mut action: Option<SettingsAction> = None;
|
||||
|
||||
Frame::default()
|
||||
.inner_margin(Margin::symmetric(10, 10))
|
||||
.show(ui, |ui| {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
if let Some(new_action) = self.appearance_section(ui) {
|
||||
action = Some(new_action);
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
if let Some(new_action) = self.storage_section(ui) {
|
||||
action = Some(new_action);
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
if let Some(new_action) = self.other_options_section(ui) {
|
||||
action = Some(new_action);
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
if let Some(new_action) = self.manage_relays_section(ui) {
|
||||
action = Some(new_action);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_size(size_bytes: u64) -> String {
|
||||
const KB: f64 = 1024.0;
|
||||
const MB: f64 = KB * 1024.0;
|
||||
const GB: f64 = MB * 1024.0;
|
||||
|
||||
let size = size_bytes as f64;
|
||||
|
||||
if size < KB {
|
||||
format!("{size:.0} Bytes")
|
||||
} else if size < MB {
|
||||
format!("{:.1} KB", size / KB)
|
||||
} else if size < GB {
|
||||
format!("{:.1} MB", size / MB)
|
||||
} else {
|
||||
format!("{:.2} GB", size / GB)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
action: SidePanelAction,
|
||||
i18n: &mut Localization,
|
||||
) -> Option<SwitchingAction> {
|
||||
let router = get_active_columns_mut(i18n, accounts, decks_cache).get_first_router();
|
||||
let router = get_active_columns_mut(i18n, accounts, decks_cache).get_selected_router();
|
||||
let mut switching_response = None;
|
||||
match action {
|
||||
/*
|
||||
@@ -280,7 +280,7 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
{
|
||||
edit_deck
|
||||
.columns_mut()
|
||||
.get_first_router()
|
||||
.get_selected_router()
|
||||
.route_to(Route::EditDeck(index));
|
||||
} else {
|
||||
error!("Cannot push EditDeck route to index {}", index);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::support::{Support, SUPPORT_EMAIL};
|
||||
use egui::{vec2, Button, Label, Layout, RichText};
|
||||
use notedeck::{tr, Localization, NamedFontFamily, NotedeckTextStyle};
|
||||
use notedeck_ui::{colors::PINK, padding};
|
||||
use robius_open::Uri;
|
||||
use tracing::error;
|
||||
|
||||
use crate::support::Support;
|
||||
|
||||
pub struct SupportView<'a> {
|
||||
support: &'a mut Support,
|
||||
i18n: &'a mut Localization,
|
||||
@@ -44,15 +44,21 @@ impl<'a> SupportView<'a> {
|
||||
"Open your default email client to get help from the Damus team",
|
||||
"Instruction to open email client"
|
||||
));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(tr!(self.i18n, "Support email:", "Support email address",));
|
||||
ui.label(RichText::new(SUPPORT_EMAIL).color(PINK))
|
||||
});
|
||||
|
||||
let size = vec2(120.0, 40.0);
|
||||
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||
let font_size =
|
||||
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
||||
let button_resp = ui.add(open_email_button(self.i18n, font_size, size));
|
||||
if button_resp.clicked() {
|
||||
if let Err(e) = open::that(self.support.get_mailto_url()) {
|
||||
if let Err(e) = Uri::new(self.support.get_mailto_url()).open() {
|
||||
error!(
|
||||
"Failed to open URL {} because: {}",
|
||||
"Failed to open URL {} because: {:?}",
|
||||
self.support.get_mailto_url(),
|
||||
e
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user