1 Commits

Author SHA1 Message Date
f866f0ca26 Add GitHub workflows for syncing Crowdin translations
Changelog-Added: Added GitHub workflows for syncing Crowdin translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 20:53:03 -04:00
169 changed files with 3672 additions and 12498 deletions

3
.envrc
View File

@@ -18,7 +18,4 @@ export OLLAMA_HOST=http://ollama.jb55.com
# simple todo reminders
export TODO_FILE=TODO
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
2>/dev/null todo.sh ls || :

33
.github/workflows/crowdin-download.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
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 }}

67
.github/workflows/crowdin-upload.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
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 }}

4
.gitignore vendored
View File

@@ -19,7 +19,3 @@ queries/damus-notifs.json
.direnv/
scripts/macos_build_secrets.sh
/tags
.zed
.lsp
.idea
local.properties

View File

@@ -1 +0,0 @@
*.ftl

417
Cargo.lock generated
View File

@@ -105,8 +105,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-activity"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
source = "git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5#a8948332c7c551303d32eb26a59d0abd676e47a5"
dependencies = [
"android-properties",
"bitflags 2.9.1",
@@ -126,7 +125,7 @@ dependencies = [
[[package]]
name = "android-activity"
version = "0.6.0"
source = "git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea#092a83b747937a2890ac219617a4252c001842ea"
source = "git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9#c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9"
dependencies = [
"android-properties",
"bitflags 2.9.1",
@@ -766,25 +765,6 @@ 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"
@@ -822,17 +802,6 @@ 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"
@@ -1009,7 +978,6 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@@ -1265,7 +1233,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@@ -1403,7 +1370,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dpi"
version = "0.1.1"
source = "git+https://github.com/damus-io/winit?rev=9e4ea9de75222d2523a20f18d3a0a108c573737d#9e4ea9de75222d2523a20f18d3a0a108c573737d"
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
[[package]]
name = "dpi"
@@ -1411,26 +1378,20 @@ 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=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"bytemuck",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"serde",
]
[[package]]
name = "eframe"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"ahash",
"bytemuck",
@@ -1466,25 +1427,24 @@ dependencies = [
[[package]]
name = "egui"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"accesskit",
"ahash",
"backtrace",
"bitflags 2.9.1",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"epaint",
"log",
"nohash-hasher",
"profiling",
"serde",
"similar",
]
[[package]]
name = "egui-wgpu"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"ahash",
"bytemuck",
@@ -1503,7 +1463,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"ahash",
"arboard",
@@ -1521,7 +1481,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"ahash",
"egui",
@@ -1538,7 +1498,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"ahash",
"bytemuck",
@@ -1555,7 +1515,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.2.0"
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
source = "git+https://github.com/damus-io/egui-nav?rev=111de8ac40b5d18df53e9691eb18a50d49cb31d8#111de8ac40b5d18df53e9691eb18a50d49cb31d8"
dependencies = [
"egui",
"egui_extras",
@@ -1617,7 +1577,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
[[package]]
name = "emath"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"bytemuck",
"serde",
@@ -1635,7 +1595,7 @@ version = "0.3.0"
dependencies = [
"bech32",
"ewebsock",
"hashbrown 0.15.4",
"hashbrown",
"hex",
"mio",
"nostr 0.37.0",
@@ -1715,13 +1675,13 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
dependencies = [
"ab_glyph",
"ahash",
"bytemuck",
"ecolor",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"epaint_default_fonts",
"log",
"nohash-hasher",
@@ -1733,7 +1693,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
[[package]]
name = "equator"
@@ -2309,7 +2269,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.9.1",
"gpu-descriptor-types",
"hashbrown 0.15.4",
"hashbrown",
]
[[package]]
@@ -2331,18 +2291,6 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -2371,9 +2319,6 @@ name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
dependencies = [
"serde",
]
[[package]]
name = "hex-conservative"
@@ -2390,17 +2335,6 @@ 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"
@@ -2562,16 +2496,6 @@ 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"
@@ -2730,17 +2654,6 @@ 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"
@@ -2748,7 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
"hashbrown",
"serde",
]
@@ -2820,6 +2733,25 @@ 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"
@@ -2936,19 +2868,6 @@ 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"
@@ -3039,7 +2958,6 @@ dependencies = [
"bech32",
"bitcoin",
"lightning-types",
"serde",
]
[[package]]
@@ -3075,22 +2993,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lnsocket"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "724c7fba2188a49ab31316e52dd410d4d3168b8e6482aa2ac3889dd840d28712"
dependencies = [
"bitcoin",
"hashbrown 0.13.2",
"hex",
"lightning-types",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "lock_api"
version = "0.4.13"
@@ -3288,7 +3190,7 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
"indexmap 2.9.0",
"indexmap",
"log",
"rustc-hash 1.1.0",
"spirv",
@@ -3402,15 +3304,6 @@ 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"
@@ -3490,8 +3383,8 @@ dependencies = [
[[package]]
name = "nostrdb"
version = "0.8.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3#2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3"
version = "0.7.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=a307f5d3863b5319c728b2782959839b8df544cb#a307f5d3863b5319c728b2782959839b8df544cb"
dependencies = [
"bindgen",
"cc",
@@ -3505,26 +3398,21 @@ dependencies = [
[[package]]
name = "notedeck"
version = "0.6.0"
version = "0.5.6"
dependencies = [
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea)",
"base32",
"bech32",
"bincode",
"bitflags 2.9.1",
"blurhash",
"chrono",
"dirs",
"eframe",
"egui",
"egui-winit",
"egui_extras",
"ehttp",
"enostr",
"fluent",
"fluent-langneg",
"fluent-resmgr",
"hashbrown 0.15.4",
"hashbrown",
"hex",
"image",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3558,9 +3446,8 @@ dependencies = [
[[package]]
name = "notedeck_chrome"
version = "0.6.0"
version = "0.5.6"
dependencies = [
"bitflags 2.9.1",
"eframe",
"egui",
"egui-winit",
@@ -3568,10 +3455,8 @@ dependencies = [
"egui_tabs",
"nostrdb",
"notedeck",
"notedeck_clndash",
"notedeck_columns",
"notedeck_dave",
"notedeck_notebook",
"notedeck_ui",
"profiling",
"puffin",
@@ -3588,28 +3473,9 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "notedeck_clndash"
version = "0.6.0"
dependencies = [
"eframe",
"egui",
"egui_extras",
"hex",
"lightning-invoice",
"lnsocket",
"nostrdb",
"notedeck",
"notedeck_ui",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "notedeck_columns"
version = "0.6.0"
version = "0.5.6"
dependencies = [
"base64 0.22.1",
"bech32",
@@ -3624,16 +3490,16 @@ dependencies = [
"egui_virtual_list",
"ehttp",
"enostr",
"hashbrown 0.15.4",
"hashbrown",
"hex",
"human_format",
"image",
"indexmap 2.9.0",
"indexmap",
"nostrdb",
"notedeck",
"notedeck_ui",
"oot_bitset",
"opener",
"open",
"poll-promise",
"pretty_assertions",
"profiling",
@@ -3641,7 +3507,6 @@ dependencies = [
"puffin_egui",
"rfd",
"rmpv",
"robius-open",
"security-framework 2.11.1",
"serde",
"serde_derive",
@@ -3663,7 +3528,7 @@ dependencies = [
[[package]]
name = "notedeck_dave"
version = "0.6.0"
version = "0.5.6"
dependencies = [
"async-openai",
"bytemuck",
@@ -3671,7 +3536,6 @@ dependencies = [
"eframe",
"egui",
"egui-wgpu",
"egui_extras",
"enostr",
"futures",
"hex",
@@ -3686,27 +3550,19 @@ dependencies = [
"tracing",
]
[[package]]
name = "notedeck_notebook"
version = "0.6.0"
dependencies = [
"egui",
"jsoncanvas",
"notedeck",
]
[[package]]
name = "notedeck_ui"
version = "0.6.0"
version = "0.5.6"
dependencies = [
"bitflags 2.9.1",
"blurhash",
"eframe",
"egui",
"egui-winit",
"egui_extras",
"ehttp",
"enostr",
"hashbrown 0.15.4",
"hashbrown",
"image",
"nostrdb",
"notedeck",
@@ -4136,14 +3992,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "opener"
version = "0.8.2"
name = "open"
version = "5.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
dependencies = [
"bstr",
"normpath",
"windows-sys 0.59.0",
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
@@ -4247,6 +4103,12 @@ 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"
@@ -4535,7 +4397,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
dependencies = [
"egui",
"egui_extras",
"indexmap 2.9.0",
"indexmap",
"natord",
"once_cell",
"parking_lot",
@@ -4863,26 +4725,6 @@ 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"
@@ -5074,30 +4916,6 @@ 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"
@@ -5244,30 +5062,6 @@ 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"
@@ -5421,7 +5215,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"indexmap 2.9.0",
"indexmap",
"itoa",
"memchr",
"ryu",
@@ -5460,38 +5254,6 @@ 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"
@@ -5553,12 +5315,6 @@ 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"
@@ -6092,7 +5848,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.9.0",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6875,7 +6631,7 @@ dependencies = [
"bitflags 2.9.1",
"cfg_aliases",
"document-features",
"indexmap 2.9.0",
"indexmap",
"log",
"naga",
"once_cell",
@@ -6996,16 +6752,6 @@ 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"
@@ -7025,16 +6771,6 @@ 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"
@@ -7111,15 +6847,6 @@ 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"
@@ -7447,10 +7174,10 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winit"
version = "0.30.8"
source = "git+https://github.com/damus-io/winit?rev=9e4ea9de75222d2523a20f18d3a0a108c573737d#9e4ea9de75222d2523a20f18d3a0a108c573737d"
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
dependencies = [
"ahash",
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea)",
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9)",
"atomic-waker",
"bitflags 2.9.1",
"block2 0.5.1",
@@ -7502,7 +7229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
dependencies = [
"ahash",
"android-activity 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5)",
"atomic-waker",
"bitflags 2.9.1",
"block2 0.5.1",

View File

@@ -1,21 +1,17 @@
[workspace]
resolver = "2"
package.version = "0.6.0"
package.version = "0.5.6"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
"crates/notedeck_columns",
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/notedeck_clndash",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
]
[workspace.dependencies]
opener = "0.8.2"
chrono = "0.4.40"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
@@ -27,7 +23,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 = "de6e2d51892478fdd516df166f866e64dedbae07" }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "111de8ac40b5d18df53e9691eb18a50d49cb31d8" }
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" }
@@ -37,7 +33,7 @@ ewebsock = { version = "0.2.0", features = ["tls"] }
fluent = "0.17.0"
fluent-resmgr = "0.0.8"
fluent-langneg = "0.13"
hex = { version = "0.4.3", features = ["serde"] }
hex = "0.4.3"
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
indexmap = "2.6.0"
log = "0.4.17"
@@ -45,18 +41,16 @@ 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 = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "a307f5d3863b5319c728b2782959839b8df544cb" }
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_clndash = { path = "crates/notedeck_clndash" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
notedeck_ui = { path = "crates/notedeck_ui" }
tokenator = { path = "crates/tokenator" }
once_cell = "1.19.0"
robius-open = "0.1"
open = "5.3.0"
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" }
@@ -81,14 +75,12 @@ mime_guess = "2.0.5"
pretty_assertions = "1.4.1"
jni = "0.21.1"
profiling = "1.0"
lightning-invoice = { version = "0.33.1", features = ["serde"] }
lightning-invoice = "0.33.1"
secp256k1 = "0.30.0"
hashbrown = "0.15.2"
openai-api-rs = "6.0.3"
re_memory = "0.23.4"
oot_bitset = "0.1.1"
blurhash = "0.2.3"
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "092a83b747937a2890ac219617a4252c001842ea", features = [ "game-activity" ] }
[profile.small]
inherits = 'release'
@@ -106,15 +98,15 @@ strip = true # Strip symbols from binary*
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
egui = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
eframe = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
epaint = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
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" }
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 = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" }
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "a8948332c7c551303d32eb26a59d0abd676e47a5" }
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }

View File

@@ -27,4 +27,4 @@ push-android-config:
android: jni
cd $(ANDROID_DIR) && ./gradlew installDebug
adb shell am start -n com.damus.notedeck/.MainActivity
adb logcat -v color -s GameActivity -s RustStdoutStderr -s threaded_app | tee logcat.txt
adb logcat -v color -s RustStdoutStderr -s threaded_app | tee logcat.txt

11
android
View File

@@ -1,11 +0,0 @@
#!/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.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="clnlogo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.078823"
inkscape:cx="396.72867"
inkscape:cy="561.25984"
inkscape:window-width="2020"
inkscape:window-height="1420"
inkscape:window-x="270"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
style="display:inline">
<g
id="g4"
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
<path
class="st1"
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
id="path3"
style="fill:#f0d003" />
<path
fill="#fffae6"
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
id="path4" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -6,7 +6,9 @@
# Regular strings
# Profile about/bio field label
About_00c0 = Über mich
About_00c0 = Über
# Display name for account management
Accounts_e233 = Konten
# Column title for account management
Accounts_f018 = Konten
# Button label to add a relay
@@ -21,10 +23,16 @@ 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
@@ -45,8 +53,6 @@ Algo_2452 = Algorithmus
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
# Label for zap amount input field
Amount_70f0 = Menge
# Label for appearance settings section
Appearance_4c7f = Darstellung
# Button to send message to Dave AI assistant
Ask_b7f4 = Fragen
# Placeholder text for Dave AI input field
@@ -61,18 +67,12 @@ Broadcast_fe43 = Senden
Broadcast_Local_7e50 = Lokal senden
# Button label to cancel an action
Cancel_ed3b = Abbrechen
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Abbrechen
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Zwischenspeicher leeren
# Hover text for editable zap amount
Click_to_edit_0414 = Zum Bearbeiten anklicken
# Display name for note composition
Compose_Note_ad11 = Notiz erstellen
# Column title for note composition
Compose_Note_c094 = Notiz erstellen
# Label for configure relays, settings section
Configure_relays_d156 = Relays konfigurieren
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Bestätigen
# Button label to confirm an action
Confirm_f8a6 = Bestätigen
# Status label for connected relay
@@ -83,6 +83,8 @@ 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
@@ -98,31 +100,33 @@ 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 }T.
# Relative time in hours
count_h_3ecb = { $count }h
count_h_3ecb = { $count }Std.
# Relative time in minutes
count_m_b41e = { $count }min
count_m_b41e = { $count }Min.
# Relative time in months
count_mo_7aba = { $count }M
count_mo_7aba = { $count }Mon.
# Relative time in seconds
count_s_aa26 = { $count }s
count_s_aa26 = { $count }Sek.
# Relative time in weeks
count_w_7468 = { $count }W
count_w_7468 = { $count }Wo.
# Relative time in years
count_y_9408 = { $count }J
count_y_9408 = { $count }J.
# Button to create a new account
Create_Account_6994 = Konto erstellen
# Button label to create a new deck
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 Theme Dark, Appearance settings section
Dark_85fe = Dunkel
# Label for deck name input field
Deck_name_cd32 = Deck-Name
# Label for decks section in side panel
@@ -143,10 +147,14 @@ 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
@@ -163,16 +171,18 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
# Label for find user button
Find_User_bd12 = Profil finden
# Label for font size, Appearance settings section
Font_size_dd73 = Schriftgröße:
# 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
Icon_b0ab = Symbol
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Bildcache Größe:
# Title for individual user column
Individual_b776 = Individuell
# Error message for invalid zap amount
@@ -193,12 +203,12 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
# Label for language, Appearance settings section
Language_e264 = Sprache:
# Title for last note per user column
Last_Note_per_User_17ad = Letzte Notiz pro Profil
# Label for Theme Light, Appearance settings section
Light_7475 = Hell
# 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
@@ -231,20 +241,20 @@ 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
Notifications_ef56 = Benachrichtigungen
# Relative time for very recent events (less than 3 seconds)
now_2181 = Gerade eben
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = An
now_2181 = Jetzt
# Button label to open email client
Open_Email_25e9 = E-Mail öffnen
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
# Label for others settings section
Others_7267 = Andere
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
# Error message for missing deck name
@@ -257,20 +267,30 @@ 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
@@ -291,10 +311,6 @@ replying_to_a_note_e0bc = Antwort auf eine Notiz
Repost_this_note_8e56 = Diese Notiz teilen
# Label for reposted notes
Reposted_61c8 = Teilen
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Zurücksetzen
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Zurücksetzen
# Heading for support section
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -305,6 +321,12 @@ 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
@@ -317,8 +339,6 @@ See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
# Button label to send a zap
Send_1ea4 = Senden
# Column title for app settings
Settings_7a4f = Einstellungen
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
# Button label to sign out of account
@@ -327,8 +347,6 @@ Sign_out_337b = Abmelden
Someone_else_s_Notes_7e5f = Notizen anderer Profile
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Neueste Antworten zuerst sortieren:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
# Description for hashtags column
@@ -347,14 +365,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Ben
Step_1_8656 = Schritt 1
# Step 2 label in support instructions
Step_2_d08d = Schritt 2
# Label for storage settings section
Storage_ed65 = Speicher
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
# Support email address
Support_email_44d9 = E-Mail Support:
# 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
@@ -363,12 +379,18 @@ Switch_to_light_mode_72ce = Zum Hellmodus wechseln
Tap_to_Load_4b05 = Zum Laden antippen
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
# Label for theme, Appearance settings section
Theme_4aac = Design:
# Column title for note thread view
Thread_0f20 = Unterhaltung
Thread_0f20 = Unterhaltungen
# Display name for thread view
Thread_9957 = Unterhaltungen
# Link text for thread references
thread_ad1f = Unterhaltung
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
# Title for universe column
Universe_e01e = Weltraum
# Column title for universe feed
@@ -379,10 +401,10 @@ Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das ak
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
# Profile username field label
Username_daa7 = Benutzername
# Label for view folder button, Storage settings section
View_folder_9742 = Ordner anzeigen
# Column title for wallet management
Wallet_5e50 = Wallet
# 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
@@ -399,8 +421,6 @@ Your_Notifications_080d = Deine Benachrichtigungen
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zappe diese Notiz
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoomstufe:
# Pluralized strings

View File

@@ -64,9 +64,6 @@ 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
@@ -88,24 +85,12 @@ 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
@@ -178,9 +163,6 @@ 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
@@ -238,9 +220,6 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
# Label for find user button
Find_User_bd12 = Find User
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
@@ -250,9 +229,6 @@ 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
@@ -283,15 +259,9 @@ 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)
@@ -349,18 +319,12 @@ Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = now
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
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...
@@ -430,12 +394,6 @@ Repost_this_note_8e56 = Repost this note
# Label for reposted notes
Reposted_61c8 = Reposted
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Reset
# Heading for support section
Running_into_a_bug_1796 = Running into a bug?
@@ -469,9 +427,6 @@ 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
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
@@ -484,9 +439,6 @@ Someone_else_s_Notes_7e5f = Someone else's Notes
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Someone else's Notifications
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list
@@ -514,18 +466,12 @@ 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
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Switch to dark mode
@@ -538,9 +484,6 @@ 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
@@ -562,9 +505,6 @@ 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
@@ -592,9 +532,6 @@ 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

View File

@@ -64,9 +64,6 @@ 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{"]"}
@@ -88,24 +85,12 @@ 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{"]"}
@@ -178,9 +163,6 @@ 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é{"]"}
@@ -238,9 +220,6 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
# Label for find user button
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
# Label for font size, Appearance settings section
Font_size_dd73 = {"["}Fóñt sízé:{"]"}
# Title for hashtags column
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
@@ -250,9 +229,6 @@ 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{"]"}
@@ -283,15 +259,9 @@ 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){"]"}
@@ -349,18 +319,12 @@ Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
# Relative time for very recent events (less than 3 seconds)
now_2181 = {"["}ñów{"]"}
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = {"["}Óñ{"]"}
# Button label to open email client
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é...{"]"}
@@ -430,12 +394,6 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
# Label for reposted notes
Reposted_61c8 = {"["}Répóstéd{"]"}
# Label for reset note body font size, Appearance settings section
Reset_4e60 = {"["}Rését{"]"}
# Label for reset zoom level, Appearance settings section
Reset_62d4 = {"["}Rését{"]"}
# Heading for support section
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
@@ -469,9 +427,6 @@ 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{"]"}
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
@@ -484,9 +439,6 @@ Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"}
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = {"["}Sórt réplíés ñéwést fírst:{"]"}
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"}
@@ -514,18 +466,12 @@ 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{"]"}
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
# Support email address
Support_email_44d9 = {"["}Súppórt émàíl:{"]"}
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
@@ -538,9 +484,6 @@ 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{"]"}
@@ -562,9 +505,6 @@ 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{"]"}
@@ -592,9 +532,6 @@ 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

View File

@@ -1,410 +0,0 @@
# 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
# Label for appearance settings section
Appearance_4c7f = Aspecto
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
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
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpiar caché
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relés
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
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 Theme Dark, Appearance settings section
Dark_85fe = Oscuro
# Label for deck name input field
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
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
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Ícono
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamaño de caché de imágenes:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
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
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Label for Theme Light, Appearance settings section
Light_7475 = Claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
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
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Label for others settings section
Others_7267 = Otros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
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
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
Running_into_a_bug_1796 = ¿Encontraste un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
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
# Column title for app settings
Settings_7a4f = Configuración
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
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
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
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
# Label for storage settings section
Storage_ed65 = Almacenamiento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
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!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# 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
# Label for view folder button, Storage settings section
View_folder_9742 = Ver carpeta
# Column title for wallet management
Wallet_5e50 = Billetera
# Hint for deck name input field
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
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nivel de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}

View File

@@ -1,410 +0,0 @@
# 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
# Label for appearance settings section
Appearance_4c7f = Aspecto
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
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
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpiar caché
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relés
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
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 Theme Dark, Appearance settings section
Dark_85fe = Oscuro
# Label for deck name input field
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
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
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Icono
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamaño de caché de imágenes:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
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
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Label for Theme Light, Appearance settings section
Light_7475 = Claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
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
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Label for others settings section
Others_7267 = Otros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
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
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
Running_into_a_bug_1796 = ¿Has encontrado un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
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
# Column title for app settings
Settings_7a4f = Configuración
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
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
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
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
# Label for storage settings section
Storage_ed65 = Almacenamiento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
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!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# 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
# Label for view folder button, Storage settings section
View_folder_9742 = Ver carpeta
# Column title for wallet management
Wallet_5e50 = Monedero
# Hint for deck name input field
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
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nivel de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}

View File

@@ -7,6 +7,8 @@
# 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
@@ -21,10 +23,16 @@ 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
@@ -45,8 +53,6 @@ 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 +67,12 @@ Broadcast_fe43 = Diffusion
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,6 +83,8 @@ 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
@@ -117,12 +119,14 @@ 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
@@ -143,10 +147,14 @@ 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
@@ -161,16 +169,18 @@ Enter_your_key_0fca = Entrez votre clé
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
# Label for find user button
Find_User_bd12 = Trouver un utilisateur
# Label for font size, Appearance settings section
Font_size_dd73 = Taille du texte :
# 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
# 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
@@ -191,12 +201,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
# Label for Theme Light, Appearance settings section
Light_7475 = Clair
# 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)
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
# Login page title
@@ -229,20 +239,20 @@ 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
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = maintenant
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Activé
# Button label to open email client
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
@@ -255,20 +265,30 @@ 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
@@ -289,10 +309,6 @@ replying_to_a_note_e0bc = répondre à une note
Repost_this_note_8e56 = Republier cette note
# Label for reposted notes
Reposted_61c8 = Republier
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Réinitialiser
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Réinitialiser
# Heading for support section
Running_into_a_bug_1796 = Vous rencontrez un problème ?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -303,6 +319,12 @@ 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
@@ -315,8 +337,6 @@ 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
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
# Button label to sign out of account
@@ -325,8 +345,6 @@ Sign_out_337b = Se déconnecter
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Trier les réponses les plus récentes en premier :
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
# Description for hashtags column
@@ -345,14 +363,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
# Support email address
Support_email_44d9 = Adresse email de l'assistance :
# 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
@@ -361,12 +377,18 @@ 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
# Title for universe column
Universe_e01e = Universel
# Column title for universe feed
@@ -377,10 +399,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
@@ -397,8 +419,6 @@ 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

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 概要
# Column title for account management
Accounts_f018 = アカウント
# Button label to add a relay
Add_269d = 追加
# Label for add column button
Add_47df = 追加
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = このアカウントでのみ使用される別のウォレットを追加
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = 続行するにはウォレットを追加してください
# Button label to add a new account
Add_account_1cfc = アカウントを追加
# Column title for adding new account
Add_Account_d06c = アカウントの追加
# Column title for adding algorithm column
Add_Algo_Column_0d75 = アルゴカラムの追加
# Column title for adding new column
Add_Column_c764 = カラムの追加
# Column title for adding new deck
Add_Deck_fabf = デッキの追加
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = 外部通知カラムの追加
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = ハッシュタグカラムの追加
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = 最後の投稿カラムの追加
# Column title for adding notifications column
Add_Notifications_Column_79f8 = 外部通知カラムの追加
# Button label to add a relay
Add_relay_269d = リレーを追加
# Button label to add a wallet
Add_Wallet_d1be = ウォレットを追加
# Title for algorithmic feeds column
Algo_2452 = アルゴ
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 投稿の発見に役立つアルゴリズムフィードです
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外観
# Button to send message to Dave AI assistant
Ask_b7f4 = 質問
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Dave に何でも質問してみましょう…
# Profile banner URL field label
Banner_52ef = バナー
# Beta version label
BETA_8e5d = ベータ
# Broadcast the note to all connected relays
Broadcast_fe43 = ブロードキャスト
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = ローカルにブロードキャスト
# Button label to cancel an action
Cancel_ed3b = キャンセル
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = キャンセル
# Label for clear cache button, Storage settings section
Clear_cache_dccb = キャッシュを消去
# Hover text for editable zap amount
Click_to_edit_0414 = クリックして編集
# Column title for note composition
Compose_Note_c094 = メモの作成
# Label for configure relays, settings section
Configure_relays_d156 = リレーを設定
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 決定
# Button label to confirm an action
Confirm_f8a6 = 決定
# Status label for connected relay
Connected_f8cc = 接続済
# Status label for connecting relay
Connecting_6b7e = 接続中…
# Title for contact list column
Contact_List_f85a = フォロイーリスト
# Column title for contact lists
Contacts_7533 = フォロー
# Column title for last notes per contact
Contacts__last_notes_3f84 = フォロー (最後の投稿)
# Button label to copy logs
Copy_a688 = コピー
# Button to copy media link to clipboard
Copy_Link_dc7c = リンクをコピー
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = 投稿 ID をコピー
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = 投稿の JSON をコピー
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = 公開鍵をコピー
# Copy the text content of the note to clipboard
Copy_Text_f81c = テキストをコピー
# Relative time in days
count_d_b9be = { $count }日
# Relative time in hours
count_h_3ecb = { $count }時間
# Relative time in minutes
count_m_b41e = { $count }分
# Relative time in months
count_mo_7aba = { $count }ヶ月
# Relative time in seconds
count_s_aa26 = { $count }秒
# Relative time in weeks
count_w_7468 = { $count }週間
# Relative time in years
count_y_9408 = { $count }年
# Button to create a new account
Create_Account_6994 = アカウントを作成
# Button label to create a new deck
Create_Deck_16b7 = デッキを作成
# Column title for custom timelines
Custom_a69e = カスタマイズ
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap 金額をカスタマイズ
# Column title for support page
Damus_Support_27c0 = Damus サポート
# Label for Theme Dark, Appearance settings section
Dark_85fe = ダーク
# Label for deck name input field
Deck_name_cd32 = デッキ名
# Label for decks section in side panel
DECKS_1fad = デッキ
# Label for default zap amount input
Default_amount_per_zap_399d = Zap ごとのデフォルトの金額:
# Name of the default deck feed
Default_Deck_fcca = 既定のデッキ
# Button label to delete a deck
Delete_Deck_bb29 = デッキを削除
# Tooltip for deleting a column
Delete_this_column_8d5a = このカラムを削除します
# Button label to delete a wallet
Delete_Wallet_d1d4 = ウォレットを削除
# Profile display name field label
Display_name_f9d9 = 表示名
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" が識別に使用されます
# Column title for editing deck
Edit_Deck_4018 = デッキの編集
# Button label to edit a deck
Edit_Deck_fd93 = デッキを編集
# Button label to edit user profile
Edit_Profile_49e6 = プロファイルを編集
# Column title for profile editing
Edit_Profile_8ad4 = プロファイルの編集
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 必要なハッシュタグをここに入力してください (複数スペースで区切る場合)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = ここにリレーを入力してください
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = ユーザーの鍵 (npub, hex, nip05) を入力してください...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = 鍵を入力してください
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 公開鍵 (npub)、nostr アドレス (例: { $address })、秘密鍵 (nsec) を入力してください。 投稿、返信などを行うには秘密鍵を入力する必要があります。
# Label for find user button
Find_User_bd12 = ユーザーを探す
# Label for font size, Appearance settings section
Font_size_dd73 = フォントサイズ:
# Title for hashtags column
Hashtags_f8e0 = ハッシュタグ
# Title for Home column
Home_8c19 = ホーム
# Label for deck icon selection
Icon_b0ab = アイコン
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 画像キャッシュのサイズ:
# Title for individual user column
Individual_b776 = 個人用
# Error message for invalid zap amount
Invalid_amount_6630 = 無効な金額です
# Error message for invalid key input
Invalid_key_4726 = 無効な鍵です。
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = 無効な NWC URI です
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 投稿と返信を記録します
# Label for language, Appearance settings section
Language_e264 = 言語:
# Title for last note per user column
Last_Note_per_User_17ad = ユーザーごとの最後の投稿
# Label for Theme Light, Appearance settings section
Light_7475 = ライト
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ライトニングネットワークアドレス (lud16)
# Login page title
Login_9eef = ログイン
# Login button text
Login_now___let_s_do_this_5630 = 今すぐログイン — レッツゴー!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = フォローしていない人のメディアです
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = このカラムを別の位置に移動します
# Title for the user's deck
My_Deck_4ac5 = あなたのデッキ
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nostr は初めてですか?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr アドレス (NIP-05)
# Default username when profile is not available
nostrich_df29 = ノス民
# Status label for disconnected relay
Not_Connected_6292 = 未接続
# Link text for note references
note_cad6 = 投稿
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck はベータ製品です。問題が発生した場合はサポートに問い合わせてください。
# Filter label for notes only view
Notes_03fb = 投稿
# Label for notes-only filter
Notes_60d2 = 投稿
# Filter label for notes and replies view
Notes___Replies_1ec2 = 投稿 & 返信
# Label for notes and replies filter
Notes___Replies_6e3b = 投稿 & 返信
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = たった今
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 有効
# Button label to open email client
Open_Email_25e9 = メールを開く
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = デフォルトのメールクライアントを開いて、Damus チームのヘルプを表示しましょう。
# Label for others settings section
Others_7267 = その他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = ここに NWC の URI を貼り付けてください...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = デッキの名前を作成してください。
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = デッキの名前を作成してアイコンを選択してください。
# Error message for missing deck icon
Please_select_an_icon_655b = アイコンを選択してください。
# Button label to post a note
Post_now_8a49 = すぐに投稿
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 下のボタンを押して、最新のログをシステムのクリップボードにコピーします。その後、メールに貼り付けてください。
# Profile picture URL field label
Profile_picture_81ff = プロフィール写真
# Column title for quote composition
Quote_475c = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 不明な投稿の引用です
# Label for read-only profile mode
Read_only_82ff = 読み取り専用
# Column title for relay management
Relays_9d89 = リレー
# Label for relay list section
Relays_ad5e = リレー
# Column title for reply composition
Reply_3bf1 = 返信
# Hover text for reply button
Reply_to_this_note_f5de = この投稿に返信
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = 不明な投稿に返信しています
# Fallback template for replying to user
replying_to__user_15ab = { $user } に返信
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = 誰かのスレッドで { $user } に返信
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = { $user }の { $note } の { $thread_user }の { $thread } に返信
# Template for replying to user's note
replying_to__user__s__note_ccba = { $user }の { $note } に返信
# Template for replying to root thread
replying_to__user__s__thread_444d = { $user }の { $thread } に返信
# Fallback text when reply note is not found
replying_to_a_note_e0bc = 投稿に返信
# Hover text for repost button
Repost_this_note_8e56 = このメモを再投稿
# Label for reposted notes
Reposted_61c8 = 再投稿
# Label for reset note body font size, Appearance settings section
Reset_4e60 = リセット
# Label for reset zoom level, Appearance settings section
Reset_62d4 = リセット
# Heading for support section
Running_into_a_bug_1796 = バグに遭遇しましたか?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 変更を保存
# Column title for search page
Search_c573 = 検索
# Placeholder for search notes input field
Search_notes_42a6 = 投稿を検索しましょう...
# Search in progress message
Searching_for___query_5d18 = 「{ $query }」を検索中
# Description for Home column
See_notes_from_your_contacts_ac16 = フォローしている人の投稿を表示
# Description for universe column
See_the_whole_nostr_universe_7694 = 全ユニバースを表示します
# Button label to send a zap
Send_1ea4 = 送信
# Column title for app settings
Settings_7a4f = 設定
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 一覧から各ユーザーの最後の投稿を表示する
# Button label to sign out of account
Sign_out_337b = サインアウト
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = 他の人の投稿
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 他の人の通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 最新の返信を最初に並べ替え:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = フォローリストにある各ユーザーの最後の投稿を取得します
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = 特定のハッシュタグで最新の情報を受け取ります
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = 通知とメンションの最新の情報を受け取ります
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 投稿と返信の最新の情報を受け取ります
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = あなたの通知とメンションの最新の情報を受け取ります
# Step 1 label in support instructions
Step_1_8656 = ステップ 1
# Step 2 label in support instructions
Step_2_d08d = ステップ 2
# Label for storage settings section
Storage_ed65 = ストレージ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 他のユーザー投稿の購読
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 投稿の購読
# Support email address
Support_email_44d9 = サポートメール:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = ダークモードに切り替える
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = ライトモードに切り替える
# Button text to load blurred media
Tap_to_Load_4b05 = タップして読み込む
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI アシスタントトライアルが終了しました: (テストしていただきありがとうございます! Zap 対応デイブは近日公開予定です!
# Label for theme, Appearance settings section
Theme_4aac = テーマ:
# Column title for note thread view
Thread_0f20 = スレッド
# Link text for thread references
thread_ad1f = スレッド
# Title for universe column
Universe_e01e = ユニバース
# Column title for universe feed
Universe_ffaa = ユニバース
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = このウォレットを現在のアカウントにのみ使用する
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $domain }" の "{ $username }" が識別に使用されます
# Profile username field label
Username_daa7 = ユーザー名
# Label for view folder button, Storage settings section
View_folder_9742 = フォルダを表示
# Column title for wallet management
Wallet_5e50 = ウォレット
# Hint for deck name input field
We_recommend_short_names_083e = 短い名前を推奨しています
# Profile website field label
Website_7980 = Web サイト
# Placeholder for note input field
Write_a_banger_note_here_bad2 = アツい一言をどうぞ...
# Placeholder text for key input field
Your_key_here_81bd = ここに鍵を入力...
# Title for your notes column
Your_Notes_f6db = 投稿
# Title for your notifications column
Your_Notifications_080d = 通知
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = この投稿に Zap
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 拡大率:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $query } の結果を '{ $count }' 件取得しました
*[other] ' { $query } の結果を '{ $count }' 件取得しました
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Transmitir
# Label for add column button
Add_47df = Adicionar coluna
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar outra carteira a ser usada apenas nesta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Obrigatório adicionar carteira
# Button label to add a new account
Add_account_1cfc = Adicionar conta nova aqui
# Column title for adding new account
Add_Account_d06c = Adicionar nova conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de #
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar última coluna de notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar transmissão
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmos
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algoritmos para pesquisar notas
# Label for zap amount input field
Amount_70f0 = Valor
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar ao Dave
# Profile banner URL field label
Banner_52ef = Destaque
# Beta version label
BETA_8e5d = Beta
# Broadcast the note to all connected relays
Broadcast_fe43 = Encaminhar
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Encaminhar especificamente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Editar valor
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar canais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectar
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contatos
# Column title for contact lists
Contacts_7533 = Contatos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contatos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar nota "JSON"
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }D
# Relative time in hours
count_h_3ecb = { $count }H
# Relative time in minutes
count_m_b41e = { $count }M
# Relative time in months
count_mo_7aba = { $count }Mes
# Relative time in seconds
count_s_aa26 = { $count }S
# Relative time in weeks
count_w_7468 = { $count }Sem
# Relative time in years
count_y_9408 = { $count }A
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizar
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do ZAP
# Column title for support page
Damus_Support_27c0 = Ajuda
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão de ZAP
# Name of the default deck feed
Default_Deck_fcca = Nome padrão de abas
# Button label to delete a deck
Delete_Deck_bb29 = Deletar aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Deletar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Deletar carteira
# Profile display name field label
Display_name_f9d9 = Nome de exibição
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será utilizado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Digite as # desejadas aqui (para múltiplos espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insira a retransmissão aqui
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Digite a chave do usuário (npub, hex, nip05) aqui...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Sua chave aqui
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Pesquisar usuário
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra
# Title for hashtags column
Hashtags_f8e0 = #
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache de imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI Inválido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100 mil
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10 mil
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20 mil
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50 mil
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5 mil
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanhe suas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma
# Title for last note per user column
Last_Note_per_User_17ad = Última Nota por Usuário
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço de rede de eletrização (lud16)
# Login page title
Login_9eef = Entrar
# Login button text
Login_now___let_s_do_this_5630 = Entrar agora! Vamos nessa!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de pessoas que você não segue
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Novo no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (Identidade NIP-05)
# Default username when profile is not available
nostrich_df29 = Nostrich
# Status label for disconnected relay
Not_Connected_6292 = Desconectado
# Link text for note references
note_cad6 = Nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere erros e entre em contato conosco quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = Agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ligar
# Button label to open email client
Open_Email_25e9 = Abrir E-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abra o seu cliente de e-mail padrão para obter ajuda do time Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cole seu URI NWC aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Por favor, crie um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Por favor, crie um nome para a aba e selecione um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Favor selecionar um ícone.
# Button label to post a note
Post_now_8a49 = Postar
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Clique abaixo para copiar seus registros mais recentes para a área de transferência do seu sistema. Em seguida, cole-os no seu E-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Modo leitura
# Column title for relay management
Relays_9d89 = Canais
# Label for relay list section
Relays_ad5e = Canais
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = Respondendo { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = Respondendo { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Resposta { $user }de { $note } em { $thread_user }' { $thread }
# Template for replying to user's note
replying_to__user__s__note_ccba = Respondendo { $user }de { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = Respondendo { $user }de { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = Respondendo nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar nota
# Label for reposted notes
Reposted_61c8 = Publicada
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Resetar
# Heading for support section
Running_into_a_bug_1796 = Precisa de ajuda?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Salvar
# Button label to save profile changes
Save_changes_00db = Salvo
# Column title for search page
Search_c573 = Pesquisar
# Placeholder for search notes input field
Search_notes_42a6 = Pesquisar notas...
# Search in progress message
Searching_for___query_5d18 = Pesquisando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
# Description for universe column
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
# Button label to sign out of account
Sign_out_337b = Sair
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes primeiro:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantenha-se atualizado com uma certa hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Ficar atualizado com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantenha-se atualizado com as notas e respostas de alguém
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantenha-se atualizado com as notificações e menções de alguém
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantenha-se atualizado com as notas e respostas de alguém
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantenha-se atualizado com suas notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toque para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nostr terminou :(. Obrigado por testar! Em breve teremos Dave habilitado para Zap
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Fio
# Link text for thread references
thread_ad1f = Fio
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Use esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Usuário
# Label for view folder button, Storage settings section
View_folder_9742 = Visualizar pasta
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes pequenos
# Profile website field label
Website_7980 = Site
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreva uma nota criativa aqui.
# Placeholder text for key input field
Your_key_here_81bd = Sua chave aqui...
# Title for your notes column
Your_Notes_f6db = Suas notas
# Title for your notifications column
Your_Notifications_080d = Suas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] Obteve um resultado { $count } para '{ $query }'
*[other] Obteve { $count } resultados para '{ $query }'
}

View File

@@ -1,410 +0,0 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Adicionar
# Label for add column button
Add_47df = Adicionar
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar uma carteira diferente que será usada apenas para esta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Adicionar uma carteira para continuar
# Button label to add a new account
Add_account_1cfc = Adicionar conta
# Column title for adding new account
Add_Account_d06c = Adicionar conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar relay
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Fontes de algoritmo para ajudar na descoberta de notas
# Label for zap amount input field
Amount_70f0 = Quantia
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar qualquer coisa...
# Profile banner URL field label
Banner_52ef = Faixa
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmissão
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmissão local
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Clica para editar
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar relays
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = A conectar...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON da nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count } mês(es)
# Relative time in seconds
count_s_aa26 = { $count } s
# Relative time in weeks
count_w_7468 = { $count } semana(s)
# Relative time in years
count_y_9408 = { $count } ano(s)
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizadas
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do zap
# Column title for support page
Damus_Support_27c0 = Suporte Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão por zap:
# Name of the default deck feed
Default_Deck_fcca = Aba padrão
# Button label to delete a deck
Delete_Deck_bb29 = Excluir aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Apagar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar carteira
# Profile display name field label
Display_name_f9d9 = Nome a mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar aba
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Insere aqui os marcadores desejados (para múltiplos com espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insere aqui o relay
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Insere aqui a chave de utilizador (npub, hex, nip05)
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Insere a tua chave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insere a tua chave públca (npub), endereço nostr (por exemplo { $address }), ou chave privada (nsec). Tens de inserir a tua chave pública para publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Encontrar utilizador
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra:
# Title for hashtags column
Hashtags_f8e0 = Marcadores
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache da imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI inválido.
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por utilizador
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço da rede Lightning (lud16)
# Login page title
Login_9eef = Iniciar sessão
# Login button text
Login_now___let_s_do_this_5630 = Entra agora — vamos fazer isto!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nov@ no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (identificação NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = Não conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere bugs e contacte-nos quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ativado
# Button label to open email client
Open_Email_25e9 = Abrir e-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre o teu cliente de e-mail padrão para obteres ajuda da equipa Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cola o teu NWC URI aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Cria um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Cria um nome para a aba e seleciona um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Seleciona um ícone.
# Button label to post a note
Post_now_8a49 = Publicar agora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Somente leitura
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = responder a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = responder a { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondendo à { $note } de { $user } no { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondendo à { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondendo ao { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondendo a uma nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar esta nota
# Label for reposted notes
Reposted_61c8 = Republicado
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Redefinir
# Heading for support section
Running_into_a_bug_1796 = Encontraste um bug?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar alterações
# Column title for search page
Search_c573 = Procurar
# Placeholder for search notes input field
Search_notes_42a6 = Procurar notas...
# Search in progress message
Searching_for___query_5d18 = Procurando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver notas de todo o universo nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada utilizador a partir de uma lista
# Button label to sign out of account
Sign_out_337b = Terminar sessão
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes antes:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Origem da última nota para cada utilizador na minha lista
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Atualizações com um dado marcador
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Atualizações com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Atualizar-me de notas e respostas de outra pessoa
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Atualizar-me de notificações e menções de outra pessoa
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Atualizar-me de notas e respostas de outra pessoa
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Atualizar-me de notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Subscrever as notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Subscrever as notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para o modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para o modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nost terminou :(. Obrigado por testares! Dave com ativação de ZAPS em breve!
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Tópico
# Link text for thread references
thread_ad1f = tópico
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Nome de utilizador
# Label for view folder button, Storage settings section
View_folder_9742 = Ver pasta
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes curtos
# Profile website field label
Website_7980 = Website
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreve uma nota sonante aqui...
# Placeholder text for key input field
Your_key_here_81bd = A tua chave aqui...
# Title for your notes column
Your_Notes_f6db = Minhas notas
# Title for your notifications column
Your_Notifications_080d = Minhas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar zaps a esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $count } resultado obtido para '{ $query }'
*[other] { $count } resultados obtidos para '{ $query }'
}

View File

@@ -1,412 +0,0 @@
# 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 = เบต้า
# 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 = ค้นหาผู้ใช้
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = แฮชแท็ก
# Title for Home column
Home_8c19 = หน้าแรก
# Label for deck icon selection
Icon_b0ab = ไอคอน
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = ขนาดแคชรูปภาพ:
# Title for individual user column
Individual_b776 = ปัจเจคบุคคล
# Error message for invalid zap amount
Invalid_amount_6630 = จำนวนเงินไม่ถูกต้อง
# Error message for invalid key input
Invalid_key_4726 = คีย์ไม่ถูกต้อง
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI ไม่ถูกต้อง
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
# Label for language, Appearance settings section
Language_e264 = ภาษา:
# Title for last note per user column
Last_Note_per_User_17ad = โน้ตล่าสุดของผู้ใช้แต่ละคน
# Label for Theme Light, Appearance settings section
Light_7475 = สว่าง
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ที่อยู่ 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 = เมื่อสักครู่
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = เปิดอีเมล
# 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 note body font size, Appearance settings section
Reset_4e60 = Reset
# 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 = การตั้งค่า
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
# Button label to sign out of account
Sign_out_337b = ออกจากระบบ
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = ติดตามความเคลื่อนไหวของแฮชแท็ก
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = ติดตามการแจ้งเตือนและการกล่าวถึง
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = ติดตามโน้ตและการตอบกลับของผู้อื่น
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = ติดตามการแจ้งเตือนและการกล่าวถึงของผู้อื่น
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = ติดตามโน้ตและการตอบกลับของผู้อื่น
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = ติดตามการแจ้งเตือนและการกล่าวถึงของคุณ
# Step 1 label in support instructions
Step_1_8656 = ขั้นตอนที่ 1
# Step 2 label in support instructions
Step_2_d08d = ขั้นตอนที่ 2
# Label for storage settings section
Storage_ed65 = พื้นที่จัดเก็บ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
# Support email address
Support_email_44d9 = Support email:
# 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 = เธรด
# 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 } รายการ
}

View File

@@ -7,6 +7,8 @@
# 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
@@ -21,10 +23,16 @@ 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
@@ -45,8 +53,6 @@ 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 +67,12 @@ Broadcast_fe43 = 广播
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,6 +83,8 @@ 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
@@ -117,12 +119,14 @@ 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
@@ -143,10 +147,14 @@ 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
@@ -161,16 +169,18 @@ Enter_your_key_0fca = 请输入你的密钥
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥npub、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
# Label for find user button
Find_User_bd12 = 查找用户
# Label for font size, Appearance settings section
Font_size_dd73 = 字体大小:
# 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 = 主页
# 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
@@ -191,12 +201,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 = 每个用户的最新笔记
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Timeline kind label for last notes per pubkey
Last_Notes_aefe = 最新笔记
# Display name for last notes per contact
Last_Per_Pubkey__Contact_33ce = 每个公钥(联系人)的最新笔记
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 闪电网络地址lud16
# Login page title
@@ -229,20 +239,20 @@ 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
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 刚刚
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 开启
# Button label to open email client
Open_Email_25e9 = 打开电子邮箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打开你的默认电子邮件客户端以获得达摩团队的帮助
# 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
@@ -255,20 +265,30 @@ 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
@@ -289,10 +309,6 @@ replying_to_a_note_e0bc = 正在回复笔记
Repost_this_note_8e56 = 转发此笔记
# Label for reposted notes
Reposted_61c8 = 已转发
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了吗?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -303,6 +319,12 @@ 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
@@ -315,8 +337,6 @@ 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 = 设置
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
# Button label to sign out of account
@@ -325,8 +345,6 @@ Sign_out_337b = 登出
Someone_else_s_Notes_7e5f = 其他人的笔记
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回复:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 获取你的联系人列表中每个用户的最新一条笔记
# Description for hashtags column
@@ -345,14 +363,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 = 订阅某人的笔记
# Support email address
Support_email_44d9 = 支持电子邮件:
# 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
@@ -361,12 +377,18 @@ 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 = 宇宙
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
@@ -377,10 +399,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
@@ -397,8 +419,6 @@ 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

View File

@@ -7,6 +7,8 @@
# 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
@@ -21,10 +23,16 @@ 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
@@ -45,8 +53,6 @@ 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 +67,12 @@ Broadcast_fe43 = 廣播
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,6 +83,8 @@ 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
@@ -117,12 +119,14 @@ 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
@@ -143,10 +147,14 @@ 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
@@ -161,16 +169,18 @@ Enter_your_key_0fca = 請輸入你的密鑰
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰npub、nostr 地址(如 { $address }、或私鑰nsec。你必須輸入你的私鑰才能發貼、回覆等等。
# Label for find user button
Find_User_bd12 = 查找用戶
# Label for font size, Appearance settings section
Font_size_dd73 = 字體大小:
# 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 = 主頁
# 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
@@ -191,12 +201,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 = 每個用戶的最新筆記
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Timeline kind label for last notes per pubkey
Last_Notes_aefe = 最新筆記
# Display name for last notes per contact
Last_Per_Pubkey__Contact_33ce = 每個公鑰(聯繫人)的最新筆記
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 閃電網絡地址lud16
# Login page title
@@ -229,20 +239,20 @@ 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
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 剛剛
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 開啟
# Button label to open email client
Open_Email_25e9 = 打開電子郵箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
# 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
@@ -255,20 +265,30 @@ 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
@@ -289,10 +309,6 @@ replying_to_a_note_e0bc = 正在回覆筆記
Repost_this_note_8e56 = 轉發此筆記
# Label for reposted notes
Reposted_61c8 = 已轉發
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了嗎?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -303,6 +319,12 @@ 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
@@ -315,8 +337,6 @@ 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 = 設置
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
# Button label to sign out of account
@@ -325,8 +345,6 @@ Sign_out_337b = 登出
Someone_else_s_Notes_7e5f = 其他人的筆記
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回覆:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 獲取你的聯繫人列表中每個用戶的最新一條筆記
# Description for hashtags column
@@ -345,14 +363,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 = 訂閱某人的筆記
# Support email address
Support_email_44d9 = 支持電子郵件:
# 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
@@ -361,12 +377,18 @@ 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 = 宇宙
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
@@ -377,10 +399,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
@@ -397,8 +419,6 @@ 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

View File

@@ -135,7 +135,7 @@ pub fn setup_multicast_relay(
std::thread::spawn(move || {
let mut events = Events::with_capacity(1);
loop {
if let Err(err) = poll.poll(&mut events, None) {
if let Err(err) = poll.poll(&mut events, Some(Duration::from_millis(100))) {
error!("multicast socket poll error: {err}. ending multicast poller.");
return;
}

View File

@@ -68,7 +68,7 @@ impl From<RelayEvent<'_>> for OwnedRelayEvent {
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct _RelaySub {
pub struct RelaySub {
pub(crate) subid: String,
pub(crate) filter: String,
}

View File

@@ -9,13 +9,11 @@ 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 }
@@ -47,9 +45,7 @@ fluent-langneg = { workspace = true }
unic-langid = { workspace = true }
once_cell = { workspace = true }
md5 = { workspace = true }
bitflags = { workspace = true }
regex = "1"
chrono = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
@@ -57,7 +53,6 @@ tokio = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
android-activity = { workspace = true }
[features]
puffin = ["puffin_egui", "dep:puffin"]

View File

@@ -207,10 +207,6 @@ 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()
}

View File

@@ -15,7 +15,6 @@ pub enum ContactState {
Received {
contacts: HashSet<Pubkey>,
note_key: NoteKey,
timestamp: u64,
},
}
@@ -42,7 +41,7 @@ impl Contacts {
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
let binding = ndb
.query(txn, std::slice::from_ref(&self.filter), 1)
.query(txn, &[self.filter.clone()], 1)
.expect("query user relays results");
let Some(res) = binding.first() else {
@@ -58,7 +57,6 @@ impl Contacts {
ContactState::Received {
contacts,
note_key: _,
timestamp: _,
} => {
if contacts.contains(other_pubkey) {
IsFollowing::Yes
@@ -84,18 +82,6 @@ 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, &note, *key);
}
@@ -110,17 +96,11 @@ 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,
timestamp,
} => {
ContactState::Received { contacts, note_key } => {
update_contacts(contacts, note);
*note_key = key;
*timestamp = note.created_at();
}
};
}

View File

@@ -33,7 +33,7 @@ impl AccountMutedData {
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, std::slice::from_ref(&self.filter), lim)
.query(txn, &[self.filter.clone()], lim)
.expect("query user muted results")
.iter()
.map(|qr| qr.note_key)

View File

@@ -1,11 +1,12 @@
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,
@@ -36,7 +37,7 @@ impl AccountRelayData {
.limit()
.unwrap_or(crate::filter::default_limit()) as i32;
let nks = ndb
.query(txn, std::slice::from_ref(&self.filter), lim)
.query(txn, &[self.filter.clone()], lim)
.expect("query user relays results")
.iter()
.map(|qr| qr.note_key)

View File

@@ -1,14 +1,13 @@
use crate::account::FALLBACK_PUBKEY;
use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, SettingsHandler};
use crate::persist::{AppSizeHandler, ZoomHandler};
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, UnknownIds,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
UnknownIds,
};
use egui::Margin;
use egui::ThemePreference;
@@ -20,10 +19,6 @@ use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
pub enum AppAction {
Note(NoteAction),
@@ -45,8 +40,9 @@ pub struct Notedeck {
global_wallet: GlobalWallet,
path: DataPath,
args: Args,
settings: SettingsHandler,
theme: ThemeHandler,
app: Option<Rc<RefCell<dyn App>>>,
zoom: ZoomHandler,
app_size: AppSizeHandler,
unrecognized_args: BTreeSet<String>,
clipboard: Clipboard,
@@ -54,9 +50,6 @@ pub struct Notedeck {
frame_history: FrameHistory,
job_pool: JobPool,
i18n: Localization,
#[cfg(target_os = "android")]
android_app: Option<AndroidApp>,
}
/// Our chrome, which is basically nothing
@@ -106,18 +99,10 @@ impl eframe::App for Notedeck {
render_notedeck(self, 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.zoom.try_save_zoom_factor(ctx);
self.app_size.try_save_app_size(ctx);
if self.args.options.contains(NotedeckOptions::RelayDebug) {
if self.args.relay_debug {
if self.pool.debug.is_none() {
self.pool.use_debug();
}
@@ -144,11 +129,6 @@ fn setup_puffin() {
}
impl Notedeck {
#[cfg(target_os = "android")]
pub fn set_android_context(&mut self, context: AndroidApp) {
self.android_app = Some(context);
}
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
#[cfg(feature = "puffin")]
setup_puffin();
@@ -179,11 +159,10 @@ impl Notedeck {
1024usize * 1024usize * 1024usize * 1024usize
};
let settings = SettingsHandler::new(&path).load();
let theme = ThemeHandler::new(&path);
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
let keystore = if parsed_args.use_keystore {
let keys_path = path.path(DataPathType::Keys);
let selected_key_path = path.path(DataPathType::SelectedKey);
Some(AccountStorage::new(
@@ -234,9 +213,13 @@ 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);
}
// migrate
if let Err(e) = img_cache.migrate_v0() {
error!("error migrating image cache: {e}");
@@ -248,22 +231,15 @@ impl Notedeck {
// Initialize localization
let mut i18n = Localization::new();
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
settings.locale().parse();
if let Ok(setting_locale) = setting_locale {
if let Err(err) = i18n.set_locale(setting_locale) {
error!("{err}");
}
}
if let Some(locale) = &parsed_args.locale {
if let Err(err) = i18n.set_locale(locale.to_owned()) {
error!("{err}");
}
}
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
Self {
ndb,
img_cache,
@@ -274,8 +250,9 @@ impl Notedeck {
global_wallet,
path: path.clone(),
args: parsed_args,
settings,
theme,
app: None,
zoom,
app_size,
unrecognized_args,
frame_history: FrameHistory::default(),
@@ -283,49 +260,9 @@ impl Notedeck {
zaps,
job_pool,
i18n,
#[cfg(target_os = "android")]
android_app: None,
}
}
/// Setup egui context
pub fn setup(&self, ctx: &egui::Context) {
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
crate::setup::setup_egui_context(
ctx,
self.args.options,
self.theme(),
self.note_body_font_size(),
self.zoom_factor(),
);
}
/// ensure we recognized all the arguments
pub fn check_args(&self, other_app_args: &BTreeSet<String>) -> Result<(), Error> {
let completely_unrecognized: Vec<String> = self
.unrecognized_args()
.intersection(other_app_args)
.cloned()
.collect();
if !completely_unrecognized.is_empty() {
let err = format!("Unrecognized arguments: {completely_unrecognized:?}");
tracing::error!("{}", &err);
return Err(Error::Generic(err));
}
Ok(())
}
#[inline]
pub fn options(&self) -> NotedeckOptions {
self.args.options
}
pub fn has_option(&self, option: NotedeckOptions) -> bool {
self.options().contains(option)
}
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
self.set_app(app);
self
@@ -342,14 +279,12 @@ impl Notedeck {
global_wallet: &mut self.global_wallet,
path: &self.path,
args: &self.args,
settings: &mut self.settings,
theme: &mut self.theme,
clipboard: &mut self.clipboard,
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool,
i18n: &mut self.i18n,
#[cfg(target_os = "android")]
android: self.android_app.as_ref().unwrap().clone(),
}
}
@@ -362,15 +297,7 @@ impl Notedeck {
}
pub fn theme(&self) -> ThemePreference {
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()
self.theme.load()
}
pub fn unrecognized_args(&self) -> &BTreeSet<String> {

View File

@@ -1,15 +1,23 @@
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 options: NotedeckOptions,
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 dbpath: Option<String>,
pub datapath: Option<String>,
}
@@ -20,8 +28,14 @@ impl Args {
let mut unrecognized_args = BTreeSet::new();
let mut res = Args {
relays: vec![],
is_mobile: None,
keys: vec![],
options: NotedeckOptions::default(),
light: false,
show_note_client: false,
debug: false,
relay_debug: false,
tests: false,
use_keystore: true,
dbpath: None,
datapath: None,
locale: None,
@@ -33,9 +47,9 @@ impl Args {
let arg = &args[i];
if arg == "--mobile" {
res.options.set(NotedeckOptions::Mobile, true);
res.is_mobile = Some(true);
} else if arg == "--light" {
res.options.set(NotedeckOptions::LightTheme, true);
res.light = true;
} else if arg == "--locale" {
i += 1;
let Some(locale) = args.get(i) else {
@@ -54,11 +68,11 @@ impl Args {
}
}
} else if arg == "--dark" {
res.options.set(NotedeckOptions::LightTheme, false);
res.light = false;
} else if arg == "--debug" {
res.options.set(NotedeckOptions::Debug, true);
res.debug = true;
} else if arg == "--testrunner" {
res.options.set(NotedeckOptions::Tests, true);
res.tests = true;
} else if arg == "--pub" || arg == "--npub" {
i += 1;
let pubstr = if let Some(next_arg) = args.get(i) {
@@ -121,13 +135,11 @@ impl Args {
};
res.relays.push(relay.clone());
} else if arg == "--no-keystore" {
res.options.set(NotedeckOptions::UseKeystore, true);
res.use_keystore = false;
} else if arg == "--relay-debug" {
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
} else if arg == "--clndash" {
res.options.set(NotedeckOptions::FeatureClnDash, true);
res.relay_debug = true;
} else if arg == "--show-note-client" {
res.show_note_client = true;
} else {
unrecognized_args.insert(arg.clone());
}

View File

@@ -1,6 +1,6 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
UnknownIds,
};
use egui_winit::clipboard::Clipboard;
@@ -8,9 +8,6 @@ use egui_winit::clipboard::Clipboard;
use enostr::RelayPool;
use nostrdb::Ndb;
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
use egui::{Pos2, Rect};
// TODO: make this interface more sandboxed
pub struct AppContext<'a> {
@@ -23,68 +20,10 @@ pub struct AppContext<'a> {
pub global_wallet: &'a mut GlobalWallet,
pub path: &'a DataPath,
pub args: &'a Args,
pub settings: &'a mut SettingsHandler,
pub theme: &'a mut ThemeHandler,
pub clipboard: &'a mut Clipboard,
pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory,
pub job_pool: &'a mut JobPool,
pub i18n: &'a mut Localization,
#[cfg(target_os = "android")]
pub android: AndroidApp,
}
#[derive(Debug, Clone)]
pub enum SoftKeyboardContext {
Virtual,
Platform { ppp: f32 },
}
impl SoftKeyboardContext {
pub fn platform(context: &egui::Context) -> Self {
Self::Platform {
ppp: context.pixels_per_point(),
}
}
}
impl<'a> AppContext<'a> {
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
match ctx {
SoftKeyboardContext::Virtual => {
let height = 400.0;
skb_rect_from_screen_rect(screen_rect, height)
}
#[allow(unused_variables)]
SoftKeyboardContext::Platform { ppp } => {
#[cfg(target_os = "android")]
{
use android_activity::InsetType;
// not sure why I need this, it seems to be consistently off by some amount of
// pixels ?
let fudge = 0.0;
let inset = self.android.get_window_insets(InsetType::Ime);
let height = (inset.bottom as f32 / ppp) - fudge;
skb_rect_from_screen_rect(screen_rect, height)
}
#[cfg(not(target_os = "android"))]
{
None
}
}
}
}
}
#[inline]
fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option<Rect> {
if height == 0.0 {
return None;
}
let min = Pos2::new(0.0, screen_rect.max.y - height);
Some(Rect::from_min_max(min, screen_rect.max))
}

View File

@@ -86,13 +86,6 @@ impl FilterStates {
}
self.states.insert(relay, state);
}
/// For contacts, since that sub is managed elsewhere
pub fn set_all_states(&mut self, state: FilterState) {
for cur_state in self.states.values_mut() {
*cur_state = state.clone();
}
}
}
/// We may need to fetch some data from relays before our filter is ready.

View File

@@ -1,9 +1,4 @@
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,
@@ -36,7 +31,6 @@ 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,
}
}
@@ -52,7 +46,6 @@ 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,
}
}
@@ -63,148 +56,3 @@ 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);
}

View File

@@ -5,32 +5,13 @@ use std::borrow::Cow;
use std::collections::HashMap;
use unic_langid::{langid, LanguageIdentifier};
const EN_US: LanguageIdentifier = langid!("en-US");
const EN_XA: LanguageIdentifier = langid!("en-XA");
const EN_US: LanguageIdentifier = langid!("en-US");
const DE: LanguageIdentifier = langid!("de");
const ES_419: LanguageIdentifier = langid!("es-419");
const ES_ES: LanguageIdentifier = langid!("es-ES");
const FR: LanguageIdentifier = langid!("fr");
const JA: LanguageIdentifier = langid!("ja");
const PT_BR: LanguageIdentifier = langid!("pt-BR");
const PT_PT: LanguageIdentifier = langid!("pt-PT");
const TH: LanguageIdentifier = langid!("th");
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
const NUM_FTLS: usize = 12;
const EN_US_NATIVE_NAME: &str = "English (US)";
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
const DE_NATIVE_NAME: &str = "Deutsch";
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
const ES_ES_NATIVE_NAME: &str = "Español (España)";
const FR_NATIVE_NAME: &str = "Français";
const JA_NATIVE_NAME: &str = "日本語";
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
const PT_PT_NATIVE_NAME: &str = "Português (Portugal)";
const TH_NATIVE_NAME: &str = "ภาษาไทย";
const ZH_CN_NATIVE_NAME: &str = "简体中文";
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
const FR: LanguageIdentifier = langid!("FR");
const ZH_CN: LanguageIdentifier = langid!("ZH_CN");
const ZH_TW: LanguageIdentifier = langid!("ZH_TW");
const NUM_FTLS: usize = 6;
struct StaticBundle {
identifier: LanguageIdentifier,
@@ -50,34 +31,10 @@ 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: JA,
ftl: include_str!("../../../../assets/translations/ja/main.ftl"),
},
StaticBundle {
identifier: PT_BR,
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
},
StaticBundle {
identifier: PT_PT,
ftl: include_str!("../../../../assets/translations/pt-PT/main.ftl"),
},
StaticBundle {
identifier: TH,
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
},
StaticBundle {
identifier: ZH_CN,
ftl: include_str!("../../../../assets/translations/zh-CN/main.ftl"),
@@ -98,8 +55,6 @@ 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>>,
@@ -122,37 +77,15 @@ impl Default for Localization {
EN_US.clone(),
EN_XA.clone(),
DE.clone(),
ES_419.clone(),
ES_ES.clone(),
FR.clone(),
JA.clone(),
PT_BR.clone(),
PT_PT.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()),
(JA, JA_NATIVE_NAME.to_owned()),
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
(PT_PT, PT_PT_NATIVE_NAME.to_owned()),
(TH, TH_NATIVE_NAME.to_owned()),
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
]);
Self {
current_locale: default_locale.to_owned(),
available_locales,
fallback_locale,
locale_native_names,
use_isolating: true,
normalized_key_cache: HashMap::new(),
string_cache: HashMap::new(),
@@ -440,10 +373,6 @@ 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;

View File

@@ -1,10 +1,4 @@
use crate::media::gif::ensure_latest_texture_from_cache;
use crate::media::images::ImageType;
use crate::media::AnimationMode;
use crate::urls::{UrlCache, UrlMimes};
use crate::ImageMetadata;
use crate::ObfuscationType;
use crate::RenderableMedia;
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
@@ -13,11 +7,9 @@ use poll_promise::Promise;
use egui::ColorImage;
use std::collections::HashMap;
use std::fs::{self, create_dir_all, File};
use std::fs::{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;
@@ -27,7 +19,7 @@ use tracing::warn;
#[derive(Default)]
pub struct TexturesCache {
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
cache: hashbrown::HashMap<String, TextureStateInternal>,
}
impl TexturesCache {
@@ -35,7 +27,7 @@ impl TexturesCache {
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> LoadableTextureState<'_> {
) -> LoadableTextureState {
let internal = self.handle_and_get_state_internal(url, true, closure);
internal.into()
@@ -45,7 +37,7 @@ impl TexturesCache {
&mut self,
url: &str,
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
) -> TextureState<'_> {
) -> TextureState {
let internal = self.handle_and_get_state_internal(url, false, closure);
internal.into()
@@ -96,7 +88,7 @@ impl TexturesCache {
});
}
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> {
self.cache.get_mut(url).map(|state| {
handle_occupied(state, true);
state.into()
@@ -147,12 +139,6 @@ 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 {
@@ -234,7 +220,6 @@ 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)]
@@ -246,29 +231,10 @@ 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,
}
}
@@ -365,14 +331,8 @@ 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 {
@@ -389,33 +349,10 @@ 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,
}
@@ -423,12 +360,10 @@ 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(),
}
}
@@ -437,65 +372,6 @@ impl Images {
self.gifs.migrate_v0()
}
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
}
pub fn find_renderable_media(
urls: &mut UrlMimes,
imeta: &HashMap<String, ImageMetadata>,
url: &str,
) -> Option<RenderableMedia> {
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
let obfuscation_type = match imeta.get(url) {
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
None => ObfuscationType::Default,
};
Some(RenderableMedia {
url: url.to_string(),
media_type,
obfuscation_type,
})
}
pub fn latest_texture(
&mut self,
ui: &mut egui::Ui,
url: &str,
img_type: ImageType,
animation_mode: AnimationMode,
) -> Option<TextureHandle> {
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
let is_loaded = self
.get_cache_mut(cache_type)
.textures_cache
.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
})
.is_loaded();
if !is_loaded {
return None;
}
let cache = match cache_type {
MediaCacheType::Image => &mut self.static_imgs,
MediaCacheType::Gif => &mut self.gifs,
};
ensure_latest_texture_from_cache(
ui,
url,
&mut self.gif_states,
&mut cache.textures_cache,
animation_mode,
)
}
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
match cache_type {
MediaCacheType::Image => &self.static_imgs,
@@ -509,26 +385,6 @@ 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>;
@@ -539,35 +395,3 @@ 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,
}

View File

@@ -23,7 +23,6 @@ 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();

View File

@@ -12,21 +12,16 @@ mod frame_history;
pub mod i18n;
mod imgcache;
mod job_pool;
mod jobs;
pub mod media;
mod muted;
pub mod name;
mod nip51_set;
pub mod note;
mod notecache;
mod options;
mod persist;
pub mod platform;
pub mod profile;
pub mod relay_debug;
pub mod relayspec;
mod result;
mod setup;
pub mod storage;
mod style;
pub mod theme;
@@ -46,33 +41,23 @@ pub use account::relay::RelayAction;
pub use account::FALLBACK_PUBKEY;
pub use app::{App, AppAction, Notedeck};
pub use args::Args;
pub use context::{AppContext, SoftKeyboardContext};
pub use context::AppContext;
pub use error::{show_one_error_message, Error, FilterError, ZapError};
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use imgcache::{
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
TexturedImage, TexturesCache,
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
};
pub use job_pool::JobPool;
pub use jobs::{
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
};
pub use media::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
};
pub use muted::{MuteFun, Muted};
pub use name::NostrName;
pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache};
pub use note::{
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
};
pub use notecache::{CachedNote, NoteCache};
pub use options::NotedeckOptions;
pub use persist::*;
pub use profile::get_profile_url;
pub use relay_debug::RelayDebugView;
@@ -82,14 +67,13 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
pub use style::NotedeckTextStyle;
pub use theme::ColorTheme;
pub use time::time_ago_since;
pub use time::time_format;
pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
pub use user_account::UserAccount;
pub use wallet::{
get_current_wallet, get_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
WalletType, WalletUIState, ZapWallet,
get_current_wallet, get_wallet_for, GlobalWallet, Wallet, WalletError, WalletType,
WalletUIState, ZapWallet,
};
pub use zaps::{
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,

View File

@@ -1,127 +0,0 @@
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);
}
}
}
}

View File

@@ -1,164 +0,0 @@
use std::{
sync::mpsc::TryRecvError,
time::{Instant, SystemTime},
};
use crate::media::AnimationMode;
use crate::Animation;
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
use egui::TextureHandle;
use std::time::Duration;
pub fn ensure_latest_texture_from_cache(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
textures: &mut TexturesCache,
animation_mode: AnimationMode,
) -> Option<TextureHandle> {
let tstate = textures.cache.get_mut(url)?;
let TextureState::Loaded(img) = tstate.into() else {
return None;
};
Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
}
struct ProcessedGifFrame {
texture: TextureHandle,
maybe_new_state: Option<GifState>,
repaint_at: Option<SystemTime>,
}
/// Process a gif state frame, and optionally present a new
/// state and when to repaint it
fn process_gif_frame(
animation: &Animation,
frame_state: Option<&GifState>,
animation_mode: AnimationMode,
) -> ProcessedGifFrame {
let now = Instant::now();
match frame_state {
Some(prev_state) => {
let should_advance = animation_mode.can_animate()
&& (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
if should_advance {
let maybe_new_index = if animation.receiver.is_some()
|| prev_state.last_frame_index < animation.num_frames() - 1
{
prev_state.last_frame_index + 1
} else {
0
};
match animation.get_frame(maybe_new_index) {
Some(frame) => {
let next_frame_time = match animation_mode {
AnimationMode::Continuous { fps } => match fps {
Some(fps) => {
let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
}
None => SystemTime::now().checked_add(frame.delay),
},
AnimationMode::NoAnimation | AnimationMode::Reactive => None,
};
ProcessedGifFrame {
texture: frame.texture.clone(),
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: frame.delay,
next_frame_time,
last_frame_index: maybe_new_index,
}),
repaint_at: next_frame_time,
}
}
None => {
let (texture, maybe_new_state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (frame.texture.clone(), None),
None => (animation.first_frame.texture.clone(), None),
};
ProcessedGifFrame {
texture,
maybe_new_state,
repaint_at: prev_state.next_frame_time,
}
}
}
} else {
let (texture, maybe_new_state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (frame.texture.clone(), None),
None => (animation.first_frame.texture.clone(), None),
};
ProcessedGifFrame {
texture,
maybe_new_state,
repaint_at: prev_state.next_frame_time,
}
}
}
None => ProcessedGifFrame {
texture: animation.first_frame.texture.clone(),
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
next_frame_time: None,
last_frame_index: 0,
}),
repaint_at: None,
},
}
}
pub fn ensure_latest_texture(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
img: &mut TexturedImage,
animation_mode: AnimationMode,
) -> TextureHandle {
match img {
TexturedImage::Static(handle) => handle.clone(),
TexturedImage::Animated(animation) => {
if let Some(receiver) = &animation.receiver {
loop {
match receiver.try_recv() {
Ok(frame) => animation.other_frames.push(frame),
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Disconnected) => {
animation.receiver = None;
break;
}
}
}
}
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
if let Some(new_state) = next_state.maybe_new_state {
gifs.insert(url.to_owned(), new_state);
}
if let Some(repaint) = next_state.repaint_at {
tracing::trace!("requesting repaint for {url} after {repaint:?}");
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
ui.ctx().request_repaint_after(dur);
}
}
next_state.texture
}
}
}

View File

@@ -1,475 +0,0 @@
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,
)
}

View File

@@ -1 +0,0 @@

View File

@@ -1,32 +0,0 @@
pub mod action;
pub mod blur;
pub mod gif;
pub mod images;
pub mod imeta;
pub mod renderable;
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
pub use blur::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
PointDimensions,
};
pub use images::ImageType;
pub use renderable::RenderableMedia;
#[derive(Copy, Clone, Debug)]
pub enum AnimationMode {
/// Only render when scrolling, network activity, etc
Reactive,
/// Continuous with an optional target fps
Continuous { fps: Option<f32> },
/// Disable animation
NoAnimation,
}
impl AnimationMode {
pub fn can_animate(&self) -> bool {
!matches!(self, Self::NoAnimation)
}
}

View File

@@ -1,9 +0,0 @@
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,
}

View File

@@ -1,195 +0,0 @@
use std::collections::HashMap;
use enostr::{Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, Note, Transaction};
use uuid::Uuid;
use crate::{UnifiedSubscription, UnknownIds};
/// Keeps track of most recent NIP-51 sets
#[derive(Debug)]
pub struct Nip51SetCache {
pub sub: UnifiedSubscription,
cached_notes: HashMap<PackId, Nip51Set>,
}
type PackId = String;
impl Nip51SetCache {
pub fn new(
pool: &mut RelayPool,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
nip51_set_filter: Vec<Filter>,
) -> Option<Self> {
let subid = Uuid::new_v4().to_string();
let mut cached_notes = HashMap::default();
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
Some(results.into_iter().map(|r| r.note).collect())
} else {
None
};
if let Some(notes) = notes {
add(notes, &mut cached_notes, ndb, txn, unknown_ids);
}
let sub = match ndb.subscribe(&nip51_set_filter) {
Ok(sub) => sub,
Err(e) => {
tracing::error!("Could not ndb subscribe: {e}");
return None;
}
};
pool.subscribe(subid.clone(), nip51_set_filter);
Some(Self {
sub: UnifiedSubscription {
local: sub,
remote: subid,
},
cached_notes,
})
}
pub fn poll_for_notes(&mut self, ndb: &Ndb, unknown_ids: &mut UnknownIds) {
let new_notes = ndb.poll_for_notes(self.sub.local, 5);
if new_notes.is_empty() {
return;
}
let txn = Transaction::new(ndb).expect("txn");
let notes: Vec<Note> = new_notes
.into_iter()
.filter_map(|new_note_key| ndb.get_note_by_key(&txn, new_note_key).ok())
.collect();
add(notes, &mut self.cached_notes, ndb, &txn, unknown_ids);
}
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
self.cached_notes.values()
}
}
fn add(
notes: Vec<Note>,
cache: &mut HashMap<PackId, Nip51Set>,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
) {
for note in notes {
let Some(new_pack) = create_nip51_set(note) else {
continue;
};
if let Some(cur_cached) = cache.get(&new_pack.identifier) {
if new_pack.created_at <= cur_cached.created_at {
continue;
}
}
for pk in &new_pack.pks {
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
}
cache.insert(new_pack.identifier.clone(), new_pack);
}
}
pub fn create_nip51_set(note: Note) -> Option<Nip51Set> {
let mut identifier = None;
let mut title = None;
let mut image = None;
let mut description = None;
let mut pks = Vec::new();
for tag in note.tags() {
if tag.count() < 2 {
continue;
}
let Some(first) = tag.get_str(0) else {
continue;
};
match first {
"p" => {
let Some(pk) = tag.get_id(1) else {
continue;
};
pks.push(Pubkey::new(*pk));
}
"d" => {
let Some(id) = tag.get_str(1) else {
continue;
};
identifier = Some(id.to_owned());
}
"image" => {
let Some(cur_img) = tag.get_str(1) else {
continue;
};
image = Some(cur_img.to_owned());
}
"title" => {
let Some(cur_title) = tag.get_str(1) else {
continue;
};
title = Some(cur_title.to_owned());
}
"description" => {
let Some(cur_desc) = tag.get_str(1) else {
continue;
};
description = Some(cur_desc.to_owned());
}
_ => {
continue;
}
};
}
let identifier = identifier?;
Some(Nip51Set {
identifier,
title,
image,
description,
pks,
created_at: note.created_at(),
})
}
/// NIP-51 Set. Read only (do not use for writing)
pub struct Nip51Set {
pub identifier: String, // 'd' tag
pub title: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
pub pks: Vec<Pubkey>,
created_at: u64,
}
impl std::fmt::Debug for Nip51Set {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Nip51Set")
.field("identifier", &self.identifier)
.field("title", &self.title)
.field("image", &self.image)
.field("description", &self.description)
.field("pks", &self.pks.len())
.field("created_at", &self.created_at)
.finish()
}
}

