Compare commits
1 Commits
master
...
fluent-poc
| Author | SHA1 | Date | |
|---|---|---|---|
|
0e87c22e55
|
12
.envrc
@@ -1,7 +1,6 @@
|
||||
# set to false if you don't care to include android stuff
|
||||
export use_android=true
|
||||
export android_emulator=false
|
||||
export ANDROID_DIR=crates/notedeck_chrome/android
|
||||
|
||||
use nix --arg use_android $use_android --arg android_emulator $android_emulator
|
||||
|
||||
@@ -14,11 +13,6 @@ source scripts/macos_build_secrets.sh || :
|
||||
export PATH=$PATH:$HOME/.cargo/bin
|
||||
export JB55=32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
|
||||
|
||||
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 || :
|
||||
export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
|
||||
export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev
|
||||
export JEFFG=npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc
|
||||
|
||||
3
.github/workflows/build-and-test.yml
vendored
@@ -22,5 +22,8 @@ jobs:
|
||||
if: ${{ inputs.additional-setup != '' }}
|
||||
run: ${{ inputs.additional-setup }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run Tests (Native Only)
|
||||
run: cargo test
|
||||
|
||||
142
.github/workflows/rust.yml
vendored
@@ -10,45 +10,31 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Rustfmt + Clippy
|
||||
runs-on: ubuntu-22.04
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
- run: |
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy
|
||||
components: rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
android:
|
||||
name: Check (android)
|
||||
runs-on: ubuntu-22.04
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
- name: Setup Java JDK
|
||||
uses: actions/setup-java@v4.5.0
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
- name: Add android rust target
|
||||
run: rustup target add aarch64-linux-android
|
||||
- name: Install Cargo NDK
|
||||
run: cargo install cargo-ndk
|
||||
- name: Run tests
|
||||
run: make jni-check
|
||||
components: clippy
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
linux-test:
|
||||
name: Test (Linux)
|
||||
uses: ./.github/workflows/build-and-test.yml
|
||||
with:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-latest
|
||||
additional-setup: |
|
||||
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
|
||||
|
||||
@@ -66,7 +52,7 @@ jobs:
|
||||
|
||||
packaging:
|
||||
name: rpm/deb
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: linux-test
|
||||
if: github.ref_name == 'master' || github.ref_name == 'ci'
|
||||
|
||||
@@ -90,6 +76,9 @@ jobs:
|
||||
fi
|
||||
cargo install cargo-generate-rpm cargo-deb
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build Cross (${{ matrix.arch }})
|
||||
if: matrix.arch != runner.arch
|
||||
run: cargo build --release --target=${{ matrix.arch }}-unknown-linux-gnu
|
||||
@@ -100,19 +89,19 @@ jobs:
|
||||
|
||||
- name: Build RPM (Cross)
|
||||
if: matrix.arch != runner.arch
|
||||
run: cargo generate-rpm -p crates/notedeck_chrome --target=${{ matrix.arch }}-unknown-linux-gnu
|
||||
run: cargo generate-rpm --target=${{ matrix.arch }}-unknown-linux-gnu
|
||||
|
||||
- name: Build RPM
|
||||
if: matrix.arch == runner.arch
|
||||
run: cargo generate-rpm -p crates/notedeck_chrome
|
||||
run: cargo generate-rpm
|
||||
|
||||
- name: Build deb (Cross)
|
||||
if: matrix.arch != runner.arch
|
||||
run: cargo deb -p notedeck_chrome --target=${{ matrix.arch }}-unknown-linux-gnu
|
||||
run: cargo deb --target=${{ matrix.arch }}-unknown-linux-gnu
|
||||
|
||||
- name: Build deb
|
||||
if: matrix.arch == runner.arch
|
||||
run: cargo deb -p notedeck_chrome
|
||||
run: cargo deb
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -189,15 +178,10 @@ jobs:
|
||||
path: packages/notedeck-${{ matrix.arch }}.dmg
|
||||
|
||||
windows-installer:
|
||||
name: Windows Installer
|
||||
name: Build Windows Installer (x86_64)
|
||||
runs-on: windows-latest
|
||||
needs: windows-test
|
||||
if: github.ref_name == 'master' || github.ref_name == 'ci'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x86_64, aarch64]
|
||||
|
||||
steps:
|
||||
# Checkout the repository
|
||||
- name: Checkout Code
|
||||
@@ -219,91 +203,19 @@ jobs:
|
||||
- name: Install Inno Setup
|
||||
run: choco install innosetup --no-progress --yes
|
||||
|
||||
# Set up Rust toolchain
|
||||
- name: Install Rust toolchain
|
||||
run: rustup target add ${{ matrix.arch }}-pc-windows-msvc
|
||||
|
||||
# Build
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$target = "${{ matrix.arch }}-pc-windows-msvc"
|
||||
Write-Output "Building for target: $target"
|
||||
cargo build --release --target=$target
|
||||
|
||||
# Generate ISS Script
|
||||
- name: Generate Inno Setup Script
|
||||
shell: pwsh
|
||||
run: |
|
||||
$arch = "${{ matrix.arch }}"
|
||||
$issContent = @"
|
||||
[Setup]
|
||||
AppName=Damus Notedeck
|
||||
AppVersion=0.1
|
||||
DefaultDirName={pf}\Notedeck
|
||||
DefaultGroupName=Damus Notedeck
|
||||
OutputDir=..\packages\$arch
|
||||
OutputBaseFilename=DamusNotedeckInstaller
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
|
||||
[Files]
|
||||
Source: "..\target\$arch-pc-windows-msvc\release\notedeck.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\Damus Notedeck"; Filename: "{app}\notedeck.exe"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\notedeck.exe"; Description: "Launch Damus Notedeck"; Flags: nowait postinstall skipifsilent
|
||||
"@
|
||||
Set-Content -Path "scripts/windows-installer-$arch.iss" -Value $issContent
|
||||
|
||||
# Build Installer
|
||||
- name: Run Inno Setup Script
|
||||
run: |
|
||||
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "scripts\windows-installer-${{ matrix.arch }}.iss"
|
||||
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "scripts\windows-installer.iss"
|
||||
|
||||
# List outputs
|
||||
- name: List Inno Script outputs
|
||||
run: dir packages
|
||||
|
||||
# Move output
|
||||
- name: Move Inno Script outputs to architecture-specific folder
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path packages\${{ matrix.arch }}
|
||||
Move-Item -Path packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe -Destination packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe
|
||||
# Upload the installer as an artifact
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: DamusNotedeckInstaller-${{ matrix.arch }}.exe
|
||||
path: packages\${{ matrix.arch }}\DamusNotedeckInstaller.exe
|
||||
|
||||
upload-artifacts:
|
||||
name: Upload Artifacts to Server
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [packaging, macos-dmg, windows-installer]
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci'
|
||||
|
||||
steps:
|
||||
- name: Download all Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Setup SSH and Upload
|
||||
run: |
|
||||
eval "$(ssh-agent -s)"
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SFTP_KEY }}" | tr -d '\r' | ssh-add -
|
||||
echo "${{ secrets.DEPLOY_IP }} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN65pj1cNMqlf96jZLr1i9+mnHIN4jjRPPTDix6sRnt" >> ~/.ssh/known_hosts
|
||||
ls -la /home/runner/work/notedeck/notedeck/notedeck-x86_64.rpm
|
||||
export ARTIFACTS=/home/runner/work/notedeck/notedeck
|
||||
sftp ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_IP }} <<EOF
|
||||
cd upload/artifacts
|
||||
put $ARTIFACTS/notedeck-x86_64.rpm/*
|
||||
put $ARTIFACTS/notedeck-x86_64.deb/*
|
||||
put $ARTIFACTS/notedeck-x86_64.dmg/*
|
||||
put $ARTIFACTS/notedeck-aarch64.rpm/*
|
||||
put $ARTIFACTS/notedeck-aarch64.deb/*
|
||||
put $ARTIFACTS/notedeck-aarch64.dmg/*
|
||||
put $ARTIFACTS/DamusNotedeckInstaller-x86_64.exe/*
|
||||
put $ARTIFACTS/DamusNotedeckInstaller-aarch64.exe/*
|
||||
bye
|
||||
EOF
|
||||
name: DamusNotedeckInstaller.exe
|
||||
path: packages\DamusNotedeckInstaller.exe
|
||||
|
||||
|
||||
24
.gitignore
vendored
@@ -1,25 +1,17 @@
|
||||
.DS_Store
|
||||
.build-result
|
||||
.buildcmd
|
||||
TODO.bak
|
||||
android-config.json
|
||||
logcat.txt
|
||||
build.log
|
||||
rusty-tags.vi
|
||||
crates/notedeck_chrome/android/app/build
|
||||
perf.data
|
||||
perf.data.old
|
||||
.privenv
|
||||
*.so
|
||||
*.swp
|
||||
*.jar
|
||||
target
|
||||
.gradle
|
||||
queries/damus-notifs.json
|
||||
.git
|
||||
cache
|
||||
/dist
|
||||
/packages
|
||||
.direnv/
|
||||
src/camera.rs
|
||||
scripts/macos_build_secrets.sh
|
||||
*.patch
|
||||
*.txt
|
||||
/tags
|
||||
.zed
|
||||
.lsp
|
||||
.idea
|
||||
local.properties
|
||||
*.mdb
|
||||
|
||||
68
CHANGELOG.md
@@ -1,68 +0,0 @@
|
||||
# Notedeck Beta - v0.4 - 2025-05-05
|
||||
|
||||
# Added
|
||||
|
||||
- Dave nostr ai assistant app
|
||||
- GIFs!
|
||||
- Fulltext note search
|
||||
- Add full screen images, add zoom & pan
|
||||
- Zaps! NWC/ Wallet ui
|
||||
- Introduce last note per pubkey feed (experimental)
|
||||
- Allow multiple media uploads per selection
|
||||
- Major Android improvements (still wip)
|
||||
- Added notedeck app sidebar
|
||||
- User Tagging
|
||||
- Note truncation
|
||||
- Local network note broadcast, broadcast notes to other notedeck notes while you're offline
|
||||
- Mute list support (reading)
|
||||
- Relay list support
|
||||
- Ctrl-enter to send notes
|
||||
- Added relay indexing (relay columns soon)
|
||||
- Click hashtags to open hashtag timeline
|
||||
|
||||
# Fixed
|
||||
|
||||
- Fix timelines sometimes not updating (stale feeds)
|
||||
- Fix ui bounciness when loading profile pictures
|
||||
- Fix unselectable post replies
|
||||
|
||||
# Notedeck Alpha 2 - v0.3 - 2025-01-31
|
||||
|
||||
## Added
|
||||
- Clicking a mention now opens profile page (William Casarin)
|
||||
- Note previews when hovering reply descriptions (William Casarin)
|
||||
- Media uploads (kernelkind)
|
||||
- Profile editing (kernelkind)
|
||||
- Add hashtags to posts (Daniel Saxton)
|
||||
- Enhanced command-line interface for user interactions (Ken Sedgwick)
|
||||
- Various Android updates and compatibility improvements (Ken Sedgwick, William Casarin)
|
||||
- Debug features for user relay-list and mute list synchronization (Ken Sedgwick)
|
||||
|
||||
## Changed
|
||||
- Add confirmation when deleting columns (kernelkind)
|
||||
- Enhance Android build and performance (Ken Sedgwick)
|
||||
- Image cache handling using sha256 hash (kieran)
|
||||
- Introduction of decks_cache and improvements (kernelkind)
|
||||
- Migrated to egui v0.29.1 (William Casarin)
|
||||
- Only show column delete button when not navigating (William Casarin)
|
||||
- Show profile pictures in column headers (William Casarin)
|
||||
- Show usernames in user columns (William Casarin)
|
||||
- Switch to only notes & replies on some tabs (William Casarin)
|
||||
- Tombstone muted notes (Ken)
|
||||
- Pointer interactions enhancements in UI (William Casarin)
|
||||
- Persistent theme setup across sessions (kernelkind)
|
||||
- Increased ping intervals for network performance (William Casarin)
|
||||
- Nostrdb update for async support (Ken Sedgwick)
|
||||
|
||||
## Fixed
|
||||
- Fix GIT_COMMIT_HASH compilation issue (William Casarin)
|
||||
- Fix avatar alignment in profile previews (William Casarin)
|
||||
- Fix broken quote repost hitbox (William Casarin)
|
||||
- Fix crash when navigating in debug mode (William Casarin)
|
||||
- Fix long delays when reconnecting (William Casarin)
|
||||
- Fix repost button size (William Casarin)
|
||||
- Fixed since kind filters (kernelkind)
|
||||
- Clippy warnings resolved (Dimitris Apostolou)
|
||||
|
||||
## Refactoring & Improvements
|
||||
- Numerous internal structural improvements and modularization (William Casarin, Ken Sedgwick)
|
||||
4950
Cargo.lock
generated
229
Cargo.toml
@@ -1,94 +1,78 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
package.version = "0.6.0"
|
||||
members = [
|
||||
"crates/notedeck",
|
||||
"crates/notedeck_chrome",
|
||||
"crates/notedeck_columns",
|
||||
"crates/notedeck_dave",
|
||||
"crates/notedeck_notebook",
|
||||
"crates/notedeck_ui",
|
||||
"crates/notedeck_clndash",
|
||||
[package]
|
||||
name = "notedeck"
|
||||
version = "0.2.0"
|
||||
authors = ["William Casarin <jb55@jb55.com>"]
|
||||
edition = "2021"
|
||||
default-run = "notedeck"
|
||||
#rust-version = "1.60"
|
||||
license = "GPLv3"
|
||||
description = "A multiplatform nostr client"
|
||||
|
||||
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
|
||||
]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[workspace.dependencies]
|
||||
opener = "0.8.2"
|
||||
chrono = "0.4.40"
|
||||
base32 = "0.4.0"
|
||||
base64 = "0.22.1"
|
||||
rmpv = "1.3.0"
|
||||
bech32 = { version = "0.11", default-features = false }
|
||||
bitflags = "2.5.0"
|
||||
dirs = "5.0.1"
|
||||
eframe = { version = "0.31.1", default-features = false, features = [ "wgpu", "wayland", "x11", "android-game-activity" ] }
|
||||
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_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" }
|
||||
ehttp = "0.5.0"
|
||||
enostr = { path = "crates/enostr" }
|
||||
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"] }
|
||||
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", features = ["serde"] }
|
||||
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = [ "wgpu", "wayland", "x11", "android-native-activity" ] }
|
||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "71154e4100775f6932ee517da4350c433ba14ec7" }
|
||||
|
||||
[dependencies]
|
||||
#egui-android = { git = "https://github.com/jb55/egui-android.git" }
|
||||
egui = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
ehttp = "0.2.0"
|
||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "fd0900bdff4be35709372e921f2b49f68b261469" }
|
||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" }
|
||||
reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] }
|
||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||
indexmap = "2.6.0"
|
||||
log = "0.4.17"
|
||||
md5 = "0.7.0"
|
||||
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
|
||||
nwc = "0.39.0"
|
||||
mio = { version = "1.0.3", features = ["os-poll", "net"] }
|
||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
|
||||
#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"
|
||||
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" }
|
||||
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
|
||||
serde_derive = "1"
|
||||
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
|
||||
tracing = "0.1.40"
|
||||
#wasm-bindgen = "0.2.83"
|
||||
nostrdb = { workspace = true }
|
||||
#nostrdb = { path = "/Users/jb55/dev/github/damus-io/nostrdb-rs" }
|
||||
#nostrdb = "0.3.4"
|
||||
enostr = { path = "enostr" }
|
||||
serde_json = "1.0.89"
|
||||
env_logger = "0.10.0"
|
||||
puffin_egui = { version = "0.27.0", optional = true }
|
||||
puffin = { version = "0.19.0", optional = true }
|
||||
hex = "0.4.3"
|
||||
base32 = "0.4.0"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
thiserror = "2.0.7"
|
||||
tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
tracing = { version = "0.1.40", features = ["log"] }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tempfile = "3.13.0"
|
||||
unic-langid = { version = "0.9.6", features = ["macros"] }
|
||||
url = "2.5.2"
|
||||
urlencoding = "2.1.3"
|
||||
bitflags = "2.5.0"
|
||||
uuid = { version = "1.10.0", features = ["v4"] }
|
||||
sha2 = "0.10.8"
|
||||
bincode = "1.3.3"
|
||||
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"] }
|
||||
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" ] }
|
||||
indexmap = "2.6.0"
|
||||
dirs = "5.0.1"
|
||||
tracing-appender = "0.2.3"
|
||||
urlencoding = "2.1.3"
|
||||
open = "5.3.0"
|
||||
url = "2.5"
|
||||
fluent-resmgr = "0.0.7"
|
||||
fluent-fallback = "0.7.1"
|
||||
fluent = "0.16.1"
|
||||
unic-langid = "0.9.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework = "2.11.0"
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
profiling = ["puffin", "puffin_egui", "eframe/puffin"]
|
||||
debug-widget-callstack = ["egui/callstack"]
|
||||
debug-interactive-widgets = []
|
||||
|
||||
[profile.small]
|
||||
inherits = 'release'
|
||||
@@ -98,23 +82,72 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations
|
||||
panic = 'abort' # Abort on panic
|
||||
strip = true # Strip symbols from binary*
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.6"
|
||||
tracing-wasm = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.11.1"
|
||||
android-activity = { version = "0.4", features = [ "native-activity" ] }
|
||||
#winit = "0.28.6"
|
||||
winit = { version = "0.29", features = [ "android-native-activity" ] }
|
||||
#winit = { git="https://github.com/rust-windowing/winit.git", rev = "2a58b785fed2a3746f7c7eebce95bce67ddfd27c", features = ["android-native-activity"] }
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "com.damus.notedeck"
|
||||
icon = ["assets/app_icon.icns"]
|
||||
|
||||
[package.metadata.android]
|
||||
package = "com.damus.app"
|
||||
apk_name = "damus"
|
||||
#assets = "assets"
|
||||
|
||||
[[package.metadata.android.uses_feature]]
|
||||
name = "android.hardware.vulkan.level"
|
||||
required = true
|
||||
version = 1
|
||||
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
max_sdk_version = 18
|
||||
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.READ_EXTERNAL_STORAGE"
|
||||
max_sdk_version = 18
|
||||
|
||||
[package.metadata.android.signing.release]
|
||||
path = "damus.keystore"
|
||||
keystore_password = "damuskeystore"
|
||||
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.INTERNET"
|
||||
|
||||
[package.metadata.android.application]
|
||||
label = "Damus"
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
{ source = "target/release/notedeck", dest = "/usr/bin/notedeck", mode = "755" },
|
||||
]
|
||||
|
||||
[[bin]]
|
||||
name = "notedeck"
|
||||
path = "src/bin/notedeck.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ui_preview"
|
||||
path = "src/ui_preview/main.rs"
|
||||
|
||||
[patch.crates-io]
|
||||
#egui = { path = "/home/jb55/dev/github/emilk/egui/crates/egui" }
|
||||
#eframe = { path = "/home/jb55/dev/github/emilk/egui/crates/eframe" }
|
||||
#egui-winit = { path = "/home/jb55/dev/github/emilk/egui/crates/egui-winit" }
|
||||
#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" }
|
||||
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 = { path = "/home/jb55/dev/github/rust-windowing/winit" }
|
||||
#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" }
|
||||
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }
|
||||
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
|
||||
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe" }
|
||||
emath = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "emath" }
|
||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras" }
|
||||
|
||||
28
Makefile
@@ -1,30 +1,8 @@
|
||||
.DEFAULT_GOAL := check
|
||||
.PHONY: fake
|
||||
|
||||
ANDROID_DIR := crates/notedeck_chrome/android
|
||||
|
||||
check:
|
||||
all:
|
||||
cargo check
|
||||
|
||||
tags: fake
|
||||
rusty-tags vi
|
||||
find . -type d -name target -prune -o -type f -name '*.rs' -print | xargs ctags
|
||||
|
||||
jni: fake
|
||||
cargo ndk --target arm64-v8a -o $(ANDROID_DIR)/app/src/main/jniLibs/ build --profile release
|
||||
|
||||
jni-check: fake
|
||||
cargo ndk --target arm64-v8a check
|
||||
|
||||
apk: jni
|
||||
cd $(ANDROID_DIR) && ./gradlew build
|
||||
|
||||
gradle:
|
||||
cd $(ANDROID_DIR) && ./gradlew build
|
||||
|
||||
push-android-config:
|
||||
adb push android-config.json /sdcard/Android/data/com.damus.notedeck/files/android-config.json
|
||||
|
||||
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
|
||||
.PHONY: fake
|
||||
|
||||
186
README.md
@@ -1,148 +1,96 @@
|
||||
# Notedeck
|
||||
# Damus Notedeck
|
||||
|
||||
[](https://github.com/damus-io/notedeck/actions/workflows/rust.yml)
|
||||
[](https://deepwiki.com/damus-io/notedeck)
|
||||
[](https://github.com/damus-io/notedeck/actions/workflows/rust.yml)
|
||||
|
||||
A modern, multiplatform Nostr client built with Rust. Notedeck provides a feature-rich experience for interacting with the Nostr protocol on both desktop and Android platforms.
|
||||
A multiplatform nostr client. Works on android and desktop
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.jb55.com/s/6130555f03db55b2.png" alt="Notedeck Desktop Screenshot" width="700">
|
||||
</p>
|
||||
The desktop client is called notedeck:
|
||||
|
||||
## ✨ Features
|
||||

|
||||
|
||||
- **Multi-column Layout**: TweetDeck-style interface for viewing different Nostr content
|
||||
- **Dave AI Assistant**: AI-powered assistant that can search and analyze Nostr content
|
||||
- **Profile Management**: View and edit Nostr profiles
|
||||
- **Media Support**: View and upload images with GIF support
|
||||
- **Lightning Integration**: Zap (tip) content creators with Bitcoin Lightning
|
||||
- **Cross-platform**: Works on desktop (Linux, macOS, Windows) and Android
|
||||
## Android
|
||||
|
||||
## 📱 Mobile Support
|
||||
Look it actually runs on android!
|
||||
|
||||
Notedeck runs smoothly on Android devices with a responsive interface:
|
||||
<img src="https://cdn.jb55.com/s/bebeeadf7001fae1.png" height="500px" />
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.jb55.com/s/bebeeadf7001fae1.png" alt="Notedeck Android Screenshot" height="500px">
|
||||
</p>
|
||||
|
||||
## 🏗️ Project Structure
|
||||
|
||||
```
|
||||
notedeck
|
||||
├── crates
|
||||
│ ├── notedeck - Core library with shared functionality
|
||||
│ ├── notedeck_chrome - UI container and navigation framework
|
||||
│ ├── notedeck_columns - TweetDeck-style column interface
|
||||
│ ├── notedeck_dave - AI assistant for Nostr
|
||||
│ ├── notedeck_ui - Shared UI components
|
||||
│ └── tokenator - String token parsing library
|
||||
```
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Desktop
|
||||
|
||||
To run on desktop platforms:
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Development build
|
||||
cargo run -- --debug
|
||||
|
||||
# Release build
|
||||
cargo run --release
|
||||
$ ./target/release/notedeck
|
||||
```
|
||||
|
||||
### Android
|
||||
# Developer Setup
|
||||
|
||||
For Android devices:
|
||||
## Desktop (Linux/MacOS, Windows?)
|
||||
|
||||
If you're running debian-based machine like Ubuntu or ElementaryOS, all you need is to install [rustup] and run `sudo apt install build-essential`.
|
||||
|
||||
```bash
|
||||
# Install required target
|
||||
rustup target add aarch64-linux-android
|
||||
|
||||
# Build and install on connected device
|
||||
cargo apk run --release -p notedeck_chrome
|
||||
$ cargo run --release
|
||||
```
|
||||
|
||||
### Android Emulator
|
||||
## Android
|
||||
|
||||
1. Install [Android Studio](https://developer.android.com/studio)
|
||||
2. Open 'Device Manager' and create a device with API level `34` and ABI `arm64-v8a`
|
||||
3. Start the emulator
|
||||
4. Run: `cargo apk run --release -p notedeck_chrome`
|
||||
The dev shell should also have all of the android-sdk dependencies needed for development, but you still need the `aarch64-linux-android` rustup target installed:
|
||||
|
||||
## 🧪 Development
|
||||
```
|
||||
$ rustup target add aarch64-linux-android
|
||||
```
|
||||
|
||||
### Android Configuration
|
||||
To run on a real device, just type:
|
||||
|
||||
Customize Android views for testing:
|
||||
```bash
|
||||
$ cargo apk run --release
|
||||
```
|
||||
|
||||
1. Copy `example-android-config.json` to `android-config.json`
|
||||
2. Run `make push-android-config` to deploy to your device
|
||||
## Android Emulator
|
||||
|
||||
### Setting Up Developer Environment
|
||||
- Install [Android Studio](https://developer.android.com/studio)
|
||||
- Open 'Device Manager' in Android Studio
|
||||
- Add a new device with API level `34` and ABI `arm64-v8a` (even though the app uses 30, the 30 emulator can't find the vulkan adapter, but 34 works fine)
|
||||
- Start up the emulator
|
||||
|
||||
while the emulator is running, run:
|
||||
|
||||
```bash
|
||||
cargo apk run --release
|
||||
```
|
||||
|
||||
The app should appear on the emulator
|
||||
|
||||
[direnv]: https://direnv.net/
|
||||
|
||||
## Previews
|
||||
|
||||
You can preview individual widgets and views by running the preview script:
|
||||
|
||||
```bash
|
||||
./preview RelayView
|
||||
./preview ProfilePreview
|
||||
# ... etc
|
||||
```
|
||||
|
||||
When adding new previews you need to implement the Preview trait for your
|
||||
view/widget and then add it to the `src/ui_preview/main.rs` bin:
|
||||
|
||||
```rust
|
||||
previews!(runner, name,
|
||||
RelayView,
|
||||
AccountLoginView,
|
||||
ProfilePreview,
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Configure the developer environment:
|
||||
|
||||
```bash
|
||||
./scripts/dev_setup.sh
|
||||
```
|
||||
|
||||
This adds pre-commit hooks for proper code formatting.
|
||||
This will add the pre-commit hook to your local repository to suggest proper formatting before commits.
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
Detailed developer documentation is available in each crate:
|
||||
|
||||
- [Notedeck Core](./crates/notedeck/DEVELOPER.md)
|
||||
- [Notedeck Chrome](./crates/notedeck_chrome/DEVELOPER.md)
|
||||
- [Notedeck Columns](./crates/notedeck_columns/DEVELOPER.md)
|
||||
- [Dave AI Assistant](./crates/notedeck_dave/docs/README.md)
|
||||
- [UI Components](./crates/notedeck_ui/docs/components.md)
|
||||
|
||||
## 🔄 Release Status
|
||||
|
||||
Notedeck is currently in **BETA** status. For the latest changes, see the [CHANGELOG](./CHANGELOG.md).
|
||||
|
||||
## Future
|
||||
|
||||
Notedeck allows for app development built on top of the performant, built specifically for nostr database [nostrdb][nostrdb]. An example app written on notedeck is [Dave](./crates/notedeck_dave)
|
||||
|
||||
Building on notedeck dev documentation is also on the roadmap.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Developers
|
||||
|
||||
Contributions are welcome! Please check the developer documentation and follow these guidelines:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
### Translators
|
||||
|
||||
Help us bring Notedeck to non-English speakers!
|
||||
|
||||
Request to join the Notedeck translations team through [Crowdin](https://crowdin.com/project/notedeck).
|
||||
|
||||
If you do not have a Crowdin account, sign up for one.
|
||||
If you do not see your language, please request it in Crowdin.
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
For security issues, please refer to our [Security Policy](./SECURITY.md).
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the GPL - see license information in individual crates.
|
||||
|
||||
## 👥 Authors
|
||||
|
||||
- William Casarin <jb55@jb55.com>
|
||||
- kernelkind <kernelkind@gmail.com>
|
||||
- And [contributors](https://github.com/damus-io/notedeck/graphs/contributors)
|
||||
|
||||
|
||||
[nostrdb]: https://github.com/damus-io/nostrdb
|
||||
[rustup]: https://rustup.rs/
|
||||
|
||||
11
android
@@ -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
|
||||
@@ -1,186 +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="svg5"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
sodipodi:docname="damus-bg.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="407.8014"
|
||||
inkscape:cy="491.88416"
|
||||
inkscape:window-width="1296"
|
||||
inkscape:window-height="916"
|
||||
inkscape:window-x="222"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg5"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
id="g407"
|
||||
inkscape:label="Logo"
|
||||
style="display:none">
|
||||
<g
|
||||
id="layer2"
|
||||
inkscape:label="LogoStroke"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.4 KiB |
@@ -1,186 +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="svg5"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
sodipodi:docname="damusfg.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="407.8014"
|
||||
inkscape:cy="491.88416"
|
||||
inkscape:window-width="1296"
|
||||
inkscape:window-height="916"
|
||||
inkscape:window-x="222"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg5"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true"
|
||||
style="display:none">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
id="g407"
|
||||
inkscape:label="Logo"
|
||||
transform="matrix(0.61641471,0,0,0.61641471,51.853453,49.401806)">
|
||||
<g
|
||||
id="layer2"
|
||||
inkscape:label="LogoStroke"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="16" fill="#2C2C2C"/>
|
||||
<g clip-path="url(#clip0_3568_3937)">
|
||||
<path opacity="0.12" d="M15.9998 21.3334C18.9454 21.3334 21.3332 18.9456 21.3332 16.0001C21.3332 13.0546 18.9454 10.6667 15.9998 10.6667C13.0543 10.6667 10.6665 13.0546 10.6665 16.0001C10.6665 18.9456 13.0543 21.3334 15.9998 21.3334Z" fill="white"/>
|
||||
<path d="M21.3335 15.9999C21.3335 18.9455 18.9457 21.3333 16.0002 21.3333M21.3335 15.9999C21.3335 13.0544 18.9457 10.6666 16.0002 10.6666M21.3335 15.9999H10.6668M16.0002 21.3333C13.0546 21.3333 10.6668 18.9455 10.6668 15.9999M16.0002 21.3333C17.3342 19.8728 18.0927 17.9775 18.1339 15.9999C18.0927 14.0223 17.3342 12.127 16.0002 10.6666M16.0002 21.3333C14.6661 19.8728 13.9084 17.9775 13.8672 15.9999C13.9084 14.0223 14.6661 12.127 16.0002 10.6666M16.0002 10.6666C13.0546 10.6666 10.6668 13.0544 10.6668 15.9999M12.0002 21.3333C12.0002 22.0697 11.4032 22.6666 10.6668 22.6666C9.93045 22.6666 9.3335 22.0697 9.3335 21.3333C9.3335 20.5969 9.93045 19.9999 10.6668 19.9999C11.4032 19.9999 12.0002 20.5969 12.0002 21.3333ZM22.6668 21.3333C22.6668 22.0697 22.0699 22.6666 21.3335 22.6666C20.5971 22.6666 20.0002 22.0697 20.0002 21.3333C20.0002 20.5969 20.5971 19.9999 21.3335 19.9999C22.0699 19.9999 22.6668 20.5969 22.6668 21.3333ZM12.0002 10.6666C12.0002 11.403 11.4032 11.9999 10.6668 11.9999C9.93045 11.9999 9.3335 11.403 9.3335 10.6666C9.3335 9.93021 9.93045 9.33325 10.6668 9.33325C11.4032 9.33325 12.0002 9.93021 12.0002 10.6666ZM22.6668 10.6666C22.6668 11.403 22.0699 11.9999 21.3335 11.9999C20.5971 11.9999 20.0002 11.403 20.0002 10.6666C20.0002 9.93021 20.5971 9.33325 21.3335 9.33325C22.0699 9.33325 22.6668 9.93021 22.6668 10.6666Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3568_3937">
|
||||
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -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 |
@@ -1,9 +0,0 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M68 144.8C68 117.917 68 104.476 73.2317 94.2085C77.8336 85.1767 85.1767 77.8336 94.2085 73.2317C104.476 68 117.917 68 144.8 68H367.2C394.083 68 407.524 68 417.792 73.2317C426.823 77.8336 434.166 85.1767 438.768 94.2085C444 104.476 444 117.917 444 144.8V367.2C444 394.083 444 407.524 438.768 417.792C434.166 426.823 426.823 434.166 417.792 438.768C407.524 444 394.083 444 367.2 444H144.8C117.917 444 104.476 444 94.2085 438.768C85.1767 434.166 77.8336 426.823 73.2317 417.792C68 407.524 68 394.083 68 367.2V144.8ZM88 139.2C88 121.278 88 112.317 91.4878 105.472C94.5557 99.4511 99.4511 94.5557 105.472 91.4878C112.317 88 121.278 88 139.2 88H188C199.201 88 204.802 88 209.08 90.1799C212.843 92.0973 215.903 95.1569 217.82 98.9202C220 103.198 220 108.799 220 120V392C220 403.201 220 408.802 217.82 413.08C215.903 416.843 212.843 419.903 209.08 421.82C204.802 424 199.201 424 188 424H139.2C121.278 424 112.317 424 105.472 420.512C99.4511 417.444 94.5557 412.549 91.4878 406.528C88 399.683 88 390.722 88 372.8V139.2ZM242.18 98.9202C240 103.198 240 108.799 240 120V392C240 403.201 240 408.802 242.18 413.08C244.097 416.843 247.157 419.903 250.92 421.82C255.198 424 260.799 424 272 424H295C306.201 424 311.802 424 316.08 421.82C319.843 419.903 322.903 416.843 324.82 413.08C327 408.802 327 403.201 327 392V120C327 108.799 327 103.198 324.82 98.9202C322.903 95.1569 319.843 92.0973 316.08 90.1799C311.802 88 306.201 88 295 88H272C260.799 88 255.198 88 250.92 90.1799C247.157 92.0973 244.097 95.1569 242.18 98.9202Z" fill="url(#paint0_linear_19_1273)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_19_1273" x1="444" y1="444" x2="-5.21356" y2="206.447" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DACAA0"/>
|
||||
<stop offset="1" stop-color="#8C93D7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 719 B |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.32844 1.4159C8.36511 1.12223 8.20384 0.839518 7.93237 0.721671C7.66091 0.603828 7.34424 0.679064 7.15478 0.906418L1.20178 8.04995C1.0989 8.17335 0.994695 8.29835 0.918828 8.40822C0.847082 8.51208 0.716075 8.71635 0.712028 8.98475C0.707388 9.29208 0.844335 9.58449 1.08338 9.77762C1.2922 9.94635 1.53297 9.97649 1.65871 9.98788C1.79166 9.99995 1.95438 9.99988 2.11504 9.99988H6.24504L5.67204 14.5838C5.63533 14.8775 5.79664 15.1602 6.06811 15.2781C6.33958 15.3959 6.65624 15.3207 6.84571 15.0933L12.7987 7.94975C12.9016 7.82635 13.0058 7.70135 13.0816 7.59149C13.1534 7.48762 13.2844 7.28335 13.2884 7.01495C13.293 6.70762 13.1561 6.41525 12.9171 6.22207C12.7082 6.05333 12.4675 6.02321 12.3418 6.01183C12.2088 5.99979 12.046 5.99982 11.8854 5.99985L7.75544 5.99986L8.32844 1.41588V1.4159Z" fill="#FFB757"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 922 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.12" d="M6 14V8H10V14" fill="white"/>
|
||||
<path d="M5.99992 14V9.06667C5.99992 8.69327 5.99992 8.5066 6.07259 8.364C6.1365 8.23853 6.23849 8.1366 6.36392 8.07267C6.50654 8 6.69325 8 7.06659 8H8.93325C9.30665 8 9.49332 8 9.63592 8.07267C9.76139 8.1366 9.86332 8.23853 9.92725 8.364C9.99992 8.5066 9.99992 8.69327 9.99992 9.06667V14M1.33325 6.33333L7.35992 1.81333C7.58945 1.64121 7.70419 1.55514 7.83019 1.52196C7.94145 1.49268 8.05838 1.49268 8.16965 1.52197C8.29565 1.55514 8.41038 1.64121 8.63992 1.81333L14.6666 6.33333M2.66659 5.33333V11.8667C2.66659 12.6134 2.66659 12.9868 2.81191 13.272C2.93975 13.5229 3.14372 13.7269 3.3946 13.8547C3.67982 14 4.05318 14 4.79992 14H11.1999C11.9467 14 12.3201 14 12.6053 13.8547C12.8561 13.7269 13.0601 13.5229 13.1879 13.272C13.3333 12.9868 13.3333 12.6134 13.3333 11.8667V5.33333L9.27992 2.29333C8.82092 1.94907 8.59138 1.77695 8.33932 1.71059C8.11685 1.65203 7.88298 1.65203 7.66052 1.71059C7.40845 1.77695 7.17892 1.94907 6.71992 2.29333L2.66659 5.33333Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#8a8a8a" xmlns="http://www.w3.org/2000/svg" class="icon-xl-heavy"><path d="M15.6729 3.91287C16.8918 2.69392 18.8682 2.69392 20.0871 3.91287C21.3061 5.13182 21.3061 7.10813 20.0871 8.32708L14.1499 14.2643C13.3849 15.0293 12.3925 15.5255 11.3215 15.6785L9.14142 15.9899C8.82983 16.0344 8.51546 15.9297 8.29289 15.7071C8.07033 15.4845 7.96554 15.1701 8.01005 14.8586L8.32149 12.6785C8.47449 11.6075 8.97072 10.615 9.7357 9.85006L15.6729 3.91287ZM18.6729 5.32708C18.235 4.88918 17.525 4.88918 17.0871 5.32708L11.1499 11.2643C10.6909 11.7233 10.3932 12.3187 10.3014 12.9613L10.1785 13.8215L11.0386 13.6986C11.6812 13.6068 12.2767 13.3091 12.7357 12.8501L18.6729 6.91287C19.1108 6.47497 19.1108 5.76499 18.6729 5.32708ZM11 3.99929C11.0004 4.55157 10.5531 4.99963 10.0008 5.00007C9.00227 5.00084 8.29769 5.00827 7.74651 5.06064C7.20685 5.11191 6.88488 5.20117 6.63803 5.32695C6.07354 5.61457 5.6146 6.07351 5.32698 6.63799C5.19279 6.90135 5.10062 7.24904 5.05118 7.8542C5.00078 8.47105 5 9.26336 5 10.4V13.6C5 14.7366 5.00078 15.5289 5.05118 16.1457C5.10062 16.7509 5.19279 17.0986 5.32698 17.3619C5.6146 17.9264 6.07354 18.3854 6.63803 18.673C6.90138 18.8072 7.24907 18.8993 7.85424 18.9488C8.47108 18.9992 9.26339 19 10.4 19H13.6C14.7366 19 15.5289 18.9992 16.1458 18.9488C16.7509 18.8993 17.0986 18.8072 17.362 18.673C17.9265 18.3854 18.3854 17.9264 18.673 17.3619C18.7988 17.1151 18.8881 16.7931 18.9393 16.2535C18.9917 15.7023 18.9991 14.9977 18.9999 13.9992C19.0003 13.4469 19.4484 12.9995 20.0007 13C20.553 13.0004 21.0003 13.4485 20.9999 14.0007C20.9991 14.9789 20.9932 15.7808 20.9304 16.4426C20.8664 17.116 20.7385 17.7136 20.455 18.2699C19.9757 19.2107 19.2108 19.9756 18.27 20.455C17.6777 20.7568 17.0375 20.8826 16.3086 20.9421C15.6008 21 14.7266 21 13.6428 21H10.3572C9.27339 21 8.39925 21 7.69138 20.9421C6.96253 20.8826 6.32234 20.7568 5.73005 20.455C4.78924 19.9756 4.02433 19.2107 3.54497 18.2699C3.24318 17.6776 3.11737 17.0374 3.05782 16.3086C2.99998 15.6007 2.99999 14.7266 3 13.6428V10.3572C2.99999 9.27337 2.99998 8.39922 3.05782 7.69134C3.11737 6.96249 3.24318 6.3223 3.54497 5.73001C4.02433 4.7892 4.78924 4.0243 5.73005 3.54493C6.28633 3.26149 6.88399 3.13358 7.55735 3.06961C8.21919 3.00673 9.02103 3.00083 9.99922 3.00007C10.5515 2.99964 10.9996 3.447 11 3.99929Z" fill="#8a8a8a"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.12" d="M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" fill="white"/>
|
||||
<path d="M9.33342 14H6.66675M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11_352)">
|
||||
<path opacity="1.0" d="M8.66663 2L9.82276 5.00591C10.0108 5.49473 10.1048 5.73914 10.251 5.94473C10.3805 6.12693 10.5397 6.28613 10.7219 6.41569C10.9275 6.56187 11.1719 6.65587 11.6607 6.84387L14.6666 8L11.6607 9.15613C11.1719 9.34413 10.9275 9.43813 10.7219 9.58433C10.5397 9.71387 10.3805 9.87307 10.251 10.0553C10.1048 10.2609 10.0108 10.5053 9.82276 10.9941L8.66663 14L7.51049 10.9941C7.32249 10.5053 7.22849 10.2609 7.08229 10.0553C6.95276 9.87307 6.79356 9.71387 6.61135 9.58433C6.40577 9.43813 6.16135 9.34413 5.67253 9.15613L2.66663 8L5.67253 6.84387C6.16135 6.65587 6.40577 6.56187 6.61135 6.41569C6.79356 6.28613 6.95276 6.12693 7.08229 5.94473C7.22849 5.73914 7.32249 5.49473 7.51049 5.00591L8.66663 2Z" fill="white"/>
|
||||
<path d="M3.00004 14.6667V11.3334M3.00004 4.66671V1.33337M1.33337 3.00004H4.66671M1.33337 13H4.66671M8.66671 2.00004L7.51057 5.00595C7.32257 5.49477 7.22857 5.73918 7.08237 5.94477C6.95284 6.12697 6.79364 6.28617 6.61143 6.41573C6.40585 6.56191 6.16143 6.65591 5.67261 6.84391L2.66671 8.00004L5.67261 9.15617C6.16143 9.34417 6.40585 9.43817 6.61143 9.58437C6.79364 9.71391 6.95284 9.87311 7.08237 10.0553C7.22857 10.2609 7.32257 10.5053 7.51057 10.9941L8.66671 14L9.82284 10.9941C10.0108 10.5053 10.1048 10.2609 10.251 10.0553C10.3806 9.87311 10.5398 9.71391 10.722 9.58437C10.9276 9.43817 11.172 9.34417 11.6608 9.15617L14.6667 8.00004L11.6608 6.84391C11.172 6.65591 10.9276 6.56191 10.722 6.41573C10.5398 6.28617 10.3806 6.12697 10.251 5.94477C10.1048 5.73918 10.0108 5.49477 9.82284 5.00595L8.66671 2.00004Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_11_352">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.12" d="M11.8667 14C12.6134 14 12.9868 14 13.272 13.8547C13.5229 13.7269 13.7269 13.5229 13.8547 13.272C14 12.9868 14 12.6134 14 11.8667V7.46671C14 6.71997 14 6.3466 13.8547 6.06139C13.7269 5.81051 13.5229 5.60653 13.272 5.4787C12.9868 5.33337 12.6134 5.33337 11.8667 5.33337H4.13333C3.3866 5.33337 3.01323 5.33337 2.72801 5.4787C2.47713 5.60653 2.27315 5.8105 2.14533 6.06139C2 6.3466 2 6.71997 2 7.46671V11.8667C2 12.6134 2 12.9868 2.14533 13.272C2.27315 13.5229 2.47713 13.7269 2.72801 13.8547C3.01323 14 3.38659 14 4.13333 14H11.8667Z" fill="white"/>
|
||||
<path d="M10.6667 5.33322V3.00032C10.6667 2.44583 10.6667 2.16858 10.5499 1.9982C10.4478 1.84934 10.2897 1.74821 10.1119 1.71794C9.9082 1.68329 9.65647 1.79947 9.153 2.03183L3.23934 4.76121C2.79034 4.96845 2.56583 5.07207 2.40141 5.23277C2.25604 5.37483 2.14508 5.54825 2.077 5.73977C2 5.95641 2 6.20367 2 6.6982V9.99987M11 9.66653H11.0067M2 7.46653V11.8665C2 12.6133 2 12.9867 2.14533 13.2719C2.27315 13.5227 2.47713 13.7267 2.72801 13.8545C3.01323 13.9999 3.38659 13.9999 4.13333 13.9999H11.8667C12.6134 13.9999 12.9868 13.9999 13.272 13.8545C13.5229 13.7267 13.7269 13.5227 13.8547 13.2719C14 12.9867 14 12.6133 14 11.8665V7.46653C14 6.7198 14 6.34645 13.8547 6.06123C13.7269 5.81035 13.5229 5.60637 13.272 5.47855C12.9868 5.33322 12.6134 5.33322 11.8667 5.33322H4.13333C3.3866 5.33322 3.01323 5.33322 2.72801 5.47854C2.47713 5.60637 2.27315 5.81035 2.14533 6.06123C2 6.34645 2 6.7198 2 7.46653ZM11.3333 9.66653C11.3333 9.85067 11.1841 9.99987 11 9.99987C10.8159 9.99987 10.6667 9.85067 10.6667 9.66653C10.6667 9.48247 10.8159 9.3332 11 9.3332C11.1841 9.3332 11.3333 9.48247 11.3333 9.66653Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 866 B |
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
MIPMAP="../crates/notedeck_chrome/android/app/src/main/res/mipmap-"
|
||||
|
||||
function mkicon() {
|
||||
local name="$1"
|
||||
echo "making icon $name"
|
||||
mkdir -p "${MIPMAP}/{l,m,h,xh,xxh,xxxh}dpi"
|
||||
inkscape "$name".svg -w 36 -h 36 -o ${MIPMAP}ldpi/"$name".png &
|
||||
inkscape "$name".svg -w 48 -h 48 -o ${MIPMAP}mdpi/"$name".png &
|
||||
inkscape "$name".svg -w 72 -h 72 -o ${MIPMAP}hdpi/"$name".png &
|
||||
inkscape "$name".svg -w 96 -h 96 -o ${MIPMAP}xhdpi/"$name".png &
|
||||
inkscape "$name".svg -w 144 -h 144 -o ${MIPMAP}xxhdpi/"$name".png &
|
||||
inkscape "$name".svg -w 192 -h 192 -o ${MIPMAP}xxxhdpi/"$name".png &
|
||||
wait
|
||||
}
|
||||
|
||||
mkicon "damusfg"
|
||||
mkicon "damusbg"
|
||||
@@ -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 = Über mich
|
||||
# Column title for account management
|
||||
Accounts_f018 = Konten
|
||||
# Button label to add a relay
|
||||
Add_269d = Hinzufügen
|
||||
# Label for add column button
|
||||
Add_47df = Hinzufügen
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Eine andere Wallet hinzufügen, die nur für dieses Konto verwendet wird
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = Wallet hinzufügen um fortzufahren
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = Konto hinzufügen
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Konto hinzufügen
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Algorithmus-Spalte hinzufügen
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Spalte hinzufügen
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Deck hinzufügen
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = Externe Benachrichtigungsspalte hinzufügen
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = Hashtag-Spalte hinzufügen
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Letzte Notizen-Spalte hinzufügen
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Benachrichtigungs-Spalte hinzufügen
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = Relay hinzufügen
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = Wallet hinzufügen
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = Algorithmus
|
||||
# Description for algorithmic feeds column
|
||||
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
|
||||
Ask_dave_anything_33d1 = Frage Dave etwas...
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = Banner
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Senden
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = Lokal senden
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Abbrechen
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = Abbrechen
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = Zwischenspeicher leeren
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
||||
# Column title for note composition
|
||||
Compose_Note_c094 = Notiz erstellen
|
||||
# Label for configure relays, settings section
|
||||
Configure_relays_d156 = Relays konfigurieren
|
||||
# Label for confirm clear cache, Storage settings section
|
||||
Confirm_9d9d = Bestätigen
|
||||
# Button label to confirm an action
|
||||
Confirm_f8a6 = Bestätigen
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = Verbunden
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = Verbinde...
|
||||
# Title for contact list column
|
||||
Contact_List_f85a = Kontaktliste
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = Kontakte
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = Kontakte (letzte Notizen)
|
||||
# Button label to copy logs
|
||||
Copy_a688 = Kopieren
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = Link kopieren
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = Notiz-ID kopieren
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Notiz-JSON kopieren
|
||||
# Copy the author's public key to clipboard
|
||||
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
|
||||
# Relative time in hours
|
||||
count_h_3ecb = { $count }h
|
||||
# Relative time in minutes
|
||||
count_m_b41e = { $count }min
|
||||
# Relative time in months
|
||||
count_mo_7aba = { $count }M
|
||||
# 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 }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
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
||||
# Column title for support page
|
||||
Damus_Support_27c0 = Damus Support
|
||||
# Label for Theme Dark, Appearance settings section
|
||||
Dark_85fe = Dunkel
|
||||
# Label for deck name input field
|
||||
Deck_name_cd32 = Deck-Name
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = DECKS
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Standardbetrag pro Zap:
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Standard-Deck
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = Deck löschen
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = Diese Spalte löschen
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = Wallet löschen
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = Anzeigename
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" wird zur Identifikation verwendet
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Deck bearbeiten
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Deck bearbeiten
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = Profil bearbeiten
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = Profil bearbeiten
|
||||
# Placeholder for hashtag input field
|
||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Gewünschte Hashtags hier eingeben (für mehrere, durch Leerzeichen trennen)
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = Relay hier eingeben
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Hier den Benutzerschlüssel (npub, hex, nip05) eingeben...
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = Gib deinen Schlüssel ein
|
||||
# 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 =
|
||||
Gib deinen öffentlichen Schlüssel (npub), eine Nostr-Adresse (z. B. {$address}) oder deinen privaten Schlüssel (nsec) ein.
|
||||
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Profil finden
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Schriftgröße:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
Home_8c19 = Startseite
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Symbol
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Bildcache Größe:
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individuell
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = Ungültiger Betrag
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = Ungültiger Schlüssel
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = Ungültige 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 = Behalte den Überblick über deine Notizen & Antworten
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Sprache:
|
||||
# Title for last note per user column
|
||||
Last_Note_per_User_17ad = Letzte Notiz pro Profil
|
||||
# Label for Theme Light, Appearance settings section
|
||||
Light_7475 = Hell
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
||||
# Login page title
|
||||
Login_9eef = Anmelden
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = Jetzt anmelden — auf geht's!
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = Medien von einem Profil, dem du nicht folgst
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = Verschiebt diese Spalte an eine andere Position
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Mein Deck
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = Neu bei Nostr?
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Nostr-Adresse (NIP-05-Identität)
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = Nostrich
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = Nicht verbunden
|
||||
# Link text for note references
|
||||
note_cad6 = Notiz
|
||||
# Beta product warning message
|
||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck ist ein Beta-Produkt. Erwarte Fehler und kontaktiere uns, wenn Probleme oder Fehler auftreten.
|
||||
# Filter label for notes only view
|
||||
Notes_03fb = Notizen
|
||||
# Label for notes-only filter
|
||||
Notes_60d2 = Notizen
|
||||
# Filter label for notes and replies view
|
||||
Notes___Replies_1ec2 = Notizen & Antworten
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notizen & Antworten
|
||||
# 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
|
||||
# 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
|
||||
Please_create_a_name_for_the_deck_38e7 = Bitte erstelle einen Namen für das Deck.
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Bitte erstelle einen Namen für das Deck und wähle ein Symbol aus.
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = Bitte wählen ein Symbol aus.
|
||||
# Button label to post a note
|
||||
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.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Profilbild
|
||||
# Column title for quote composition
|
||||
Quote_475c = 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
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relays
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relays
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Antwort
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = Auf diese Notiz antworten
|
||||
# Error message when reply note cannot be found
|
||||
Reply_to_unknown_note_4401 = Antwort auf unbekannte Notiz
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = Antwort an { $user }
|
||||
# Template for replying to user in unknown thread
|
||||
replying_to__user__in_someone_s_thread_e148 = Antwort an { $user } im Beitrag von jemandem
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Antwort auf { $user }'s { $note } in { $thread_user }'s { $thread }
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = Antwort auf { $user }'s { $note }
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = Antwort auf { $user }'s { $thread }
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = Antwort auf eine Notiz
|
||||
# Hover text for repost button
|
||||
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
|
||||
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 = Speichern
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = Änderungen speichern
|
||||
# Column title for search page
|
||||
Search_c573 = Suche
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = Notizen suchen...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Suche nach '{ $query }'
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
|
||||
# Button 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
|
||||
Sign_out_337b = Abmelden
|
||||
# Title for someone else's notes column
|
||||
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
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mit einem bestimmten Hashtag auf dem Laufenden bleiben
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Bleibe auf dem Laufenden mit Benachrichtigungen und Erwähnungen
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Bleib auf dem Laufenden bei den Notizen & Antworten anderer
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Bleib bei den Benachrichtigungen und Erwähnungen anderer auf dem Laufenden
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Bleib bei den Notizen & Antworten eines anderen auf dem Laufenden
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Benachrichtigungen und Erwähnungen auf dem Laufenden
|
||||
# Step 1 label in support instructions
|
||||
Step_1_8656 = Schritt 1
|
||||
# Step 2 label in support instructions
|
||||
Step_2_d08d = Schritt 2
|
||||
# Label for storage settings section
|
||||
Storage_ed65 = Speicher
|
||||
# Column title for subscribing to external user
|
||||
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
|
||||
# Support email address
|
||||
Support_email_44d9 = E-Mail Support:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = Zum Hellmodus wechseln
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = Zum Laden antippen
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Design:
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Unterhaltung
|
||||
# Link text for thread references
|
||||
thread_ad1f = Unterhaltung
|
||||
# Title for universe column
|
||||
Universe_e01e = Weltraum
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = Weltraum
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das aktuelle Konto verwenden
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
|
||||
# Profile username field label
|
||||
Username_daa7 = Benutzername
|
||||
# Label for view folder button, Storage settings section
|
||||
View_folder_9742 = Ordner anzeigen
|
||||
# Column title for wallet management
|
||||
Wallet_5e50 = Wallet
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Wir empfehlen kurze Namen
|
||||
# Profile website field label
|
||||
Website_7980 = Website
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = Schreib hier eine richtig coole Notiz...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = Dein Schlüssel hier...
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = Deine Notizen
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = Deine Benachrichtigungen
|
||||
# Heading for zap (tip) action
|
||||
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
|
||||
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[one] { $count } Ergebnis für '{ $query } gefunden'
|
||||
*[other] { $count } Ergebnisse für '{ $query } gefunden'
|
||||
}
|
||||
@@ -1,605 +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 = About
|
||||
|
||||
# Column title for account management
|
||||
Accounts_f018 = Accounts
|
||||
|
||||
# Button label to add a relay
|
||||
Add_269d = Add
|
||||
|
||||
# Label for add column button
|
||||
Add_47df = Add
|
||||
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Add a different wallet that will only be used for this account
|
||||
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = Add a wallet to continue
|
||||
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = Add account
|
||||
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Add Account
|
||||
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Add Algo Column
|
||||
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Add Column
|
||||
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Add Deck
|
||||
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = Add External Notifications Column
|
||||
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = Add Hashtag Column
|
||||
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Add Last Notes Column
|
||||
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Add Notifications Column
|
||||
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = Add relay
|
||||
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = Add Wallet
|
||||
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = Algo
|
||||
|
||||
# Description for algorithmic feeds column
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in note discovery
|
||||
|
||||
# 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
|
||||
|
||||
# Placeholder text for Dave AI input field
|
||||
Ask_dave_anything_33d1 = Ask dave anything...
|
||||
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = Banner
|
||||
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Broadcast
|
||||
|
||||
# Broadcast the note only to local network relays
|
||||
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
|
||||
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = Connected
|
||||
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = Connecting...
|
||||
|
||||
# Title for contact list column
|
||||
Contact_List_f85a = Contact List
|
||||
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = Contacts
|
||||
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = Contacts (last notes)
|
||||
|
||||
# Button label to copy logs
|
||||
Copy_a688 = Copy
|
||||
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = Copy Link
|
||||
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = Copy Note ID
|
||||
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copy Note JSON
|
||||
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copy Pubkey
|
||||
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = Copy Text
|
||||
|
||||
# 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 = Create Account
|
||||
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = Create Deck
|
||||
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Custom
|
||||
|
||||
# Column title for zap amount customization
|
||||
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
|
||||
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = DECKS
|
||||
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Default amount per zap:
|
||||
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Default Deck
|
||||
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = Delete Deck
|
||||
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = Delete this column
|
||||
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = Delete Wallet
|
||||
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = Display name
|
||||
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
|
||||
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Edit Deck
|
||||
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = Edit Deck
|
||||
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = Edit Profile
|
||||
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = Edit Profile
|
||||
|
||||
# Placeholder for hashtag input field
|
||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Enter the desired hashtags here (for multiple space-separated)
|
||||
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = Enter the relay here
|
||||
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Enter the user's key (npub, hex, nip05) here...
|
||||
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = Enter your key
|
||||
|
||||
# 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 = 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.
|
||||
|
||||
# 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
|
||||
|
||||
# Title for Home column
|
||||
Home_8c19 = Home
|
||||
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = Icon
|
||||
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = Image cache size:
|
||||
|
||||
# Title for individual user column
|
||||
Individual_b776 = Individual
|
||||
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = Invalid amount
|
||||
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = Invalid key.
|
||||
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = Invalid 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 = 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)
|
||||
|
||||
# Login page title
|
||||
Login_9eef = Login
|
||||
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = Login now — let's do this!
|
||||
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = Media from someone you don't follow
|
||||
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = Moves this column to another position
|
||||
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = My Deck
|
||||
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = New to Nostr?
|
||||
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Nostr address (NIP-05 identity)
|
||||
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = nostrich
|
||||
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = Not Connected
|
||||
|
||||
# Link text for note references
|
||||
note_cad6 = note
|
||||
|
||||
# Beta product warning message
|
||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck is a beta product. Expect bugs and contact us when you run into issues.
|
||||
|
||||
# Filter label for notes only view
|
||||
Notes_03fb = Notes
|
||||
|
||||
# Label for notes-only filter
|
||||
Notes_60d2 = Notes
|
||||
|
||||
# Filter label for notes and replies view
|
||||
Notes___Replies_1ec2 = Notes & Replies
|
||||
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notes & Replies
|
||||
|
||||
# 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 = 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...
|
||||
|
||||
# Error message for missing deck name
|
||||
Please_create_a_name_for_the_deck_38e7 = Please create a name for the deck.
|
||||
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Please create a name for the deck and select an icon.
|
||||
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = Please select an icon.
|
||||
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = Post now
|
||||
|
||||
# Instruction for copying logs
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.
|
||||
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Profile picture
|
||||
|
||||
# Column title for quote composition
|
||||
Quote_475c = Quote
|
||||
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = Quote of unknown note
|
||||
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = Read only
|
||||
|
||||
# Column title for relay management
|
||||
Relays_9d89 = Relays
|
||||
|
||||
# Label for relay list section
|
||||
Relays_ad5e = Relays
|
||||
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = Reply
|
||||
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = Reply to this note
|
||||
|
||||
# Error message when reply note cannot be found
|
||||
Reply_to_unknown_note_4401 = Reply to unknown note
|
||||
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = replying to {$user}
|
||||
|
||||
# Template for replying to user in unknown thread
|
||||
replying_to__user__in_someone_s_thread_e148 = replying to {$user} in someone's thread
|
||||
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = replying to {$user}'s {$note} in {$thread_user}'s {$thread}
|
||||
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = replying to {$user}'s {$note}
|
||||
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = replying to {$user}'s {$thread}
|
||||
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = replying to a note
|
||||
|
||||
# Hover text for repost button
|
||||
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?
|
||||
|
||||
# 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 = Save
|
||||
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = Save changes
|
||||
|
||||
# Column title for search page
|
||||
Search_c573 = Search
|
||||
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = Search notes...
|
||||
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Searching for '{$query}'
|
||||
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = See notes from your contacts
|
||||
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = See the whole nostr universe
|
||||
|
||||
# Button 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
|
||||
|
||||
# Button label to sign out of account
|
||||
Sign_out_337b = Sign out
|
||||
|
||||
# Title for someone else's notes column
|
||||
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
|
||||
|
||||
# Description for hashtags column
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Stay up to date with a certain hashtag
|
||||
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Stay up to date with notifications and mentions
|
||||
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Stay up to date with someone else's notes & replies
|
||||
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Stay up to date with someone else's notifications and mentions
|
||||
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Stay up to date with someone's notes & replies
|
||||
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Stay up to date with your notifications and mentions
|
||||
|
||||
# Step 1 label in support instructions
|
||||
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
|
||||
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = Switch to light mode
|
||||
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = Tap to Load
|
||||
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!
|
||||
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = Theme:
|
||||
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = Thread
|
||||
|
||||
# Link text for thread references
|
||||
thread_ad1f = thread
|
||||
|
||||
# Title for universe column
|
||||
Universe_e01e = Universe
|
||||
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = Universe
|
||||
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = Use this wallet for the current account only
|
||||
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at "{$domain}" will be used for identification
|
||||
|
||||
# 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
|
||||
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = We recommend short names
|
||||
|
||||
# Profile website field label
|
||||
Website_7980 = Website
|
||||
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = Write a banger note here...
|
||||
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = Your key here...
|
||||
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = Your Notes
|
||||
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = Your Notifications
|
||||
|
||||
# Heading for zap (tip) action
|
||||
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
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[one] Got {$count} result for '{$query}'
|
||||
*[other] Got {$count} results for '{$query}'
|
||||
}
|
||||
1
assets/translations/en-US/notedeck.ftl
Normal file
@@ -0,0 +1 @@
|
||||
universe-title = Universe with Fluent Translation
|
||||
@@ -1,605 +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 = {"["}Àbóút{"]"}
|
||||
|
||||
# Column title for account management
|
||||
Accounts_f018 = {"["}Àççóúñts{"]"}
|
||||
|
||||
# Button label to add a relay
|
||||
Add_269d = {"["}Àdd{"]"}
|
||||
|
||||
# Label for add column button
|
||||
Add_47df = {"["}Àdd{"]"}
|
||||
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = {"["}Àdd à dífféréñt wàllét thàt wíll óñly bé úséd fór thís àççóúñt{"]"}
|
||||
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = {"["}Àdd à wàllét tó çóñtíñúé{"]"}
|
||||
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = {"["}Àdd àççóúñt{"]"}
|
||||
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = {"["}Àdd Àççóúñt{"]"}
|
||||
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = {"["}Àdd Àlgó Çólúmñ{"]"}
|
||||
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = {"["}Àdd Çólúmñ{"]"}
|
||||
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = {"["}Àdd Déçk{"]"}
|
||||
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = {"["}Àdd Éxtérñàl Ñótífíçàtíóñs Çólúmñ{"]"}
|
||||
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"}
|
||||
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
|
||||
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
|
||||
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = {"["}Àdd rélày{"]"}
|
||||
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = {"["}Àdd Wàllét{"]"}
|
||||
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = {"["}Àlgó{"]"}
|
||||
|
||||
# Description for algorithmic feeds column
|
||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds tó àíd íñ ñóté dísçóvéry{"]"}
|
||||
|
||||
# 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{"]"}
|
||||
|
||||
# Placeholder text for Dave AI input field
|
||||
Ask_dave_anything_33d1 = {"["}Àsk dàvé àñythíñg...{"]"}
|
||||
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = {"["}Bàññér{"]"}
|
||||
|
||||
# Beta version label
|
||||
BETA_8e5d = {"["}BÉTÀ{"]"}
|
||||
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
|
||||
|
||||
# Broadcast the note only to local network relays
|
||||
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{"]"}
|
||||
|
||||
# Status label for connected relay
|
||||
Connected_f8cc = {"["}Çóññéçtéd{"]"}
|
||||
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = {"["}Çóññéçtíñg...{"]"}
|
||||
|
||||
# Title for contact list column
|
||||
Contact_List_f85a = {"["}Çóñtàçt Líst{"]"}
|
||||
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = {"["}Çóñtàçts{"]"}
|
||||
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = {"["}Çóñtàçts (làst ñótés){"]"}
|
||||
|
||||
# Button label to copy logs
|
||||
Copy_a688 = {"["}Çópy{"]"}
|
||||
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = {"["}Çópy Líñk{"]"}
|
||||
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
|
||||
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
|
||||
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
|
||||
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = {"["}Çópy Téxt{"]"}
|
||||
|
||||
# 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ó{"]"}
|
||||
|
||||
# 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 = {"["}Çréàté Àççóúñt{"]"}
|
||||
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = {"["}Çréàté Déçk{"]"}
|
||||
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = {"["}Çústóm{"]"}
|
||||
|
||||
# Column title for zap amount customization
|
||||
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é{"]"}
|
||||
|
||||
# Label for decks section in side panel
|
||||
DECKS_1fad = {"["}DÉÇKS{"]"}
|
||||
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = {"["}Défàúlt àmóúñt pér zàp:{"]"}
|
||||
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = {"["}Défàúlt Déçk{"]"}
|
||||
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = {"["}Délété Déçk{"]"}
|
||||
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = {"["}Délété thís çólúmñ{"]"}
|
||||
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = {"["}Délété Wàllét{"]"}
|
||||
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = {"["}Dísplày ñàmé{"]"}
|
||||
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
||||
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
|
||||
|
||||
# Button label to edit a deck
|
||||
Edit_Deck_fd93 = {"["}Édít Déçk{"]"}
|
||||
|
||||
# Button label to edit user profile
|
||||
Edit_Profile_49e6 = {"["}Édít Prófílé{"]"}
|
||||
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = {"["}Édít Prófílé{"]"}
|
||||
|
||||
# Placeholder for hashtag input field
|
||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = {"["}Éñtér thé désíréd hàshtàgs héré (fór múltíplé spàçé-sépàràtéd){"]"}
|
||||
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = {"["}Éñtér thé rélày héré{"]"}
|
||||
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = {"["}Éñtér thé úsér's kéy (ñpúb, héx, ñíp05) héré...{"]"}
|
||||
|
||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
||||
Enter_your_key_0fca = {"["}Éñtér yóúr kéy{"]"}
|
||||
|
||||
# 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 = {"["}Éñtér yóúr públíç kéy (ñpúb), ñóstr àddréss (é.g. {$address}), ór prívàté kéy (ñséç). Yóú múst éñtér yóúr prívàté kéy tó bé àblé tó póst, réply, étç.{"]"}
|
||||
|
||||
# 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{"]"}
|
||||
|
||||
# Title for Home column
|
||||
Home_8c19 = {"["}Hómé{"]"}
|
||||
|
||||
# Label for deck icon selection
|
||||
Icon_b0ab = {"["}Íçóñ{"]"}
|
||||
|
||||
# Label for Image cache size, Storage settings section
|
||||
Image_cache_size_3004 = {"["}Ímàgé çàçhé sízé:{"]"}
|
||||
|
||||
# Title for individual user column
|
||||
Individual_b776 = {"["}Íñdívídúàl{"]"}
|
||||
|
||||
# Error message for invalid zap amount
|
||||
Invalid_amount_6630 = {"["}Íñvàlíd àmóúñt{"]"}
|
||||
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = {"["}Íñvàlíd kéy.{"]"}
|
||||
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = {"["}Íñvàlíd ÑWÇ ÚRÍ{"]"}
|
||||
|
||||
# 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 = {"["}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){"]"}
|
||||
|
||||
# Login page title
|
||||
Login_9eef = {"["}Lógíñ{"]"}
|
||||
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = {"["}Lógíñ ñów — lét's dó thís!{"]"}
|
||||
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = {"["}Médíà fróm sóméóñé yóú dóñ't fóllów{"]"}
|
||||
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó àñóthér pósítíóñ{"]"}
|
||||
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = {"["}My Déçk{"]"}
|
||||
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
|
||||
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = {"["}Ñóstr àddréss (ÑÍP-05 ídéñtíty){"]"}
|
||||
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = {"["}ñóstríçh{"]"}
|
||||
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = {"["}Ñót Çóññéçtéd{"]"}
|
||||
|
||||
# Link text for note references
|
||||
note_cad6 = {"["}ñóté{"]"}
|
||||
|
||||
# Beta product warning message
|
||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = {"["}Ñótédéçk ís à bétà pródúçt. Éxpéçt búgs àñd çóñtàçt ús whéñ yóú rúñ íñtó íssúés.{"]"}
|
||||
|
||||
# Filter label for notes only view
|
||||
Notes_03fb = {"["}Ñótés{"]"}
|
||||
|
||||
# Label for notes-only filter
|
||||
Notes_60d2 = {"["}Ñótés{"]"}
|
||||
|
||||
# Filter label for notes and replies view
|
||||
Notes___Replies_1ec2 = {"["}Ñótés & Réplíés{"]"}
|
||||
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = {"["}Ñótés & Réplíés{"]"}
|
||||
|
||||
# Column title for notifications
|
||||
Notifications_d673 = {"["}Ñótífíçàtíóñs{"]"}
|
||||
|
||||
# Title for notifications column
|
||||
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é...{"]"}
|
||||
|
||||
# Error message for missing deck name
|
||||
Please_create_a_name_for_the_deck_38e7 = {"["}Pléàsé çréàté à ñàmé fór thé déçk.{"]"}
|
||||
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = {"["}Pléàsé çréàté à ñàmé fór thé déçk àñd séléçt àñ íçóñ.{"]"}
|
||||
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = {"["}Pléàsé séléçt àñ íçóñ.{"]"}
|
||||
|
||||
# Button label to post a note
|
||||
Post_now_8a49 = {"["}Póst ñów{"]"}
|
||||
|
||||
# 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 = {"["}Préss thé búttóñ bélów tó çópy yóúr móst réçéñt lógs tó yóúr systém's çlípbóàrd. Théñ pàsté ít íñtó yóúr émàíl.{"]"}
|
||||
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
|
||||
|
||||
# Column title for quote composition
|
||||
Quote_475c = {"["}Qúóté{"]"}
|
||||
|
||||
# Error message when quote note cannot be found
|
||||
Quote_of_unknown_note_e4f0 = {"["}Qúóté óf úñkñówñ ñóté{"]"}
|
||||
|
||||
# Label for read-only profile mode
|
||||
Read_only_82ff = {"["}Réàd óñly{"]"}
|
||||
|
||||
# Column title for relay management
|
||||
Relays_9d89 = {"["}Rélàys{"]"}
|
||||
|
||||
# Label for relay list section
|
||||
Relays_ad5e = {"["}Rélàys{"]"}
|
||||
|
||||
# Column title for reply composition
|
||||
Reply_3bf1 = {"["}Réply{"]"}
|
||||
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = {"["}Réply tó thís ñóté{"]"}
|
||||
|
||||
# Error message when reply note cannot be found
|
||||
Reply_to_unknown_note_4401 = {"["}Réply tó úñkñówñ ñóté{"]"}
|
||||
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = {"["}réplyíñg tó {$user}{"]"}
|
||||
|
||||
# Template for replying to user in unknown thread
|
||||
replying_to__user__in_someone_s_thread_e148 = {"["}réplyíñg tó {$user} íñ sóméóñé's thréàd{"]"}
|
||||
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = {"["}réplyíñg tó {$user}'s {$note} íñ {$thread_user}'s {$thread}{"]"}
|
||||
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = {"["}réplyíñg tó {$user}'s {$note}{"]"}
|
||||
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = {"["}réplyíñg tó {$user}'s {$thread}{"]"}
|
||||
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = {"["}réplyíñg tó à ñóté{"]"}
|
||||
|
||||
# Hover text for repost button
|
||||
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?{"]"}
|
||||
|
||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||
SATS_45d7 = {"["}SÀTS{"]"}
|
||||
|
||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
||||
sats_e5ec = {"["}sàts{"]"}
|
||||
|
||||
# Button to save default zap amount
|
||||
Save_6f7c = {"["}Sàvé{"]"}
|
||||
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = {"["}Sàvé çhàñgés{"]"}
|
||||
|
||||
# Column title for search page
|
||||
Search_c573 = {"["}Séàrçh{"]"}
|
||||
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
|
||||
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
|
||||
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
|
||||
|
||||
# Description for universe column
|
||||
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{"]"}
|
||||
|
||||
# Button label to sign out of account
|
||||
Sign_out_337b = {"["}Sígñ óút{"]"}
|
||||
|
||||
# Title for someone else's notes column
|
||||
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{"]"}
|
||||
|
||||
# Description for hashtags column
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = {"["}Stày úp tó dàté wíth à çértàíñ hàshtàg{"]"}
|
||||
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = {"["}Stày úp tó dàté wíth ñótífíçàtíóñs àñd méñtíóñs{"]"}
|
||||
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótés & réplíés{"]"}
|
||||
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótífíçàtíóñs àñd méñtíóñs{"]"}
|
||||
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = {"["}Stày úp tó dàté wíth sóméóñé's ñótés & réplíés{"]"}
|
||||
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = {"["}Stày úp tó dàté wíth yóúr ñótífíçàtíóñs àñd méñtíóñs{"]"}
|
||||
|
||||
# Step 1 label in support instructions
|
||||
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é{"]"}
|
||||
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = {"["}Swítçh tó líght módé{"]"}
|
||||
|
||||
# Button text to load blurred media
|
||||
Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"}
|
||||
|
||||
# Message shown when Dave trial period has ended
|
||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"}
|
||||
|
||||
# Label for theme, Appearance settings section
|
||||
Theme_4aac = {"["}Thémé:{"]"}
|
||||
|
||||
# Column title for note thread view
|
||||
Thread_0f20 = {"["}Thréàd{"]"}
|
||||
|
||||
# Link text for thread references
|
||||
thread_ad1f = {"["}thréàd{"]"}
|
||||
|
||||
# Title for universe column
|
||||
Universe_e01e = {"["}Úñívérsé{"]"}
|
||||
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = {"["}Úñívérsé{"]"}
|
||||
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = {"["}Úsé thís wàllét fór thé çúrréñt àççóúñt óñly{"]"}
|
||||
|
||||
# Username and domain identification message
|
||||
username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username}" àt "{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
||||
|
||||
# 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{"]"}
|
||||
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = {"["}Wé réçómméñd shórt ñàmés{"]"}
|
||||
|
||||
# Profile website field label
|
||||
Website_7980 = {"["}Wébsíté{"]"}
|
||||
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = {"["}Wríté à bàñgér ñóté héré...{"]"}
|
||||
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = {"["}Yóúr kéy héré...{"]"}
|
||||
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = {"["}Yóúr Ñótés{"]"}
|
||||
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = {"["}Yóúr Ñótífíçàtíóñs{"]"}
|
||||
|
||||
# Heading for zap (tip) action
|
||||
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
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
|
||||
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
|
||||
}
|
||||
@@ -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 }'
|
||||
}
|
||||
@@ -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 }'
|
||||
}
|
||||
@@ -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 = A propos
|
||||
# Column title for account management
|
||||
Accounts_f018 = Comptes
|
||||
# Button label to add a relay
|
||||
Add_269d = Ajouter
|
||||
# Label for add column button
|
||||
Add_47df = Ajouter
|
||||
# Button label to add a different wallet
|
||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Ajouter un portefeuille différent qui ne sera utilisé que pour ce compte
|
||||
# Error message for missing wallet
|
||||
Add_a_wallet_to_continue_d170 = Ajouter un portefeuille pour continuer
|
||||
# Button label to add a new account
|
||||
Add_account_1cfc = Ajouter un compte
|
||||
# Column title for adding new account
|
||||
Add_Account_d06c = Ajouter un compte
|
||||
# Column title for adding algorithm column
|
||||
Add_Algo_Column_0d75 = Ajouter une colonne Algo
|
||||
# Column title for adding new column
|
||||
Add_Column_c764 = Ajouter une colonne
|
||||
# Column title for adding new deck
|
||||
Add_Deck_fabf = Ajouter un deck
|
||||
# Column title for adding external notifications column
|
||||
Add_External_Notifications_Column_41ae = Ajouter une colonne pour les notifications externes
|
||||
# Column title for adding hashtag column
|
||||
Add_Hashtag_Column_ebf4 = Ajouter une colonne Hashtag
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Ajouter une colonne pour les dernières notes
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications
|
||||
# Button label to add a relay
|
||||
Add_relay_269d = Ajouter un relai
|
||||
# Button label to add a wallet
|
||||
Add_Wallet_d1be = Ajouter un portefeuille
|
||||
# Title for algorithmic feeds column
|
||||
Algo_2452 = Algo
|
||||
# Description for algorithmic feeds column
|
||||
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
|
||||
Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
|
||||
# Profile banner URL field label
|
||||
Banner_52ef = Bannière
|
||||
# Beta version label
|
||||
BETA_8e5d = BETA
|
||||
# Broadcast the note to all connected relays
|
||||
Broadcast_fe43 = Diffusion
|
||||
# Broadcast the note only to local network relays
|
||||
Broadcast_Local_7e50 = Diffusion locale
|
||||
# Button label to cancel an action
|
||||
Cancel_ed3b = Annuler
|
||||
# Label for cancel clear cache, Storage settings section
|
||||
Cancel_fd8b = Annuler
|
||||
# Label for clear cache button, Storage settings section
|
||||
Clear_cache_dccb = Vider le cache
|
||||
# Hover text for editable zap amount
|
||||
Click_to_edit_0414 = Cliquer pour modifier
|
||||
# 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
|
||||
Connected_f8cc = Connecté
|
||||
# Status label for connecting relay
|
||||
Connecting_6b7e = Connexion...
|
||||
# Title for contact list column
|
||||
Contact_List_f85a = Liste de contacts
|
||||
# Column title for contact lists
|
||||
Contacts_7533 = Contacts
|
||||
# Column title for last notes per contact
|
||||
Contacts__last_notes_3f84 = Contacts (dernières notes)
|
||||
# Button label to copy logs
|
||||
Copy_a688 = Copier
|
||||
# Button to copy media link to clipboard
|
||||
Copy_Link_dc7c = Copier le lien
|
||||
# Copy the unique note identifier to clipboard
|
||||
Copy_Note_ID_6b45 = Copier l'ID de la note
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copier le JSON de la note
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copier la Pubkey
|
||||
# Copy the text content of the note to clipboard
|
||||
Copy_Text_f81c = Copier le texte
|
||||
# Relative time in days
|
||||
count_d_b9be = { $count }j
|
||||
# Relative time in hours
|
||||
count_h_3ecb = { $count }h
|
||||
# Relative time in minutes
|
||||
count_m_b41e = { $count }min
|
||||
# Relative time in months
|
||||
count_mo_7aba = { $count }m
|
||||
# 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 = Créer un compte
|
||||
# Button label to create a new deck
|
||||
Create_Deck_16b7 = Créer un deck
|
||||
# Column title for custom timelines
|
||||
Custom_a69e = Personnaliser
|
||||
# Column title for zap amount customization
|
||||
Customize_Zap_Amount_cfc4 = 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
|
||||
DECKS_1fad = DECKS
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = Montant par défaut pour un Zap :
|
||||
# Name of the default deck feed
|
||||
Default_Deck_fcca = Deck par défaut
|
||||
# Button label to delete a deck
|
||||
Delete_Deck_bb29 = Supprimer le deck
|
||||
# Tooltip for deleting a column
|
||||
Delete_this_column_8d5a = Supprimer cette colonne
|
||||
# Button label to delete a wallet
|
||||
Delete_Wallet_d1d4 = Supprimer le portefeuille
|
||||
# Profile display name field label
|
||||
Display_name_f9d9 = Nom d'utilisateur
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = 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
|
||||
# Column title for profile editing
|
||||
Edit_Profile_8ad4 = Modifier le profil
|
||||
# Placeholder for hashtag input field
|
||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Entrez les hashtags souhaités ici (séparez-les avec un espace)
|
||||
# Placeholder for relay input field
|
||||
Enter_the_relay_here_1c8b = Entrer un relai ici
|
||||
# Hint text to prompt entering the user's public key.
|
||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Entrer ici la clé de l'utilisateur (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 = Entrez votre clé
|
||||
# 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 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Trouver un utilisateur
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Taille du texte :
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# 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
|
||||
Invalid_amount_6630 = Montant invalide
|
||||
# Error message for invalid key input
|
||||
Invalid_key_4726 = Clé non valide.
|
||||
# Error message for invalid Nostr Wallet Connect URI
|
||||
Invalid_NWC_URI_031b = Invalide 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 = 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
|
||||
# Bitcoin Lightning network address field label
|
||||
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
|
||||
# Login page title
|
||||
Login_9eef = Se connecter
|
||||
# Login button text
|
||||
Login_now___let_s_do_this_5630 = Se connecter maintenant - c'est parti !
|
||||
# Text shown on blurred media from unfollowed users
|
||||
Media_from_someone_you_don_t_follow_5611 = Média d'une personne que vous ne suivez pas
|
||||
# Tooltip for moving a column
|
||||
Moves_this_column_to_another_position_0d4b = Déplace cette colonne vers une autre position
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Mon deck
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = Nouveau sur Nostr ?
|
||||
# NIP-05 identity field label
|
||||
Nostr_address__NIP-05_identity_74a2 = Adresse Nostr (NIP-05 identité)
|
||||
# Default username when profile is not available
|
||||
nostrich_df29 = nostrich
|
||||
# Status label for disconnected relay
|
||||
Not_Connected_6292 = Non connecté
|
||||
# Link text for note references
|
||||
note_cad6 = note
|
||||
# Beta product warning message
|
||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck est un produit en phase beta. Attendez-vous à des bugs et contactez-nous si vous rencontrez des problèmes.
|
||||
# Filter label for notes only view
|
||||
Notes_03fb = Notes
|
||||
# Label for notes-only filter
|
||||
Notes_60d2 = Notes
|
||||
# Filter label for notes and replies view
|
||||
Notes___Replies_1ec2 = Notes & Réponses
|
||||
# Label for notes and replies filter
|
||||
Notes___Replies_6e3b = Notes & Réponses
|
||||
# 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
|
||||
Please_create_a_name_for_the_deck_38e7 = Veuillez créer un nom pour le deck.
|
||||
# Error message for missing deck name and icon
|
||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Veuillez créer un nom pour le deck et sélectionner une icône.
|
||||
# Error message for missing deck icon
|
||||
Please_select_an_icon_655b = Veuillez choisir une icône.
|
||||
# Button label to post a note
|
||||
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.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Photo de profil
|
||||
# Column title for quote composition
|
||||
Quote_475c = 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
|
||||
# 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
|
||||
# Hover text for reply button
|
||||
Reply_to_this_note_f5de = Répondre à cette note
|
||||
# Error message when reply note cannot be found
|
||||
Reply_to_unknown_note_4401 = Répondre à la note inconnue
|
||||
# Fallback template for replying to user
|
||||
replying_to__user_15ab = répondre à { $user }
|
||||
# Template for replying to user in unknown thread
|
||||
replying_to__user__in_someone_s_thread_e148 = répondre à { $user } dans le fil de discussion
|
||||
# Template for replying to note in different user's thread
|
||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = répondre à la { $note } de { $user } dans le { $thread } sur le { $thread_user }
|
||||
# Template for replying to user's note
|
||||
replying_to__user__s__note_ccba = répondre à la { $note } de { $user }
|
||||
# Template for replying to root thread
|
||||
replying_to__user__s__thread_444d = répondre dans le { $thread } de { $user }
|
||||
# Fallback text when reply note is not found
|
||||
replying_to_a_note_e0bc = répondre à une note
|
||||
# Hover text for repost button
|
||||
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
|
||||
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 = Enregistrer
|
||||
# Button label to save profile changes
|
||||
Save_changes_00db = Enregistrer les modifications
|
||||
# Column title for search page
|
||||
Search_c573 = Rechercher
|
||||
# Placeholder for search notes input field
|
||||
Search_notes_42a6 = Rechercher des notes...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Recherche par '{ $query }'
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
|
||||
# Button 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
|
||||
Sign_out_337b = Se déconnecter
|
||||
# Title for someone else's notes column
|
||||
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
|
||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Restez informé sur un hashtag
|
||||
# Description for notifications column
|
||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Restez informé avec les notifications et les mentions
|
||||
# Description for someone else's notes column
|
||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Restez informé des notes et des réponses de quelqu'un d'autre
|
||||
# Description for someone else's notifications column
|
||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Restez informé des notifications et mentions de quelqu'un d'autre
|
||||
# Description for individual user column
|
||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Restez informé des notes et réponses de quelqu'un
|
||||
# Description for your notifications column
|
||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour vos notifications et mentions
|
||||
# Step 1 label in support instructions
|
||||
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 :
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Passer en mode sombre
|
||||
# Hover text for light mode toggle button
|
||||
Switch_to_light_mode_72ce = Passer en mode clair
|
||||
# Button text to load blurred media
|
||||
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
|
||||
# Link text for thread references
|
||||
thread_ad1f = fil
|
||||
# Title for universe column
|
||||
Universe_e01e = Universel
|
||||
# Column title for universe feed
|
||||
Universe_ffaa = Universel
|
||||
# Checkbox label for using wallet only for current account
|
||||
Use_this_wallet_for_the_current_account_only_61dc = Utiliser ce portefeuille pour le compte actuel
|
||||
# Username and domain identification message
|
||||
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
|
||||
# Hint for deck name input field
|
||||
We_recommend_short_names_083e = Nous recommandons des noms courts
|
||||
# Profile website field label
|
||||
Website_7980 = Site web
|
||||
# Placeholder for note input field
|
||||
Write_a_banger_note_here_bad2 = Écrivez une note banger ici...
|
||||
# Placeholder text for key input field
|
||||
Your_key_here_81bd = Votre clé ici...
|
||||
# Title for your notes column
|
||||
Your_Notes_f6db = Vos Notes
|
||||
# Title for your notifications column
|
||||
Your_Notifications_080d = Vos notifications
|
||||
# Heading for zap (tip) action
|
||||
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
|
||||
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[one] A obtenu { $count } pour '{ $query }'
|
||||
*[other] A obtenu { $count } pour '{ $query }'
|
||||
}
|
||||
@@ -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 }' 件取得しました
|
||||
}
|
||||
@@ -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 }'
|
||||
}
|
||||
@@ -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 }'
|
||||
}
|
||||
@@ -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 } รายการ
|
||||
}
|
||||
@@ -1,411 +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 = BETA
|
||||
# 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 = 自定义打闪金额
|
||||
# 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
|
||||
DECKS_1fad = 仪表板
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = 打闪默认金额:
|
||||
# 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 = 10万
|
||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_10K_f7e6 = 1万
|
||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_20K_4977 = 2万
|
||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_50K_c2dc = 5万
|
||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
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 = 亮色
|
||||
# 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 = nostr 用户
|
||||
# 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 = 打开你的默认电子邮件客户端以获得达摩团队的帮助
|
||||
# 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 = 正在回复在{ $thread_user }的{ $thread }中的{ $user }的{ $note }
|
||||
# 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 = 聪
|
||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
||||
sats_e5ec = 聪
|
||||
# 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 = 按最新排序回复:
|
||||
# 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 = 第一步
|
||||
# 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 = 支持电子邮件:
|
||||
# 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 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
|
||||
# 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 = 打闪
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = 打闪此笔记
|
||||
# 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 }条结果
|
||||
}
|
||||
@@ -1,411 +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 = 自訂打閃金額
|
||||
# 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
|
||||
DECKS_1fad = 儀表板
|
||||
# Label for default zap amount input
|
||||
Default_amount_per_zap_399d = 默認打閃金額:
|
||||
# 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 = 10萬
|
||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_10K_f7e6 = 1萬
|
||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_20K_4977 = 2萬
|
||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
k_50K_c2dc = 5萬
|
||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
||||
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 = 亮色
|
||||
# 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 = nostr 用戶
|
||||
# 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 = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
|
||||
# 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 = 正在回覆在{ $thread_user }的{ $thread }中的{ $user }的{ $note }
|
||||
# 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 = 聰
|
||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
||||
sats_e5ec = 聰
|
||||
# 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 = 按最新排序回覆:
|
||||
# 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 = 第一步
|
||||
# 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 = 支持電子郵件:
|
||||
# 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 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
|
||||
# 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 = 打閃
|
||||
# Hover text for zap button
|
||||
Zap_this_note_42b2 = 打閃此筆記
|
||||
# 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 }條結果
|
||||
}
|
||||
10
build.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
|
||||
if output.status.success() {
|
||||
let hash = String::from_utf8_lossy(&output.stdout);
|
||||
println!("cargo:rustc-env=GIT_COMMIT_HASH={}", hash.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "enostr"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ewebsock = { version = "0.8.0", features = ["tls"] }
|
||||
serde_derive = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] } # You only need this if you want app persistence
|
||||
serde_json = { workspace = true }
|
||||
nostr = { workspace = true }
|
||||
bech32 = { workspace = true }
|
||||
nostrdb = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
mio = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokenator = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
@@ -1,3 +0,0 @@
|
||||
mod message;
|
||||
|
||||
pub use message::{ClientMessage, EventClientMessage};
|
||||
@@ -1,61 +0,0 @@
|
||||
//use nostr::prelude::secp256k1;
|
||||
use std::array::TryFromSliceError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("message is empty")]
|
||||
Empty,
|
||||
|
||||
#[error("decoding failed: {0}")]
|
||||
DecodeFailed(String),
|
||||
|
||||
#[error("hex decoding failed")]
|
||||
HexDecodeFailed,
|
||||
|
||||
#[error("invalid bech32")]
|
||||
InvalidBech32,
|
||||
|
||||
#[error("invalid byte size")]
|
||||
InvalidByteSize,
|
||||
|
||||
#[error("invalid signature")]
|
||||
InvalidSignature,
|
||||
|
||||
#[error("invalid public key")]
|
||||
InvalidPublicKey,
|
||||
|
||||
#[error("invalid relay url")]
|
||||
InvalidRelayUrl,
|
||||
|
||||
// Secp(secp256k1::Error),
|
||||
#[error("json error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("nostrdb error: {0}")]
|
||||
Nostrdb(#[from] nostrdb::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Generic(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TryFromSliceError> for Error {
|
||||
fn from(_e: TryFromSliceError) -> Self {
|
||||
Error::InvalidByteSize
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for Error {
|
||||
fn from(_e: hex::FromHexError) -> Self {
|
||||
Error::HexDecodeFailed
|
||||
}
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
use nostr::nips::nip19::FromBech32;
|
||||
use nostr::nips::nip19::ToBech32;
|
||||
use nostr::nips::nip49::EncryptedSecretKey;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokenator::ParseError;
|
||||
use tokenator::TokenParser;
|
||||
use tokenator::TokenSerializable;
|
||||
|
||||
use crate::Pubkey;
|
||||
use crate::SecretKey;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct Keypair {
|
||||
pub pubkey: Pubkey,
|
||||
pub secret_key: Option<SecretKey>,
|
||||
}
|
||||
|
||||
pub struct KeypairUnowned<'a> {
|
||||
pub pubkey: &'a Pubkey,
|
||||
pub secret_key: Option<&'a SecretKey>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Keypair> for KeypairUnowned<'a> {
|
||||
fn from(value: &'a Keypair) -> Self {
|
||||
Self {
|
||||
pubkey: &value.pubkey,
|
||||
secret_key: value.secret_key.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Keypair {
|
||||
pub fn from_secret(secret_key: SecretKey) -> Self {
|
||||
let cloned_secret_key = secret_key.clone();
|
||||
let nostr_keys = nostr::Keys::new(secret_key);
|
||||
Keypair {
|
||||
pubkey: Pubkey::new(nostr_keys.public_key().to_bytes()),
|
||||
secret_key: Some(cloned_secret_key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(pubkey: Pubkey, secret_key: Option<SecretKey>) -> Self {
|
||||
Keypair { pubkey, secret_key }
|
||||
}
|
||||
|
||||
pub fn only_pubkey(pubkey: Pubkey) -> Self {
|
||||
Keypair {
|
||||
pubkey,
|
||||
secret_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_full(&self) -> Option<FilledKeypair<'_>> {
|
||||
self.secret_key.as_ref().map(|secret_key| FilledKeypair {
|
||||
pubkey: &self.pubkey,
|
||||
secret_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct FullKeypair {
|
||||
pub pubkey: Pubkey,
|
||||
pub secret_key: SecretKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub struct FilledKeypair<'a> {
|
||||
pub pubkey: &'a Pubkey,
|
||||
pub secret_key: &'a SecretKey,
|
||||
}
|
||||
|
||||
impl<'a> FilledKeypair<'a> {
|
||||
pub fn new(pubkey: &'a Pubkey, secret_key: &'a SecretKey) -> Self {
|
||||
FilledKeypair { pubkey, secret_key }
|
||||
}
|
||||
|
||||
pub fn to_full(&self) -> FullKeypair {
|
||||
FullKeypair {
|
||||
pubkey: self.pubkey.to_owned(),
|
||||
secret_key: self.secret_key.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a FilledKeypair<'a>> for KeypairUnowned<'a> {
|
||||
fn from(value: &'a FilledKeypair<'a>) -> Self {
|
||||
Self {
|
||||
pubkey: value.pubkey,
|
||||
secret_key: Some(value.secret_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FullKeypair {
|
||||
pub fn new(pubkey: Pubkey, secret_key: SecretKey) -> Self {
|
||||
FullKeypair { pubkey, secret_key }
|
||||
}
|
||||
|
||||
pub fn to_filled(&self) -> FilledKeypair<'_> {
|
||||
FilledKeypair::new(&self.pubkey, &self.secret_key)
|
||||
}
|
||||
|
||||
pub fn generate() -> Self {
|
||||
let mut rng = nostr::secp256k1::rand::rngs::OsRng;
|
||||
let (secret_key, _) = &nostr::SECP256K1.generate_keypair(&mut rng);
|
||||
let (xopk, _) = secret_key.x_only_public_key(&nostr::SECP256K1);
|
||||
let secret_key = nostr::SecretKey::from(*secret_key);
|
||||
FullKeypair {
|
||||
pubkey: Pubkey::new(xopk.serialize()),
|
||||
secret_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_keypair(self) -> Keypair {
|
||||
Keypair {
|
||||
pubkey: self.pubkey,
|
||||
secret_key: Some(self.secret_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Keypair {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Keypair:\n\tpublic: {}\n\tsecret: {}",
|
||||
self.pubkey,
|
||||
match self.secret_key {
|
||||
Some(_) => "Some(<hidden>)",
|
||||
None => "None",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FullKeypair {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Keypair:\n\tpublic: {}\n\tsecret: <hidden>", self.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SerializableKeypair {
|
||||
pub pubkey: Pubkey,
|
||||
pub encrypted_secret_key: Option<EncryptedSecretKey>,
|
||||
}
|
||||
|
||||
impl SerializableKeypair {
|
||||
pub fn from_keypair(kp: &Keypair, pass: &str, log_n: u8) -> Self {
|
||||
Self {
|
||||
pubkey: kp.pubkey,
|
||||
encrypted_secret_key: kp.secret_key.clone().and_then(|s| {
|
||||
EncryptedSecretKey::new(&s, pass, log_n, nostr::nips::nip49::KeySecurity::Weak).ok()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_keypair(&self, pass: &str) -> Keypair {
|
||||
Keypair::new(
|
||||
self.pubkey,
|
||||
self.encrypted_secret_key
|
||||
.and_then(|e| e.to_secret_key(pass).ok()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenSerializable for Pubkey {
|
||||
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
|
||||
parser.parse_token(PUBKEY_TOKEN)?;
|
||||
let raw = parser.pull_token()?;
|
||||
let pubkey =
|
||||
Pubkey::try_from_bech32_string(raw, true).map_err(|_| ParseError::DecodeFailed)?;
|
||||
Ok(pubkey)
|
||||
}
|
||||
|
||||
fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) {
|
||||
writer.write_token(PUBKEY_TOKEN);
|
||||
|
||||
let Some(bech) = self.npub() else {
|
||||
tracing::error!("Could not convert pubkey to bech: {}", self.hex());
|
||||
return;
|
||||
};
|
||||
|
||||
writer.write_token(&bech);
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenSerializable for Keypair {
|
||||
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
|
||||
TokenParser::alt(
|
||||
parser,
|
||||
&[
|
||||
|p| Ok(Keypair::only_pubkey(Pubkey::parse_from_tokens(p)?)),
|
||||
|p| Ok(Keypair::from_secret(parse_seckey(p)?)),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) {
|
||||
if let Some(seckey) = &self.secret_key {
|
||||
writer.write_token(ESECKEY_TOKEN);
|
||||
let maybe_eseckey = EncryptedSecretKey::new(
|
||||
seckey,
|
||||
ESECKEY_PASS,
|
||||
7,
|
||||
nostr::nips::nip49::KeySecurity::Unknown,
|
||||
);
|
||||
|
||||
let Ok(eseckey) = maybe_eseckey else {
|
||||
tracing::error!("Could not convert seckey to EncryptedSecretKey");
|
||||
return;
|
||||
};
|
||||
let Ok(serialized) = eseckey.to_bech32() else {
|
||||
tracing::error!("Could not serialize ncryptsec");
|
||||
return;
|
||||
};
|
||||
|
||||
writer.write_token(&serialized);
|
||||
} else {
|
||||
self.pubkey.serialize_tokens(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ESECKEY_TOKEN: &str = "eseckey";
|
||||
const ESECKEY_PASS: &str = "notedeck";
|
||||
const PUBKEY_TOKEN: &str = "pubkey";
|
||||
|
||||
fn parse_seckey<'a>(parser: &mut TokenParser<'a>) -> Result<SecretKey, ParseError<'a>> {
|
||||
parser.parse_token(ESECKEY_TOKEN)?;
|
||||
|
||||
let raw = parser.pull_token()?;
|
||||
|
||||
let eseckey = EncryptedSecretKey::from_bech32(raw).map_err(|_| ParseError::DecodeFailed)?;
|
||||
|
||||
let seckey = eseckey
|
||||
.to_secret_key(ESECKEY_PASS)
|
||||
.map_err(|_| ParseError::DecodeFailed)?;
|
||||
|
||||
Ok(seckey)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use tokenator::{TokenParser, TokenSerializable, TokenWriter};
|
||||
|
||||
use super::{FullKeypair, Keypair};
|
||||
|
||||
#[test]
|
||||
fn test_token_eseckey_serialize_deserialize() {
|
||||
let kp = FullKeypair::generate();
|
||||
|
||||
let mut writer = TokenWriter::new("\t");
|
||||
kp.clone().to_keypair().serialize_tokens(&mut writer);
|
||||
|
||||
let serialized = writer.str();
|
||||
|
||||
let data = &serialized.split("\t").collect::<Vec<&str>>();
|
||||
|
||||
let mut parser = TokenParser::new(data);
|
||||
let m_new_kp = Keypair::parse_from_tokens(&mut parser);
|
||||
assert!(m_new_kp.is_ok());
|
||||
|
||||
let new_kp = m_new_kp.unwrap();
|
||||
|
||||
assert_eq!(kp, new_kp.to_full().unwrap().to_full());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_pubkey_serialize_deserialize() {
|
||||
let kp = Keypair::only_pubkey(FullKeypair::generate().pubkey);
|
||||
|
||||
let mut writer = TokenWriter::new("\t");
|
||||
kp.clone().serialize_tokens(&mut writer);
|
||||
|
||||
let serialized = writer.str();
|
||||
|
||||
let data = &serialized.split("\t").collect::<Vec<&str>>();
|
||||
|
||||
let mut parser = TokenParser::new(data);
|
||||
let m_new_kp = Keypair::parse_from_tokens(&mut parser);
|
||||
assert!(m_new_kp.is_ok());
|
||||
|
||||
let new_kp = m_new_kp.unwrap();
|
||||
|
||||
assert_eq!(kp, new_kp);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfileState(Value);
|
||||
|
||||
impl Default for ProfileState {
|
||||
fn default() -> Self {
|
||||
ProfileState::new(Map::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileState {
|
||||
pub fn new(value: Map<String, Value>) -> Self {
|
||||
Self(Value::Object(value))
|
||||
}
|
||||
|
||||
pub fn get_str(&self, name: &str) -> Option<&str> {
|
||||
self.0.get(name).and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
pub fn values_mut(&mut self) -> &mut Map<String, Value> {
|
||||
self.0.as_object_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Insert or overwrite an existing value with a string
|
||||
pub fn str_mut(&mut self, name: &str) -> &mut String {
|
||||
let val = self
|
||||
.values_mut()
|
||||
.entry(name)
|
||||
.or_insert(Value::String("".to_string()));
|
||||
|
||||
// if its not a string, make it one. this will overrwrite
|
||||
// the old value, so be careful
|
||||
if !val.is_string() {
|
||||
*val = Value::String("".to_string());
|
||||
}
|
||||
|
||||
match val {
|
||||
Value::String(s) => s,
|
||||
// SAFETY: we replace it above, so its impossible to be something
|
||||
// other than a string
|
||||
_ => panic!("impossible"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &Value {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
// SAFETY: serializing a value should be irrefutable
|
||||
serde_json::to_string(self.value()).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
self.get_str("name")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn banner(&self) -> Option<&str> {
|
||||
self.get_str("name")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
self.get_str("display_name")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn lud06(&self) -> Option<&str> {
|
||||
self.get_str("lud06")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn nip05(&self) -> Option<&str> {
|
||||
self.get_str("nip05")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn lud16(&self) -> Option<&str> {
|
||||
self.get_str("lud16")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn about(&self) -> Option<&str> {
|
||||
self.get_str("about")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn picture(&self) -> Option<&str> {
|
||||
self.get_str("picture")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn website(&self) -> Option<&str> {
|
||||
self.get_str("website")
|
||||
}
|
||||
|
||||
pub fn from_note_contents(contents: &str) -> Self {
|
||||
let json = serde_json::from_str(contents);
|
||||
let data = if let Ok(Value::Object(data)) = json {
|
||||
data
|
||||
} else {
|
||||
Map::new()
|
||||
};
|
||||
|
||||
Self::new(data)
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
use ewebsock::{Options, WsEvent, WsMessage, WsReceiver, WsSender};
|
||||
use mio::net::UdpSocket;
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use std::net::{SocketAddr, SocketAddrV4};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{ClientMessage, EventClientMessage, Result};
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::net::Ipv4Addr;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub mod message;
|
||||
pub mod pool;
|
||||
pub mod subs_debug;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum RelayStatus {
|
||||
Connected,
|
||||
Connecting,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
pub struct MulticastRelay {
|
||||
last_join: Instant,
|
||||
status: RelayStatus,
|
||||
address: SocketAddrV4,
|
||||
socket: UdpSocket,
|
||||
interface: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl MulticastRelay {
|
||||
pub fn new(address: SocketAddrV4, socket: UdpSocket, interface: Ipv4Addr) -> Self {
|
||||
let last_join = Instant::now();
|
||||
let status = RelayStatus::Connected;
|
||||
MulticastRelay {
|
||||
status,
|
||||
address,
|
||||
socket,
|
||||
interface,
|
||||
last_join,
|
||||
}
|
||||
}
|
||||
|
||||
/// Multicast seems to fail every 260 seconds. We force a rejoin every 200 seconds or
|
||||
/// so to ensure we are always in the group
|
||||
pub fn rejoin(&mut self) -> Result<()> {
|
||||
self.last_join = Instant::now();
|
||||
self.status = RelayStatus::Disconnected;
|
||||
self.socket
|
||||
.leave_multicast_v4(self.address.ip(), &self.interface)?;
|
||||
self.socket
|
||||
.join_multicast_v4(self.address.ip(), &self.interface)?;
|
||||
self.status = RelayStatus::Connected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn should_rejoin(&self) -> bool {
|
||||
(Instant::now() - self.last_join) >= Duration::from_secs(200)
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<WsEvent> {
|
||||
let mut buffer = [0u8; 65535];
|
||||
// Read the size header
|
||||
match self.socket.recv_from(&mut buffer) {
|
||||
Ok((size, src)) => {
|
||||
let parsed_size = u32::from_be_bytes(buffer[0..4].try_into().ok()?) as usize;
|
||||
debug!("multicast: read size {} from start of header", size - 4);
|
||||
|
||||
if size != parsed_size + 4 {
|
||||
error!(
|
||||
"multicast: partial data received: expected {}, got {}",
|
||||
parsed_size, size
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&buffer[4..size]);
|
||||
debug!("multicast: received {} bytes from {}: {}", size, src, &text);
|
||||
Some(WsEvent::Message(WsMessage::Text(text.to_string())))
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
// No data available, continue
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
error!("multicast: error receiving data: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&self, msg: &EventClientMessage) -> Result<()> {
|
||||
let json = msg.to_json();
|
||||
let len = json.len();
|
||||
|
||||
debug!("writing to multicast relay");
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(4 + len);
|
||||
|
||||
// Write the length of the message as 4 bytes (big-endian)
|
||||
buf.extend_from_slice(&(len as u32).to_be_bytes());
|
||||
|
||||
// Append the JSON message bytes
|
||||
buf.extend_from_slice(json.as_bytes());
|
||||
|
||||
self.socket.send_to(&buf, SocketAddr::V4(self.address))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_multicast_relay(
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Result<MulticastRelay> {
|
||||
use mio::{Events, Interest, Poll, Token};
|
||||
|
||||
let port = 9797;
|
||||
let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), port);
|
||||
let multicast_ip = Ipv4Addr::new(239, 19, 88, 1);
|
||||
|
||||
let mut socket = UdpSocket::bind(address)?;
|
||||
let interface = Ipv4Addr::UNSPECIFIED;
|
||||
let multicast_address = SocketAddrV4::new(multicast_ip, port);
|
||||
|
||||
socket.join_multicast_v4(&multicast_ip, &interface)?;
|
||||
|
||||
let mut poll = Poll::new()?;
|
||||
poll.registry().register(
|
||||
&mut socket,
|
||||
Token(0),
|
||||
Interest::READABLE | Interest::WRITABLE,
|
||||
)?;
|
||||
|
||||
// wakeup our render thread when we have new stuff on the socket
|
||||
std::thread::spawn(move || {
|
||||
let mut events = Events::with_capacity(1);
|
||||
loop {
|
||||
if let Err(err) = poll.poll(&mut events, None) {
|
||||
error!("multicast socket poll error: {err}. ending multicast poller.");
|
||||
return;
|
||||
}
|
||||
wakeup();
|
||||
|
||||
std::thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(MulticastRelay::new(multicast_address, socket, interface))
|
||||
}
|
||||
|
||||
pub struct Relay {
|
||||
pub url: nostr::RelayUrl,
|
||||
pub status: RelayStatus,
|
||||
pub sender: WsSender,
|
||||
pub receiver: WsReceiver,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Relay {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Relay")
|
||||
.field("url", &self.url)
|
||||
.field("status", &self.status)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Relay {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// Hashes the Relay by hashing the URL
|
||||
self.url.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Relay {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.url == other.url
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Relay {}
|
||||
|
||||
impl Relay {
|
||||
pub fn new(url: nostr::RelayUrl, wakeup: impl Fn() + Send + Sync + 'static) -> Result<Self> {
|
||||
let status = RelayStatus::Connecting;
|
||||
let (sender, receiver) =
|
||||
ewebsock::connect_with_wakeup(url.as_str(), Options::default(), wakeup)?;
|
||||
|
||||
Ok(Self {
|
||||
url,
|
||||
sender,
|
||||
receiver,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&mut self, msg: &ClientMessage) {
|
||||
let json = match msg.to_json() {
|
||||
Ok(json) => {
|
||||
debug!("sending {} to {}", json, self.url);
|
||||
json
|
||||
}
|
||||
Err(e) => {
|
||||
error!("error serializing json for filter: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let txt = WsMessage::Text(json);
|
||||
self.sender.send(txt);
|
||||
}
|
||||
|
||||
pub fn connect(&mut self, wakeup: impl Fn() + Send + Sync + 'static) -> Result<()> {
|
||||
let (sender, receiver) =
|
||||
ewebsock::connect_with_wakeup(self.url.as_str(), Options::default(), wakeup)?;
|
||||
self.status = RelayStatus::Connecting;
|
||||
self.sender = sender;
|
||||
self.receiver = receiver;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ping(&mut self) {
|
||||
let msg = WsMessage::Ping(vec![]);
|
||||
self.sender.send(msg);
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
use crate::relay::{setup_multicast_relay, MulticastRelay, Relay, RelayStatus};
|
||||
use crate::{ClientMessage, Error, Result};
|
||||
use nostrdb::Filter;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use ewebsock::{WsEvent, WsMessage};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tracing::{debug, error};
|
||||
|
||||
use super::subs_debug::SubsDebug;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolEvent<'a> {
|
||||
pub relay: &'a str,
|
||||
pub event: ewebsock::WsEvent,
|
||||
}
|
||||
|
||||
impl PoolEvent<'_> {
|
||||
pub fn into_owned(self) -> PoolEventBuf {
|
||||
PoolEventBuf {
|
||||
relay: self.relay.to_owned(),
|
||||
event: self.event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolEventBuf {
|
||||
pub relay: String,
|
||||
pub event: ewebsock::WsEvent,
|
||||
}
|
||||
|
||||
pub enum PoolRelay {
|
||||
Websocket(WebsocketRelay),
|
||||
Multicast(MulticastRelay),
|
||||
}
|
||||
|
||||
pub struct WebsocketRelay {
|
||||
pub relay: Relay,
|
||||
pub last_ping: Instant,
|
||||
pub last_connect_attempt: Instant,
|
||||
pub retry_connect_after: Duration,
|
||||
}
|
||||
|
||||
impl PoolRelay {
|
||||
pub fn url(&self) -> &str {
|
||||
match self {
|
||||
Self::Websocket(wsr) => wsr.relay.url.as_str(),
|
||||
Self::Multicast(_wsr) => "multicast",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: RelayStatus) {
|
||||
match self {
|
||||
Self::Websocket(wsr) => {
|
||||
wsr.relay.status = status;
|
||||
}
|
||||
Self::Multicast(_mcr) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<WsEvent> {
|
||||
match self {
|
||||
Self::Websocket(recvr) => recvr.relay.receiver.try_recv(),
|
||||
Self::Multicast(recvr) => recvr.try_recv(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> RelayStatus {
|
||||
match self {
|
||||
Self::Websocket(wsr) => wsr.relay.status,
|
||||
Self::Multicast(mcr) => mcr.status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&mut self, msg: &ClientMessage) -> Result<()> {
|
||||
match self {
|
||||
Self::Websocket(wsr) => {
|
||||
wsr.relay.send(msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Self::Multicast(mcr) => {
|
||||
// we only send event client messages at the moment
|
||||
if let ClientMessage::Event(ecm) = msg {
|
||||
mcr.send(ecm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) -> Result<()> {
|
||||
self.send(&ClientMessage::req(subid, filter))
|
||||
}
|
||||
|
||||
pub fn websocket(relay: Relay) -> Self {
|
||||
Self::Websocket(WebsocketRelay::new(relay))
|
||||
}
|
||||
|
||||
pub fn multicast(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> Result<Self> {
|
||||
Ok(Self::Multicast(setup_multicast_relay(wakeup)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl WebsocketRelay {
|
||||
pub fn new(relay: Relay) -> Self {
|
||||
Self {
|
||||
relay,
|
||||
last_ping: Instant::now(),
|
||||
last_connect_attempt: Instant::now(),
|
||||
retry_connect_after: Self::initial_reconnect_duration(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initial_reconnect_duration() -> Duration {
|
||||
Duration::from_secs(5)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RelayPool {
|
||||
pub relays: Vec<PoolRelay>,
|
||||
pub ping_rate: Duration,
|
||||
pub debug: Option<SubsDebug>,
|
||||
}
|
||||
|
||||
impl Default for RelayPool {
|
||||
fn default() -> Self {
|
||||
RelayPool::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RelayPool {
|
||||
// Constructs a new, empty RelayPool.
|
||||
pub fn new() -> RelayPool {
|
||||
RelayPool {
|
||||
relays: vec![],
|
||||
ping_rate: Duration::from_secs(45),
|
||||
debug: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_multicast_relay(
|
||||
&mut self,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Result<()> {
|
||||
let multicast_relay = PoolRelay::multicast(wakeup)?;
|
||||
self.relays.push(multicast_relay);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn use_debug(&mut self) {
|
||||
self.debug = Some(SubsDebug::default());
|
||||
}
|
||||
|
||||
pub fn ping_rate(&mut self, duration: Duration) -> &mut Self {
|
||||
self.ping_rate = duration;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has(&self, url: &str) -> bool {
|
||||
for relay in &self.relays {
|
||||
if relay.url() == url {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn urls(&self) -> BTreeSet<String> {
|
||||
self.relays
|
||||
.iter()
|
||||
.map(|pool_relay| pool_relay.url().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn send(&mut self, cmd: &ClientMessage) {
|
||||
for relay in &mut self.relays {
|
||||
if let Some(debug) = &mut self.debug {
|
||||
debug.send_cmd(relay.url().to_owned(), cmd);
|
||||
}
|
||||
if let Err(err) = relay.send(cmd) {
|
||||
error!("error sending {:?} to {}: {err}", cmd, relay.url());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&mut self, subid: String) {
|
||||
for relay in &mut self.relays {
|
||||
let cmd = ClientMessage::close(subid.clone());
|
||||
if let Some(debug) = &mut self.debug {
|
||||
debug.send_cmd(relay.url().to_owned(), &cmd);
|
||||
}
|
||||
if let Err(err) = relay.send(&cmd) {
|
||||
error!(
|
||||
"error unsubscribing from {} on {}: {err}",
|
||||
&subid,
|
||||
relay.url()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) {
|
||||
for relay in &mut self.relays {
|
||||
if let Some(debug) = &mut self.debug {
|
||||
debug.send_cmd(
|
||||
relay.url().to_owned(),
|
||||
&ClientMessage::req(subid.clone(), filter.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = relay.send(&ClientMessage::req(subid.clone(), filter.clone())) {
|
||||
error!("error subscribing to {}: {err}", relay.url());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep relay connectiongs alive by pinging relays that haven't been
|
||||
/// pinged in awhile. Adjust ping rate with [`ping_rate`].
|
||||
pub fn keepalive_ping(&mut self, wakeup: impl Fn() + Send + Sync + Clone + 'static) {
|
||||
for relay in &mut self.relays {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
match relay {
|
||||
PoolRelay::Multicast(_) => {}
|
||||
PoolRelay::Websocket(relay) => {
|
||||
match relay.relay.status {
|
||||
RelayStatus::Disconnected => {
|
||||
let reconnect_at =
|
||||
relay.last_connect_attempt + relay.retry_connect_after;
|
||||
if now > reconnect_at {
|
||||
relay.last_connect_attempt = now;
|
||||
let next_duration = Duration::from_millis(3000);
|
||||
debug!(
|
||||
"bumping reconnect duration from {:?} to {:?} and retrying connect",
|
||||
relay.retry_connect_after, next_duration
|
||||
);
|
||||
relay.retry_connect_after = next_duration;
|
||||
if let Err(err) = relay.relay.connect(wakeup.clone()) {
|
||||
error!("error connecting to relay: {}", err);
|
||||
}
|
||||
} else {
|
||||
// let's wait a bit before we try again
|
||||
}
|
||||
}
|
||||
|
||||
RelayStatus::Connected => {
|
||||
relay.retry_connect_after =
|
||||
WebsocketRelay::initial_reconnect_duration();
|
||||
|
||||
let should_ping = now - relay.last_ping > self.ping_rate;
|
||||
if should_ping {
|
||||
debug!("pinging {}", relay.relay.url);
|
||||
relay.relay.ping();
|
||||
relay.last_ping = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
RelayStatus::Connecting => {
|
||||
// cool story bro
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to(&mut self, cmd: &ClientMessage, relay_url: &str) {
|
||||
for relay in &mut self.relays {
|
||||
if relay.url() == relay_url {
|
||||
if let Some(debug) = &mut self.debug {
|
||||
debug.send_cmd(relay.url().to_owned(), cmd);
|
||||
}
|
||||
if let Err(err) = relay.send(cmd) {
|
||||
error!("send_to err: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// check whether a relay url is valid to add
|
||||
pub fn is_valid_url(&self, url: &str) -> bool {
|
||||
if url.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let url = match Url::parse(url) {
|
||||
Ok(parsed_url) => parsed_url.to_string(),
|
||||
Err(_err) => {
|
||||
// debug!("bad relay url \"{}\": {:?}", url, err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if self.has(&url) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Adds a websocket url to the RelayPool.
|
||||
pub fn add_url(
|
||||
&mut self,
|
||||
url: String,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Result<()> {
|
||||
let url = Self::canonicalize_url(url);
|
||||
// Check if the URL already exists in the pool.
|
||||
if self.has(&url) {
|
||||
return Ok(());
|
||||
}
|
||||
let relay = Relay::new(
|
||||
nostr::RelayUrl::parse(url).map_err(|_| Error::InvalidRelayUrl)?,
|
||||
wakeup,
|
||||
)?;
|
||||
let pool_relay = PoolRelay::websocket(relay);
|
||||
|
||||
self.relays.push(pool_relay);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_urls(
|
||||
&mut self,
|
||||
urls: BTreeSet<String>,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Result<()> {
|
||||
for url in urls {
|
||||
self.add_url(url, wakeup.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_urls(&mut self, urls: &BTreeSet<String>) {
|
||||
self.relays
|
||||
.retain(|pool_relay| !urls.contains(pool_relay.url()));
|
||||
}
|
||||
|
||||
// standardize the format (ie, trailing slashes)
|
||||
fn canonicalize_url(url: String) -> String {
|
||||
match Url::parse(&url) {
|
||||
Ok(parsed_url) => parsed_url.to_string(),
|
||||
Err(_) => url, // If parsing fails, return the original URL.
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to receive a pool event from a list of relays. The
|
||||
/// function searches each relay in the list in order, attempting to
|
||||
/// receive a message from each. If a message is received, return it.
|
||||
/// If no message is received from any relays, None is returned.
|
||||
pub fn try_recv(&mut self) -> Option<PoolEvent<'_>> {
|
||||
for relay in &mut self.relays {
|
||||
if let PoolRelay::Multicast(mcr) = relay {
|
||||
// try rejoin on multicast
|
||||
if mcr.should_rejoin() {
|
||||
if let Err(err) = mcr.rejoin() {
|
||||
error!("multicast: rejoin error: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(event) = relay.try_recv() {
|
||||
match &event {
|
||||
WsEvent::Opened => {
|
||||
relay.set_status(RelayStatus::Connected);
|
||||
}
|
||||
WsEvent::Closed => {
|
||||
relay.set_status(RelayStatus::Disconnected);
|
||||
}
|
||||
WsEvent::Error(err) => {
|
||||
error!("{:?}", err);
|
||||
relay.set_status(RelayStatus::Disconnected);
|
||||
}
|
||||
WsEvent::Message(ev) => {
|
||||
// let's just handle pongs here.
|
||||
// We only need to do this natively.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let WsMessage::Ping(ref bs) = ev {
|
||||
debug!("pong {}", relay.url());
|
||||
match relay {
|
||||
PoolRelay::Websocket(wsr) => {
|
||||
wsr.relay.sender.send(WsMessage::Pong(bs.to_owned()));
|
||||
}
|
||||
PoolRelay::Multicast(_mcr) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(debug) = &mut self.debug {
|
||||
debug.receive_cmd(relay.url().to_owned(), (&event).into());
|
||||
}
|
||||
|
||||
let pool_event = PoolEvent {
|
||||
event,
|
||||
relay: relay.url(),
|
||||
};
|
||||
|
||||
return Some(pool_event);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
use std::{collections::HashMap, mem, time::SystemTime};
|
||||
|
||||
use ewebsock::WsMessage;
|
||||
use nostrdb::Filter;
|
||||
|
||||
use crate::{ClientMessage, Error, RelayEvent, RelayMessage};
|
||||
|
||||
use super::message::calculate_command_result_size;
|
||||
|
||||
type RelayId = String;
|
||||
type SubId = String;
|
||||
|
||||
pub struct SubsDebug {
|
||||
data: HashMap<RelayId, RelayStats>,
|
||||
time_incd: SystemTime,
|
||||
pub relay_events_selection: Option<RelayId>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RelayStats {
|
||||
pub count: TransferStats,
|
||||
pub events: Vec<RelayLogEvent>,
|
||||
pub sub_data: HashMap<SubId, SubStats>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RelayLogEvent {
|
||||
Send(ClientMessage),
|
||||
Recieve(OwnedRelayEvent),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum OwnedRelayEvent {
|
||||
Opened,
|
||||
Closed,
|
||||
Other(String),
|
||||
Error(String),
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl From<RelayEvent<'_>> for OwnedRelayEvent {
|
||||
fn from(value: RelayEvent<'_>) -> Self {
|
||||
match value {
|
||||
RelayEvent::Opened => OwnedRelayEvent::Opened,
|
||||
RelayEvent::Closed => OwnedRelayEvent::Closed,
|
||||
RelayEvent::Other(ws_message) => {
|
||||
let ws_str = match ws_message {
|
||||
WsMessage::Binary(_) => "Binary".to_owned(),
|
||||
WsMessage::Text(t) => format!("Text:{t}"),
|
||||
WsMessage::Unknown(u) => format!("Unknown:{u}"),
|
||||
WsMessage::Ping(_) => "Ping".to_owned(),
|
||||
WsMessage::Pong(_) => "Pong".to_owned(),
|
||||
};
|
||||
OwnedRelayEvent::Other(ws_str)
|
||||
}
|
||||
RelayEvent::Error(error) => OwnedRelayEvent::Error(error.to_string()),
|
||||
RelayEvent::Message(relay_message) => {
|
||||
let relay_msg = match relay_message {
|
||||
RelayMessage::OK(_) => "OK".to_owned(),
|
||||
RelayMessage::Eose(s) => format!("EOSE:{s}"),
|
||||
RelayMessage::Event(_, s) => format!("EVENT:{s}"),
|
||||
RelayMessage::Notice(s) => format!("NOTICE:{s}"),
|
||||
};
|
||||
OwnedRelayEvent::Message(relay_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub struct _RelaySub {
|
||||
pub(crate) subid: String,
|
||||
pub(crate) filter: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SubStats {
|
||||
pub filter: String,
|
||||
pub count: TransferStats,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TransferStats {
|
||||
pub up_total: usize,
|
||||
pub down_total: usize,
|
||||
|
||||
// 1 sec < last tick < 2 sec
|
||||
pub up_sec_prior: usize,
|
||||
pub down_sec_prior: usize,
|
||||
|
||||
// < 1 sec since last tick
|
||||
up_sec_cur: usize,
|
||||
down_sec_cur: usize,
|
||||
}
|
||||
|
||||
impl Default for SubsDebug {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: Default::default(),
|
||||
time_incd: SystemTime::now(),
|
||||
relay_events_selection: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubsDebug {
|
||||
pub fn get_data(&self) -> &HashMap<RelayId, RelayStats> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub(crate) fn send_cmd(&mut self, relay: String, cmd: &ClientMessage) {
|
||||
let data = self.data.entry(relay).or_default();
|
||||
let msg_num_bytes = calculate_client_message_size(cmd);
|
||||
match cmd {
|
||||
ClientMessage::Req { sub_id, filters } => {
|
||||
data.sub_data.insert(
|
||||
sub_id.to_string(),
|
||||
SubStats {
|
||||
filter: filters_to_string(filters),
|
||||
count: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ClientMessage::Close { sub_id } => {
|
||||
data.sub_data.remove(sub_id);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
data.count.up_sec_cur += msg_num_bytes;
|
||||
|
||||
data.events.push(RelayLogEvent::Send(cmd.clone()));
|
||||
}
|
||||
|
||||
pub(crate) fn receive_cmd(&mut self, relay: String, cmd: RelayEvent) {
|
||||
let data = self.data.entry(relay).or_default();
|
||||
let msg_num_bytes = calculate_relay_event_size(&cmd);
|
||||
if let RelayEvent::Message(RelayMessage::Event(sid, _)) = cmd {
|
||||
if let Some(sub_data) = data.sub_data.get_mut(sid) {
|
||||
let c = &mut sub_data.count;
|
||||
c.down_sec_cur += msg_num_bytes;
|
||||
}
|
||||
};
|
||||
|
||||
data.count.down_sec_cur += msg_num_bytes;
|
||||
|
||||
data.events.push(RelayLogEvent::Recieve(cmd.into()));
|
||||
}
|
||||
|
||||
pub fn try_increment_stats(&mut self) {
|
||||
let cur_time = SystemTime::now();
|
||||
if let Ok(dur) = cur_time.duration_since(self.time_incd) {
|
||||
if dur.as_secs() >= 1 {
|
||||
self.time_incd = cur_time;
|
||||
self.internal_inc_stats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_inc_stats(&mut self) {
|
||||
for relay_data in self.data.values_mut() {
|
||||
let c = &mut relay_data.count;
|
||||
inc_data_count(c);
|
||||
|
||||
for sub in relay_data.sub_data.values_mut() {
|
||||
inc_data_count(&mut sub.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inc_data_count(c: &mut TransferStats) {
|
||||
c.up_total += c.up_sec_cur;
|
||||
c.up_sec_prior = c.up_sec_cur;
|
||||
|
||||
c.down_total += c.down_sec_cur;
|
||||
c.down_sec_prior = c.down_sec_cur;
|
||||
|
||||
c.up_sec_cur = 0;
|
||||
c.down_sec_cur = 0;
|
||||
}
|
||||
|
||||
fn calculate_client_message_size(message: &ClientMessage) -> usize {
|
||||
match message {
|
||||
ClientMessage::Event(note) => note.note_json.len() + 10, // 10 is ["EVENT",]
|
||||
ClientMessage::Req { sub_id, filters } => {
|
||||
mem::size_of_val(message)
|
||||
+ mem::size_of_val(sub_id)
|
||||
+ sub_id.len()
|
||||
+ filters.iter().map(mem::size_of_val).sum::<usize>()
|
||||
}
|
||||
ClientMessage::Close { sub_id } => {
|
||||
mem::size_of_val(message) + mem::size_of_val(sub_id) + sub_id.len()
|
||||
}
|
||||
ClientMessage::Raw(data) => mem::size_of_val(message) + data.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_relay_event_size(event: &RelayEvent<'_>) -> usize {
|
||||
let base_size = mem::size_of_val(event); // Size of the enum on the stack
|
||||
|
||||
let variant_size = match event {
|
||||
RelayEvent::Opened | RelayEvent::Closed => 0, // No additional data
|
||||
RelayEvent::Other(ws_message) => calculate_ws_message_size(ws_message),
|
||||
RelayEvent::Error(error) => calculate_error_size(error),
|
||||
RelayEvent::Message(message) => calculate_relay_message_size(message),
|
||||
};
|
||||
|
||||
base_size + variant_size
|
||||
}
|
||||
|
||||
fn calculate_ws_message_size(message: &WsMessage) -> usize {
|
||||
match message {
|
||||
WsMessage::Binary(vec) | WsMessage::Ping(vec) | WsMessage::Pong(vec) => {
|
||||
mem::size_of_val(message) + vec.len()
|
||||
}
|
||||
WsMessage::Text(string) | WsMessage::Unknown(string) => {
|
||||
mem::size_of_val(message) + string.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_error_size(error: &Error) -> usize {
|
||||
match error {
|
||||
Error::Empty
|
||||
| Error::HexDecodeFailed
|
||||
| Error::InvalidBech32
|
||||
| Error::InvalidByteSize
|
||||
| Error::InvalidSignature
|
||||
| Error::InvalidRelayUrl
|
||||
| Error::Io(_)
|
||||
| Error::InvalidPublicKey => mem::size_of_val(error), // No heap usage
|
||||
|
||||
Error::DecodeFailed(string) => mem::size_of_val(error) + string.len(),
|
||||
|
||||
Error::Json(json_err) => mem::size_of_val(error) + json_err.to_string().len(),
|
||||
|
||||
Error::Nostrdb(nostrdb_err) => mem::size_of_val(error) + nostrdb_err.to_string().len(),
|
||||
|
||||
Error::Generic(string) => mem::size_of_val(error) + string.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_relay_message_size(message: &RelayMessage) -> usize {
|
||||
match message {
|
||||
RelayMessage::OK(result) => calculate_command_result_size(result),
|
||||
RelayMessage::Eose(str_ref)
|
||||
| RelayMessage::Event(str_ref, _)
|
||||
| RelayMessage::Notice(str_ref) => mem::size_of_val(message) + str_ref.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn filters_to_string(f: &Vec<Filter>) -> String {
|
||||
let mut cur_str = String::new();
|
||||
for filter in f {
|
||||
if let Ok(json) = filter.json() {
|
||||
if !cur_str.is_empty() {
|
||||
cur_str.push_str(", ");
|
||||
}
|
||||
cur_str.push_str(&json);
|
||||
}
|
||||
}
|
||||
|
||||
cur_str
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
[package]
|
||||
name = "notedeck"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
description = "The APIs and data structures used by notedeck apps"
|
||||
|
||||
[dependencies]
|
||||
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 }
|
||||
poll-promise = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
puffin = { workspace = true, optional = true }
|
||||
puffin_egui = { workspace = true, optional = true }
|
||||
sha2 = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
ehttp = {workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
egui-winit = { workspace = true }
|
||||
tokenator = { workspace = true }
|
||||
profiling = { workspace = true }
|
||||
nwc = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
bech32 = { workspace = true }
|
||||
lightning-invoice = { workspace = true }
|
||||
secp256k1 = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
fluent = { workspace = true }
|
||||
fluent-resmgr = { workspace = true }
|
||||
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 }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = { workspace = true }
|
||||
android-activity = { workspace = true }
|
||||
|
||||
[features]
|
||||
puffin = ["puffin_egui", "dep:puffin"]
|
||||
@@ -1,396 +0,0 @@
|
||||
# Notedeck Developer Documentation
|
||||
|
||||
This document provides technical details and guidance for developers working with the Notedeck crate.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Notedeck is built around a modular architecture that separates concerns into distinct components:
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **App Framework (`app.rs`)**
|
||||
- `Notedeck` - The main application framework that ties everything together
|
||||
- `App` - The trait that specific applications must implement
|
||||
|
||||
2. **Data Layer**
|
||||
- `Ndb` - NostrDB database for efficient storage and querying
|
||||
- `NoteCache` - In-memory cache for expensive-to-compute note data like nip10 structure
|
||||
- `Images` - Image and GIF cache management
|
||||
|
||||
3. **Network Layer**
|
||||
- `RelayPool` - Manages connections to Nostr relays
|
||||
- `UnknownIds` - Tracks and resolves unknown profiles and notes
|
||||
|
||||
4. **User Accounts**
|
||||
- `Accounts` - Manages user keypairs and account information
|
||||
- `AccountStorage` - Handles persistent storage of account data
|
||||
|
||||
5. **Wallet Integration**
|
||||
- `Wallet` - Lightning wallet integration
|
||||
- `Zaps` - Handles Nostr zap functionality
|
||||
|
||||
6. **UI Components**
|
||||
- `NotedeckTextStyle` - Text styling utilities
|
||||
- `ColorTheme` - Theme management
|
||||
- Various UI helpers
|
||||
|
||||
7. **Localization System**
|
||||
- `LocalizationManager` - Core localization functionality
|
||||
- `LocalizationContext` - Thread-safe context for sharing localization
|
||||
- Fluent-based translation system
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Note Context and Actions
|
||||
|
||||
Notes have associated context and actions that define how users can interact with them:
|
||||
|
||||
```rust
|
||||
pub enum NoteAction {
|
||||
Reply(NoteId), // Reply to a note
|
||||
Quote(NoteId), // Quote a note
|
||||
Hashtag(String), // Click on a hashtag
|
||||
Profile(Pubkey), // View a profile
|
||||
Note(NoteId), // View a note
|
||||
Context(ContextSelection), // Context menu options
|
||||
Zap(ZapAction), // Zap (tip) interaction
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Management
|
||||
|
||||
Notedeck handles relays through the `RelaySpec` structure, which implements NIP-65 functionality for marking relays as read or write.
|
||||
|
||||
### Filtering and Subscriptions
|
||||
|
||||
The `FilterState` enum manages the state of subscriptions to Nostr relays:
|
||||
|
||||
```rust
|
||||
pub enum FilterState {
|
||||
NeedsRemote(Vec<Filter>),
|
||||
FetchingRemote(UnifiedSubscription),
|
||||
GotRemote(Subscription),
|
||||
Ready(Vec<Filter>),
|
||||
Broken(FilterError),
|
||||
}
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Setting Up Your Environment
|
||||
|
||||
1. Clone the repository
|
||||
2. Build with `cargo build`
|
||||
3. Test with `cargo test`
|
||||
|
||||
### Creating a New Notedeck App
|
||||
|
||||
1. Import the notedeck crate
|
||||
2. Implement the `App` trait
|
||||
3. Use the `Notedeck` struct as your application framework
|
||||
|
||||
Example:
|
||||
|
||||
```rust
|
||||
use notedeck::{App, Notedeck, AppContext};
|
||||
|
||||
struct MyNostrApp {
|
||||
// Your app-specific state
|
||||
}
|
||||
|
||||
impl App for MyNostrApp {
|
||||
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) {
|
||||
// Your app's UI and logic here
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let notedeck = Notedeck::new(...).app(MyNostrApp { /* ... */ });
|
||||
// Run your app
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Notes
|
||||
|
||||
Notes are the core data structure in Nostr. Here's how to work with them:
|
||||
|
||||
```rust
|
||||
// Get a note by ID
|
||||
let txn = Transaction::new(&ndb).expect("txn");
|
||||
if let Ok(note) = ndb.get_note_by_id(&txn, note_id.bytes()) {
|
||||
// Process the note
|
||||
}
|
||||
|
||||
// Create a cached note
|
||||
let cached_note = note_cache.cached_note_or_insert(note_key, ¬e);
|
||||
```
|
||||
|
||||
### Adding Account Management
|
||||
|
||||
Account management is handled through the `Accounts` struct:
|
||||
|
||||
```rust
|
||||
// Add a new account
|
||||
let action = accounts.add_account(keypair);
|
||||
action.process_action(&mut unknown_ids, &ndb, &txn);
|
||||
|
||||
// Get the current account
|
||||
if let Some(account) = accounts.get_selected_account() {
|
||||
// Use the account
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Zaps Implementation
|
||||
|
||||
Notedeck implements the zap (tipping) functionality according to the Nostr protocol:
|
||||
|
||||
1. Creates a zap request note (kind 9734)
|
||||
2. Fetches a Lightning invoice via LNURL or LUD-16
|
||||
3. Pays the invoice using a connected wallet
|
||||
4. Tracks the zap state
|
||||
|
||||
### Image Caching
|
||||
|
||||
The image caching system efficiently manages images and animated GIFs:
|
||||
|
||||
1. Downloads images from URLs
|
||||
2. Stores them in a local cache
|
||||
3. Handles conversion between formats
|
||||
4. Manages memory usage
|
||||
|
||||
### Persistent Storage
|
||||
|
||||
Notedeck provides several persistence mechanisms:
|
||||
|
||||
- `AccountStorage` - For user accounts
|
||||
- `TimedSerializer` - For settings that need to be saved after a delay
|
||||
- Various handlers for specific settings (zoom, theme, app size)
|
||||
|
||||
### Localization System
|
||||
|
||||
Notedeck includes a comprehensive internationalization system built on the [Fluent](https://projectfluent.org/) translation framework. The system is designed for performance and developer experience.
|
||||
|
||||
#### Architecture
|
||||
|
||||
The localization system consists of several key components:
|
||||
|
||||
1. **LocalizationManager** - Core functionality for managing locales and translations
|
||||
2. **LocalizationContext** - Thread-safe context for sharing localization across the application
|
||||
3. **Fluent Resources** - Translation files in `.ftl` format stored in `assets/translations/`
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Efficient Caching**: Parsed Fluent resources and formatted strings are cached for performance
|
||||
- **Thread Safety**: Uses `RwLock` for safe concurrent access
|
||||
- **Dynamic Locale Switching**: Change languages at runtime without restarting
|
||||
- **Argument Support**: Localized strings can include dynamic arguments
|
||||
- **Development Tools**: Pseudolocale support for testing UI layout
|
||||
|
||||
#### Using the tr! and tr_plural! Macros
|
||||
|
||||
The `tr!` and `tr_plural!` macros are the primary way to use localization in Notedeck code. They provide a convenient, type-safe interface for getting localized strings.
|
||||
|
||||
##### The tr! Macro
|
||||
|
||||
```rust
|
||||
use notedeck::tr;
|
||||
|
||||
// Simple string with comment
|
||||
let welcome = tr!("Welcome to Notedeck!", "Main welcome message");
|
||||
let cancel = tr!("Cancel", "Button label to cancel an action");
|
||||
|
||||
// String with parameters
|
||||
let greeting = tr!("Hello, {name}!", "Greeting message", name="Alice");
|
||||
|
||||
// Multiple parameters
|
||||
let message = tr!(
|
||||
"Welcome {name} to {app}!",
|
||||
"Welcome message with app name",
|
||||
name="Alice",
|
||||
app="Notedeck"
|
||||
);
|
||||
|
||||
// In UI components
|
||||
ui.button(tr!("Reply to {user}", "Reply button text", user="alice@example.com"));
|
||||
```
|
||||
|
||||
##### The tr_plural! Macro
|
||||
|
||||
Use tr_plural! when there can be multiple variations of the same string depending on
|
||||
some numeric count.
|
||||
|
||||
Not all languages follow the same pluralization rules
|
||||
|
||||
```rust
|
||||
use notedeck::tr_plural;
|
||||
|
||||
// Simple pluralization
|
||||
let count = 5;
|
||||
let message = tr_plural!(
|
||||
"You have {count} note", // Singular form
|
||||
"You have {count} notes", // Plural form
|
||||
"Note count message", // Comment
|
||||
count // Count value
|
||||
);
|
||||
|
||||
// With additional parameters
|
||||
let user = "Alice";
|
||||
let message = tr_plural!(
|
||||
"{user} has {count} note", // Singular
|
||||
"{user} has {count} notes", // Plural
|
||||
"User note count message", // Comment
|
||||
count, // Count
|
||||
user=user // Additional parameter
|
||||
);
|
||||
```
|
||||
|
||||
##### Key Features
|
||||
|
||||
- **Automatic Key Normalization**: Converts messages and comments into valid FTL keys
|
||||
- **Fallback Handling**: Falls back to original message if translation not found
|
||||
- **Parameter Interpolation**: Automatically handles named parameters
|
||||
- **Comment Context**: Provides context for translators
|
||||
|
||||
##### Best Practices
|
||||
|
||||
1. **Always Include Comments**: Comments provide context for translators
|
||||
```rust
|
||||
// Good
|
||||
tr!("Add", "Button label to add something")
|
||||
|
||||
// Bad
|
||||
tr!("Add", "")
|
||||
```
|
||||
|
||||
2. **Use Descriptive Comments**: Make comments specific and helpful
|
||||
```rust
|
||||
// Good
|
||||
tr!("Reply", "Button to reply to a note")
|
||||
|
||||
// Bad
|
||||
tr!("Reply", "Reply")
|
||||
```
|
||||
|
||||
3. **Consistent Parameter Names**: Use consistent parameter names across related strings
|
||||
```rust
|
||||
// Consistent
|
||||
tr!("Follow {user}", "Follow button", user="alice")
|
||||
tr!("Unfollow {user}", "Unfollow button", user="alice")
|
||||
```
|
||||
|
||||
4. **Always use tr_plural! for plural strings**: Not all languages follow English pluralization rules
|
||||
```rust
|
||||
// Good
|
||||
// Each language can have more (or less) than just two pluralization forms.
|
||||
// Let the translators and the localization system help you figure that out implicitly.
|
||||
let message = tr_plural!(
|
||||
"You have {count} note", // Singular form
|
||||
"You have {count} notes", // Plural form
|
||||
"Note count message", // Comment
|
||||
count // Count value
|
||||
);
|
||||
|
||||
// Bad
|
||||
// Not all languages follow pluralization rules of English.
|
||||
// Some languages can have more (or less) than two variations!
|
||||
if count == 1 {
|
||||
tr!("You have 1 note", "Note count message")
|
||||
} else {
|
||||
tr!("You have {count} notes", "Note count message")
|
||||
}
|
||||
```
|
||||
|
||||
#### Translation File Format
|
||||
|
||||
Translation files use the [Fluent](https://projectfluent.org/) format (`.ftl`).
|
||||
|
||||
Developers should never create their own `.ftl` files. Whenever user-facing strings are changed in code, run `python3 scripts/export_source_strings.py`. This script will generate `assets/translations/en-US/main.ftl` and `assets/translations/en-XA/main.ftl`. The format of the files look like the following:
|
||||
|
||||
```ftl
|
||||
# Simple string
|
||||
welcome_message = Welcome to Notedeck!
|
||||
|
||||
# String with arguments
|
||||
welcome_user = Welcome {$name}!
|
||||
|
||||
# String with pluralization
|
||||
note_count = {$count ->
|
||||
[1] One note
|
||||
*[other] {$count} notes
|
||||
}
|
||||
```
|
||||
|
||||
#### Adding New Languages
|
||||
|
||||
TODO
|
||||
|
||||
#### Development with Pseudolocale (en-XA)
|
||||
|
||||
For testing that all user-facing strings are going through the localization system and that the
|
||||
UI layout renders well with different language translations, enable the pseudolocale:
|
||||
|
||||
```bash
|
||||
cargo run -- --debug --locale en-XA
|
||||
```
|
||||
|
||||
The pseudolocale (`en-XA`) transforms English text in a way that is still readable but makes adjustments obvious enough that they are different from the original text (such as replacing English letters with accented equivalents), helping identify potential UI layout issues once it gets translated
|
||||
to other languages.
|
||||
|
||||
Example transformations:
|
||||
- "Add relay" → "[Àdd rélày]"
|
||||
- "Cancel" → "[Çàñçél]"
|
||||
- "Confirm" → "[Çóñfírm]"
|
||||
|
||||
#### Performance Considerations
|
||||
|
||||
- **Resource Caching**: Parsed Fluent resources are cached per locale
|
||||
- **String Caching**: Simple strings (without arguments) are cached for repeated access
|
||||
- **Cache Management**: Caches are automatically cleared when switching locales
|
||||
- **Memory Limits**: String cache size can be limited to prevent memory growth
|
||||
|
||||
#### Testing Localization
|
||||
|
||||
The localization system includes comprehensive tests:
|
||||
|
||||
```bash
|
||||
# Run localization tests
|
||||
cargo test i18n
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Relay Connection Issues**
|
||||
- Check network connectivity
|
||||
- Verify relay URLs are correct
|
||||
- Look for relay debug messages
|
||||
|
||||
2. **Database Errors**
|
||||
- Ensure the database path is writable
|
||||
- Check for database corruption
|
||||
- Increase map size if needed
|
||||
|
||||
3. **Performance Issues**
|
||||
- Monitor the frame history
|
||||
- Check for large image caches
|
||||
- Consider reducing the number of active subscriptions
|
||||
|
||||
4. **Localization Issues**
|
||||
- Verify translation files exist in the correct directory structure
|
||||
- Check that locale codes are valid (e.g., `en-US`, `es-ES`)
|
||||
- Ensure FTL files are properly formatted
|
||||
- Look for missing translation keys in logs
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to Notedeck:
|
||||
|
||||
1. Follow the existing code style
|
||||
2. Add tests for new functionality
|
||||
3. Update documentation as needed
|
||||
4. Keep performance in mind, especially for mobile targets
|
||||
5. For UI changes, test with pseudolocale enabled
|
||||
6. When adding new strings, ensure they are properly localized
|
||||
@@ -1,30 +0,0 @@
|
||||
# Notedeck
|
||||
|
||||
Notedeck is a shared Rust library that provides the core functionality for building Nostr client applications. It serves as the foundation for various Notedeck applications like notedeck_chrome, notedeck_columns, and notedeck_dave.
|
||||
|
||||
## Overview
|
||||
|
||||
The Notedeck crate implements common data types, utilities, and logic used across all Notedeck applications. It provides a unified interface for interacting with the Nostr protocol, managing accounts, handling note data, and rendering UI components.
|
||||
|
||||
Key features include:
|
||||
|
||||
- **Nostr Protocol Integration**: Connect to relays, subscribe to events, publish notes
|
||||
- **Account Management**: Handle user accounts, keypairs, and profiles
|
||||
- **Note Handling**: Cache and process notes efficiently
|
||||
- **UI Components**: Common UI elements and styles
|
||||
- **Image Caching**: Efficient image and GIF caching system
|
||||
- **Wallet Integration**: Lightning wallet support with zaps functionality
|
||||
- **Theme Support**: Customizable themes and styles
|
||||
- **Storage**: Persistent storage for settings and data
|
||||
|
||||
## Applications
|
||||
|
||||
This crate serves as the foundation for several Notedeck applications:
|
||||
|
||||
- **notedeck_chrome** - The browser chrome, manages a toolbar for switching between different clients
|
||||
- **notedeck_columns** - A column-based Nostr client interface
|
||||
- **notedeck_dave** - A nostr ai assistant
|
||||
|
||||
## License
|
||||
|
||||
GPLv2
|
||||
@@ -1,526 +0,0 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::account::cache::AccountCache;
|
||||
use crate::account::contacts::Contacts;
|
||||
use crate::account::mute::AccountMutedData;
|
||||
use crate::account::relay::{
|
||||
modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction,
|
||||
RelayDefaults,
|
||||
};
|
||||
use crate::storage::AccountStorageWriter;
|
||||
use crate::user_account::UserAccountSerializable;
|
||||
use crate::{
|
||||
AccountStorage, MuteFun, SingleUnkIdAction, UnifiedSubscription, UnknownIds, UserAccount,
|
||||
ZapWallet,
|
||||
};
|
||||
use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Note, Transaction};
|
||||
|
||||
// TODO: remove this
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The interface for managing the user's accounts.
|
||||
/// Represents all user-facing operations related to account management.
|
||||
pub struct Accounts {
|
||||
pub cache: AccountCache,
|
||||
storage_writer: Option<AccountStorageWriter>,
|
||||
relay_defaults: RelayDefaults,
|
||||
subs: AccountSubs,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
key_store: Option<AccountStorage>,
|
||||
forced_relays: Vec<String>,
|
||||
fallback: Pubkey,
|
||||
ndb: &mut Ndb,
|
||||
txn: &Transaction,
|
||||
pool: &mut RelayPool,
|
||||
ctx: &egui::Context,
|
||||
unknown_ids: &mut UnknownIds,
|
||||
) -> Self {
|
||||
let (mut cache, unknown_id) = AccountCache::new(UserAccount::new(
|
||||
Keypair::only_pubkey(fallback),
|
||||
AccountData::new(fallback.bytes()),
|
||||
));
|
||||
|
||||
unknown_id.process_action(unknown_ids, ndb, txn);
|
||||
|
||||
let mut storage_writer = None;
|
||||
if let Some(keystore) = key_store {
|
||||
let (reader, writer) = keystore.rw();
|
||||
match reader.get_accounts() {
|
||||
Ok(accounts) => {
|
||||
for account in accounts {
|
||||
add_account_from_storage(&mut cache, account).process_action(
|
||||
unknown_ids,
|
||||
ndb,
|
||||
txn,
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("could not get keys: {e}");
|
||||
}
|
||||
}
|
||||
if let Some(selected) = reader.get_selected_key().ok().flatten() {
|
||||
cache.select(selected);
|
||||
}
|
||||
|
||||
storage_writer = Some(writer);
|
||||
};
|
||||
|
||||
let relay_defaults = RelayDefaults::new(forced_relays);
|
||||
|
||||
let selected = cache.selected_mut();
|
||||
let selected_data = &mut selected.data;
|
||||
|
||||
selected_data.query(ndb, txn);
|
||||
|
||||
let subs = {
|
||||
AccountSubs::new(
|
||||
ndb,
|
||||
pool,
|
||||
&relay_defaults,
|
||||
&selected.key.pubkey,
|
||||
selected_data,
|
||||
create_wakeup(ctx),
|
||||
)
|
||||
};
|
||||
|
||||
Accounts {
|
||||
cache,
|
||||
storage_writer,
|
||||
relay_defaults,
|
||||
subs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_account(
|
||||
&mut self,
|
||||
pk: &Pubkey,
|
||||
ndb: &mut Ndb,
|
||||
pool: &mut RelayPool,
|
||||
ctx: &egui::Context,
|
||||
) -> bool {
|
||||
let Some(resp) = self.cache.remove(pk) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if pk != self.cache.fallback() {
|
||||
if let Some(key_store) = &self.storage_writer {
|
||||
if let Err(e) = key_store.remove_key(&resp.deleted) {
|
||||
tracing::error!("Could not remove account {pk}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(swap_to) = resp.swap_to {
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
self.select_account_internal(&swap_to, ndb, &txn, pool, ctx);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool {
|
||||
self.cache
|
||||
.get(pubkey)
|
||||
.is_some_and(|u| u.key.secret_key.is_some())
|
||||
}
|
||||
|
||||
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
|
||||
pub fn add_account(&mut self, kp: Keypair) -> Option<AddAccountResponse> {
|
||||
let acc = if let Some(acc) = self.cache.get_mut(&kp.pubkey) {
|
||||
if kp.secret_key.is_none() || acc.key.secret_key.is_some() {
|
||||
tracing::info!("Already have account, not adding");
|
||||
return None;
|
||||
}
|
||||
|
||||
acc.key = kp.clone();
|
||||
AccType::Acc(&*acc)
|
||||
} else {
|
||||
let new_account_data = AccountData::new(kp.pubkey.bytes());
|
||||
AccType::Entry(
|
||||
self.cache
|
||||
.add(UserAccount::new(kp.clone(), new_account_data)),
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(key_store) = &self.storage_writer {
|
||||
if let Err(e) = key_store.write_account(&acc.get_acc().into()) {
|
||||
tracing::error!("Could not add key for {:?}: {e}", kp.pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
Some(AddAccountResponse {
|
||||
switch_to: kp.pubkey,
|
||||
unk_id_action: SingleUnkIdAction::pubkey(kp.pubkey),
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the `UserAccount` via callback and save the result to disk.
|
||||
/// return true if the update was successful
|
||||
pub fn update_current_account(&mut self, update: impl FnOnce(&mut UserAccount)) -> bool {
|
||||
let cur_account = self.get_selected_account_mut();
|
||||
|
||||
update(cur_account);
|
||||
|
||||
let cur_acc = self.get_selected_account();
|
||||
|
||||
let Some(key_store) = &self.storage_writer else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Err(err) = key_store.write_account(&cur_acc.into()) {
|
||||
tracing::error!("Could not add account {:?} to storage: {err}", cur_acc.key);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn selected_filled(&self) -> Option<FilledKeypair<'_>> {
|
||||
self.get_selected_account().key.to_full()
|
||||
}
|
||||
|
||||
/// Get the selected account's pubkey as bytes. Common operation so
|
||||
/// we make it a helper here.
|
||||
pub fn selected_account_pubkey_bytes(&self) -> &[u8; 32] {
|
||||
self.get_selected_account().key.pubkey.bytes()
|
||||
}
|
||||
|
||||
pub fn selected_account_pubkey(&self) -> &Pubkey {
|
||||
&self.get_selected_account().key.pubkey
|
||||
}
|
||||
|
||||
pub fn get_selected_account(&self) -> &UserAccount {
|
||||
self.cache.selected()
|
||||
}
|
||||
|
||||
pub fn selected_account_has_wallet(&self) -> bool {
|
||||
self.get_selected_account().wallet.is_some()
|
||||
}
|
||||
|
||||
fn get_selected_account_mut(&mut self) -> &mut UserAccount {
|
||||
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()
|
||||
}
|
||||
|
||||
fn get_selected_account_data(&self) -> &AccountData {
|
||||
&self.cache.selected().data
|
||||
}
|
||||
|
||||
pub fn select_account(
|
||||
&mut self,
|
||||
pk_to_select: &Pubkey,
|
||||
ndb: &mut Ndb,
|
||||
txn: &Transaction,
|
||||
pool: &mut RelayPool,
|
||||
ctx: &egui::Context,
|
||||
) {
|
||||
if !self.cache.select(*pk_to_select) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.select_account_internal(pk_to_select, ndb, txn, pool, ctx);
|
||||
}
|
||||
|
||||
/// Have already selected in `AccountCache`, updating other things
|
||||
fn select_account_internal(
|
||||
&mut self,
|
||||
pk_to_select: &Pubkey,
|
||||
ndb: &mut Ndb,
|
||||
txn: &Transaction,
|
||||
pool: &mut RelayPool,
|
||||
ctx: &egui::Context,
|
||||
) {
|
||||
if let Some(key_store) = &self.storage_writer {
|
||||
if let Err(e) = key_store.select_key(Some(*pk_to_select)) {
|
||||
tracing::error!("Could not select key {:?}: {e}", pk_to_select);
|
||||
}
|
||||
}
|
||||
|
||||
self.get_selected_account_mut().data.query(ndb, txn);
|
||||
self.subs.swap_to(
|
||||
ndb,
|
||||
pool,
|
||||
&self.relay_defaults,
|
||||
pk_to_select,
|
||||
&self.cache.selected().data,
|
||||
create_wakeup(ctx),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn mutefun(&self) -> Box<MuteFun> {
|
||||
let account_data = self.get_selected_account_data();
|
||||
|
||||
let muted = Arc::clone(&account_data.muted.muted);
|
||||
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
|
||||
}
|
||||
|
||||
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
|
||||
let data = &self.get_selected_account().data;
|
||||
// send the active account's relay list subscription
|
||||
pool.send_to(
|
||||
&ClientMessage::req(
|
||||
self.subs.relay.remote.clone(),
|
||||
vec![data.relay.filter.clone()],
|
||||
),
|
||||
relay_url,
|
||||
);
|
||||
// send the active account's muted subscription
|
||||
pool.send_to(
|
||||
&ClientMessage::req(
|
||||
self.subs.mute.remote.clone(),
|
||||
vec![data.muted.filter.clone()],
|
||||
),
|
||||
relay_url,
|
||||
);
|
||||
pool.send_to(
|
||||
&ClientMessage::req(
|
||||
self.subs.contacts.remote.clone(),
|
||||
vec![data.contacts.filter.clone()],
|
||||
),
|
||||
relay_url,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
|
||||
// IMPORTANT - This function is called in the UI update loop,
|
||||
// make sure it is fast when idle
|
||||
|
||||
let Some(update) = self
|
||||
.cache
|
||||
.selected_mut()
|
||||
.data
|
||||
.poll_for_updates(ndb, &self.subs)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
match update {
|
||||
// If needed, update the relay configuration
|
||||
AccountDataUpdate::Relay => {
|
||||
let acc = self.cache.selected();
|
||||
update_relay_configuration(
|
||||
pool,
|
||||
&self.relay_defaults,
|
||||
&acc.key.pubkey,
|
||||
&acc.data.relay,
|
||||
create_wakeup(ctx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> {
|
||||
self.cache.get(pubkey).and_then(|r| r.key.to_full())
|
||||
}
|
||||
|
||||
pub fn process_relay_action(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
pool: &mut RelayPool,
|
||||
action: RelayAction,
|
||||
) {
|
||||
let acc = self.cache.selected_mut();
|
||||
modify_advertised_relays(&acc.key, action, pool, &self.relay_defaults, &mut acc.data);
|
||||
|
||||
update_relay_configuration(
|
||||
pool,
|
||||
&self.relay_defaults,
|
||||
&acc.key.pubkey,
|
||||
&acc.data.relay,
|
||||
create_wakeup(ctx),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_subs(&self) -> &AccountSubs {
|
||||
&self.subs
|
||||
}
|
||||
}
|
||||
|
||||
enum AccType<'a> {
|
||||
Entry(hashbrown::hash_map::OccupiedEntry<'a, Pubkey, UserAccount>),
|
||||
Acc(&'a UserAccount),
|
||||
}
|
||||
|
||||
impl<'a> AccType<'a> {
|
||||
fn get_acc(&'a self) -> &'a UserAccount {
|
||||
match self {
|
||||
AccType::Entry(occupied_entry) => occupied_entry.get(),
|
||||
AccType::Acc(user_account) => user_account,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static {
|
||||
let ctx = ctx.clone();
|
||||
move || {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn add_account_from_storage(
|
||||
cache: &mut AccountCache,
|
||||
user_account_serializable: UserAccountSerializable,
|
||||
) -> SingleUnkIdAction {
|
||||
let Some(acc) = get_acc_from_storage(user_account_serializable) else {
|
||||
return SingleUnkIdAction::NoAction;
|
||||
};
|
||||
|
||||
let pk = acc.key.pubkey;
|
||||
cache.add(acc);
|
||||
|
||||
SingleUnkIdAction::pubkey(pk)
|
||||
}
|
||||
|
||||
fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> Option<UserAccount> {
|
||||
let keypair = user_account_serializable.key;
|
||||
let new_account_data = AccountData::new(keypair.pubkey.bytes());
|
||||
|
||||
let mut wallet = None;
|
||||
if let Some(wallet_s) = user_account_serializable.wallet {
|
||||
let m_wallet: Result<crate::ZapWallet, crate::Error> = wallet_s.into();
|
||||
match m_wallet {
|
||||
Ok(w) => wallet = Some(w),
|
||||
Err(e) => {
|
||||
tracing::error!("Problem creating wallet from disk: {e}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some(UserAccount {
|
||||
key: keypair,
|
||||
wallet,
|
||||
data: new_account_data,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AccountData {
|
||||
pub(crate) relay: AccountRelayData,
|
||||
pub(crate) muted: AccountMutedData,
|
||||
pub contacts: Contacts,
|
||||
}
|
||||
|
||||
impl AccountData {
|
||||
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||
Self {
|
||||
relay: AccountRelayData::new(pubkey),
|
||||
muted: AccountMutedData::new(pubkey),
|
||||
contacts: Contacts::new(pubkey),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn poll_for_updates(
|
||||
&mut self,
|
||||
ndb: &Ndb,
|
||||
subs: &AccountSubs,
|
||||
) -> Option<AccountDataUpdate> {
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
let mut resp = None;
|
||||
if self.relay.poll_for_updates(ndb, &txn, subs.relay.local) {
|
||||
resp = Some(AccountDataUpdate::Relay);
|
||||
}
|
||||
|
||||
self.muted.poll_for_updates(ndb, &txn, subs.mute.local);
|
||||
self.contacts
|
||||
.poll_for_updates(ndb, &txn, subs.contacts.local);
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
/// Note: query should be called as close to the subscription as possible
|
||||
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||
self.relay.query(ndb, txn);
|
||||
self.muted.query(ndb, txn);
|
||||
self.contacts.query(ndb, txn);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum AccountDataUpdate {
|
||||
Relay,
|
||||
}
|
||||
|
||||
pub struct AddAccountResponse {
|
||||
pub switch_to: Pubkey,
|
||||
pub unk_id_action: SingleUnkIdAction,
|
||||
}
|
||||
|
||||
pub struct AccountSubs {
|
||||
relay: UnifiedSubscription,
|
||||
mute: UnifiedSubscription,
|
||||
pub contacts: UnifiedSubscription,
|
||||
}
|
||||
|
||||
impl AccountSubs {
|
||||
pub(super) fn new(
|
||||
ndb: &mut Ndb,
|
||||
pool: &mut RelayPool,
|
||||
relay_defaults: &RelayDefaults,
|
||||
pk: &Pubkey,
|
||||
data: &AccountData,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Self {
|
||||
let relay = subscribe(ndb, pool, &data.relay.filter);
|
||||
let mute = subscribe(ndb, pool, &data.muted.filter);
|
||||
let contacts = subscribe(ndb, pool, &data.contacts.filter);
|
||||
update_relay_configuration(pool, relay_defaults, pk, &data.relay, wakeup);
|
||||
|
||||
Self {
|
||||
relay,
|
||||
mute,
|
||||
contacts,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn swap_to(
|
||||
&mut self,
|
||||
ndb: &mut Ndb,
|
||||
pool: &mut RelayPool,
|
||||
relay_defaults: &RelayDefaults,
|
||||
pk: &Pubkey,
|
||||
new_selection_data: &AccountData,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) {
|
||||
unsubscribe(ndb, pool, &self.relay);
|
||||
unsubscribe(ndb, pool, &self.mute);
|
||||
unsubscribe(ndb, pool, &self.contacts);
|
||||
|
||||
*self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup);
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe(ndb: &Ndb, pool: &mut RelayPool, filter: &nostrdb::Filter) -> UnifiedSubscription {
|
||||
let filters = vec![filter.clone()];
|
||||
let sub = ndb
|
||||
.subscribe(&filters)
|
||||
.expect("ndb relay list subscription");
|
||||
|
||||
// remote subscription
|
||||
let subid = Uuid::new_v4().to_string();
|
||||
pool.subscribe(subid.clone(), filters);
|
||||
|
||||
UnifiedSubscription {
|
||||
local: sub,
|
||||
remote: subid,
|
||||
}
|
||||
}
|
||||
|
||||
fn unsubscribe(ndb: &mut Ndb, pool: &mut RelayPool, sub: &UnifiedSubscription) {
|
||||
pool.unsubscribe(sub.remote.clone());
|
||||
|
||||
// local subscription
|
||||
ndb.unsubscribe(sub.local)
|
||||
.expect("ndb relay list unsubscribe");
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
use enostr::Pubkey;
|
||||
use hashbrown::{hash_map::OccupiedEntry, HashMap};
|
||||
|
||||
use crate::{SingleUnkIdAction, UserAccount};
|
||||
|
||||
pub struct AccountCache {
|
||||
selected: Pubkey,
|
||||
fallback: Pubkey,
|
||||
fallback_account: UserAccount,
|
||||
|
||||
// never empty at rest
|
||||
accounts: HashMap<Pubkey, UserAccount>,
|
||||
}
|
||||
|
||||
impl AccountCache {
|
||||
pub(super) fn new(fallback: UserAccount) -> (Self, SingleUnkIdAction) {
|
||||
let mut accounts = HashMap::with_capacity(1);
|
||||
|
||||
let pk = fallback.key.pubkey;
|
||||
accounts.insert(pk, fallback.clone());
|
||||
|
||||
(
|
||||
Self {
|
||||
selected: pk,
|
||||
fallback: pk,
|
||||
fallback_account: fallback,
|
||||
accounts,
|
||||
},
|
||||
SingleUnkIdAction::pubkey(pk),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get(&self, pk: &Pubkey) -> Option<&UserAccount> {
|
||||
self.accounts.get(pk)
|
||||
}
|
||||
|
||||
pub fn get_bytes(&self, pk: &[u8; 32]) -> Option<&UserAccount> {
|
||||
self.accounts.get(pk)
|
||||
}
|
||||
|
||||
pub(super) fn get_mut(&mut self, pk: &Pubkey) -> Option<&mut UserAccount> {
|
||||
self.accounts.get_mut(pk)
|
||||
}
|
||||
|
||||
pub(super) fn add<'a>(
|
||||
&'a mut self,
|
||||
account: UserAccount,
|
||||
) -> OccupiedEntry<'a, Pubkey, UserAccount> {
|
||||
let pk = account.key.pubkey;
|
||||
self.accounts.entry(pk).insert(account)
|
||||
}
|
||||
|
||||
pub(super) fn remove(&mut self, pk: &Pubkey) -> Option<AccountDeletionResponse> {
|
||||
if *pk == self.fallback && self.accounts.len() == 1 {
|
||||
// no point in removing it since it'll just get re-added anyway
|
||||
return None;
|
||||
}
|
||||
|
||||
let removed = self.accounts.remove(pk)?;
|
||||
|
||||
if self.accounts.is_empty() {
|
||||
self.accounts
|
||||
.insert(self.fallback, self.fallback_account.clone());
|
||||
}
|
||||
|
||||
if self.selected == *pk {
|
||||
// TODO(kernelkind): choose next better
|
||||
let (next, _) = self
|
||||
.accounts
|
||||
.iter()
|
||||
.next()
|
||||
.expect("accounts can never be empty");
|
||||
self.selected = *next;
|
||||
|
||||
return Some(AccountDeletionResponse {
|
||||
deleted: removed.key,
|
||||
swap_to: Some(*next),
|
||||
});
|
||||
}
|
||||
|
||||
Some(AccountDeletionResponse {
|
||||
deleted: removed.key,
|
||||
swap_to: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// guarenteed that all selected exist in accounts
|
||||
pub(super) fn select(&mut self, pk: Pubkey) -> bool {
|
||||
if !self.accounts.contains_key(&pk) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.selected = pk;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> &UserAccount {
|
||||
self.accounts
|
||||
.get(&self.selected)
|
||||
.expect("guarenteed that selected exists in accounts")
|
||||
}
|
||||
|
||||
pub(super) fn selected_mut(&mut self) -> &mut UserAccount {
|
||||
self.accounts
|
||||
.get_mut(&self.selected)
|
||||
.expect("guarenteed that selected exists in accounts")
|
||||
}
|
||||
|
||||
pub fn fallback(&self) -> &Pubkey {
|
||||
&self.fallback
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a AccountCache {
|
||||
type Item = (&'a Pubkey, &'a UserAccount);
|
||||
type IntoIter = hashbrown::hash_map::Iter<'a, Pubkey, UserAccount>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.accounts.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccountDeletionResponse {
|
||||
pub deleted: enostr::Keypair,
|
||||
pub swap_to: Option<Pubkey>,
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Contacts {
|
||||
pub filter: Filter,
|
||||
pub(super) state: ContactState,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ContactState {
|
||||
Unreceived,
|
||||
Received {
|
||||
contacts: HashSet<Pubkey>,
|
||||
note_key: NoteKey,
|
||||
timestamp: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
|
||||
pub enum IsFollowing {
|
||||
/// We don't have the contact list, so we don't know
|
||||
Unknown,
|
||||
|
||||
/// We are follow
|
||||
Yes,
|
||||
|
||||
No,
|
||||
}
|
||||
|
||||
impl Contacts {
|
||||
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||
let filter = Filter::new().authors([pubkey]).kinds([3]).limit(1).build();
|
||||
|
||||
Self {
|
||||
filter,
|
||||
state: ContactState::Unreceived,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||
let binding = ndb
|
||||
.query(txn, std::slice::from_ref(&self.filter), 1)
|
||||
.expect("query user relays results");
|
||||
|
||||
let Some(res) = binding.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
update_state(&mut self.state, &res.note, res.note_key);
|
||||
}
|
||||
|
||||
pub fn is_following(&self, other_pubkey: &[u8; 32]) -> IsFollowing {
|
||||
match &self.state {
|
||||
ContactState::Unreceived => IsFollowing::Unknown,
|
||||
ContactState::Received {
|
||||
contacts,
|
||||
note_key: _,
|
||||
timestamp: _,
|
||||
} => {
|
||||
if contacts.contains(other_pubkey) {
|
||||
IsFollowing::Yes
|
||||
} else {
|
||||
IsFollowing::No
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
|
||||
let nks = ndb.poll_for_notes(sub, 1);
|
||||
|
||||
let Some(key) = nks.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let note = match ndb.get_note_by_key(txn, *key) {
|
||||
Ok(note) => note,
|
||||
Err(e) => {
|
||||
tracing::error!("Could not find note at key {:?}: {e}", key);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let ContactState::Received {
|
||||
contacts: _,
|
||||
note_key: _,
|
||||
timestamp,
|
||||
} = self.get_state()
|
||||
{
|
||||
if *timestamp > note.created_at() {
|
||||
// the current contact list is more up to date than the one we just received. ignore it.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
update_state(&mut self.state, ¬e, *key);
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> &ContactState {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
|
||||
match state {
|
||||
ContactState::Unreceived => {
|
||||
*state = ContactState::Received {
|
||||
contacts: get_contacts_owned(note),
|
||||
note_key: key,
|
||||
timestamp: note.created_at(),
|
||||
};
|
||||
}
|
||||
ContactState::Received {
|
||||
contacts,
|
||||
note_key,
|
||||
timestamp,
|
||||
} => {
|
||||
update_contacts(contacts, note);
|
||||
*note_key = key;
|
||||
*timestamp = note.created_at();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn get_contacts<'a>(note: &Note<'a>) -> HashSet<&'a [u8; 32]> {
|
||||
let mut contacts = HashSet::with_capacity(note.tags().count().into());
|
||||
|
||||
for tag in note.tags() {
|
||||
if tag.count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some("p") = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(cur_id) = tag.get_id(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
contacts.insert(cur_id);
|
||||
}
|
||||
|
||||
contacts
|
||||
}
|
||||
|
||||
fn get_contacts_owned(note: &Note<'_>) -> HashSet<Pubkey> {
|
||||
get_contacts(note)
|
||||
.iter()
|
||||
.map(|p| Pubkey::new(**p))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_contacts(cur: &mut HashSet<Pubkey>, new: &Note<'_>) {
|
||||
let new_contacts = get_contacts(new);
|
||||
|
||||
cur.retain(|pk| new_contacts.contains(pk.bytes()));
|
||||
|
||||
new_contacts.iter().for_each(|c| {
|
||||
if !cur.contains(*c) {
|
||||
cur.insert(Pubkey::new(**c));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
pub mod accounts;
|
||||
pub mod cache;
|
||||
pub mod contacts;
|
||||
pub mod mute;
|
||||
pub mod relay;
|
||||
|
||||
pub const FALLBACK_PUBKEY: fn() -> enostr::Pubkey = || {
|
||||
enostr::Pubkey::new([
|
||||
170, 115, 48, 129, 228, 240, 247, 157, 212, 48, 35, 216, 152, 50, 101, 89, 63, 43, 65, 169,
|
||||
136, 103, 28, 252, 239, 63, 72, 155, 145, 173, 147, 254,
|
||||
])
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::Muted;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AccountMutedData {
|
||||
pub filter: Filter,
|
||||
pub muted: Arc<Muted>,
|
||||
}
|
||||
|
||||
impl AccountMutedData {
|
||||
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||
// Construct a filter for the user's NIP-51 muted list
|
||||
let filter = Filter::new()
|
||||
.authors([pubkey])
|
||||
.kinds([10000])
|
||||
.limit(1)
|
||||
.build();
|
||||
|
||||
AccountMutedData {
|
||||
filter,
|
||||
muted: Arc::new(Muted::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||
// Query the ndb immediately to see if the user's muted list is already there
|
||||
let lim = self
|
||||
.filter
|
||||
.limit()
|
||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let nks = ndb
|
||||
.query(txn, std::slice::from_ref(&self.filter), lim)
|
||||
.expect("query user muted results")
|
||||
.iter()
|
||||
.map(|qr| qr.note_key)
|
||||
.collect::<Vec<NoteKey>>();
|
||||
let muted = Self::harvest_nip51_muted(ndb, txn, &nks);
|
||||
debug!("initial muted {:?}", muted);
|
||||
|
||||
self.muted = Arc::new(muted);
|
||||
}
|
||||
|
||||
pub(crate) fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted {
|
||||
let mut muted = Muted::default();
|
||||
for nk in nks.iter() {
|
||||
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
|
||||
for tag in note.tags() {
|
||||
match tag.get(0).and_then(|t| t.variant().str()) {
|
||||
Some("p") => {
|
||||
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
|
||||
muted.pubkeys.insert(*id);
|
||||
}
|
||||
}
|
||||
Some("t") => {
|
||||
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
|
||||
muted.hashtags.insert(str.to_string());
|
||||
}
|
||||
}
|
||||
Some("word") => {
|
||||
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
|
||||
muted.words.insert(str.to_string());
|
||||
}
|
||||
}
|
||||
Some("e") => {
|
||||
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
|
||||
muted.threads.insert(*id);
|
||||
}
|
||||
}
|
||||
Some("alt") => {
|
||||
// maybe we can ignore these?
|
||||
}
|
||||
Some(x) => error!("query_nip51_muted: unexpected tag: {}", x),
|
||||
None => error!(
|
||||
"query_nip51_muted: bad tag value: {:?}",
|
||||
tag.get_unchecked(0).variant()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
muted
|
||||
}
|
||||
|
||||
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
|
||||
let nks = ndb.poll_for_notes(sub, 1);
|
||||
|
||||
if nks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let muted = AccountMutedData::harvest_nip51_muted(ndb, txn, &nks);
|
||||
debug!("updated muted {:?}", muted);
|
||||
self.muted = Arc::new(muted);
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
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;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AccountRelayData {
|
||||
pub filter: Filter,
|
||||
pub local: BTreeSet<RelaySpec>, // used locally but not advertised
|
||||
pub advertised: BTreeSet<RelaySpec>, // advertised via NIP-65
|
||||
}
|
||||
|
||||
impl AccountRelayData {
|
||||
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||
// Construct a filter for the user's NIP-65 relay list
|
||||
let filter = Filter::new()
|
||||
.authors([pubkey])
|
||||
.kinds([10002])
|
||||
.limit(1)
|
||||
.build();
|
||||
|
||||
AccountRelayData {
|
||||
filter,
|
||||
local: BTreeSet::new(),
|
||||
advertised: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||
// Query the ndb immediately to see if the user list is already there
|
||||
let lim = self
|
||||
.filter
|
||||
.limit()
|
||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let nks = ndb
|
||||
.query(txn, std::slice::from_ref(&self.filter), lim)
|
||||
.expect("query user relays results")
|
||||
.iter()
|
||||
.map(|qr| qr.note_key)
|
||||
.collect::<Vec<NoteKey>>();
|
||||
let relays = Self::harvest_nip65_relays(ndb, txn, &nks);
|
||||
debug!("initial relays {:?}", relays);
|
||||
|
||||
self.advertised = relays.into_iter().collect()
|
||||
}
|
||||
|
||||
// standardize the format (ie, trailing slashes) to avoid dups
|
||||
pub fn canonicalize_url(url: &str) -> String {
|
||||
match Url::parse(url) {
|
||||
Ok(parsed_url) => parsed_url.to_string(),
|
||||
Err(_) => url.to_owned(), // If parsing fails, return the original URL.
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn harvest_nip65_relays(
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
nks: &[NoteKey],
|
||||
) -> Vec<RelaySpec> {
|
||||
let mut relays = Vec::new();
|
||||
for nk in nks.iter() {
|
||||
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
|
||||
for tag in note.tags() {
|
||||
match tag.get(0).and_then(|t| t.variant().str()) {
|
||||
Some("r") => {
|
||||
if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) {
|
||||
let has_read_marker = tag
|
||||
.get(2)
|
||||
.is_some_and(|m| m.variant().str() == Some("read"));
|
||||
let has_write_marker = tag
|
||||
.get(2)
|
||||
.is_some_and(|m| m.variant().str() == Some("write"));
|
||||
relays.push(RelaySpec::new(
|
||||
Self::canonicalize_url(url),
|
||||
has_read_marker,
|
||||
has_write_marker,
|
||||
));
|
||||
}
|
||||
}
|
||||
Some("alt") => {
|
||||
// ignore for now
|
||||
}
|
||||
Some(x) => {
|
||||
error!("harvest_nip65_relays: unexpected tag type: {}", x);
|
||||
}
|
||||
None => {
|
||||
error!("harvest_nip65_relays: invalid tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
relays
|
||||
}
|
||||
|
||||
pub fn publish_nip65_relays(&self, seckey: &[u8; 32], pool: &mut RelayPool) {
|
||||
let mut builder = NoteBuilder::new().kind(10002).content("");
|
||||
for rs in &self.advertised {
|
||||
builder = builder.start_tag().tag_str("r").tag_str(&rs.url);
|
||||
if rs.has_read_marker {
|
||||
builder = builder.tag_str("read");
|
||||
} else if rs.has_write_marker {
|
||||
builder = builder.tag_str("write");
|
||||
}
|
||||
}
|
||||
let note = builder.sign(seckey).build().expect("note build");
|
||||
pool.send(&enostr::ClientMessage::event(¬e).expect("note client message"));
|
||||
}
|
||||
|
||||
pub fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) -> bool {
|
||||
let nks = ndb.poll_for_notes(sub, 1);
|
||||
|
||||
if nks.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let relays = AccountRelayData::harvest_nip65_relays(ndb, txn, &nks);
|
||||
debug!("updated relays {:?}", relays);
|
||||
self.advertised = relays.into_iter().collect();
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RelayDefaults {
|
||||
pub forced_relays: BTreeSet<RelaySpec>,
|
||||
pub bootstrap_relays: BTreeSet<RelaySpec>,
|
||||
}
|
||||
|
||||
impl RelayDefaults {
|
||||
pub(crate) fn new(forced_relays: Vec<String>) -> Self {
|
||||
let forced_relays: BTreeSet<RelaySpec> = forced_relays
|
||||
.into_iter()
|
||||
.map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false))
|
||||
.collect();
|
||||
let bootstrap_relays = [
|
||||
"wss://relay.damus.io",
|
||||
// "wss://pyramid.fiatjaf.com", // Uncomment if needed
|
||||
"wss://nos.lol",
|
||||
"wss://nostr.wine",
|
||||
"wss://purplepag.es",
|
||||
]
|
||||
.iter()
|
||||
.map(|&url| url.to_string())
|
||||
.map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
forced_relays,
|
||||
bootstrap_relays,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_relay_configuration(
|
||||
pool: &mut RelayPool,
|
||||
relay_defaults: &RelayDefaults,
|
||||
pk: &Pubkey,
|
||||
data: &AccountRelayData,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) {
|
||||
debug!(
|
||||
"updating relay configuration for currently selected {:?}",
|
||||
pk.hex()
|
||||
);
|
||||
|
||||
// If forced relays are set use them only
|
||||
let mut desired_relays = relay_defaults.forced_relays.clone();
|
||||
|
||||
// Compose the desired relay lists from the selected account
|
||||
if desired_relays.is_empty() {
|
||||
desired_relays.extend(data.local.iter().cloned());
|
||||
desired_relays.extend(data.advertised.iter().cloned());
|
||||
}
|
||||
|
||||
// If no relays are specified at this point use the bootstrap list
|
||||
if desired_relays.is_empty() {
|
||||
desired_relays = relay_defaults.bootstrap_relays.clone();
|
||||
}
|
||||
|
||||
debug!("current relays: {:?}", pool.urls());
|
||||
debug!("desired relays: {:?}", desired_relays);
|
||||
|
||||
let pool_specs = pool
|
||||
.urls()
|
||||
.iter()
|
||||
.map(|url| RelaySpec::new(url.clone(), false, false))
|
||||
.collect();
|
||||
let add: BTreeSet<RelaySpec> = desired_relays.difference(&pool_specs).cloned().collect();
|
||||
let mut sub: BTreeSet<RelaySpec> = pool_specs.difference(&desired_relays).cloned().collect();
|
||||
if !add.is_empty() {
|
||||
debug!("configuring added relays: {:?}", add);
|
||||
let _ = pool.add_urls(add.iter().map(|r| r.url.clone()).collect(), wakeup);
|
||||
}
|
||||
if !sub.is_empty() {
|
||||
// certain relays are persistent like the multicast relay,
|
||||
// although we should probably have a way to explicitly
|
||||
// disable it
|
||||
sub.remove(&RelaySpec::new("multicast", false, false));
|
||||
|
||||
debug!("removing unwanted relays: {:?}", sub);
|
||||
pool.remove_urls(&sub.iter().map(|r| r.url.clone()).collect());
|
||||
}
|
||||
|
||||
debug!("current relays: {:?}", pool.urls());
|
||||
}
|
||||
|
||||
pub enum RelayAction {
|
||||
Add(String),
|
||||
Remove(String),
|
||||
}
|
||||
|
||||
impl RelayAction {
|
||||
pub(super) fn get_url(&self) -> &str {
|
||||
match self {
|
||||
RelayAction::Add(url) => url,
|
||||
RelayAction::Remove(url) => url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn modify_advertised_relays(
|
||||
kp: &Keypair,
|
||||
action: RelayAction,
|
||||
pool: &mut RelayPool,
|
||||
relay_defaults: &RelayDefaults,
|
||||
account_data: &mut AccountData,
|
||||
) {
|
||||
let relay_url = AccountRelayData::canonicalize_url(action.get_url());
|
||||
match action {
|
||||
RelayAction::Add(_) => info!("add advertised relay \"{}\"", relay_url),
|
||||
RelayAction::Remove(_) => info!("remove advertised relay \"{}\"", relay_url),
|
||||
}
|
||||
|
||||
// let selected = self.cache.selected_mut();
|
||||
|
||||
let advertised = &mut account_data.relay.advertised;
|
||||
if advertised.is_empty() {
|
||||
// If the selected account has no advertised relays,
|
||||
// initialize with the bootstrapping set.
|
||||
advertised.extend(relay_defaults.bootstrap_relays.iter().cloned());
|
||||
}
|
||||
match action {
|
||||
RelayAction::Add(_) => {
|
||||
advertised.insert(RelaySpec::new(relay_url, false, false));
|
||||
}
|
||||
RelayAction::Remove(_) => {
|
||||
advertised.remove(&RelaySpec::new(relay_url, false, false));
|
||||
}
|
||||
}
|
||||
|
||||
// If we have the secret key publish the NIP-65 relay list
|
||||
if let Some(secretkey) = &kp.secret_key {
|
||||
account_data
|
||||
.relay
|
||||
.publish_nip65_relays(&secretkey.to_secret_bytes(), pool);
|
||||
}
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
use crate::account::FALLBACK_PUBKEY;
|
||||
use crate::i18n::Localization;
|
||||
use crate::persist::{AppSizeHandler, SettingsHandler};
|
||||
use crate::wallet::GlobalWallet;
|
||||
use crate::zaps::Zaps;
|
||||
use crate::Error;
|
||||
use crate::JobPool;
|
||||
use crate::NotedeckOptions;
|
||||
use crate::{
|
||||
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
|
||||
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
|
||||
};
|
||||
use egui::Margin;
|
||||
use egui::ThemePreference;
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
use enostr::RelayPool;
|
||||
use nostrdb::{Config, Ndb, Transaction};
|
||||
use std::cell::RefCell;
|
||||
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),
|
||||
ToggleChrome,
|
||||
}
|
||||
|
||||
pub trait App {
|
||||
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction>;
|
||||
}
|
||||
|
||||
/// Main notedeck app framework
|
||||
pub struct Notedeck {
|
||||
ndb: Ndb,
|
||||
img_cache: Images,
|
||||
unknown_ids: UnknownIds,
|
||||
pool: RelayPool,
|
||||
note_cache: NoteCache,
|
||||
accounts: Accounts,
|
||||
global_wallet: GlobalWallet,
|
||||
path: DataPath,
|
||||
args: Args,
|
||||
settings: SettingsHandler,
|
||||
app: Option<Rc<RefCell<dyn App>>>,
|
||||
app_size: AppSizeHandler,
|
||||
unrecognized_args: BTreeSet<String>,
|
||||
clipboard: Clipboard,
|
||||
zaps: Zaps,
|
||||
frame_history: FrameHistory,
|
||||
job_pool: JobPool,
|
||||
i18n: Localization,
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
android_app: Option<AndroidApp>,
|
||||
}
|
||||
|
||||
/// Our chrome, which is basically nothing
|
||||
fn main_panel(style: &egui::Style) -> egui::CentralPanel {
|
||||
egui::CentralPanel::default().frame(egui::Frame {
|
||||
inner_margin: Margin::ZERO,
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_notedeck(notedeck: &mut Notedeck, ctx: &egui::Context) {
|
||||
main_panel(&ctx.style()).show(ctx, |ui| {
|
||||
// render app
|
||||
let Some(app) = ¬edeck.app else {
|
||||
return;
|
||||
};
|
||||
|
||||
let app = app.clone();
|
||||
app.borrow_mut().update(&mut notedeck.app_context(), ui);
|
||||
|
||||
// Move the screen up when we have a virtual keyboard
|
||||
// NOTE: actually, we only want to do this if the keyboard is covering the focused element?
|
||||
/*
|
||||
let keyboard_height = crate::platform::virtual_keyboard_height() as f32;
|
||||
if keyboard_height > 0.0 {
|
||||
ui.ctx().transform_layer_shapes(
|
||||
ui.layer_id(),
|
||||
egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -(keyboard_height/2.0))),
|
||||
);
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
impl eframe::App for Notedeck {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
profiling::finish_frame!();
|
||||
self.frame_history
|
||||
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
|
||||
// handle account updates
|
||||
self.accounts.update(&mut self.ndb, &mut self.pool, ctx);
|
||||
|
||||
self.zaps
|
||||
.process(&mut self.accounts, &mut self.global_wallet, &self.ndb);
|
||||
|
||||
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.app_size.try_save_app_size(ctx);
|
||||
|
||||
if self.args.options.contains(NotedeckOptions::RelayDebug) {
|
||||
if self.pool.debug.is_none() {
|
||||
self.pool.use_debug();
|
||||
}
|
||||
|
||||
if let Some(debug) = &mut self.pool.debug {
|
||||
RelayDebugView::window(ctx, debug);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin_egui::profiler_window(ctx);
|
||||
}
|
||||
|
||||
/// Called by the framework to save state before shutdown.
|
||||
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
|
||||
//eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "puffin")]
|
||||
fn setup_puffin() {
|
||||
info!("setting up puffin");
|
||||
puffin::set_scopes_on(true); // tell puffin to collect data
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Skip the first argument, which is the program name.
|
||||
let (parsed_args, unrecognized_args) = Args::parse(&args[1..]);
|
||||
|
||||
let data_path = parsed_args
|
||||
.datapath
|
||||
.clone()
|
||||
.unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
|
||||
let path = DataPath::new(&data_path);
|
||||
let dbpath_str = parsed_args
|
||||
.dbpath
|
||||
.clone()
|
||||
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
|
||||
|
||||
let _ = std::fs::create_dir_all(&dbpath_str);
|
||||
|
||||
let img_cache_dir = path.path(DataPathType::Cache);
|
||||
let _ = std::fs::create_dir_all(img_cache_dir.clone());
|
||||
|
||||
let map_size = if cfg!(target_os = "windows") {
|
||||
// 16 Gib on windows because it actually creates the file
|
||||
1024usize * 1024usize * 1024usize * 16usize
|
||||
} else {
|
||||
// 1 TiB for everything else since its just virtually mapped
|
||||
1024usize * 1024usize * 1024usize * 1024usize
|
||||
};
|
||||
|
||||
let settings = SettingsHandler::new(&path).load();
|
||||
|
||||
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
|
||||
|
||||
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
|
||||
let keys_path = path.path(DataPathType::Keys);
|
||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||
Some(AccountStorage::new(
|
||||
Directory::new(keys_path),
|
||||
Directory::new(selected_key_path),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// AccountManager will setup the pool on first update
|
||||
let mut pool = RelayPool::new();
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) {
|
||||
error!("error setting up multicast relay: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut unknown_ids = UnknownIds::default();
|
||||
let mut ndb = Ndb::new(&dbpath_str, &config).expect("ndb");
|
||||
let txn = Transaction::new(&ndb).expect("txn");
|
||||
|
||||
let mut accounts = Accounts::new(
|
||||
keystore,
|
||||
parsed_args.relays.clone(),
|
||||
FALLBACK_PUBKEY(),
|
||||
&mut ndb,
|
||||
&txn,
|
||||
&mut pool,
|
||||
ctx,
|
||||
&mut unknown_ids,
|
||||
);
|
||||
|
||||
{
|
||||
for key in &parsed_args.keys {
|
||||
info!("adding account: {}", &key.pubkey);
|
||||
if let Some(resp) = accounts.add_account(key.clone()) {
|
||||
resp.unk_id_action
|
||||
.process_action(&mut unknown_ids, &ndb, &txn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(first) = parsed_args.keys.first() {
|
||||
accounts.select_account(&first.pubkey, &mut ndb, &txn, &mut pool, ctx);
|
||||
}
|
||||
|
||||
let img_cache = Images::new(img_cache_dir);
|
||||
let note_cache = NoteCache::default();
|
||||
|
||||
let app_size = AppSizeHandler::new(&path);
|
||||
|
||||
// migrate
|
||||
if let Err(e) = img_cache.migrate_v0() {
|
||||
error!("error migrating image cache: {e}");
|
||||
}
|
||||
|
||||
let global_wallet = GlobalWallet::new(&path);
|
||||
let zaps = Zaps::default();
|
||||
let job_pool = JobPool::default();
|
||||
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
ndb,
|
||||
img_cache,
|
||||
unknown_ids,
|
||||
pool,
|
||||
note_cache,
|
||||
accounts,
|
||||
global_wallet,
|
||||
path: path.clone(),
|
||||
args: parsed_args,
|
||||
settings,
|
||||
app: None,
|
||||
app_size,
|
||||
unrecognized_args,
|
||||
frame_history: FrameHistory::default(),
|
||||
clipboard: Clipboard::new(None),
|
||||
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
|
||||
}
|
||||
|
||||
pub fn app_context(&mut self) -> AppContext<'_> {
|
||||
AppContext {
|
||||
ndb: &mut self.ndb,
|
||||
img_cache: &mut self.img_cache,
|
||||
unknown_ids: &mut self.unknown_ids,
|
||||
pool: &mut self.pool,
|
||||
note_cache: &mut self.note_cache,
|
||||
accounts: &mut self.accounts,
|
||||
global_wallet: &mut self.global_wallet,
|
||||
path: &self.path,
|
||||
args: &self.args,
|
||||
settings: &mut self.settings,
|
||||
clipboard: &mut self.clipboard,
|
||||
zaps: &mut self.zaps,
|
||||
frame_history: &mut self.frame_history,
|
||||
job_pool: &mut self.job_pool,
|
||||
i18n: &mut self.i18n,
|
||||
#[cfg(target_os = "android")]
|
||||
android: self.android_app.as_ref().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_app<T: App + 'static>(&mut self, app: T) {
|
||||
self.app = Some(Rc::new(RefCell::new(app)));
|
||||
}
|
||||
|
||||
pub fn args(&self) -> &Args {
|
||||
&self.args
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
||||
&self.unrecognized_args
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
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 locale: Option<LanguageIdentifier>,
|
||||
pub keys: Vec<Keypair>,
|
||||
pub options: NotedeckOptions,
|
||||
pub dbpath: Option<String>,
|
||||
pub datapath: Option<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
// parse arguments, return set of unrecognized args
|
||||
pub fn parse(args: &[String]) -> (Self, BTreeSet<String>) {
|
||||
let mut unrecognized_args = BTreeSet::new();
|
||||
let mut res = Args {
|
||||
relays: vec![],
|
||||
keys: vec![],
|
||||
options: NotedeckOptions::default(),
|
||||
dbpath: None,
|
||||
datapath: None,
|
||||
locale: None,
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
let len = args.len();
|
||||
while i < len {
|
||||
let arg = &args[i];
|
||||
|
||||
if arg == "--mobile" {
|
||||
res.options.set(NotedeckOptions::Mobile, true);
|
||||
} else if arg == "--light" {
|
||||
res.options.set(NotedeckOptions::LightTheme, true);
|
||||
} else if arg == "--locale" {
|
||||
i += 1;
|
||||
let Some(locale) = args.get(i) else {
|
||||
panic!("locale argument missing?");
|
||||
};
|
||||
let parsed: Result<LanguageIdentifier, LanguageIdentifierError> = locale.parse();
|
||||
match parsed {
|
||||
Err(err) => {
|
||||
panic!("locale failed to parse: {err}");
|
||||
}
|
||||
Ok(locale) => {
|
||||
tracing::info!(
|
||||
"parsed locale '{locale}' from args, not sure if we have it yet though."
|
||||
);
|
||||
res.locale = Some(locale);
|
||||
}
|
||||
}
|
||||
} else if arg == "--dark" {
|
||||
res.options.set(NotedeckOptions::LightTheme, false);
|
||||
} else if arg == "--debug" {
|
||||
res.options.set(NotedeckOptions::Debug, true);
|
||||
} else if arg == "--testrunner" {
|
||||
res.options.set(NotedeckOptions::Tests, true);
|
||||
} else if arg == "--pub" || arg == "--npub" {
|
||||
i += 1;
|
||||
let pubstr = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("sec argument missing?");
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(pk) = Pubkey::parse(pubstr) {
|
||||
res.keys.push(Keypair::only_pubkey(pk));
|
||||
} else {
|
||||
error!(
|
||||
"failed to parse {} argument. Make sure to use hex or npub.",
|
||||
arg
|
||||
);
|
||||
}
|
||||
} else if arg == "--sec" || arg == "--nsec" {
|
||||
i += 1;
|
||||
let secstr = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("sec argument missing?");
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(sec) = SecretKey::parse(secstr) {
|
||||
res.keys.push(Keypair::from_secret(sec));
|
||||
} else {
|
||||
error!(
|
||||
"failed to parse {} argument. Make sure to use hex or nsec.",
|
||||
arg
|
||||
);
|
||||
}
|
||||
} else if arg == "--dbpath" {
|
||||
i += 1;
|
||||
let path = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("dbpath argument missing?");
|
||||
continue;
|
||||
};
|
||||
res.dbpath = Some(path.clone());
|
||||
} else if arg == "--datapath" {
|
||||
i += 1;
|
||||
let path = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("datapath argument missing?");
|
||||
continue;
|
||||
};
|
||||
res.datapath = Some(path.clone());
|
||||
} else if arg == "-r" || arg == "--relay" {
|
||||
i += 1;
|
||||
let relay = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("relay argument missing?");
|
||||
continue;
|
||||
};
|
||||
res.relays.push(relay.clone());
|
||||
} else if arg == "--no-keystore" {
|
||||
res.options.set(NotedeckOptions::UseKeystore, true);
|
||||
} 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);
|
||||
} else {
|
||||
unrecognized_args.insert(arg.clone());
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
(res, unrecognized_args)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use crate::{
|
||||
filter::{self, HybridFilter},
|
||||
Error,
|
||||
};
|
||||
use nostrdb::{Filter, Note};
|
||||
|
||||
pub fn contacts_filter(pk: &[u8; 32]) -> Filter {
|
||||
Filter::new().authors([pk]).kinds([3]).limit(1).build()
|
||||
}
|
||||
|
||||
/// Contact filters have an additional kind0 in the remote filter so it can fetch profiles as well
|
||||
/// we don't need this in the local filter since we only care about the kind1 results
|
||||
pub fn hybrid_contacts_filter(
|
||||
note: &Note,
|
||||
add_pk: Option<&[u8; 32]>,
|
||||
with_hashtags: bool,
|
||||
) -> Result<HybridFilter, Error> {
|
||||
let local = filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_filter([1], filter::default_limit());
|
||||
let remote = filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_filter([1, 0], filter::default_remote_limit());
|
||||
|
||||
Ok(HybridFilter::split(local, remote))
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use crate::{
|
||||
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
|
||||
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
|
||||
UnknownIds,
|
||||
};
|
||||
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> {
|
||||
pub ndb: &'a mut Ndb,
|
||||
pub img_cache: &'a mut Images,
|
||||
pub unknown_ids: &'a mut UnknownIds,
|
||||
pub pool: &'a mut RelayPool,
|
||||
pub note_cache: &'a mut NoteCache,
|
||||
pub accounts: &'a mut Accounts,
|
||||
pub global_wallet: &'a mut GlobalWallet,
|
||||
pub path: &'a DataPath,
|
||||
pub args: &'a Args,
|
||||
pub settings: &'a mut SettingsHandler,
|
||||
pub clipboard: &'a mut Clipboard,
|
||||
pub zaps: &'a mut Zaps,
|
||||
pub frame_history: &'a mut FrameHistory,
|
||||
pub job_pool: &'a mut JobPool,
|
||||
pub i18n: &'a mut Localization,
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub android: AndroidApp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SoftKeyboardContext {
|
||||
Virtual,
|
||||
Platform { ppp: f32 },
|
||||
}
|
||||
|
||||
impl SoftKeyboardContext {
|
||||
pub fn platform(context: &egui::Context) -> Self {
|
||||
Self::Platform {
|
||||
ppp: context.pixels_per_point(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AppContext<'a> {
|
||||
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
|
||||
match ctx {
|
||||
SoftKeyboardContext::Virtual => {
|
||||
let height = 400.0;
|
||||
skb_rect_from_screen_rect(screen_rect, height)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
SoftKeyboardContext::Platform { ppp } => {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use android_activity::InsetType;
|
||||
|
||||
// not sure why I need this, it seems to be consistently off by some amount of
|
||||
// pixels ?
|
||||
let fudge = 0.0;
|
||||
|
||||
let inset = self.android.get_window_insets(InsetType::Ime);
|
||||
let height = (inset.bottom as f32 / ppp) - fudge;
|
||||
skb_rect_from_screen_rect(screen_rect, height)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option<Rect> {
|
||||
if height == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let min = Pos2::new(0.0, screen_rect.max.y - height);
|
||||
Some(Rect::from_min_max(min, screen_rect.max))
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A simple debouncer that tracks when an action was last performed
|
||||
/// and determines if enough time has passed to perform it again.
|
||||
#[derive(Debug)]
|
||||
pub struct Debouncer {
|
||||
delay: Duration,
|
||||
last_action: Instant,
|
||||
}
|
||||
|
||||
impl Debouncer {
|
||||
/// Creates a new Debouncer with the specified delay
|
||||
pub fn new(delay: Duration) -> Self {
|
||||
Self {
|
||||
delay,
|
||||
last_action: Instant::now() - delay, // Start ready to act
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a new delay value and returns self for method chaining
|
||||
pub fn with_delay(mut self, delay: Duration) -> Self {
|
||||
self.delay = delay;
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks if enough time has passed since the last action
|
||||
pub fn should_act(&self) -> bool {
|
||||
self.last_action.elapsed() >= self.delay
|
||||
}
|
||||
|
||||
/// Marks an action as performed, updating the timestamp
|
||||
pub fn bounce(&mut self) {
|
||||
self.last_action = Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
/// App related errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("image error: {0}")]
|
||||
Image(#[from] image::error::ImageError),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("subscription error: {0}")]
|
||||
SubscriptionError(SubscriptionError),
|
||||
|
||||
#[error("filter error: {0}")]
|
||||
Filter(FilterError),
|
||||
|
||||
#[error("json error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Nostrdb(#[from] nostrdb::Error),
|
||||
|
||||
#[error("generic error: {0}")]
|
||||
Generic(String),
|
||||
|
||||
#[error("zaps error: {0}")]
|
||||
Zap(#[from] ZapError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, Clone)]
|
||||
pub enum ZapError {
|
||||
#[error("invalid lud16")]
|
||||
InvalidLud16(String),
|
||||
#[error("invalid endpoint response")]
|
||||
EndpointError(String),
|
||||
#[error("bech encoding/decoding error")]
|
||||
Bech(String),
|
||||
#[error("serialization/deserialization problem")]
|
||||
Serialization(String),
|
||||
#[error("nwc error")]
|
||||
NWC(String),
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Generic(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
|
||||
pub enum FilterError {
|
||||
#[error("empty contact list")]
|
||||
EmptyContactList,
|
||||
|
||||
#[error("filter not ready")]
|
||||
FilterNotReady,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)]
|
||||
pub enum SubscriptionError {
|
||||
#[error("no active subscriptions")]
|
||||
NoActive,
|
||||
|
||||
/// When a timeline has an unexpected number
|
||||
/// of active subscriptions. Should only happen if there
|
||||
/// is a bug in notedeck
|
||||
#[error("unexpected subscription count")]
|
||||
UnexpectedSubscriptionCount(i32),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn unexpected_sub_count(c: i32) -> Self {
|
||||
Error::SubscriptionError(SubscriptionError::UnexpectedSubscriptionCount(c))
|
||||
}
|
||||
|
||||
pub fn no_active_sub() -> Self {
|
||||
Error::SubscriptionError(SubscriptionError::NoActive)
|
||||
}
|
||||
|
||||
pub fn empty_contact_list() -> Self {
|
||||
Error::Filter(FilterError::EmptyContactList)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_one_error_message(ui: &mut egui::Ui, message: &str) {
|
||||
let id = ui.id().with(("error", message));
|
||||
let res: Option<()> = ui.ctx().data(|d| d.get_temp(id));
|
||||
|
||||
if res.is_none() {
|
||||
ui.ctx().data_mut(|d| d.insert_temp(id, ()));
|
||||
tracing::error!(message);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use super::IntlKeyBuf;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
/// App related errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum IntlError {
|
||||
#[error("message not found: {0}")]
|
||||
NotFound(IntlKeyBuf),
|
||||
|
||||
#[error("message has no value: {0}")]
|
||||
NoValue(IntlKeyBuf),
|
||||
|
||||
#[error("Locale({0}) parse error: {1}")]
|
||||
LocaleParse(LanguageIdentifier, String),
|
||||
|
||||
#[error("locale not available: {0}")]
|
||||
LocaleNotAvailable(LanguageIdentifier),
|
||||
|
||||
#[error("FTL for '{0}' is not available")]
|
||||
NoFtl(LanguageIdentifier),
|
||||
|
||||
#[error("Bundle for '{0}' is not available")]
|
||||
NoBundle(LanguageIdentifier),
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
/// An owned key used to lookup i18n translations. Mostly used for errors
|
||||
#[derive(Eq, PartialEq, Clone, Debug)]
|
||||
pub struct IntlKeyBuf(String);
|
||||
|
||||
/// A key used to lookup i18n translations
|
||||
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
|
||||
pub struct IntlKey<'a>(&'a str);
|
||||
|
||||
impl fmt::Display for IntlKey<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Use `self.number` to refer to each positional data point.
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IntlKeyBuf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Use `self.number` to refer to each positional data point.
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntlKeyBuf {
|
||||
pub fn new(string: impl Into<String>) -> Self {
|
||||
IntlKeyBuf(string.into())
|
||||
}
|
||||
|
||||
pub fn borrow<'a>(&'a self) -> IntlKey<'a> {
|
||||
IntlKey::new(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntlKey<'a> {
|
||||
pub fn new(string: &'a str) -> IntlKey<'a> {
|
||||
IntlKey(string)
|
||||
}
|
||||
|
||||
pub fn to_owned(&self) -> IntlKeyBuf {
|
||||
IntlKeyBuf::new(self.0)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'a str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -1,710 +0,0 @@
|
||||
use super::{IntlError, IntlKey, IntlKeyBuf};
|
||||
use fluent::{FluentArgs, FluentBundle, FluentResource};
|
||||
use fluent_langneg::negotiate_languages;
|
||||
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 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 = "繁體中文";
|
||||
|
||||
struct StaticBundle {
|
||||
identifier: LanguageIdentifier,
|
||||
ftl: &'static str,
|
||||
}
|
||||
|
||||
const FTLS: [StaticBundle; NUM_FTLS] = [
|
||||
StaticBundle {
|
||||
identifier: EN_US,
|
||||
ftl: include_str!("../../../../assets/translations/en-US/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: EN_XA,
|
||||
ftl: include_str!("../../../../assets/translations/en-XA/main.ftl"),
|
||||
},
|
||||
StaticBundle {
|
||||
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"),
|
||||
},
|
||||
StaticBundle {
|
||||
identifier: ZH_TW,
|
||||
ftl: include_str!("../../../../assets/translations/zh-TW/main.ftl"),
|
||||
},
|
||||
];
|
||||
|
||||
type Bundle = FluentBundle<FluentResource>;
|
||||
|
||||
/// Manages localization resources and provides localized strings
|
||||
pub struct Localization {
|
||||
/// Current locale
|
||||
current_locale: LanguageIdentifier,
|
||||
/// Available locales
|
||||
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>>,
|
||||
/// Cached normalized keys
|
||||
normalized_key_cache: HashMap<String, IntlKeyBuf>,
|
||||
/// Bundles
|
||||
bundles: HashMap<LanguageIdentifier, Bundle>,
|
||||
|
||||
use_isolating: bool,
|
||||
}
|
||||
|
||||
impl Default for Localization {
|
||||
fn default() -> Self {
|
||||
// Default to English (US)
|
||||
let default_locale = &EN_US;
|
||||
let fallback_locale = default_locale.to_owned();
|
||||
|
||||
// Build available locales list
|
||||
let available_locales = vec![
|
||||
EN_US.clone(),
|
||||
EN_XA.clone(),
|
||||
DE.clone(),
|
||||
ES_419.clone(),
|
||||
ES_ES.clone(),
|
||||
FR.clone(),
|
||||
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(),
|
||||
bundles: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Localization {
|
||||
/// Creates a new Localization with the specified resource directory
|
||||
pub fn new() -> Self {
|
||||
Localization::default()
|
||||
}
|
||||
|
||||
/// Disable bidirectional isolation markers. mostly useful for tests
|
||||
pub fn no_bidi() -> Self {
|
||||
Localization {
|
||||
use_isolating: false,
|
||||
..Localization::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a localized string by its ID
|
||||
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
|
||||
self.get_cached_string(id, None)
|
||||
}
|
||||
|
||||
/// Load a fluent bundle given a language identifier. Only looks in the static
|
||||
/// ftl files baked into the binary
|
||||
fn load_bundle(lang: &LanguageIdentifier) -> Result<Bundle, IntlError> {
|
||||
for ftl in &FTLS {
|
||||
if &ftl.identifier == lang {
|
||||
let mut bundle = FluentBundle::new(vec![lang.to_owned()]);
|
||||
let resource = FluentResource::try_new(ftl.ftl.to_string());
|
||||
match resource {
|
||||
Err((resource, errors)) => {
|
||||
for error in errors {
|
||||
tracing::error!("load_bundle ({lang}): {error}");
|
||||
}
|
||||
|
||||
tracing::warn!("load_bundle ({}: loading bundle with errors", lang);
|
||||
if let Err(errs) = bundle.add_resource(resource) {
|
||||
for err in errs {
|
||||
tracing::error!("adding resource: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resource) => {
|
||||
tracing::info!("loaded {} bundle OK!", lang);
|
||||
if let Err(errs) = bundle.add_resource(resource) {
|
||||
for err in errs {
|
||||
tracing::error!("adding resource 2: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
// no static ftl for this LanguageIdentifier
|
||||
Err(IntlError::NoFtl(lang.to_owned()))
|
||||
}
|
||||
|
||||
fn get_bundle<'a>(&'a self, lang: &LanguageIdentifier) -> &'a Bundle {
|
||||
self.bundles
|
||||
.get(lang)
|
||||
.expect("make sure to call ensure_bundle!")
|
||||
}
|
||||
|
||||
fn has_bundle(&self, lang: &LanguageIdentifier) -> bool {
|
||||
self.bundles.contains_key(lang)
|
||||
}
|
||||
|
||||
fn try_load_bundle(&mut self, lang: &LanguageIdentifier) -> Result<(), IntlError> {
|
||||
let mut bundle = Self::load_bundle(lang)?;
|
||||
if !self.use_isolating {
|
||||
bundle.set_use_isolating(false);
|
||||
}
|
||||
self.bundles.insert(lang.to_owned(), bundle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalized_ftl_key(&mut self, key: &str, comment: &str) -> IntlKeyBuf {
|
||||
match self.get_ftl_key(key) {
|
||||
Some(intl_key) => intl_key,
|
||||
None => {
|
||||
self.insert_ftl_key(key, comment);
|
||||
self.get_ftl_key(key).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ftl_key(&self, cache_key: &str) -> Option<IntlKeyBuf> {
|
||||
self.normalized_key_cache.get(cache_key).cloned()
|
||||
}
|
||||
|
||||
fn insert_ftl_key(&mut self, cache_key: &str, comment: &str) {
|
||||
let mut result = fixup_key(cache_key);
|
||||
|
||||
// Ensure the key starts with a letter (Fluent requirement)
|
||||
if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
|
||||
result = format!("k_{result}");
|
||||
}
|
||||
|
||||
// If we have a comment, append a hash of it to reduce collisions
|
||||
let hash_str = format!("_{}", simple_hash(comment));
|
||||
result.push_str(&hash_str);
|
||||
|
||||
tracing::debug!(
|
||||
"normalize_ftl_key: original='{}', final='{}'",
|
||||
cache_key,
|
||||
result
|
||||
);
|
||||
|
||||
self.normalized_key_cache
|
||||
.insert(cache_key.to_owned(), IntlKeyBuf::new(result));
|
||||
}
|
||||
|
||||
fn get_cached_string_no_args<'key>(
|
||||
&'key self,
|
||||
lang: &LanguageIdentifier,
|
||||
id: IntlKey<'key>,
|
||||
) -> Result<Cow<'key, str>, IntlError> {
|
||||
// Try to get from string cache first
|
||||
if let Some(locale_cache) = self.string_cache.get(lang) {
|
||||
if let Some(cached_string) = locale_cache.get(id.as_str()) {
|
||||
/*
|
||||
tracing::trace!(
|
||||
"Using cached string result for '{}' in locale: {}",
|
||||
id,
|
||||
&lang
|
||||
);
|
||||
*/
|
||||
|
||||
return Ok(Cow::Borrowed(cached_string));
|
||||
}
|
||||
}
|
||||
|
||||
Err(IntlError::NotFound(id.to_owned()))
|
||||
}
|
||||
|
||||
fn ensure_bundle(&mut self) -> Result<(), IntlError> {
|
||||
let locale = self.current_locale.clone();
|
||||
if !self.has_bundle(&locale) {
|
||||
match self.try_load_bundle(&locale) {
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"tried to load bundle {} but failed with '{err}'. using fallback {}",
|
||||
&locale,
|
||||
&self.fallback_locale
|
||||
);
|
||||
self.try_load_bundle(&locale)
|
||||
.expect("failed to load fallback bundle!?");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(()) => Ok(()),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_bundle(&self) -> &Bundle {
|
||||
if self.has_bundle(&self.current_locale) {
|
||||
return self.get_bundle(&self.current_locale);
|
||||
}
|
||||
|
||||
self.get_bundle(&self.fallback_locale)
|
||||
}
|
||||
|
||||
/// Gets cached string result, or formats it and caches the result
|
||||
pub fn get_cached_string(
|
||||
&mut self,
|
||||
id: IntlKey<'_>,
|
||||
args: Option<&FluentArgs>,
|
||||
) -> Result<String, IntlError> {
|
||||
self.ensure_bundle()?;
|
||||
|
||||
if args.is_none() {
|
||||
if let Ok(result) = self.get_cached_string_no_args(&self.current_locale, id) {
|
||||
return Ok(result.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let result = {
|
||||
let bundle = self.get_current_bundle();
|
||||
|
||||
let message = bundle
|
||||
.get_message(id.as_str())
|
||||
.ok_or_else(|| IntlError::NotFound(id.to_owned()))?;
|
||||
|
||||
let pattern = message
|
||||
.value()
|
||||
.ok_or_else(|| IntlError::NoValue(id.to_owned()))?;
|
||||
|
||||
let mut errors = Vec::with_capacity(0);
|
||||
let result = bundle.format_pattern(pattern, args, &mut errors);
|
||||
|
||||
if !errors.is_empty() {
|
||||
tracing::warn!("Localization errors for {}: {:?}", id, &errors);
|
||||
}
|
||||
|
||||
result.to_string()
|
||||
};
|
||||
|
||||
// Only cache simple strings without arguments
|
||||
// This prevents caching issues when the same message ID is used with different arguments
|
||||
if args.is_none() {
|
||||
self.cache_string(self.current_locale.clone(), id, result.as_str());
|
||||
tracing::debug!(
|
||||
"Cached string result for '{}' in locale: {}",
|
||||
id,
|
||||
&self.current_locale
|
||||
);
|
||||
} else {
|
||||
tracing::trace!("Not caching string '{}' due to arguments", id);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn cache_string<'a>(&mut self, locale: LanguageIdentifier, id: IntlKey<'a>, result: &str) {
|
||||
tracing::debug!("Cached string result for '{}' in locale: {}", id, &locale);
|
||||
let locale_cache = self.string_cache.entry(locale).or_default();
|
||||
locale_cache.insert(id.to_owned().to_string(), result.to_owned());
|
||||
}
|
||||
|
||||
/// Sets the current locale
|
||||
pub fn set_locale(&mut self, locale: LanguageIdentifier) -> Result<(), IntlError> {
|
||||
tracing::info!("Attempting to set locale to: {}", locale);
|
||||
tracing::info!("Available locales: {:?}", self.available_locales);
|
||||
|
||||
// Validate that the locale is available
|
||||
if !self.available_locales.contains(&locale) {
|
||||
tracing::error!(
|
||||
"Locale {} is not available. Available locales: {:?}",
|
||||
locale,
|
||||
self.available_locales
|
||||
);
|
||||
return Err(IntlError::LocaleNotAvailable(locale));
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Switching locale from {} to {}",
|
||||
&self.current_locale,
|
||||
&locale
|
||||
);
|
||||
self.current_locale = locale;
|
||||
|
||||
// Clear caches when locale changes since they are locale-specific
|
||||
self.string_cache.clear();
|
||||
tracing::debug!("String cache cleared due to locale change");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the parsed FluentResource cache (useful for development when FTL files change)
|
||||
pub fn clear_cache(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.bundles.clear();
|
||||
tracing::debug!("Parsed FluentResource cache cleared");
|
||||
|
||||
self.string_cache.clear();
|
||||
tracing::debug!("String result cache cleared");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current locale
|
||||
pub fn get_current_locale(&self) -> &LanguageIdentifier {
|
||||
&self.current_locale
|
||||
}
|
||||
|
||||
/// Gets all available locales
|
||||
pub fn get_available_locales(&self) -> &[LanguageIdentifier] {
|
||||
&self.available_locales
|
||||
}
|
||||
|
||||
/// Gets the fallback locale
|
||||
pub fn get_fallback_locale(&self) -> &LanguageIdentifier {
|
||||
&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;
|
||||
for locale_cache in self.string_cache.values() {
|
||||
total_strings += locale_cache.len();
|
||||
}
|
||||
|
||||
Ok(CacheStats {
|
||||
resource_cache_size: self.bundles.len(),
|
||||
string_cache_size: total_strings,
|
||||
cached_locales: self.bundles.keys().cloned().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Limits the string cache size to prevent memory growth
|
||||
pub fn limit_string_cache_size(
|
||||
&mut self,
|
||||
max_strings_per_locale: usize,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
for locale_cache in self.string_cache.values_mut() {
|
||||
if locale_cache.len() > max_strings_per_locale {
|
||||
// Remove oldest entries (simple approach: just clear and let it rebuild)
|
||||
// In a more sophisticated implementation, you might use an LRU cache
|
||||
locale_cache.clear();
|
||||
tracing::debug!("Cleared string cache for locale due to size limit");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Negotiates the best locale from a list of preferred locales
|
||||
pub fn negotiate_locale(&self, preferred: &[LanguageIdentifier]) -> LanguageIdentifier {
|
||||
let available = self.available_locales.clone();
|
||||
let negotiated = negotiate_languages(
|
||||
preferred,
|
||||
&available,
|
||||
Some(&self.fallback_locale),
|
||||
fluent_langneg::NegotiationStrategy::Filtering,
|
||||
);
|
||||
negotiated
|
||||
.first()
|
||||
.map_or(self.fallback_locale.clone(), |v| (*v).clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about cache usage
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheStats {
|
||||
pub resource_cache_size: usize,
|
||||
pub string_cache_size: usize,
|
||||
pub cached_locales: Vec<LanguageIdentifier>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
//
|
||||
// TODO(jb55): write tests that work, i broke all these during the refacto
|
||||
//
|
||||
|
||||
/*
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_locale_management() {
|
||||
let i18n = Localization::default();
|
||||
|
||||
// Test default locale
|
||||
let current = i18n.get_current_locale();
|
||||
assert_eq!(current.to_string(), "en-US");
|
||||
|
||||
// Test available locales
|
||||
let available = i18n.get_available_locales();
|
||||
assert_eq!(available.len(), 2);
|
||||
assert_eq!(available[0].to_string(), "en-US");
|
||||
assert_eq!(available[1].to_string(), "en-XA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_clearing() {
|
||||
let mut i18n = Localization::default();
|
||||
|
||||
// Load and cache the FTL content
|
||||
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
|
||||
assert!(result1.is_ok());
|
||||
|
||||
// Clear the cache
|
||||
let clear_result = i18n.clear_cache();
|
||||
assert!(clear_result.is_ok());
|
||||
|
||||
// Should still work after clearing cache (will reload)
|
||||
let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(result2.unwrap(), "Test Value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_caching() {
|
||||
let mut i18n = Localization::default();
|
||||
|
||||
// Debug: check what the normalized key should be
|
||||
let normalized_key = i18n.normalized_ftl_key("test_key", "comment");
|
||||
println!("Normalized key: '{}'", normalized_key);
|
||||
|
||||
// First call should load and cache the FTL content
|
||||
let result1 = i18n.get_string(normalized_key.borrow());
|
||||
println!("First result: {:?}", result1);
|
||||
assert!(result1.is_ok());
|
||||
assert_eq!(result1.unwrap(), "Test Value");
|
||||
|
||||
// Second call should use cached FTL content
|
||||
let result2 = i18n.get_string(normalized_key.borrow());
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(result2.unwrap(), "Test Value");
|
||||
|
||||
// Test cache clearing through context
|
||||
let clear_result = i18n.clear_cache();
|
||||
assert!(clear_result.is_ok());
|
||||
|
||||
// Should still work after clearing cache
|
||||
let result3 = i18n.get_string(normalized_key.borrow());
|
||||
assert!(result3.is_ok());
|
||||
assert_eq!(result3.unwrap(), "Test Value");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_ftl_caching() {
|
||||
let mut i18n = Localization::default();
|
||||
|
||||
// First call should load and cache the FTL content
|
||||
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
|
||||
assert!(result1.is_ok());
|
||||
assert_eq!(result1.as_ref().unwrap(), "Test Value");
|
||||
|
||||
// Second call should use cached FTL content
|
||||
let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(result2.unwrap(), "Test Value");
|
||||
|
||||
// Test another key from the same FTL content
|
||||
let result3 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
|
||||
assert!(result3.is_ok());
|
||||
assert_eq!(result3.unwrap(), "Another Value");
|
||||
}
|
||||
#[test]
|
||||
fn test_bundle_caching() {
|
||||
let mut i18n = Localization::default();
|
||||
|
||||
// First call should create bundle and cache the resource
|
||||
let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
|
||||
assert!(result1.is_ok());
|
||||
assert_eq!(result1.unwrap(), "Test Value");
|
||||
|
||||
// Second call should use cached resource but create new bundle
|
||||
let result2 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(result2.unwrap(), "Another Value");
|
||||
|
||||
// Check cache stats
|
||||
let stats = i18n.get_cache_stats().unwrap();
|
||||
assert_eq!(stats.resource_cache_size, 1);
|
||||
assert_eq!(stats.string_cache_size, 2); // Both strings should be cached
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_caching() {
|
||||
let mut i18n = Localization::default();
|
||||
let key = i18n.normalized_ftl_key("test_key", "comment");
|
||||
|
||||
// First call should format and cache the string
|
||||
let result1 = i18n.get_string(key.borrow());
|
||||
assert!(result1.is_ok());
|
||||
assert_eq!(result1.unwrap(), "Test Value");
|
||||
|
||||
// Second call should use cached string
|
||||
let result2 = i18n.get_string(key.borrow());
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(result2.unwrap(), "Test Value");
|
||||
|
||||
// Check cache stats
|
||||
let stats = i18n.get_cache_stats().unwrap();
|
||||
assert_eq!(stats.string_cache_size, 1);
|
||||
}
|
||||
#[test]
|
||||
fn test_string_caching_with_arguments() {
|
||||
let mut manager = Localization::default();
|
||||
|
||||
// First call with arguments should not be cached
|
||||
let mut args = fluent::FluentArgs::new();
|
||||
args.set("name", "Alice");
|
||||
let key = IntlKeyBuf::new("welcome_message");
|
||||
let result1 = manager
|
||||
.get_cached_string(key.borrow(), Some(&args))
|
||||
.unwrap();
|
||||
assert!(result1.contains("Alice"));
|
||||
|
||||
// Check that it's not in the string cache
|
||||
let stats1 = manager.get_cache_stats().unwrap();
|
||||
assert_eq!(stats1.string_cache_size, 0);
|
||||
|
||||
// Second call with different arguments should work correctly
|
||||
let mut args2 = fluent::FluentArgs::new();
|
||||
args2.set("name", "Bob");
|
||||
let result2 = manager.get_cached_string(key.borrow(), Some(&args2));
|
||||
assert!(result2.is_ok());
|
||||
let result2_str = result2.unwrap();
|
||||
assert!(result2_str.contains("Bob"));
|
||||
|
||||
// Check that it's still not in the string cache
|
||||
let stats2 = manager.get_cache_stats().unwrap();
|
||||
assert_eq!(stats2.string_cache_size, 0);
|
||||
|
||||
// Clear cache to start fresh
|
||||
manager.clear_cache().unwrap();
|
||||
|
||||
let result3 = manager.get_string(key.borrow());
|
||||
assert!(result3.is_ok());
|
||||
assert_eq!(result3.unwrap(), "Hello World");
|
||||
|
||||
// Check that simple string is cached
|
||||
let stats3 = manager.get_cache_stats().unwrap();
|
||||
assert_eq!(stats3.string_cache_size, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_clearing_on_locale_change() {
|
||||
let mut i18n = Localization::default();
|
||||
|
||||
// Check that caches are populated
|
||||
let stats1 = i18n.get_cache_stats().unwrap();
|
||||
assert!(stats1.resource_cache_size > 0);
|
||||
assert!(stats1.string_cache_size > 0);
|
||||
|
||||
// Switch to en-XA
|
||||
let en_xa: LanguageIdentifier = langid!("en-XA");
|
||||
i18n.set_locale(en_xa).unwrap();
|
||||
|
||||
// Check that string cache is cleared (resource cache remains for both locales)
|
||||
let stats2 = i18n.get_cache_stats().unwrap();
|
||||
assert_eq!(stats2.string_cache_size, 0);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// Replace each invalid character with exactly one underscore
|
||||
/// This matches the behavior of the Python extraction script
|
||||
pub fn fixup_key(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => out.push(ch),
|
||||
_ => out.push('_'), // always push
|
||||
}
|
||||
}
|
||||
let trimmed = out.trim_matches('_');
|
||||
trimmed.to_owned()
|
||||
}
|
||||
|
||||
fn simple_hash(s: &str) -> String {
|
||||
let digest = md5::compute(s.as_bytes());
|
||||
// Take the first 2 bytes and convert to 4 hex characters
|
||||
format!("{:02x}{:02x}", digest[0], digest[1])
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
//! Internationalization (i18n) module for Notedeck
|
||||
//!
|
||||
//! This module provides localization support using fluent and fluent-resmgr.
|
||||
//! It handles loading translation files, managing locales, and providing
|
||||
//! localized strings throughout the application.
|
||||
|
||||
mod error;
|
||||
mod key;
|
||||
pub mod manager;
|
||||
|
||||
pub use error::IntlError;
|
||||
pub use key::{IntlKey, IntlKeyBuf};
|
||||
|
||||
pub use manager::CacheStats;
|
||||
pub use manager::Localization;
|
||||
|
||||
/// Re-export commonly used types for convenience
|
||||
pub use fluent::FluentArgs;
|
||||
pub use fluent::FluentValue;
|
||||
pub use unic_langid::LanguageIdentifier;
|
||||
|
||||
/// Macro for getting localized strings with format-like syntax
|
||||
///
|
||||
/// Syntax: tr!("message", comment)
|
||||
/// tr!("message with {param}", comment, param="value")
|
||||
/// tr!("message with {first} and {second}", comment, first="value1", second="value2")
|
||||
///
|
||||
/// The first argument is the source message (like format!).
|
||||
/// The second argument is always the comment to provide context for translators.
|
||||
/// If `{name}` placeholders are found, there must be corresponding named arguments after the comment.
|
||||
/// All placeholders must be named and start with a letter (a-zA-Z).
|
||||
#[macro_export]
|
||||
macro_rules! tr {
|
||||
($i18n:expr, $message:expr, $comment:expr) => {
|
||||
{
|
||||
let key = $i18n.normalized_ftl_key($message, $comment);
|
||||
match $i18n.get_string(key.borrow()) {
|
||||
Ok(r) => r,
|
||||
Err(_err) => {
|
||||
$message.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Case with named parameters: message, comment, param=value, ...
|
||||
($i18n:expr, $message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
|
||||
{
|
||||
let key = $i18n.normalized_ftl_key($message, $comment);
|
||||
let mut args = $crate::i18n::FluentArgs::new();
|
||||
$(
|
||||
args.set(stringify!($param), $value);
|
||||
)*
|
||||
match $i18n.get_cached_string(key.borrow(), Some(&args)) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
// Fallback: replace placeholders with values
|
||||
let mut result = $message.to_string();
|
||||
$(
|
||||
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
|
||||
)*
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro for getting localized pluralized strings with count and named arguments
|
||||
///
|
||||
/// Syntax: tr_plural!(one, other, comment, count, param1=..., param2=...)
|
||||
/// - one: Message for the singular ("one") plural rule
|
||||
/// - other: Message for the "other" plural rule
|
||||
/// - comment: Context for translators
|
||||
/// - count: The count value
|
||||
/// - named arguments: Any additional named parameters for interpolation
|
||||
#[macro_export]
|
||||
macro_rules! tr_plural {
|
||||
// With named parameters
|
||||
($i18n:expr, $one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
|
||||
let norm_key = $i18n.normalized_ftl_key($other, $comment);
|
||||
let mut args = $crate::i18n::FluentArgs::new();
|
||||
args.set("count", $count);
|
||||
$(args.set(stringify!($param), $value);)*
|
||||
match $i18n.get_cached_string(norm_key.borrow(), Some(&args)) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
// Fallback: use simple pluralization
|
||||
if $count == 1 {
|
||||
let mut result = $one.to_string();
|
||||
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
|
||||
result = result.replace("{count}", &$count.to_string());
|
||||
result
|
||||
} else {
|
||||
let mut result = $other.to_string();
|
||||
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
|
||||
result = result.replace("{count}", &$count.to_string());
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
// Without named parameters
|
||||
($one:expr, $other:expr, $comment:expr, $count:expr) => {{
|
||||
$crate::tr_plural!($one, $other, $comment, $count, )
|
||||
}};
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
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};
|
||||
use poll_promise::Promise;
|
||||
|
||||
use egui::ColorImage;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, create_dir_all, File};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::{io, thread};
|
||||
|
||||
use hex::ToHex;
|
||||
use sha2::Digest;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{self, Path};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TexturesCache {
|
||||
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
|
||||
}
|
||||
|
||||
impl TexturesCache {
|
||||
pub fn handle_and_get_or_insert_loadable(
|
||||
&mut self,
|
||||
url: &str,
|
||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||
) -> LoadableTextureState<'_> {
|
||||
let internal = self.handle_and_get_state_internal(url, true, closure);
|
||||
|
||||
internal.into()
|
||||
}
|
||||
|
||||
pub fn handle_and_get_or_insert(
|
||||
&mut self,
|
||||
url: &str,
|
||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||
) -> TextureState<'_> {
|
||||
let internal = self.handle_and_get_state_internal(url, false, closure);
|
||||
|
||||
internal.into()
|
||||
}
|
||||
|
||||
fn handle_and_get_state_internal(
|
||||
&mut self,
|
||||
url: &str,
|
||||
use_loading: bool,
|
||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||
) -> &mut TextureStateInternal {
|
||||
let state = match self.cache.raw_entry_mut().from_key(url) {
|
||||
hashbrown::hash_map::RawEntryMut::Occupied(entry) => {
|
||||
let state = entry.into_mut();
|
||||
handle_occupied(state, use_loading);
|
||||
|
||||
state
|
||||
}
|
||||
hashbrown::hash_map::RawEntryMut::Vacant(entry) => {
|
||||
let res = closure();
|
||||
let (_, state) = entry.insert(url.to_owned(), TextureStateInternal::Pending(res));
|
||||
|
||||
state
|
||||
}
|
||||
};
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
pub fn insert_pending(&mut self, url: &str, promise: Promise<Option<Result<TexturedImage>>>) {
|
||||
self.cache
|
||||
.insert(url.to_owned(), TextureStateInternal::Pending(promise));
|
||||
}
|
||||
|
||||
pub fn move_to_loaded(&mut self, url: &str) {
|
||||
let hashbrown::hash_map::RawEntryMut::Occupied(entry) =
|
||||
self.cache.raw_entry_mut().from_key(url)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
entry.replace_entry_with(|_, v| {
|
||||
let TextureStateInternal::Loading(textured) = v else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(TextureStateInternal::Loaded(textured))
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
|
||||
self.cache.get_mut(url).map(|state| {
|
||||
handle_occupied(state, true);
|
||||
state.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_occupied(state: &mut TextureStateInternal, use_loading: bool) {
|
||||
let TextureStateInternal::Pending(promise) = state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(res) = promise.ready_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(res) = res.take() else {
|
||||
tracing::error!("Failed to take the promise");
|
||||
*state =
|
||||
TextureStateInternal::Error(crate::Error::Generic("Promise already taken".to_owned()));
|
||||
return;
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(textured) => {
|
||||
*state = if use_loading {
|
||||
TextureStateInternal::Loading(textured)
|
||||
} else {
|
||||
TextureStateInternal::Loaded(textured)
|
||||
}
|
||||
}
|
||||
Err(e) => *state = TextureStateInternal::Error(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LoadableTextureState<'a> {
|
||||
Pending,
|
||||
Error(&'a crate::Error),
|
||||
Loading {
|
||||
actual_image_tex: &'a mut TexturedImage,
|
||||
}, // the texture is in the loading state, for transitioning between the pending and loaded states
|
||||
Loaded(&'a mut TexturedImage),
|
||||
}
|
||||
|
||||
pub enum TextureState<'a> {
|
||||
Pending,
|
||||
Error(&'a crate::Error),
|
||||
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 {
|
||||
TextureStateInternal::Pending(_) => TextureState::Pending,
|
||||
TextureStateInternal::Error(error) => TextureState::Error(error),
|
||||
TextureStateInternal::Loading(textured_image) => TextureState::Loaded(textured_image),
|
||||
TextureStateInternal::Loaded(textured_image) => TextureState::Loaded(textured_image),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TextureStateInternal {
|
||||
Pending(Promise<Option<Result<TexturedImage>>>),
|
||||
Error(crate::Error),
|
||||
Loading(TexturedImage), // the image is in the loading state, for transitioning between blur and image
|
||||
Loaded(TexturedImage),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut TextureStateInternal> for LoadableTextureState<'a> {
|
||||
fn from(value: &'a mut TextureStateInternal) -> Self {
|
||||
match value {
|
||||
TextureStateInternal::Pending(_) => LoadableTextureState::Pending,
|
||||
TextureStateInternal::Error(error) => LoadableTextureState::Error(error),
|
||||
TextureStateInternal::Loading(textured_image) => LoadableTextureState::Loading {
|
||||
actual_image_tex: textured_image,
|
||||
},
|
||||
TextureStateInternal::Loaded(textured_image) => {
|
||||
LoadableTextureState::Loaded(textured_image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TexturedImage {
|
||||
Static(TextureHandle),
|
||||
Animated(Animation),
|
||||
}
|
||||
|
||||
impl TexturedImage {
|
||||
pub fn get_first_texture(&self) -> &TextureHandle {
|
||||
match self {
|
||||
TexturedImage::Static(texture_handle) => texture_handle,
|
||||
TexturedImage::Animated(animation) => &animation.first_frame.texture,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Animation {
|
||||
pub first_frame: TextureFrame,
|
||||
pub other_frames: Vec<TextureFrame>,
|
||||
pub receiver: Option<Receiver<TextureFrame>>,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> {
|
||||
if index == 0 {
|
||||
Some(&self.first_frame)
|
||||
} else {
|
||||
self.other_frames.get(index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_frames(&self) -> usize {
|
||||
self.other_frames.len() + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextureFrame {
|
||||
pub delay: Duration,
|
||||
pub texture: TextureHandle,
|
||||
}
|
||||
|
||||
pub struct ImageFrame {
|
||||
pub delay: Duration,
|
||||
pub image: ColorImage,
|
||||
}
|
||||
|
||||
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)]
|
||||
pub enum MediaCacheType {
|
||||
Image,
|
||||
Gif,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rel_dir(cache_type: MediaCacheType) -> &'static str {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => "img",
|
||||
MediaCacheType::Gif => "gif",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
|
||||
let file = Self::create_file(cache_dir, url)?;
|
||||
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
|
||||
|
||||
encoder.encode(
|
||||
data.as_raw(),
|
||||
data.size[0] as u32,
|
||||
data.size[1] as u32,
|
||||
image::ColorType::Rgba8.into(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> {
|
||||
let file_path = cache_dir.join(Self::key(url));
|
||||
if let Some(p) = file_path.parent() {
|
||||
create_dir_all(p)?;
|
||||
}
|
||||
Ok(File::options()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(file_path)?)
|
||||
}
|
||||
|
||||
pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
|
||||
let file = Self::create_file(cache_dir, url)?;
|
||||
|
||||
let mut encoder = image::codecs::gif::GifEncoder::new(file);
|
||||
for img in data {
|
||||
let buf = color_image_to_rgba(img.image);
|
||||
let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay));
|
||||
if let Err(e) = encoder.encode_frame(frame) {
|
||||
tracing::error!("problem encoding frame: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn key(url: &str) -> String {
|
||||
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
|
||||
PathBuf::from(&k[0..2])
|
||||
.join(&k[2..4])
|
||||
.join(k)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Migrate from base32 encoded url to sha256 url + sub-dir structure
|
||||
pub fn migrate_v0(&self) -> Result<()> {
|
||||
for file in std::fs::read_dir(&self.cache_dir)? {
|
||||
let file = if let Ok(f) = file {
|
||||
f
|
||||
} else {
|
||||
// not sure how this could fail, skip entry
|
||||
continue;
|
||||
};
|
||||
if !file.path().is_file() {
|
||||
continue;
|
||||
}
|
||||
let old_filename = file.file_name().to_string_lossy().to_string();
|
||||
let old_url = if let Some(u) =
|
||||
base32::decode(base32::Alphabet::Crockford, &old_filename)
|
||||
.and_then(|s| String::from_utf8(s).ok())
|
||||
{
|
||||
u
|
||||
} else {
|
||||
warn!("Invalid base32 filename: {}", &old_filename);
|
||||
continue;
|
||||
};
|
||||
let new_path = self.cache_dir.join(Self::key(&old_url));
|
||||
if let Some(p) = new_path.parent() {
|
||||
create_dir_all(p)?;
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::rename(file.path(), &new_path) {
|
||||
warn!(
|
||||
"Failed to migrate file from {} to {}: {:?}",
|
||||
file.path().display(),
|
||||
new_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let width = color_image.width() as u32;
|
||||
let height = color_image.height() as u32;
|
||||
|
||||
let rgba_pixels: Vec<u8> = color_image
|
||||
.pixels
|
||||
.iter()
|
||||
.flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]`
|
||||
.collect();
|
||||
|
||||
image::RgbaImage::from_raw(width, height, rgba_pixels)
|
||||
.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,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn migrate_v0(&self) -> Result<()> {
|
||||
self.static_imgs.migrate_v0()?;
|
||||
self.gifs.migrate_v0()
|
||||
}
|
||||
|
||||
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
|
||||
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
|
||||
}
|
||||
|
||||
pub fn find_renderable_media(
|
||||
urls: &mut UrlMimes,
|
||||
imeta: &HashMap<String, ImageMetadata>,
|
||||
url: &str,
|
||||
) -> Option<RenderableMedia> {
|
||||
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
|
||||
|
||||
let obfuscation_type = match imeta.get(url) {
|
||||
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
|
||||
None => ObfuscationType::Default,
|
||||
};
|
||||
|
||||
Some(RenderableMedia {
|
||||
url: url.to_string(),
|
||||
media_type,
|
||||
obfuscation_type,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn latest_texture(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
url: &str,
|
||||
img_type: ImageType,
|
||||
animation_mode: AnimationMode,
|
||||
) -> Option<TextureHandle> {
|
||||
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
||||
|
||||
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
|
||||
let is_loaded = self
|
||||
.get_cache_mut(cache_type)
|
||||
.textures_cache
|
||||
.handle_and_get_or_insert(url, || {
|
||||
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
|
||||
})
|
||||
.is_loaded();
|
||||
|
||||
if !is_loaded {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cache = match cache_type {
|
||||
MediaCacheType::Image => &mut self.static_imgs,
|
||||
MediaCacheType::Gif => &mut self.gifs,
|
||||
};
|
||||
|
||||
ensure_latest_texture_from_cache(
|
||||
ui,
|
||||
url,
|
||||
&mut self.gif_states,
|
||||
&mut cache.textures_cache,
|
||||
animation_mode,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => &self.static_imgs,
|
||||
MediaCacheType::Gif => &self.gifs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cache_mut(&mut self, cache_type: MediaCacheType) -> &mut MediaCache {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => &mut self.static_imgs,
|
||||
MediaCacheType::Gif => &mut self.gifs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_folder_contents(&mut self) -> io::Result<()> {
|
||||
for entry in fs::read_dir(self.base_path.clone())? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
fs::remove_dir_all(path)?;
|
||||
} else {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.urls.cache.clear();
|
||||
self.static_imgs.clear();
|
||||
self.gifs.clear();
|
||||
self.gif_states.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type GifStateMap = HashMap<String, GifState>;
|
||||
|
||||
pub struct GifState {
|
||||
pub last_frame_rendered: Instant,
|
||||
pub last_frame_duration: Duration,
|
||||
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,
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
use std::{
|
||||
future::Future,
|
||||
sync::{
|
||||
mpsc::{self, Sender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
type Job = Box<dyn FnOnce() + Send + 'static>;
|
||||
|
||||
pub struct JobPool {
|
||||
tx: Sender<Job>,
|
||||
}
|
||||
|
||||
impl Default for JobPool {
|
||||
fn default() -> Self {
|
||||
JobPool::new(2)
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
std::thread::spawn(move || loop {
|
||||
let job = {
|
||||
let Ok(unlocked) = arc_rx_clone.lock() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(job) = unlocked.recv() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
job
|
||||
};
|
||||
|
||||
job();
|
||||
});
|
||||
}
|
||||
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
pub fn schedule<F, T>(&self, job: F) -> impl Future<Output = T>
|
||||
where
|
||||
F: FnOnce() -> T + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let (tx_result, rx_result) = oneshot::channel::<T>();
|
||||
|
||||
let job = Box::new(move || {
|
||||
let output = job();
|
||||
let _ = tx_result.send(output);
|
||||
});
|
||||
|
||||
self.tx
|
||||
.send(job)
|
||||
.expect("receiver should not be deallocated");
|
||||
|
||||
async move {
|
||||
rx_result.await.unwrap_or_else(|_| {
|
||||
panic!("Worker thread or channel dropped before returning the result.")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::job_pool::JobPool;
|
||||
|
||||
fn test_fn(a: u32, b: u32) -> u32 {
|
||||
a + b
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test() {
|
||||
let pool = JobPool::default();
|
||||
|
||||
// Now each job can return different T
|
||||
let future_str = pool.schedule(|| -> String { "hello from string job".into() });
|
||||
|
||||
let a = 5;
|
||||
let b = 6;
|
||||
let future_int = pool.schedule(move || -> u32 { test_fn(a, b) });
|
||||
|
||||
println!("(Meanwhile we can do more async work) ...");
|
||||
|
||||
let s = future_str.await;
|
||||
let i = future_int.await;
|
||||
|
||||
println!("Got string: {:?}", s);
|
||||
println!("Got integer: {}", i);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use crate::JobPool;
|
||||
use egui::TextureHandle;
|
||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||
use poll_promise::Promise;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JobsCache {
|
||||
jobs: HashMap<JobIdOwned, JobState>,
|
||||
}
|
||||
|
||||
pub enum JobState {
|
||||
Pending(Promise<Option<Result<Job, JobError>>>),
|
||||
Error(JobError),
|
||||
Completed(Job),
|
||||
}
|
||||
|
||||
pub enum JobError {
|
||||
InvalidParameters,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JobParams<'a> {
|
||||
Blurhash(BlurhashParams<'a>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JobParamsOwned {
|
||||
Blurhash(BlurhashParamsOwned),
|
||||
}
|
||||
|
||||
impl<'a> From<BlurhashParams<'a>> for BlurhashParamsOwned {
|
||||
fn from(params: BlurhashParams<'a>) -> Self {
|
||||
BlurhashParamsOwned {
|
||||
blurhash: params.blurhash.to_owned(),
|
||||
url: params.url.to_owned(),
|
||||
ctx: params.ctx.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<JobParams<'a>> for JobParamsOwned {
|
||||
fn from(params: JobParams<'a>) -> Self {
|
||||
match params {
|
||||
JobParams::Blurhash(bp) => JobParamsOwned::Blurhash(bp.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BlurhashParams<'a> {
|
||||
pub blurhash: &'a str,
|
||||
pub url: &'a str,
|
||||
pub ctx: &'a egui::Context,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BlurhashParamsOwned {
|
||||
pub blurhash: String,
|
||||
pub url: String,
|
||||
pub ctx: egui::Context,
|
||||
}
|
||||
|
||||
impl JobsCache {
|
||||
pub fn get_or_insert_with<
|
||||
'a,
|
||||
F: FnOnce(Option<JobParamsOwned>) -> Result<Job, JobError> + Send + 'static,
|
||||
>(
|
||||
&'a mut self,
|
||||
job_pool: &mut JobPool,
|
||||
jobid: &JobId,
|
||||
params: Option<JobParams>,
|
||||
run_job: F,
|
||||
) -> &'a mut JobState {
|
||||
match self.jobs.raw_entry_mut().from_key(jobid) {
|
||||
RawEntryMut::Occupied(entry) => 's: {
|
||||
let mut state = entry.into_mut();
|
||||
|
||||
let JobState::Pending(promise) = &mut state else {
|
||||
break 's state;
|
||||
};
|
||||
|
||||
let Some(res) = promise.ready_mut() else {
|
||||
break 's state;
|
||||
};
|
||||
|
||||
let Some(res) = res.take() else {
|
||||
tracing::error!("Failed to take the promise for job: {:?}", jobid);
|
||||
break 's state;
|
||||
};
|
||||
|
||||
*state = match res {
|
||||
Ok(j) => JobState::Completed(j),
|
||||
Err(e) => JobState::Error(e),
|
||||
};
|
||||
|
||||
state
|
||||
}
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let owned_params = params.map(JobParams::into);
|
||||
let wrapped: Box<dyn FnOnce() -> Option<Result<Job, JobError>> + Send + 'static> =
|
||||
Box::new(move || Some(run_job(owned_params)));
|
||||
|
||||
let promise = Promise::spawn_async(job_pool.schedule(wrapped));
|
||||
|
||||
let (_, state) = entry.insert(jobid.into(), JobState::Pending(promise));
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, jobid: &JobId) -> Option<&JobState> {
|
||||
self.jobs.get(jobid)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&JobId<'a>> for JobIdOwned {
|
||||
fn from(jobid: &JobId<'a>) -> Self {
|
||||
match jobid {
|
||||
JobId::Blurhash(s) => JobIdOwned::Blurhash(s.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl hashbrown::Equivalent<JobIdOwned> for JobId<'_> {
|
||||
fn equivalent(&self, key: &JobIdOwned) -> bool {
|
||||
match (self, key) {
|
||||
(JobId::Blurhash(a), JobIdOwned::Blurhash(b)) => *a == b.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
enum JobIdOwned {
|
||||
Blurhash(String), // image URL
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash)]
|
||||
pub enum JobId<'a> {
|
||||
Blurhash(&'a str), // image URL
|
||||
}
|
||||
|
||||
pub enum Job {
|
||||
Blurhash(Option<TextureHandle>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Job {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Job::Blurhash(_) => write!(f, "Blurhash"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
pub mod abbrev;
|
||||
mod account;
|
||||
mod app;
|
||||
mod args;
|
||||
pub mod contacts;
|
||||
mod context;
|
||||
pub mod debouncer;
|
||||
mod error;
|
||||
pub mod filter;
|
||||
pub mod fonts;
|
||||
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;
|
||||
mod time;
|
||||
mod timecache;
|
||||
mod timed_serializer;
|
||||
pub mod ui;
|
||||
mod unknowns;
|
||||
mod urls;
|
||||
mod user_account;
|
||||
mod wallet;
|
||||
mod zaps;
|
||||
|
||||
pub use account::accounts::{AccountData, AccountSubs, Accounts};
|
||||
pub use account::contacts::{ContactState, IsFollowing};
|
||||
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 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,
|
||||
};
|
||||
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;
|
||||
pub use relayspec::RelaySpec;
|
||||
pub use result::Result;
|
||||
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,
|
||||
};
|
||||
pub use zaps::{
|
||||
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
|
||||
NoteZapTargetOwned, PendingDefaultZapState, ZapTarget, ZapTargetOwned, ZappingError,
|
||||
};
|
||||
|
||||
// export libs
|
||||
pub use enostr;
|
||||
pub use nostrdb;
|
||||
|
||||
pub use zaps::Zaps;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use nostrdb::Note;
|
||||
|
||||
use crate::jobs::{Job, JobError, JobParamsOwned};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImageMetadata {
|
||||
pub blurhash: String,
|
||||
pub dimensions: Option<PixelDimensions>, // width and height in pixels
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PixelDimensions {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
impl PixelDimensions {
|
||||
pub fn to_points(&self, ppp: f32) -> PointDimensions {
|
||||
PointDimensions {
|
||||
x: (self.x as f32) / ppp,
|
||||
y: (self.y as f32) / ppp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PointDimensions {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl PointDimensions {
|
||||
pub fn to_pixels(self, ui: &egui::Ui) -> PixelDimensions {
|
||||
PixelDimensions {
|
||||
x: (self.x * ui.pixels_per_point()).round() as u32,
|
||||
y: (self.y * ui.pixels_per_point()).round() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_vec(self) -> egui::Vec2 {
|
||||
egui::Vec2::new(self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageMetadata {
|
||||
pub fn scaled_pixel_dimensions(
|
||||
&self,
|
||||
ui: &egui::Ui,
|
||||
available_points: PointDimensions,
|
||||
) -> PixelDimensions {
|
||||
let max_pixels = available_points.to_pixels(ui);
|
||||
|
||||
let Some(defined_dimensions) = &self.dimensions else {
|
||||
return max_pixels;
|
||||
};
|
||||
|
||||
if defined_dimensions.x == 0 || defined_dimensions.y == 0 {
|
||||
tracing::error!("The blur dimensions should not be zero");
|
||||
return max_pixels;
|
||||
}
|
||||
|
||||
if defined_dimensions.y <= max_pixels.y {
|
||||
return defined_dimensions.clone();
|
||||
}
|
||||
|
||||
let scale_factor = (max_pixels.y as f32) / (defined_dimensions.y as f32);
|
||||
let max_width_scaled = scale_factor * (defined_dimensions.x as f32);
|
||||
|
||||
PixelDimensions {
|
||||
x: (max_width_scaled.round() as u32),
|
||||
y: max_pixels.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find blurhashes in image metadata and update our cache
|
||||
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
|
||||
for tag in note.tags() {
|
||||
let mut tag_iter = tag.into_iter();
|
||||
if tag_iter
|
||||
.next()
|
||||
.and_then(|s| s.str())
|
||||
.filter(|s| *s == "imeta")
|
||||
.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((url, blur)) = find_blur(tag_iter) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
blurs.insert(url.to_string(), blur);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
|
||||
let mut url = None;
|
||||
let mut blurhash = None;
|
||||
let mut dims = None;
|
||||
|
||||
for tag_elem in tag_iter {
|
||||
let Some(s) = tag_elem.str() else { continue };
|
||||
let mut split = s.split_whitespace();
|
||||
|
||||
let Some(first) = split.next() else { continue };
|
||||
let Some(second) = split.next() else { continue };
|
||||
|
||||
match first {
|
||||
"url" => url = Some(second),
|
||||
"blurhash" => blurhash = Some(second),
|
||||
"dim" => dims = Some(second),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if url.is_some() && blurhash.is_some() && dims.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let url = url?;
|
||||
let blurhash = blurhash?;
|
||||
|
||||
let dimensions = dims.and_then(|d| {
|
||||
let mut split = d.split('x');
|
||||
let width = split.next()?.parse::<u32>().ok()?;
|
||||
let height = split.next()?.parse::<u32>().ok()?;
|
||||
|
||||
Some(PixelDimensions {
|
||||
x: width,
|
||||
y: height,
|
||||
})
|
||||
});
|
||||
|
||||
Some((
|
||||
url.to_string(),
|
||||
ImageMetadata {
|
||||
blurhash: blurhash.to_string(),
|
||||
dimensions,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ObfuscationType {
|
||||
Blurhash(ImageMetadata),
|
||||
Default,
|
||||
}
|
||||
|
||||
pub fn compute_blurhash(
|
||||
params: Option<JobParamsOwned>,
|
||||
dims: PixelDimensions,
|
||||
) -> Result<Job, JobError> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let Some(JobParamsOwned::Blurhash(params)) = params
|
||||
else {
|
||||
return Err(JobError::InvalidParameters);
|
||||
};
|
||||
|
||||
let maybe_handle = match generate_blurhash_texturehandle(
|
||||
¶ms.ctx,
|
||||
¶ms.blurhash,
|
||||
¶ms.url,
|
||||
dims.x,
|
||||
dims.y,
|
||||
) {
|
||||
Ok(tex) => Some(tex),
|
||||
Err(e) => {
|
||||
tracing::error!("failed to render blurhash: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Job::Blurhash(maybe_handle))
|
||||
}
|
||||
|
||||
fn generate_blurhash_texturehandle(
|
||||
ctx: &egui::Context,
|
||||
blurhash: &str,
|
||||
url: &str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<egui::TextureHandle, crate::Error> {
|
||||
let bytes = blurhash::decode(blurhash, width, height, 1.0)
|
||||
.map_err(|e| crate::Error::Generic(e.to_string()))?;
|
||||
|
||||
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
|
||||
Ok(ctx.load_texture(url, img, Default::default()))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use nostrdb::ProfileRecord;
|
||||
|
||||
pub struct NostrName<'a> {
|
||||
pub username: Option<&'a str>,
|
||||
pub display_name: Option<&'a str>,
|
||||
pub nip05: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> NostrName<'a> {
|
||||
/// Our nostr name is usually our display_name, if we don't have
|
||||
/// that then its just the username
|
||||
pub fn name(&self) -> &'a str {
|
||||
if let Some(name) = self.display_name {
|
||||
name
|
||||
} else if let Some(name) = self.username {
|
||||
name
|
||||
} else {
|
||||
self.nip05.unwrap_or("??")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn username_or_displayname(&self) -> &'a str {
|
||||
if let Some(name) = self.username {
|
||||
name
|
||||
} else if let Some(name) = self.display_name {
|
||||
name
|
||||
} else {
|
||||
self.nip05.unwrap_or("??")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self {
|
||||
username: None,
|
||||
display_name: None,
|
||||
nip05: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(s: &str) -> bool {
|
||||
s.chars().all(|c| c.is_whitespace())
|
||||
}
|
||||
|
||||
pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> {
|
||||
let Some(record) = record else {
|
||||
return NostrName::unknown();
|
||||
};
|
||||
|
||||
let Some(profile) = record.record().profile() else {
|
||||
return NostrName::unknown();
|
||||
};
|
||||
|
||||
let display_name = profile.display_name().filter(|n| !is_empty(n));
|
||||
let username = profile.name().filter(|n| !is_empty(n));
|
||||
|
||||
let nip05 = if let Some(raw_nip05) = profile.nip05() {
|
||||
if let Some(at_pos) = raw_nip05.find('@') {
|
||||
if raw_nip05.starts_with('_') {
|
||||
raw_nip05.get(at_pos + 1..)
|
||||
} else {
|
||||
Some(raw_nip05)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
NostrName {
|
||||
username,
|
||||
display_name,
|
||||
nip05,
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use super::context::ContextSelection;
|
||||
use crate::{zaps::NoteZapTargetOwned, MediaAction};
|
||||
use egui::Vec2;
|
||||
use enostr::{NoteId, Pubkey};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScrollInfo {
|
||||
pub velocity: Vec2,
|
||||
pub offset: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NoteAction {
|
||||
/// User has clicked the quote reply action
|
||||
Reply(NoteId),
|
||||
|
||||
/// User has clicked the quote repost action
|
||||
Quote(NoteId),
|
||||
|
||||
/// User has clicked a hashtag
|
||||
Hashtag(String),
|
||||
|
||||
/// User has clicked a profile
|
||||
Profile(Pubkey),
|
||||
|
||||
/// User has clicked a note link
|
||||
Note {
|
||||
note_id: NoteId,
|
||||
preview: bool,
|
||||
scroll_offset: f32,
|
||||
},
|
||||
|
||||
/// User has selected some context option
|
||||
Context(ContextSelection),
|
||||
|
||||
/// User has clicked the zap action
|
||||
Zap(ZapAction),
|
||||
|
||||
/// User clicked on media
|
||||
Media(MediaAction),
|
||||
|
||||
/// User scrolled the timeline
|
||||
Scroll(ScrollInfo),
|
||||
}
|
||||
|
||||
impl NoteAction {
|
||||
pub fn note(id: NoteId) -> NoteAction {
|
||||
NoteAction::Note {
|
||||
note_id: id,
|
||||
preview: false,
|
||||
scroll_offset: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum ZapAction {
|
||||
Send(ZapTargetAmount),
|
||||
CustomizeAmount(NoteZapTargetOwned),
|
||||
ClearError(NoteZapTargetOwned),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct ZapTargetAmount {
|
||||
pub target: NoteZapTargetOwned,
|
||||
pub specified_msats: Option<u64>, // if None use default amount
|
||||
}
|
||||