View File

@@ -1,7 +1,8 @@
use super::context::ContextSelection;
use crate::{zaps::NoteZapTargetOwned, MediaAction};
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
use egui::Vec2;
use enostr::{NoteId, Pubkey};
use poll_promise::Promise;
#[derive(Debug)]
pub struct ScrollInfo {
@@ -24,11 +25,7 @@ pub enum NoteAction {
Profile(Pubkey),
/// User has clicked a note link
Note {
note_id: NoteId,
preview: bool,
scroll_offset: f32,
},
Note { note_id: NoteId, preview: bool },
/// User has selected some context option
Context(ContextSelection),
@@ -48,7 +45,6 @@ impl NoteAction {
NoteAction::Note {
note_id: id,
preview: false,
scroll_offset: 0.0,
}
}
}
@@ -65,3 +61,62 @@ 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);
}
}
}
}

View File

@@ -1,11 +1,10 @@
mod action;
mod context;
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
pub use action::{MediaAction, 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;
@@ -21,7 +20,6 @@ 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,
@@ -30,6 +28,7 @@ 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)]

View File

@@ -1,39 +0,0 @@
use bitflags::bitflags;
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NotedeckOptions: u64 {
// ===== Settings ======
/// Are we on light theme?
const LightTheme = 1 << 0;
/// Debug controls, fps stats
const Debug = 1 << 1;
/// Show relay debug window?
const RelayDebug = 1 << 2;
/// Are we running as tests?
const Tests = 1 << 3;
/// Use keystore?
const UseKeystore = 1 << 4;
/// Simulate is_compiled_as_mobile ?
const Mobile = 1 << 6;
// ===== Feature Flags ======
/// Is notebook enabled?
const FeatureNotebook = 1 << 32;
/// Is clndash enabled?
const FeatureClnDash = 1 << 33;
}
}
impl Default for NotedeckOptions {
fn default() -> Self {
NotedeckOptions::UseKeystore
}
}

View File

@@ -1,9 +1,9 @@
mod app_size;
mod settings_handler;
mod theme_handler;
mod token_handler;
mod zoom;
pub use app_size::AppSizeHandler;
pub use settings_handler::Settings;
pub use settings_handler::SettingsHandler;
pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE;
pub use theme_handler::ThemeHandler;
pub use token_handler::TokenHandler;
pub use zoom::ZoomHandler;

View File

@@ -1,253 +0,0 @@
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)
}
}

View File

@@ -0,0 +1,76 @@
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,
}
}

View File

@@ -0,0 +1,26 @@
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()
}
}

View File

@@ -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.max(0), Ordering::SeqCst);
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
}
/// Gets the current Android virtual keyboard height. Useful for transforming

View File

@@ -1,32 +1,12 @@
#[cfg(target_os = "android")]
pub mod android;
const VIRT_HEIGHT: i32 = 400;
#[cfg(target_os = "android")]
pub fn virtual_keyboard_height(virt: bool) -> i32 {
if virt {
VIRT_HEIGHT
} else {
android::virtual_keyboard_height()
}
pub fn virtual_keyboard_height() -> i32 {
android::virtual_keyboard_height()
}
#[cfg(not(target_os = "android"))]
pub fn virtual_keyboard_height(virt: bool) -> i32 {
if virt {
VIRT_HEIGHT
} else {
0
}
}
pub fn virtual_keyboard_rect(ui: &egui::Ui, virt: bool) -> Option<egui::Rect> {
let height = virtual_keyboard_height(virt);
if height <= 0 {
return None;
}
let screen_rect = ui.ctx().screen_rect();
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
Some(egui::Rect::from_min_max(min, screen_rect.max))
pub fn virtual_keyboard_height() -> i32 {
0
}

View File

@@ -77,7 +77,6 @@ impl PartialEq for RelaySpec {
impl Eq for RelaySpec {}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for RelaySpec {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.url.cmp(&other.url))

View File

@@ -1,46 +0,0 @@
use crate::fonts;
use crate::theme;
use crate::NotedeckOptions;
use crate::NotedeckTextStyle;
use egui::FontId;
use egui::ThemePreference;
pub fn setup_egui_context(
ctx: &egui::Context,
options: NotedeckOptions,
theme: ThemePreference,
note_body_font_size: f32,
zoom_factor: f32,
) {
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
let is_oled = crate::ui::is_oled(is_mobile);
ctx.options_mut(|o| {
tracing::info!("Loaded theme {:?} from disk", theme);
o.theme_preference = theme;
});
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
fonts::setup_fonts(ctx);
if crate::ui::is_compiled_as_mobile() {
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
}
egui_extras::install_image_loaders(ctx);
ctx.options_mut(|o| {
o.input_options.max_click_duration = 0.4;
});
ctx.all_styles_mut(|style| crate::theme::add_custom_style(is_mobile, style));
ctx.set_zoom_factor(zoom_factor);
let mut style = (*ctx.style()).clone();
style.text_styles.insert(
NotedeckTextStyle::NoteBody.text_style(),
FontId::proportional(note_body_font_size),
);
ctx.set_style(style);
}

View File

@@ -15,7 +15,6 @@ pub enum NotedeckTextStyle {
Button,
Small,
Tiny,
NoteBody,
}
impl NotedeckTextStyle {
@@ -30,7 +29,6 @@ impl NotedeckTextStyle {
Self::Button => TextStyle::Button,
Self::Small => TextStyle::Small,
Self::Tiny => TextStyle::Name("Tiny".into()),
Self::NoteBody => TextStyle::Name("NoteBody".into()),
}
}
@@ -45,7 +43,6 @@ impl NotedeckTextStyle {
Self::Button => FontFamily::Proportional,
Self::Small => FontFamily::Proportional,
Self::Tiny => FontFamily::Proportional,
Self::NoteBody => FontFamily::Proportional,
}
}

View File

@@ -1,35 +1,7 @@
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%
use egui::{
style::{Selection, WidgetVisuals, Widgets},
Color32, CornerRadius, Stroke, Visuals,
};
pub struct ColorTheme {
// VISUALS
@@ -114,131 +86,3 @@ 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(),
)
}

View File

@@ -1,5 +1,4 @@
use crate::{tr, Localization};
use chrono::DateTime;
use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds
@@ -84,14 +83,6 @@ fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String
}
}
pub fn time_format(_i18n: &mut Localization, timestamp: u64) -> String {
// TODO: format this using the selected locale
DateTime::from_timestamp(timestamp as i64, 0)
.unwrap()
.format("%l:%M %p %b %d, %Y")
.to_string()
}
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)

View File

@@ -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;
use tracing::info; // Adjust this import path as needed
pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> {
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
directory: Directory,
file_name: String,
debouncer: Debouncer,
saved_item: Option<T>,
}
impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
impl<T: PartialEq + Copy + 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 + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializ
self
}
/// Returns whether it actually wrote the new value
// returns whether successful
pub fn try_save(&mut self, cur_item: T) -> bool {
if self.debouncer.should_act() {
if let Some(ref saved_item) = self.saved_item {
if *saved_item != cur_item {
if let Some(saved_item) = self.saved_item {
if saved_item != cur_item {
return self.save(cur_item);
}
} else {
@@ -45,8 +45,8 @@ impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializ
}
pub fn get_item(&self) -> Option<T> {
if let Some(ref item) = self.saved_item {
return Some(item.clone());
if self.saved_item.is_some() {
return self.saved_item;
}
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {

View File

@@ -1,23 +1,12 @@
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 < NARROW_SCREEN_WIDTH
screen_size.x < 550.0
}
pub fn is_oled(is_mobile_override: bool) -> bool {
is_mobile_override || is_compiled_as_mobile()
pub fn is_oled() -> bool {
is_compiled_as_mobile()
}
#[inline]

View File

@@ -68,17 +68,6 @@ 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) {

View File

@@ -22,7 +22,7 @@ impl UserAccount {
}
}
pub fn keypair(&self) -> KeypairUnowned<'_> {
pub fn keypair(&self) -> KeypairUnowned {
KeypairUnowned {
pubkey: &self.key.pubkey,
secret_key: self.key.secret_key.as_ref(),

View File

@@ -24,7 +24,7 @@ pub fn get_wallet_for<'a>(
global_wallet.wallet.as_ref()
}
pub fn get_current_wallet_mut<'a>(
pub fn get_current_wallet<'a>(
accounts: &'a mut Accounts,
global_wallet: &'a mut GlobalWallet,
) -> Option<&'a mut ZapWallet> {
@@ -35,17 +35,6 @@ pub fn get_current_wallet_mut<'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,

View File

@@ -9,7 +9,6 @@ license = "GPLv3"
description = "The nostr browser"
[dependencies]
bitflags = { workspace = true }
eframe = { workspace = true }
egui_tabs = { workspace = true }
egui_extras = { workspace = true }
@@ -17,8 +16,6 @@ egui = { workspace = true }
notedeck_columns = { workspace = true }
notedeck_ui = { workspace = true }
notedeck_dave = { workspace = true }
notedeck_notebook = { workspace = true }
notedeck_clndash = { workspace = true }
notedeck = { workspace = true }
nostrdb = { workspace = true }
puffin = { workspace = true, optional = true }
@@ -66,12 +63,6 @@ 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"

View File

@@ -8,9 +8,8 @@
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
>
<intent-filter>
@@ -24,16 +23,9 @@
</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" />

View File

@@ -26,15 +26,12 @@ public class MainActivity extends GameActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Shrink view so it does not get covered by insets.
super.onCreate(savedInstanceState);
setupInsets();
//setupFullscreen()
//keyboardHelper = new KeyboardHeightHelper(this);
keyboardHelper = new KeyboardHeightHelper(this);
super.onCreate(savedInstanceState);
}
private void setupFullscreen() {
@@ -64,18 +61,6 @@ public class MainActivity extends GameActivity {
}
private void setupInsets() {
// NOTE(jb55): This is needed for keyboard visibility. Without this the
// window still gets the right insets, but theyre consumed before they
// reach the NDK side.
//WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
// NOTE(jb55): This is needed for keyboard visibility. If the bars are
// permanently gone, Android routes the keyboard over the GL surface and
// doesnt change insets.
//WindowInsetsControllerCompat ic = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
//ic.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
View content = getContent();
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
@@ -87,13 +72,12 @@ public class MainActivity extends GameActivity {
mlp.rightMargin = insets.right;
v.setLayoutParams(mlp);
return windowInsets;
return WindowInsetsCompat.CONSUMED;
});
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
}
/*
@Override
public void onResume() {
super.onResume();
@@ -111,7 +95,6 @@ public class MainActivity extends GameActivity {
super.onDestroy();
keyboardHelper.close();
}
*/
@Override
public boolean onTouchEvent(MotionEvent event) {

View File

@@ -2,8 +2,10 @@
//use egui_android::run_android;
use egui_winit::winit::platform::android::activity::AndroidApp;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use crate::chrome::Chrome;
use crate::{app::NotedeckApp, chrome::Chrome, setup::setup_chrome};
use notedeck::Notedeck;
#[no_mangle]
@@ -17,7 +19,7 @@ pub async fn android_main(app: AndroidApp) {
//std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
std::env::set_var(
"RUST_LOG",
"egui=debug,egui-winit=debug,winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
"egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
);
//std::env::set_var(
@@ -57,7 +59,7 @@ pub async fn android_main(app: AndroidApp) {
options.android_app = Some(app.clone());
let app_args = get_app_args(app.clone());
let app_args = get_app_args(app);
let _res = eframe::run_native(
"Damus Notedeck",
@@ -65,9 +67,31 @@ 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);
notedeck.set_android_context(app.clone());
notedeck.setup(ctx);
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
setup_chrome(ctx, &notedeck.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.set_app(chrome);
Ok(Box::new(notedeck))

View File

@@ -1,15 +1,11 @@
use notedeck::{AppAction, AppContext};
use notedeck_clndash::ClnDash;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_notebook::Notebook;
#[allow(clippy::large_enum_variant)]
pub enum NotedeckApp {
Dave(Box<Dave>),
Columns(Box<Damus>),
Notebook(Box<Notebook>),
ClnDash(Box<ClnDash>),
Dave(Dave),
Columns(Damus),
Other(Box<dyn notedeck::App>),
}
@@ -18,8 +14,6 @@ impl notedeck::App for NotedeckApp {
match self {
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
NotedeckApp::Other(other) => other.update(ctx, ui),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
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);
}

View File

@@ -1,12 +1,12 @@
pub mod fonts;
pub mod setup;
pub mod theme;
#[cfg(target_os = "android")]
mod android;
mod app;
mod chrome;
mod options;
pub use app::NotedeckApp;
pub use chrome::Chrome;
pub use options::ChromeOptions;

View File

@@ -10,7 +10,12 @@ static GLOBAL: AccountingAllocator<std::alloc::System> =
AccountingAllocator::new(std::alloc::System);
use notedeck::{DataPath, DataPathType, Notedeck};
use notedeck_chrome::{setup::generate_native_options, Chrome};
use notedeck_chrome::{
setup::{generate_native_options, setup_chrome},
Chrome, NotedeckApp,
};
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
@@ -86,8 +91,29 @@ async fn main() {
let ctx = &cc.egui_ctx;
let mut notedeck = Notedeck::new(ctx, base_path, &args);
notedeck.setup(ctx);
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
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.set_app(chrome);
Ok(Box::new(notedeck))
@@ -121,8 +147,7 @@ pub fn main() {
#[cfg(test)]
mod tests {
use super::Notedeck;
use notedeck_columns::Damus;
use super::{Damus, Notedeck};
use std::path::{Path, PathBuf};
fn create_tmp_dir() -> PathBuf {
@@ -187,6 +212,17 @@ 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

View File

@@ -1,38 +0,0 @@
use bitflags::bitflags;
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChromeOptions: u64 {
/// Is the chrome currently open?
const NoOptions = 0;
/// Is the chrome currently open?
const IsOpen = 1 << 0;
/// Are we simulating a virtual keyboard? This is mostly for debugging
/// if we are too lazy to open up a real mobile device with soft
/// keyboard
const VirtualKeyboard = 1 << 1;
/// Are we showing the memory debug window?
const MemoryDebug = 1 << 2;
/// Repaint debug
const RepaintDebug = 1 << 3;
/// We need soft keyboard visibility
const KeyboardVisibility = 1 << 4;
}
}
impl Default for ChromeOptions {
fn default() -> Self {
let mut options = ChromeOptions::NoOptions;
options.set(
ChromeOptions::IsOpen,
!notedeck::ui::is_compiled_as_mobile(),
);
options
}
}

View File

@@ -38,13 +38,7 @@ impl PreviewRunner {
"unrecognized args: {:?}",
notedeck.unrecognized_args()
);
setup_chrome(
ctx,
notedeck.args(),
notedeck.theme(),
notedeck.note_body_font_size(),
notedeck.zoom_factor(),
);
setup_chrome(ctx, notedeck.args(), notedeck.theme());
notedeck.set_app(PreviewApp::new(preview));

View File

@@ -1,6 +1,55 @@
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: &notedeck::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| {

View File

@@ -0,0 +1,149 @@
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;
}
}

View File

@@ -1,21 +0,0 @@
[package]
name = "notedeck_clndash"
edition = "2024"
version.workspace = true
[dependencies]
egui = { workspace = true }
notedeck = { workspace = true }
#notedeck_ui = { workspace = true }
eframe = { workspace = true }
tracing = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
egui_extras = { workspace = true }
lightning-invoice = { workspace = true }
hex = { workspace = true }
nostrdb = { workspace = true }
notedeck_ui = { workspace = true }
lnsocket = "0.5.1"

View File

@@ -1,77 +0,0 @@
# ⚡ clndash
Your Core Lightning dashboard, **without the server nonsense**.
clndash is a weird little experiment: a [notedeck][notedeck] app that talks to your node **directly over the Lightning Network** using [lnsocket][lnsocket] + [Commando][commando] RPCs.
No HTTP. No nginx. No VPS.
Just open clndash, point it at your node, and boom — youre in.
<img src="https://jb55.com/s/476285c50d06c3ce.png" width="50%" />
---
## 🤯 Why?
Because sometimes you just want to *see your channels* and *check invoices* without SSH-ing into a box and typing `lightning-cli`.
And because LN is already a secure, encrypted connection layer — why not just use that?
---
## 🔥 Features (as of today)
* **Plug-and-play LN connection** powered by [lnsocket][lnsocket]
* **Commando RPC** all dashboard data is fetched directly from your CLN node over Lightning
* **Channel overview** total capacity, inbound/outbound liquidity, largest channel, and pretty bars
* **Invoices** shows recent paid invoices (with zap previews if they came from Nostr)
* **No extra daemons** you dont need to run a server to use it
---
## 🪄 Nostr Bonus
Because its a notedeck app, clndash can **render zaps** inline.
Yes, your Core Lightning dashboard can now show you when someone on Nostr just sent you sats and why.
---
## 🏗 Still Baking
This is WIP.
Youll probably hit bugs. UI might be janky. Some features may vanish or suddenly mutate.
If youre reading this and still excited — youre the exact audience.
---
## 🛠 How to connect
1. Get your nodes **public address** (host\:port) and a **Commando rune** with safe permissions.
2. Set them as environment variables:
```bash
export CLNDASH_ID="03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71"
export CLNDASH_HOST="node.example.com:9735"
export CLNDASH_RUNE="your_rune_here"
```
3. Run clndash inside notedeck by calling notedeck with the `--clndash` argument.
4. Bask in the glow of real-time LN data over an LN connection.
---
## ⚠️ Disclaimer
* Dont give it a rune that can spend your funds.
* Dont blame me if you break something — this is experimental territory.
* If it connects on the first try, buy yourself a beer.
---
If you like living on the edge of LN/Nostr tooling, youll like this.
If you dont… youll probably want to wait a bit.
[commando]: https://docs.corelightning.org/reference/commando
[lnsocket]: https://github.com/jb55/lnsocket-rs
[notedeck]: https://github.com/damus-io/notedeck

View File

@@ -1,124 +0,0 @@
use crate::event::LoadingState;
use crate::ui;
use egui::Color32;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct ListPeerChannel {
pub short_channel_id: String,
pub our_reserve_msat: i64,
pub to_us_msat: i64,
pub total_msat: i64,
pub their_reserve_msat: i64,
}
pub struct Channel {
pub to_us: i64,
pub to_them: i64,
pub original: ListPeerChannel,
}
pub struct Channels {
pub max_total_msat: i64,
pub avail_in: i64,
pub avail_out: i64,
pub channels: Vec<Channel>,
}
pub fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
match channels {
LoadingState::Loaded(channels) => {
if channels.channels.is_empty() {
ui.label("no channels yet...");
return;
}
for channel in &channels.channels {
channel_ui(ui, channel, channels.max_total_msat);
}
ui.label(format!(
"available out {}",
ui::human_sat(channels.avail_out)
));
ui.label(format!("available in {}", ui::human_sat(channels.avail_in)));
}
LoadingState::Failed(err) => {
ui.label(format!("error fetching channels: {err}"));
}
LoadingState::Loading => {
ui.label("fetching channels...");
}
}
}
pub fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
// ---------- numbers ----------
let short_channel_id = &c.original.short_channel_id;
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
// Feel free to switch to log scaling if you have whales:
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
// ---------- colors & style ----------
let out_color = Color32::from_rgb(84, 69, 201); // blue
let in_color = Color32::from_rgb(158, 56, 180); // purple
// Thickness scales with capacity, but keeps a nice minimum
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
let row_h = thickness + 14.0;
// ---------- layout ----------
let (rect, response) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), row_h),
egui::Sense::hover(),
);
let painter = ui.painter_at(rect);
let bar_rect = egui::Rect::from_min_max(
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
);
let corner_radius = (thickness * 0.5) as u8;
let out_radius = egui::CornerRadius {
ne: 0,
nw: corner_radius,
sw: corner_radius,
se: 0,
};
let in_radius = egui::CornerRadius {
ne: corner_radius,
nw: 0,
sw: 0,
se: corner_radius,
};
/*
painter.rect_filled(bar_rect, rounding, track_color);
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
*/
// Split widths
let usable = (c.to_us + c.to_them).max(1) as f32;
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
let split_x = bar_rect.left() + out_w;
// Outbound fill (left)
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
if out_rect.width() > 0.5 {
painter.rect_filled(out_rect, out_radius, out_color);
}
// Inbound fill (right)
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
if in_rect.width() > 0.5 {
painter.rect_filled(in_rect, in_radius, in_color);
}
// Tooltip
response.on_hover_text_at_pointer(format!(
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
ui::human_sat(c.to_us),
ui::human_sat(c.to_them),
ui::human_sat(c.original.total_msat),
));
}

View File

@@ -1,80 +0,0 @@
use crate::channels::Channels;
use crate::invoice::Invoice;
use serde::Serialize;
use serde_json::Value;
pub enum ConnectionState {
Dead(String),
Connecting,
Active,
}
pub enum LoadingState<T, E> {
Loading,
Failed(E),
Loaded(T),
}
impl<T, E> Default for LoadingState<T, E> {
fn default() -> Self {
Self::Loading
}
}
impl<T, E> LoadingState<T, E> {
fn _as_ref(&self) -> LoadingState<&T, &E> {
match self {
Self::Loading => LoadingState::<&T, &E>::Loading,
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
}
}
pub fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
match res {
Ok(r) => LoadingState::Loaded(r),
Err(err) => LoadingState::Failed(err),
}
}
/*
fn unwrap(self) -> T {
let Self::Loaded(t) = self else {
panic!("unwrap in LoadingState");
};
t
}
*/
}
#[derive(Serialize, Debug, Clone)]
pub struct _WaitRequest {
pub indexname: String,
pub subsystem: String,
pub nextvalue: u64,
}
#[derive(Clone, Debug)]
pub enum Request {
GetInfo,
ListPeerChannels,
PaidInvoices(u32),
}
/// Responses from the socket
pub enum ClnResponse {
GetInfo(Value),
ListPeerChannels(Result<Channels, lnsocket::Error>),
PaidInvoices(Result<Vec<Invoice>, lnsocket::Error>),
}
pub enum Event {
/// We lost the socket somehow
Ended {
reason: String,
},
Connected,
Response(ClnResponse),
}

View File

@@ -1,77 +0,0 @@
use crate::event::LoadingState;
use crate::ui;
use lightning_invoice::Bolt11Invoice;
use notedeck::AppContext;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize)]
pub struct Invoice {
pub lastpay_index: Option<u64>,
pub label: String,
pub bolt11: Bolt11Invoice,
pub payment_hash: String,
pub amount_msat: u64,
pub status: String,
pub description: String,
pub expires_at: u64,
pub created_index: u64,
pub updated_index: u64,
}
pub fn invoices_ui(
ui: &mut egui::Ui,
invoice_notes: &HashMap<String, [u8; 32]>,
ctx: &mut AppContext,
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
) {
match invoices {
LoadingState::Loading => {
ui.label("loading invoices...");
}
LoadingState::Failed(err) => {
ui.label(format!("failed to load invoices: {err}"));
}
LoadingState::Loaded(invoices) => {
use egui_extras::{Column, TableBuilder};
TableBuilder::new(ui)
.column(Column::auto().resizable(true))
.column(Column::remainder())
.vscroll(false)
.header(20.0, |mut header| {
header.col(|ui| {
ui.strong("description");
});
header.col(|ui| {
ui.strong("amount");
});
})
.body(|mut body| {
for invoice in invoices {
body.row(20.0, |mut row| {
row.col(|ui| {
if invoice.description.starts_with("{") {
ui.label("Zap!").on_hover_ui_at_pointer(|ui| {
ui::note_hover_ui(ui, &invoice.label, ctx, invoice_notes);
});
} else {
ui.label(&invoice.description);
}
});
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
None => {
ui.label("any");
}
Some(amt) => {
ui.label(ui::human_verbose_sat(amt as i64));
}
});
});
}
});
}
}
}

View File

@@ -1,290 +0,0 @@
use crate::channels::Channel;
use crate::channels::Channels;
use crate::channels::ListPeerChannel;
use crate::event::ClnResponse;
use crate::event::ConnectionState;
use crate::event::Event;
use crate::event::LoadingState;
use crate::event::Request;
use crate::invoice::Invoice;
use crate::summary::Summary;
use crate::watch::fetch_paid_invoices;
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
use lnsocket::{CommandoClient, LNSocket};
use nostrdb::Ndb;
use notedeck::{AppAction, AppContext};
use serde_json::json;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
mod channels;
mod event;
mod invoice;
mod summary;
mod ui;
mod watch;
#[derive(Default)]
pub struct ClnDash {
initialized: bool,
connection_state: ConnectionState,
summary: LoadingState<Summary, lnsocket::Error>,
get_info: LoadingState<String, lnsocket::Error>,
channels: LoadingState<Channels, lnsocket::Error>,
invoices: LoadingState<Vec<Invoice>, lnsocket::Error>,
channel: Option<CommChannel>,
last_summary: Option<Summary>,
// invoice label to zapreq id
invoice_zap_reqs: HashMap<String, [u8; 32]>,
}
#[derive(serde::Deserialize)]
pub struct ZapReqId {
#[serde(with = "hex::serde")]
id: [u8; 32],
}
impl Default for ConnectionState {
fn default() -> Self {
ConnectionState::Dead("uninitialized".to_string())
}
}
struct CommChannel {
req_tx: UnboundedSender<Request>,
event_rx: UnboundedReceiver<Event>,
}
impl notedeck::App for ClnDash {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
if !self.initialized {
self.connection_state = ConnectionState::Connecting;
self.setup_connection();
self.initialized = true;
}
self.process_events(ctx.ndb);
self.show(ui, ctx);
None
}
}
impl ClnDash {
fn show(&mut self, ui: &mut egui::Ui, ctx: &mut AppContext) {
egui::Frame::new()
.inner_margin(egui::Margin::same(20))
.show(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui::connection_state_ui(ui, &self.connection_state);
crate::summary::summary_ui(ui, self.last_summary.as_ref(), &self.summary);
crate::invoice::invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
crate::channels::channels_ui(ui, &self.channels);
crate::ui::get_info_ui(ui, &self.get_info);
});
});
}
fn setup_connection(&mut self) {
let (req_tx, mut req_rx) = unbounded_channel::<Request>();
let (event_tx, event_rx) = unbounded_channel::<Event>();
self.channel = Some(CommChannel { req_tx, event_rx });
tokio::spawn(async move {
let key = SecretKey::new(&mut rand::thread_rng());
let their_pubkey = PublicKey::from_str(&std::env::var("CLNDASH_ID").unwrap_or(
"03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71".to_string(),
))
.unwrap();
let host = std::env::var("CLNDASH_HOST").unwrap_or("ln.damus.io:9735".to_string());
let lnsocket = match LNSocket::connect_and_init(key, their_pubkey, &host).await {
Err(err) => {
let _ = event_tx.send(Event::Ended {
reason: err.to_string(),
});
return;
}
Ok(lnsocket) => {
let _ = event_tx.send(Event::Connected);
lnsocket
}
};
let rune = std::env::var("CLNDASH_RUNE").unwrap_or(
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
);
let commando = Arc::new(CommandoClient::spawn(lnsocket, &rune));
loop {
match req_rx.recv().await {
None => {
let _ = event_tx.send(Event::Ended {
reason: "channel dead?".to_string(),
});
break;
}
Some(req) => {
tracing::debug!("calling {req:?}");
match req {
Request::GetInfo => {
let event_tx = event_tx.clone();
let commando = commando.clone();
tokio::spawn(async move {
match commando.call("getinfo", json!({})).await {
Ok(v) => {
let _ = event_tx
.send(Event::Response(ClnResponse::GetInfo(v)));
}
Err(err) => {
tracing::error!("get_info error {}", err);
}
}
});
}
Request::PaidInvoices(n) => {
let event_tx = event_tx.clone();
let commando = commando.clone();
tokio::spawn(async move {
let invoices = fetch_paid_invoices(commando, n).await;
let _ = event_tx
.send(Event::Response(ClnResponse::PaidInvoices(invoices)));
});
}
Request::ListPeerChannels => {
let event_tx = event_tx.clone();
let commando = commando.clone();
tokio::spawn(async move {
let peer_channels =
commando.call("listpeerchannels", json!({})).await;
let channels = peer_channels.map(|v| {
let peer_channels: Vec<ListPeerChannel> =
serde_json::from_value(v["channels"].clone()).unwrap();
to_channels(peer_channels)
});
let _ = event_tx.send(Event::Response(
ClnResponse::ListPeerChannels(channels),
));
});
}
}
}
}
}
});
}
fn process_events(&mut self, ndb: &Ndb) {
let Some(channel) = &mut self.channel else {
return;
};
while let Ok(event) = channel.event_rx.try_recv() {
match event {
Event::Ended { reason } => {
self.connection_state = ConnectionState::Dead(reason);
}
Event::Connected => {
self.connection_state = ConnectionState::Active;
let _ = channel.req_tx.send(Request::GetInfo);
let _ = channel.req_tx.send(Request::ListPeerChannels);
let _ = channel.req_tx.send(Request::PaidInvoices(100));
}
Event::Response(resp) => match resp {
ClnResponse::ListPeerChannels(chans) => {
if let LoadingState::Loaded(prev) = &self.channels {
self.last_summary = Some(crate::summary::compute_summary(prev));
}
self.summary = match &chans {
Ok(chans) => {
LoadingState::Loaded(crate::summary::compute_summary(chans))
}
Err(err) => LoadingState::Failed(err.clone()),
};
self.channels = LoadingState::from_result(chans);
}
ClnResponse::GetInfo(value) => {
let res = serde_json::to_string_pretty(&value);
self.get_info =
LoadingState::from_result(res.map_err(|_| lnsocket::Error::Json));
}
ClnResponse::PaidInvoices(invoices) => {
// process zap requests
if let Ok(invoices) = &invoices {
for invoice in invoices {
let zap_req_id: Option<ZapReqId> =
serde_json::from_str(&invoice.description).ok();
if let Some(zap_req_id) = zap_req_id {
self.invoice_zap_reqs
.insert(invoice.label.clone(), zap_req_id.id);
let _ = ndb.process_event(&format!(
"[\"EVENT\",\"a\",{}]",
&invoice.description
));
}
}
}
self.invoices = LoadingState::from_result(invoices);
}
},
}
}
}
}
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
let mut avail_out: i64 = 0;
let mut avail_in: i64 = 0;
let mut max_total_msat: i64 = 0;
let mut channels: Vec<Channel> = peer_channels
.into_iter()
.map(|c| {
let to_us = (c.to_us_msat - c.our_reserve_msat).max(0);
let to_them_raw = (c.total_msat - c.to_us_msat).max(0);
let to_them = (to_them_raw - c.their_reserve_msat).max(0);
avail_out += to_us;
avail_in += to_them;
if c.total_msat > max_total_msat {
max_total_msat = c.total_msat; // <-- max, not sum
}
Channel {
to_us,
to_them,
original: c,
}
})
.collect();
channels.sort_by(|a, b| {
let a_capacity = a.to_them + a.to_us;
let b_capacity = b.to_them + b.to_us;
a_capacity.partial_cmp(&b_capacity).unwrap().reverse()
});
Channels {
max_total_msat,
avail_out,
avail_in,
channels,
}
}

View File

@@ -1,140 +0,0 @@
use crate::channels::Channels;
use crate::event::LoadingState;
use crate::ui;
#[derive(Clone, Default)]
pub struct Summary {
pub total_msat: i64,
pub avail_out_msat: i64,
pub avail_in_msat: i64,
pub channel_count: usize,
pub largest_msat: i64,
pub outbound_pct: f32, // fraction of total capacity
}
pub fn compute_summary(ch: &Channels) -> Summary {
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
let largest_msat: i64 = ch
.channels
.iter()
.map(|c| c.original.total_msat)
.max()
.unwrap_or(0);
let outbound_pct = if total_msat > 0 {
ch.avail_out as f32 / total_msat as f32
} else {
0.0
};
Summary {
total_msat,
avail_out_msat: ch.avail_out,
avail_in_msat: ch.avail_in,
channel_count: ch.channels.len(),
largest_msat,
outbound_pct,
}
}
pub fn summary_ui(
ui: &mut egui::Ui,
last_summary: Option<&Summary>,
summary: &LoadingState<Summary, lnsocket::Error>,
) {
match summary {
LoadingState::Loading => {
ui.label("loading summary");
}
LoadingState::Failed(err) => {
ui.label(format!("Failed to get summary: {err}"));
}
LoadingState::Loaded(summary) => {
summary_cards_ui(ui, summary, last_summary);
ui.add_space(8.0);
}
}
}
pub fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
let old = prev.cloned().unwrap_or_default();
let items: [(&str, String, Option<String>); 6] = [
(
"Total capacity",
ui::human_sat(s.total_msat),
prev.map(|_| ui::delta_str(s.total_msat, old.total_msat)),
),
(
"Avail out",
ui::human_sat(s.avail_out_msat),
prev.map(|_| ui::delta_str(s.avail_out_msat, old.avail_out_msat)),
),
(
"Avail in",
ui::human_sat(s.avail_in_msat),
prev.map(|_| ui::delta_str(s.avail_in_msat, old.avail_in_msat)),
),
("# Channels", s.channel_count.to_string(), None),
("Largest", ui::human_sat(s.largest_msat), None),
(
"Outbound %",
format!("{:.0}%", s.outbound_pct * 100.0),
None,
),
];
// --- responsive columns ---
let min_card = 160.0;
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
egui::Grid::new("summary_grid")
.num_columns(cols)
.min_col_width(min_card)
.spacing(egui::vec2(8.0, 8.0))
.show(ui, |ui| {
let items_len = items.len();
for (i, (t, v, d)) in items.into_iter().enumerate() {
card_cell(ui, t, v, d, min_card);
// End the row when we filled a row worth of cells
if (i + 1) % cols == 0 {
ui.end_row();
}
}
// If the last row wasn't full, close it anyway
if items_len % cols != 0 {
ui.end_row();
}
});
}
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
let weak = ui.visuals().weak_text_color();
egui::Frame::group(ui.style())
.fill(ui.visuals().extreme_bg_color)
.corner_radius(egui::CornerRadius::same(10))
.inner_margin(egui::Margin::same(10))
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.show(ui, |ui| {
ui.set_min_width(min_card);
ui.vertical(|ui| {
ui.add(
egui::Label::new(egui::RichText::new(title).small().color(weak))
.wrap_mode(egui::TextWrapMode::Wrap),
);
ui.add_space(4.0);
ui.add(
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
.wrap_mode(egui::TextWrapMode::Wrap),
);
if let Some(d) = delta {
ui.add_space(2.0);
ui.add(
egui::Label::new(egui::RichText::new(d).small().color(weak))
.wrap_mode(egui::TextWrapMode::Wrap),
);
}
});
ui.set_min_height(20.0);
});
}

View File

@@ -1,145 +0,0 @@
use crate::event::ConnectionState;
use crate::event::LoadingState;
use egui::Color32;
use egui::Label;
use egui::RichText;
use egui::Widget;
use notedeck::AppContext;
use std::collections::HashMap;
pub fn note_hover_ui(
ui: &mut egui::Ui,
label: &str,
ctx: &mut AppContext,
invoice_notes: &HashMap<String, [u8; 32]>,
) -> Option<notedeck::NoteAction> {
let zap_req_id = invoice_notes.get(label)?;
let Ok(txn) = nostrdb::Transaction::new(ctx.ndb) else {
return None;
};
let Ok(zapreq_note) = ctx.ndb.get_note_by_id(&txn, zap_req_id) else {
return None;
};
for tag in zapreq_note.tags() {
let Some("e") = tag.get_str(0) else {
continue;
};
let Some(target_id) = tag.get_id(1) else {
continue;
};
let Ok(note) = ctx.ndb.get_note_by_id(&txn, target_id) else {
return None;
};
let author = ctx
.ndb
.get_profile_by_pubkey(&txn, zapreq_note.pubkey())
.ok();
// TODO(jb55): make this less horrible
let mut note_context = notedeck::NoteContext {
ndb: ctx.ndb,
accounts: ctx.accounts,
img_cache: ctx.img_cache,
note_cache: ctx.note_cache,
zaps: ctx.zaps,
pool: ctx.pool,
job_pool: ctx.job_pool,
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
i18n: ctx.i18n,
global_wallet: ctx.global_wallet,
};
let mut jobs = notedeck::JobsCache::default();
let options = notedeck_ui::NoteOptions::default();
notedeck_ui::ProfilePic::from_profile_or_default(note_context.img_cache, author.as_ref())
.ui(ui);
let nostr_name = notedeck::name::get_display_name(author.as_ref());
ui.label(format!("{} zapped you", nostr_name.name()));
return notedeck_ui::NoteView::new(&mut note_context, &note, options, &mut jobs)
.preview_style()
.hide_media(true)
.show(ui)
.action;
}
None
}
pub fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
ui.horizontal_wrapped(|ui| match info {
LoadingState::Loading => {}
LoadingState::Failed(err) => {
ui.label(format!("failed to fetch node info: {err}"));
}
LoadingState::Loaded(info) => {
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
}
});
}
pub fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
match state {
ConnectionState::Active => {
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
}
ConnectionState::Connecting => {
ui.add(Label::new(
RichText::new("Connecting").color(Color32::YELLOW),
));
}
ConnectionState::Dead(reason) => {
ui.add(Label::new(
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
));
}
}
}
// ---------- helper ----------
pub fn human_sat(msat: i64) -> String {
let sats = msat / 1000;
if sats >= 1_000_000 {
format!("{:.1}M", sats as f64 / 1_000_000.0)
} else if sats >= 1_000 {
format!("{:.1}k", sats as f64 / 1_000.0)
} else {
sats.to_string()
}
}
pub fn human_verbose_sat(msat: i64) -> String {
if msat < 1_000 {
// less than 1 sat
format!("{msat} msat")
} else {
let sats = msat / 1_000;
if sats < 100_000_000 {
// less than 1 BTC
format!("{sats} sat")
} else {
let btc = sats / 100_000_000;
format!("{btc} BTC")
}
}
}
pub fn delta_str(new: i64, old: i64) -> String {
let d = new - old;
match d.cmp(&0) {
std::cmp::Ordering::Greater => format!("{}", human_sat(d)),
std::cmp::Ordering::Less => format!("{}", human_sat(-d)),
std::cmp::Ordering::Equal => "·".into(),
}
}

View File

@@ -1,198 +0,0 @@
use crate::invoice::Invoice;
use lnsocket::CallOpts;
use lnsocket::CommandoClient;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;
#[derive(Deserialize)]
struct UpdatedInvoicesResponse {
updated: u64,
}
#[derive(Deserialize)]
struct PayIndexInvoices {
invoices: Vec<PayIndexScan>,
}
#[derive(Deserialize)]
struct PayIndexScan {
pay_index: Option<u64>,
}
async fn find_lastpay_index(commando: Arc<CommandoClient>) -> Result<Option<u64>, lnsocket::Error> {
const PAGE: u64 = 250;
// 1) get the current updated tail
let created_value = commando
.call(
"wait",
json!({"subsystem":"invoices","indexname":"updated","nextvalue":0}),
)
.await?;
let response: UpdatedInvoicesResponse =
serde_json::from_value(created_value).map_err(|_| lnsocket::Error::Json)?;
// start our window at the tail
let mut start_at = response
.updated
.saturating_add(1) // +1 because we want max(1, updated - PAGE + 1)
.saturating_sub(PAGE)
.max(1);
loop {
// 2) fetch a window (indexed by "updated")
let val = commando
.call_with_opts(
"listinvoices",
json!({
"index": "updated",
"start": start_at,
"limit": PAGE,
}),
// only fetch the one field we care about
CallOpts::default().filter(json!({
"invoices": [{"pay_index": true}]
})),
)
.await?;
let parsed: PayIndexInvoices =
serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
if let Some(pi) = parsed.invoices.iter().filter_map(|inv| inv.pay_index).max() {
return Ok(Some(pi));
}
// 4) no paid invoice in this slice—step back or bail
if start_at == 1 {
return Ok(None);
}
start_at = start_at.saturating_sub(PAGE).max(1);
}
}
pub async fn fetch_paid_invoices(
commando: Arc<CommandoClient>,
limit: u32,
) -> Result<Vec<Invoice>, lnsocket::Error> {
use tokio::task::JoinSet;
// look for an invoice with the last paid index
let Some(lastpay_index) = find_lastpay_index(commando.clone()).await? else {
// no paid invoices
return Ok(vec![]);
};
let mut set: JoinSet<Result<Invoice, lnsocket::Error>> = JoinSet::new();
let start = lastpay_index.saturating_sub(limit as u64);
// 3) Fire off at most `concurrency` `waitanyinvoice` calls at a time,
// collect all successful responses into a Vec.
// fire them ALL at once
for idx in start..lastpay_index {
let c = commando.clone();
set.spawn(async move {
let val = c
.call(
"waitanyinvoice",
serde_json::json!({ "lastpay_index": idx }),
)
.await?;
let parsed: Invoice = serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
Ok(parsed)
});
}
let mut results = Vec::with_capacity(limit as usize);
while let Some(res) = set.join_next().await {
results.push(res.map_err(|_| lnsocket::Error::Io(std::io::ErrorKind::Interrupted))??);
}
results.sort_by(|a, b| a.updated_index.cmp(&b.updated_index).reverse());
Ok(results)
}
// wip watch subsystem
/*
async fn watch_subsystem(
commando: CommandoClient,
subsystem: WaitSubsystem,
index: WaitIndex,
event_tx: UnboundedSender<Event>,
mut cancel_rx: Receiver<()>,
) {
// Step 1: Fetch current index value so we can back up ~20
let mut nextvalue: u64 = match commando
.call(
"wait",
serde_json::json!({
"indexname": index.as_str(),
"subsystem": subsystem.as_str(),
"nextvalue": 0
}),
)
.await
{
Ok(v) => {
// You showed the result has `updated` as the current highest index
let current = v.get("updated").and_then(|x| x.as_u64()).unwrap_or(0);
current.saturating_sub(20) // back up 20, clamp at 0
}
Err(err) => {
tracing::warn!("initial wait(…nextvalue=0) failed: {}", err);
0
}
};
loop {
// You can add a timeout to avoid hanging forever in weird network states.
let fut = commando.call(
"wait",
serde_json::to_value(WaitRequest {
indexname: "invoices".into(),
subsystem: "lightningd".into(),
nextvalue,
})
.unwrap(),
);
tokio::select! {
_ = &mut cancel_rx => {
// graceful shutdown
break;
}
res = fut => {
match res {
Ok(v) => {
// Typical shape: { "nextvalue": n, "invoicestatus": { ... } } (varies by plugin/index)
// Adjust these lookups for your nodes actual wait payload.
if let Some(nv) = v.get("nextvalue").and_then(|x| x.as_u64()) {
nextvalue = nv + 1;
} else {
// Defensive: never get stuck — bump at least by 1
nextvalue += 1;
}
// Inspect/route
let kind = v.get("status").and_then(|s| s.as_str());
let ev = match kind {
Some("paid") => ClnResponse::Invoice(InvoiceEvent::Paid(v.clone())),
Some("created") => ClnResponse::Invoice(InvoiceEvent::Created(v.clone())),
_ => ClnResponse::Invoice(InvoiceEvent::Other(v.clone())),
};
let _ = event_tx.send(Event::Response(ev));
}
Err(err) => {
tracing::warn!("wait(invoices) error: {err}");
// small backoff so we don't tight-loop on persistent errors
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
}
}
}
}
}
*/

View File

@@ -11,7 +11,6 @@ description = "A tweetdeck-style notedeck app"
crate-type = ["lib", "cdylib"]
[dependencies]
opener = { workspace = true }
rmpv = { workspace = true }
bech32 = { workspace = true }
notedeck = { workspace = true }
@@ -32,7 +31,7 @@ image = { workspace = true }
indexmap = { workspace = true }
nostrdb = { workspace = true }
notedeck_ui = { workspace = true }
robius-open = { workspace = true }
open = { workspace = true }
poll-promise = { workspace = true }
puffin = { workspace = true, optional = true }
puffin_egui = { workspace = true, optional = true }

View File

@@ -1,19 +1,15 @@
use enostr::{FullKeypair, Pubkey};
use nostrdb::{Ndb, Transaction};
use notedeck::{Accounts, AppContext, JobsCache, Localization, SingleUnkIdAction, UnknownIds};
use notedeck_ui::nip51_set::Nip51SetUiCache;
use notedeck::{Accounts, AppContext, Localization, SingleUnkIdAction, UnknownIds};
pub use crate::accounts::route::AccountsResponse;
use crate::app::get_active_columns_mut;
use crate::decks::DecksCache;
use crate::onboarding::Onboarding;
use crate::profile::send_new_contact_list;
use crate::subscriptions::Subscriptions;
use crate::ui::onboarding::{FollowPackOnboardingView, FollowPacksResponse, OnboardingResponse};
use crate::{
login_manager::AcquireKeyState,
route::Route,
timeline::TimelineCache,
ui::{
account_login_view::{AccountLoginResponse, AccountLoginView},
accounts::{AccountsView, AccountsViewResponse},
@@ -41,7 +37,6 @@ pub struct SwitchAccountAction {
/// The account to switch to
pub switch_to: Pubkey,
pub switching_to_new: bool,
}
impl SwitchAccountAction {
@@ -49,14 +44,8 @@ impl SwitchAccountAction {
SwitchAccountAction {
source_column,
switch_to,
switching_to_new: false,
}
}
pub fn switching_to_new(mut self) -> Self {
self.switching_to_new = true;
self
}
}
#[derive(Debug)]
@@ -76,13 +65,13 @@ pub struct AddAccountAction {
pub fn render_accounts_route(
ui: &mut egui::Ui,
app_ctx: &mut AppContext,
jobs: &mut JobsCache,
col: usize,
decks: &mut DecksCache,
timeline_cache: &mut TimelineCache,
login_state: &mut AcquireKeyState,
onboarding: &Onboarding,
follow_packs_ui: &mut Nip51SetUiCache,
route: AccountsRoute,
) -> Option<AccountsResponse> {
match route {
) -> AddAccountAction {
let resp = match route {
AccountsRoute::Accounts => AccountsView::new(
app_ctx.ndb,
app_ctx.accounts,
@@ -91,33 +80,47 @@ pub fn render_accounts_route(
)
.ui(ui)
.inner
.map(AccountsRouteResponse::Accounts)
.map(AccountsResponse::Account),
.map(AccountsRouteResponse::Accounts),
AccountsRoute::AddAccount => {
AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
.ui(ui)
.inner
.map(AccountsRouteResponse::AddAccount)
.map(AccountsResponse::Account)
}
AccountsRoute::Onboarding => FollowPackOnboardingView::new(
onboarding,
follow_packs_ui,
app_ctx.ndb,
app_ctx.img_cache,
app_ctx.i18n,
app_ctx.job_pool,
jobs,
)
.ui(ui)
.map(|r| match r {
OnboardingResponse::FollowPacks(follow_packs_response) => {
AccountsResponse::Account(AccountsRouteResponse::AddAccount(
AccountLoginResponse::Onboarding(follow_packs_response),
))
};
if let Some(resp) = resp {
match resp {
AccountsRouteResponse::Accounts(response) => {
let action = process_accounts_view_response(
app_ctx.i18n,
app_ctx.accounts,
decks,
col,
response,
);
AddAccountAction {
accounts_action: action,
unk_id_action: SingleUnkIdAction::no_action(),
}
}
OnboardingResponse::ViewProfile(pubkey) => AccountsResponse::ViewProfile(pubkey),
}),
AccountsRouteResponse::AddAccount(response) => {
let action =
process_login_view_response(app_ctx, timeline_cache, decks, col, response);
*login_state = Default::default();
let router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
.column_mut(col)
.router_mut();
router.go_back();
action
}
}
} else {
AddAccountAction {
accounts_action: None,
unk_id_action: SingleUnkIdAction::no_action(),
}
}
}
@@ -152,53 +155,31 @@ pub fn process_accounts_view_response(
pub fn process_login_view_response(
app_ctx: &mut AppContext,
timeline_cache: &mut TimelineCache,
decks: &mut DecksCache,
subs: &mut Subscriptions,
onboarding: &mut Onboarding,
col: usize,
response: AccountLoginResponse,
) -> AddAccountAction {
let cur_router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
.column_mut(col)
.router_mut();
let r = match response {
let (r, pubkey) = match response {
AccountLoginResponse::CreateNew => {
let kp = FullKeypair::generate();
let pubkey = kp.pubkey;
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool);
(app_ctx.accounts.add_account(kp.to_keypair()), pubkey)
}
AccountLoginResponse::LoginWith(keypair) => {
cur_router.go_back();
app_ctx.accounts.add_account(keypair)
let pubkey = keypair.pubkey;
(app_ctx.accounts.add_account(keypair), pubkey)
}
AccountLoginResponse::CreatingNew => {
cur_router.route_to(Route::Accounts(AccountsRoute::Onboarding));
onboarding.process(app_ctx.pool, app_ctx.ndb, subs, app_ctx.unknown_ids);
None
}
AccountLoginResponse::Onboarding(onboarding_response) => match onboarding_response {
FollowPacksResponse::NoFollowPacks => {
onboarding.process(app_ctx.pool, app_ctx.ndb, subs, app_ctx.unknown_ids);
None
}
FollowPacksResponse::UserSelectedPacks(nip51_sets_ui_state) => {
let pks_to_follow = nip51_sets_ui_state.get_all_selected();
let kp = FullKeypair::generate();
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool, pks_to_follow);
cur_router.go_back();
onboarding.end_onboarding(app_ctx.pool, app_ctx.ndb);
app_ctx.accounts.add_account(kp.to_keypair())
}
},
};
decks.add_deck_default(app_ctx, timeline_cache, pubkey);
if let Some(action) = r {
AddAccountAction {
accounts_action: Some(AccountsAction::Switch(SwitchAccountAction {
source_column: col,
switch_to: action.switch_to,
switching_to_new: true,
})),
unk_id_action: action.unk_id_action,
}
@@ -209,41 +190,3 @@ pub fn process_login_view_response(
}
}
}
impl AccountsRouteResponse {
pub fn process(
self,
app_ctx: &mut AppContext,
app: &mut crate::Damus,
col: usize,
) -> AddAccountAction {
match self {
AccountsRouteResponse::Accounts(response) => {
let action = process_accounts_view_response(
app_ctx.i18n,
app_ctx.accounts,
&mut app.decks_cache,
col,
response,
);
AddAccountAction {
accounts_action: action,
unk_id_action: notedeck::SingleUnkIdAction::no_action(),
}
}
AccountsRouteResponse::AddAccount(response) => {
let action = process_login_view_response(
app_ctx,
&mut app.decks_cache,
&mut app.subscriptions,
&mut app.onboarding,
col,
response,
);
app.view_state.login = Default::default();
action
}
}
}
}

View File

@@ -7,16 +7,10 @@ pub enum AccountsRouteResponse {
AddAccount(AccountLoginResponse),
}
pub enum AccountsResponse {
ViewProfile(enostr::Pubkey),
Account(AccountsRouteResponse),
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum AccountsRoute {
Accounts,
AddAccount,
Onboarding,
}
impl AccountsRoute {
@@ -25,7 +19,6 @@ impl AccountsRoute {
match self {
Self::Accounts => &["accounts", "show"],
Self::AddAccount => &["accounts", "new"],
Self::Onboarding => &["accounts", "onboarding"],
}
}
}

View File

@@ -8,7 +8,6 @@ use crate::{
},
ThreadSelection, TimelineCache, TimelineKind,
},
view_state::ViewState,
};
use enostr::{NoteId, Pubkey, RelayPool};
@@ -17,7 +16,6 @@ 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 {
@@ -53,7 +51,6 @@ 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,
@@ -81,11 +78,7 @@ fn execute_note_action(
.open(ndb, note_cache, txn, pool, &kind)
.map(NotesOpenResult::Timeline);
}
NoteAction::Note {
note_id,
preview,
scroll_offset,
} => 'ex: {
NoteAction::Note { note_id, preview } => 'ex: {
let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id)
else {
tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes()));
@@ -93,15 +86,7 @@ fn execute_note_action(
};
timeline_res = threads
.open(
ndb,
txn,
pool,
&thread_selection,
preview,
col,
scroll_offset,
)
.open(ndb, txn, pool, &thread_selection, preview, col)
.map(NotesOpenResult::Thread);
let route = Route::Thread(thread_selection);
@@ -168,16 +153,7 @@ fn execute_note_action(
}
},
NoteAction::Media(media_action) => {
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)
media_action.process(images);
}
}
@@ -204,7 +180,6 @@ 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 = {
@@ -229,7 +204,6 @@ pub fn execute_and_process_note_action(
global_wallet,
zaps,
images,
view_state,
router_type,
ui,
col,

View File

@@ -4,31 +4,28 @@ use crate::{
decks::{Decks, DecksCache},
draft::Drafts,
nav::{self, ProcessNavResult},
onboarding::Onboarding,
options::AppOptions,
route::Route,
storage,
subscriptions::{SubKind, Subscriptions},
support::Support,
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
toolbar::unseen_notification,
ui::{self, toolbar::toolbar, DesktopSidePanel, SidePanelAction},
ui::{self, DesktopSidePanel, SidePanelAction},
view_state::ViewState,
Result,
};
use egui_extras::{Size, StripBuilder};
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
use nostrdb::Transaction;
use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds,
};
use notedeck_ui::{
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
NoteOptions,
Localization, UnknownIds,
};
use notedeck_ui::{jobs::JobsCache, NoteOptions};
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use std::time::Duration;
use tracing::{debug, error, info, trace, warn};
use uuid::Uuid;
@@ -41,7 +38,6 @@ 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,
@@ -59,20 +55,18 @@ pub struct Damus {
pub note_options: NoteOptions,
pub unrecognized_args: BTreeSet<String>,
/// keep track of follow packs
pub onboarding: Onboarding,
}
fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
for event in &input.raw.events {
match event {
egui::Event::Key { key, pressed, .. } if *pressed => match key {
if let egui::Event::Key {
key, pressed: true, ..
} = event
{
match key {
egui::Key::J => {
//columns.select_down();
{}
columns.select_down();
}
/*
egui::Key::K => {
columns.select_up();
}
@@ -82,18 +76,13 @@ fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
egui::Key::L => {
columns.select_left();
}
*/
egui::Key::BrowserBack | egui::Key::Escape => {
columns.get_selected_router().go_back();
if let Some(column) = columns.selected_mut() {
column.router_mut().go_back();
}
}
_ => {}
},
egui::Event::InsetsChanged => {
tracing::debug!("insets have changed!");
}
_ => {}
}
}
}
@@ -105,7 +94,7 @@ fn try_process_event(
) -> Result<()> {
let current_columns =
get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
ctx.input(|i| handle_egui_events(i, current_columns));
ctx.input(|i| handle_key_events(i, current_columns));
let ctx2 = ctx.clone();
let wakeup = move || {
@@ -148,7 +137,7 @@ fn try_process_event(
}
}
for (kind, timeline) in &mut damus.timeline_cache {
for (_kind, timeline) in &mut damus.timeline_cache {
let is_ready = timeline::is_timeline_ready(
app_ctx.ndb,
app_ctx.pool,
@@ -173,16 +162,9 @@ fn try_process_event(
}
} else {
// TODO: show loading?
if matches!(kind, TimelineKind::List(ListKind::Contact(_))) {
timeline::fetch_contact_list(&mut damus.subscriptions, timeline, app_ctx.accounts);
}
}
}
if let Some(follow_packs) = damus.onboarding.get_follow_packs_mut() {
follow_packs.poll_for_notes(app_ctx.ndb, app_ctx.unknown_ids);
}
if app_ctx.unknown_ids.ready_to_send() {
unknown_id_send(app_ctx.unknown_ids, app_ctx.pool);
}
@@ -378,54 +360,18 @@ fn render_damus(
app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
damus
.note_options
.set(NoteOptions::Wide, is_narrow(ui.ctx()));
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
render_damus_mobile(damus, app_ctx, ui)
} else {
render_damus_desktop(damus, app_ctx, ui)
};
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
// We use this for keeping timestamps and things up to date
//ui.ctx().request_repaint_after(Duration::from_secs(5));
ui.ctx().request_repaint_after(Duration::from_secs(5));
app_action
}
/// Present a fullscreen media viewer if the FullscreenMedia AppOptions flag is set. This is
/// typically set by image carousels using a MediaAction's on_view_media callback when
/// an image is clicked
fn fullscreen_media_viewer_ui(
ui: &mut egui::Ui,
state: &mut MediaViewerState,
img_cache: &mut Images,
) {
if !state.should_show(ui) {
if state.scene_rect.is_some() {
// if we shouldn't show yet we will have a scene
// rect, then we should clear it for next time
tracing::debug!("fullscreen_media_viewer_ui: resetting scene rect");
state.scene_rect = None;
}
return;
}
let resp = MediaViewer::new(state).fullscreen(true).ui(img_cache, ui);
if resp.clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
fullscreen_media_close(state);
}
}
/// Close the fullscreen media player. This also resets the scene_rect state
fn fullscreen_media_close(state: &mut MediaViewerState) {
state.flags.set(MediaViewerFlags::Open, false);
}
/*
fn determine_key_storage_type() -> KeyStorageType {
#[cfg(target_os = "macos")]
@@ -447,58 +393,47 @@ fn determine_key_storage_type() -> KeyStorageType {
impl Damus {
/// Called once before the first frame.
pub fn new(app_context: &mut AppContext<'_>, args: &[String]) -> Self {
pub fn new(ctx: &mut AppContext<'_>, args: &[String]) -> Self {
// arg parsing
let (parsed_args, unrecognized_args) =
ColumnsArgs::parse(args, Some(app_context.accounts.selected_account_pubkey()));
ColumnsArgs::parse(args, Some(ctx.accounts.selected_account_pubkey()));
let account = app_context.accounts.selected_account_pubkey_bytes();
let account = ctx.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(app_context.ndb).unwrap();
let txn = Transaction::new(ctx.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,
app_context.ndb,
app_context.note_cache,
app_context.pool,
ctx.ndb,
ctx.note_cache,
ctx.pool,
&timeline_kind,
) {
add_result.process(
app_context.ndb,
app_context.note_cache,
ctx.ndb,
ctx.note_cache,
&txn,
&mut timeline_cache,
app_context.unknown_ids,
ctx.unknown_ids,
);
}
}
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,
) {
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)
{
info!(
"DecksCache: loading from disk {}",
crate::storage::DECKS_CACHE_FILE
@@ -506,15 +441,38 @@ impl Damus {
decks_cache
} else {
info!("DecksCache: creating new with demo configuration");
DecksCache::new_with_demo_config(&mut timeline_cache, app_context)
//for (pk, _) in &app_context.accounts.cache {
DecksCache::new_with_demo_config(&mut timeline_cache, ctx)
//for (pk, _) in &ctx.accounts.cache {
// cache.add_deck_default(*pk);
//}
};
let support = Support::new(app_context.path);
let note_options = get_note_options(parsed_args, 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 jobs = JobsCache::default();
let threads = Threads::default();
Self {
@@ -531,7 +489,6 @@ impl Damus {
unrecognized_args,
jobs,
threads,
onboarding: Onboarding::default(),
}
}
@@ -582,7 +539,6 @@ impl Damus {
unrecognized_args: BTreeSet::default(),
jobs: JobsCache::default(),
threads: Threads::default(),
onboarding: Onboarding::default(),
}
}
@@ -593,36 +549,6 @@ impl Damus {
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
&self.unrecognized_args
}
pub fn toolbar_height() -> f32 {
48.0
}
pub fn initially_selected_toolbar_index() -> i32 {
0
}
}
fn get_note_options(args: ColumnsArgs, settings_handler: &mut SettingsHandler) -> NoteOptions {
let mut note_options = NoteOptions::default();
note_options.set(
NoteOptions::Textmode,
args.is_flag_set(ColumnsFlag::Textmode),
);
note_options.set(
NoteOptions::ScrambleText,
args.is_flag_set(ColumnsFlag::Scramble),
);
note_options.set(
NoteOptions::HideMedia,
args.is_flag_set(ColumnsFlag::NoMedia),
);
note_options.set(
NoteOptions::RepliesNewestFirst,
settings_handler.show_replies_newest_first(),
);
note_options
}
/*
@@ -642,71 +568,35 @@ fn render_damus_mobile(
) -> Option<AppAction> {
//let routes = app.timelines[0].routes.clone();
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
let rect = ui.available_rect_before_wrap();
let mut app_action: Option<AppAction> = None;
// don't show toolbar if soft keyboard is open
let skb_rect = app_ctx.soft_keyboard_rect(
ui.ctx().screen_rect(),
notedeck::SoftKeyboardContext::platform(ui.ctx()),
);
let toolbar_height = if skb_rect.is_none() {
Damus::toolbar_height()
} else {
0.0
};
StripBuilder::new(ui)
.size(Size::remainder()) // top cell
.size(Size::exact(toolbar_height)) // bottom cell
.vertical(|mut strip| {
strip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
if !app.columns(app_ctx.accounts).columns().is_empty() {
let r = nav::render_nav(
active_col,
ui.available_rect_before_wrap(),
app,
app_ctx,
ui,
)
.process_render_nav_response(app, app_ctx, ui);
if let Some(r) = &r {
match r {
ProcessNavResult::SwitchOccurred => {
if !app.options.contains(AppOptions::TmpColumns) {
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
}
}
ProcessNavResult::PfpClicked => {
app_action = Some(AppAction::ToggleChrome);
}
}
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,
ui.available_rect_before_wrap(),
app,
app_ctx,
ui,
)
.process_render_nav_response(app, app_ctx, ui);
if let Some(r) = &r {
match r {
ProcessNavResult::SwitchOccurred => {
if !app.options.contains(AppOptions::TmpColumns) {
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
}
}
hovering_post_button(ui, app, app_ctx, rect);
});
strip.cell(|ui| 'brk: {
if toolbar_height <= 0.0 {
break 'brk;
ProcessNavResult::PfpClicked => {
app_action = Some(AppAction::ToggleChrome);
}
}
}
}
let unseen_notif = unseen_notification(
app,
app_ctx.ndb,
app_ctx.accounts.get_selected_account().key.pubkey,
);
if skb_rect.is_none() {
let resp = toolbar(ui, unseen_notif);
if let Some(action) = resp {
action.process(app, app_ctx);
}
}
});
});
hovering_post_button(ui, app, app_ctx, rect);
app_action
}
@@ -722,10 +612,8 @@ fn hovering_post_button(
let button_y = ui
.ctx()
.animate_bool_responsive(btn_id, should_show_compose);
rect.min.x = rect.max.x - (if is_narrow(ui.ctx()) { 60.0 } else { 100.0 } * button_y);
rect.min.y = rect.max.y - 100.0;
rect.max.x += 48.0 * (1.0 - button_y);
rect.min.x = rect.max.x - if is_narrow(ui.ctx()) { 60.0 } else { 100.0 };
rect.min.y = rect.max.y - 100.0 * button_y;
let darkmode = ui.ctx().style().visuals.dark_mode;
@@ -774,7 +662,6 @@ 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,
@@ -902,13 +789,7 @@ fn timelines_view(
let mut save_cols = false;
if let Some(action) = side_panel_action {
save_cols = save_cols
|| action.process(
&mut app.timeline_cache,
&mut app.decks_cache,
ctx,
&mut app.subscriptions,
ui.ctx(),
);
|| action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx, ui.ctx());
}
let mut app_action: Option<AppAction> = None;

View File

@@ -11,6 +11,7 @@ pub enum ColumnsFlag {
Textmode,
Scramble,
NoMedia,
ShowNoteClient,
}
pub struct ColumnsArgs {
@@ -52,6 +53,8 @@ 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 == "--no-media" {
res.set_flag(ColumnsFlag::NoMedia);
} else if arg == "--filter" {

View File

@@ -1,6 +1,5 @@
use crate::{
actionbar::TimelineOpenResult,
drag::DragSwitch,
route::{Route, Router, SingletonRouter},
timeline::{Timeline, TimelineCache, TimelineKind},
};
@@ -14,7 +13,6 @@ use tracing::warn;
pub struct Column {
pub router: Router<Route>,
pub sheet_router: SingletonRouter<Route>,
pub drag: DragSwitch,
}
impl Column {
@@ -23,7 +21,6 @@ impl Column {
Column {
router,
sheet_router: SingletonRouter::default(),
drag: DragSwitch::default(),
}
}
@@ -75,33 +72,25 @@ impl Columns {
/// Select the column based on the timeline kind.
///
/// TODO: add timeline if missing?
pub fn select_by_route(&mut self, desired_route: Route) -> SelectionResult {
pub fn select_by_kind(&mut self, kind: &TimelineKind) -> SelectionResult {
for (i, col) in self.columns.iter().enumerate() {
for route in col.router().routes() {
if *route == desired_route {
if self.selected as usize == i {
return SelectionResult::AlreadySelected(i);
} else {
self.select_column(i as i32);
return SelectionResult::NewSelection(i);
if let Some(timeline) = route.timeline_id() {
if timeline == kind {
tracing::info!("selecting {kind:?} column");
if self.selected as usize == i {
return SelectionResult::AlreadySelected(i);
} else {
self.select_column(i as i32);
return SelectionResult::NewSelection(i);
}
}
}
}
}
if matches!(&desired_route, Route::Timeline(_))
|| matches!(&desired_route, Route::Thread(_))
{
// these require additional handling to add state
tracing::error!("failed to select {desired_route:?} column");
return SelectionResult::Failed;
}
self.add_column(Column::new(vec![desired_route]));
let selected_index = self.columns.len() - 1;
self.select_column(selected_index as i32);
SelectionResult::NewSelection(selected_index)
tracing::error!("failed to select {kind:?} column");
SelectionResult::Failed
}
pub fn add_new_timeline_column(
@@ -167,9 +156,11 @@ 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_selected_router(&mut self) -> &mut Router<Route> {
self.ensure_column();
self.selected_mut().router_mut()
pub fn get_first_router(&mut self) -> &mut Router<Route> {
if self.columns.is_empty() {
self.new_column_picker();
}
self.columns[0].router_mut()
}
#[inline]
@@ -190,25 +181,16 @@ impl Columns {
Some(&self.columns[self.selected as usize])
}
// TODO(jb55): switch to non-empty container for columns?
fn ensure_column(&mut self) {
if self.columns.is_empty() {
self.new_column_picker();
}
}
/// 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]
pub fn selected_mut(&mut self) -> Option<&mut Column> {
if self.columns.is_empty() {
return None;
}
Some(&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]
}

View File

@@ -35,7 +35,7 @@ impl DecksCache {
accounts: &notedeck::Accounts,
) -> Option<&mut Column> {
self.active_columns_mut(i18n, accounts)
.map(|ad| ad.selected_mut())
.and_then(|ad| ad.selected_mut())
}
pub fn selected_column(&self, accounts: &notedeck::Accounts) -> Option<&Column> {
@@ -190,7 +190,7 @@ impl DecksCache {
&self.fallback_pubkey
}
pub fn get_all_decks_mut(&mut self) -> ValuesMut<'_, Pubkey, Decks> {
pub fn get_all_decks_mut(&mut self) -> ValuesMut<Pubkey, Decks> {
self.account_to_decks.values_mut()
}

View File

@@ -1,103 +0,0 @@
#[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")
}

View File

@@ -12,13 +12,11 @@ pub mod column;
mod deck_state;
mod decks;
mod draft;
mod drag;
mod key_parsing;
pub mod login_manager;
mod media_upload;
mod multi_subscriber;
mod nav;
mod onboarding;
pub mod options;
mod post;
mod profile;
@@ -28,7 +26,6 @@ mod subscriptions;
mod support;
mod test_data;
pub mod timeline;
mod toolbar;
pub mod ui;
mod unknowns;
mod view_state;

View File

@@ -11,7 +11,7 @@ use sha2::{Digest, Sha256};
use url::Url;
use crate::Error;
use notedeck::media::images::fetch_binary_from_disk;
use notedeck_ui::images::fetch_binary_from_disk;
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
@@ -75,7 +75,7 @@ pub fn get_nostr_build_upload_url() -> Promise<Result<String, Error>> {
get_upload_url_from_provider(NOSTR_BUILD_URL())
}
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note<'_> {
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note {
NoteBuilder::new()
.kind(27235)
.start_tag()
@@ -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}"
))));
))))
}
};

View File

@@ -474,10 +474,12 @@ impl TimelineSub {
let before = self.state.clone();
's: {
match &mut self.state {
SubState::NoSub { dependers } => *dependers = dependers.saturating_sub(1),
SubState::NoSub { dependers } => {
*dependers -= 1;
}
SubState::LocalOnly { local, dependers } => {
if *dependers > 1 {
*dependers = dependers.saturating_sub(1);
*dependers -= 1;
break 's;
}
@@ -490,7 +492,7 @@ impl TimelineSub {
}
SubState::RemoteOnly { remote, dependers } => {
if *dependers > 1 {
*dependers = dependers.saturating_sub(1);
*dependers -= 1;
break 's;
}
@@ -500,7 +502,7 @@ impl TimelineSub {
}
SubState::Unified { unified, dependers } => {
if *dependers > 1 {
*dependers = dependers.saturating_sub(1);
*dependers -= 1;
break 's;
}

View File

@@ -1,34 +1,28 @@
use crate::{
accounts::{render_accounts_route, AccountsAction, AccountsResponse},
accounts::{render_accounts_route, AccountsAction},
app::{get_active_columns_mut, get_decks_mut},
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},
subscriptions::Subscriptions,
timeline::{
kind::ListKind,
route::{render_thread_route, render_timeline_route},
TimelineCache, TimelineKind,
TimelineCache,
},
ui::{
self,
add_column::{render_add_column_routes, AddColumnView},
add_column::render_add_column_routes,
column::NavTitle,
configure_deck::ConfigureDeckView,
edit_deck::{EditDeckResponse, EditDeckView},
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
onboarding::FollowPackOnboardingView,
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType},
profile::EditProfileView,
search::{FocusState, SearchView},
settings::SettingsAction,
support::SupportView,
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
TimelineView,
RelayView,
},
Damus,
};
@@ -37,10 +31,9 @@ use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, P
use enostr::ProfileState;
use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
RelayAction,
get_current_default_msats, get_current_wallet, tr, ui::is_narrow, Accounts, AppContext,
NoteAction, NoteContext, RelayAction,
};
use notedeck_ui::NoteOptions;
use tracing::error;
/// The result of processing a nav response
@@ -67,7 +60,6 @@ pub enum RenderNavAction {
SwitchingAction(SwitchingAction),
WalletAction(WalletAction),
RelayAction(RelayAction),
SettingsAction(SettingsAction),
}
pub enum SwitchingAction {
@@ -83,36 +75,19 @@ impl SwitchingAction {
timeline_cache: &mut TimelineCache,
decks_cache: &mut DecksCache,
ctx: &mut AppContext<'_>,
subs: &mut Subscriptions,
ui_ctx: &egui::Context,
) -> bool {
match &self {
SwitchingAction::Accounts(account_action) => match account_action {
AccountsAction::Switch(switch_action) => {
{
let txn = Transaction::new(ctx.ndb).expect("txn");
ctx.accounts.select_account(
&switch_action.switch_to,
ctx.ndb,
&txn,
ctx.pool,
ui_ctx,
);
let contacts_sub = ctx.accounts.get_subs().contacts.remote.clone();
// this is cringe but we're gonna get a new sub manager soon...
subs.subs.insert(
contacts_sub,
crate::subscriptions::SubKind::FetchingContactList(TimelineKind::List(
ListKind::Contact(*ctx.accounts.selected_account_pubkey()),
)),
);
}
if switch_action.switching_to_new {
decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to);
}
let txn = Transaction::new(ctx.ndb).expect("txn");
ctx.accounts.select_account(
&switch_action.switch_to,
ctx.ndb,
&txn,
ctx.pool,
ui_ctx,
);
// pop nav after switch
get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
.column_mut(switch_action.source_column)
@@ -479,7 +454,6 @@ fn process_render_nav_action(
ctx.global_wallet,
ctx.zaps,
ctx.img_cache,
&mut app.view_state,
ui,
)
}
@@ -488,7 +462,6 @@ fn process_render_nav_action(
&mut app.timeline_cache,
&mut app.decks_cache,
ctx,
&mut app.subscriptions,
ui.ctx(),
) {
return Some(ProcessNavResult::SwitchOccurred);
@@ -507,9 +480,6 @@ 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 {
@@ -527,12 +497,13 @@ 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,
@@ -543,8 +514,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) => {
@@ -567,8 +538,6 @@ 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);
@@ -585,47 +554,25 @@ fn render_nav_body(
&mut note_context,
&mut app.jobs,
),
Route::Accounts(amr) => 's: {
let Some(action) = render_accounts_route(
Route::Accounts(amr) => {
let mut action = render_accounts_route(
ui,
ctx,
&mut app.jobs,
col,
&mut app.decks_cache,
&mut app.timeline_cache,
&mut app.view_state.login,
&app.onboarding,
&mut app.view_state.follow_packs,
*amr,
) else {
break 's None;
};
match action {
AccountsResponse::ViewProfile(pubkey) => {
Some(RenderNavAction::NoteAction(NoteAction::Profile(pubkey)))
}
AccountsResponse::Account(accounts_route_response) => {
let mut action = accounts_route_response.process(ctx, app, col);
let txn = Transaction::new(ctx.ndb).expect("txn");
action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
action
.accounts_action
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
}
}
);
let txn = Transaction::new(ctx.ndb).expect("txn");
action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
action
.accounts_action
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
}
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
@@ -649,25 +596,27 @@ 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 mut options = app.note_options;
options.set(NoteOptions::Wide, false);
let response = ui::PostReplyView::new(
&mut note_context,
poster,
draft,
&note,
inner_rect,
options,
&mut app.jobs,
col,
)
.show(ui);
let response = egui::ScrollArea::vertical()
.show(ui, |ui| {
ui::PostReplyView::new(
&mut note_context,
poster,
draft,
&note,
inner_rect,
app.note_options,
&mut app.jobs,
)
.id_source(id)
.show(ui)
})
.inner;
response.action
};
@@ -688,20 +637,26 @@ 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 = crate::ui::note::QuoteRepostView::new(
&mut note_context,
poster,
draft,
&note,
inner_rect,
app.note_options,
&mut app.jobs,
col,
)
.show(ui);
let response = egui::ScrollArea::vertical()
.show(ui, |ui| {
crate::ui::note::QuoteRepostView::new(
&mut note_context,
poster,
draft,
&note,
inner_rect,
app.note_options,
&mut app.jobs,
)
.id_source(id)
.show(ui)
})
.inner;
response.action.map(Into::into)
}
@@ -781,7 +736,7 @@ fn render_nav_body(
new_deck_state.clear();
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_selected_router()
.get_first_router()
.go_back();
}
resp
@@ -812,7 +767,7 @@ fn render_nav_body(
}
}
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_selected_router()
.get_first_router()
.go_back();
}
@@ -832,7 +787,7 @@ fn render_nav_body(
return action;
};
if EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard).ui(ui) {
if EditProfileView::new(ctx.i18n, state, ctx.img_cache).ui(ui) {
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
SaveProfileChanges::new(kp.to_full(), state.clone()),
@@ -887,7 +842,7 @@ fn render_nav_body(
}
};
WalletView::new(state, ctx.i18n, ctx.clipboard)
WalletView::new(state, ctx.i18n)
.ui(ui)
.map(RenderNavAction::WalletAction)
}
@@ -960,7 +915,7 @@ pub fn render_nav(
ctx.ndb,
ctx.img_cache,
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
std::slice::from_ref(route),
&[route.clone()],
col,
ctx.i18n,
)
@@ -974,169 +929,47 @@ pub fn render_nav(
}
};
let routes = app
.columns(ctx.accounts)
.column(col)
.router()
.routes()
.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,
let nav_response = Nav::new(
&app.columns(ctx.accounts)
.column(col)
.router()
.routes()
.clone(),
)
.navigating(
app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.navigating,
)
.returning(
app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.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,
ctx.img_cache,
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
nav.routes(),
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)
.router_mut()
.navigating,
ctx.i18n,
)
.returning(
app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.returning,
)
.show_mut(ui, |ui, render_type, nav| match render_type {
NavUiType::Title => NavTitle::new(
ctx.ndb,
ctx.img_cache,
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
nav.routes(),
col,
ctx.i18n,
)
.show_move_button(!narrow)
.show_delete_button(!narrow)
.show(ui),
.show_move_button(!narrow)
.show_delete_button(!narrow)
.show(ui),
NavUiType::Body => {
if let Some(top) = nav.routes().last() {
render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
} else {
None
}
NavUiType::Body => {
if let Some(top) = nav.routes().last() {
render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
} else {
None
}
});
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,
crate::accounts::AccountsRoute::Onboarding => {
Some(FollowPackOnboardingView::scroll_id())
}
},
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,
crate::accounts::AccountsRoute::Onboarding => 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,
}
}

Some files were not shown because too many files have changed in this diff Show More