44 Commits

Author SHA1 Message Date
tyiu 04f5725a9d Add Japanese and Portuguese (Portugal) languages
Changelog-Added: Added Japanese and Portuguese (Portugal) languages
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-10 20:02:01 -04:00
tyiu 59199d8197 Import translations
Changelog-Changed: Imported translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-10 20:01:54 -04:00
tyiu e6a27a53fe Remove unused strings from translation files
Changelog-Removed: Removed unused strings from translation files
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-09 21:00:18 -04:00
William Casarin 8138a0a1ca clndash: include listpeerchannel errors
in response

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 20:19:58 -07:00
William Casarin 2444e24fb5 clndash: summary cards
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 19:57:43 -07:00
William Casarin fc509b1b26 clndash: channels ui
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 18:34:42 -07:00
William Casarin 1fd92e9e00 default logs
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 15:32:28 -07:00
William Casarin 382ef772f5 clndash: initial peer channel listing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 15:31:58 -07:00
William Casarin 53b4a8da5c notedeck app: add clndash
a core-lightning dashboard i'm working on

feature-gate it behind --clndash

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-08 13:19:39 -07:00
William Casarin cb72592f4b android: fix dark/light mode and folding screen crash
We have to tell android not to restart the activity when a dark/light
mode is switched or when the phone is folded/unfolded. Otherwise
it will crash.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-07 16:27:29 -07:00
William Casarin c60e1af3eb chrome: add virtual keyboard ui 2025-08-06 19:00:30 -07:00
William Casarin 87cb5ed515 Merge thread scroll fix by kernel
kernelkind (5):
      TMP: use new egui-nav to fix scroll offset issues
      add `scroll_offset` to `NoteAction::Note`
      add `ThreadNote::set_scroll_offset`
      set scroll offset when routing to thread
      appease clippy
2025-08-04 15:08:32 -07:00
William Casarin 9cbba37507 debug: add repaint causes debug tool
enable with --debug, click on fps/frame time counter

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 15:04:38 -07:00
William Casarin b94e715539 ui: add AnimationMode to control GIF rendering behavior
Introduces an `AnimationMode` enum with `Reactive`, `Continuous`, and
`NoAnimation` variants to allow fine-grained control over GIF playback
across the UI. This supports performance optimizations and accessibility
features, such as disabling animations when requested.

- Plumbs AnimationMode through image rendering paths
- Replaces hardcoded gif frame logic with reusable `process_gif_frame`
- Supports customizable FPS in Continuous mode
- Enables global animation opt-out via `NoteOptions::NoAnimations`
- Applies mode-specific logic in profile pictures, posts, media carousels, and viewer

Animation behavior by context
-----------------------------

- Profile pictures: Reactive (render only on interaction/activity)
- PostView: NoAnimation if disabled in NoteOptions, else Continuous (uncapped)
- Media carousels: NoAnimation or Continuous (capped at 24fps)
- Viewer/gallery: Always Continuous (full animation)

In the future, we can customize these by power settings.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 13:41:24 -07:00
kernelkind d12f66e5cd appease clippy
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:13:53 -04:00
kernelkind e8be471608 set scroll offset when routing to thread
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:45 -04:00
kernelkind 97d15e41e7 add ThreadNote::set_scroll_offset
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:42 -04:00
kernelkind ea5c876da6 add scroll_offset to NoteAction::Note
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:38 -04:00
kernelkind 75eefcbf72 TMP: use new egui-nav to fix scroll offset issues
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-08-04 16:12:27 -04:00
William Casarin 54b86ee5a6 gif: disable continuous gif rendering
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 12:19:21 -07:00
William Casarin f6c44bba8a force oled with --mobile flag
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 12:06:58 -07:00
William Casarin 3451206f1a dave: switch to logical time
CI / Rustfmt + Clippy (push) Has been cancelled
CI / Check (android) (push) Has been cancelled
CI / Test (Linux) (push) Has been cancelled
CI / Test (macOS) (push) Has been cancelled
CI / Test (Windows) (push) Has been cancelled
CI / rpm/deb (aarch64) (push) Has been cancelled
CI / rpm/deb (x86_64) (push) Has been cancelled
CI / macOS dmg (aarch64) (push) Has been cancelled
CI / macOS dmg (x86_64) (push) Has been cancelled
CI / Windows Installer (aarch64) (push) Has been cancelled
CI / Windows Installer (x86_64) (push) Has been cancelled
CI / Upload Artifacts to Server (push) Has been cancelled
this fixes jumpy animations when we stop rendering

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 11:35:22 -07:00
William Casarin 0770bab37c battery: disable render every 100ms
our multicast poller was causing this

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 11:29:03 -07:00
William Casarin 48a11b9bab v0.6.0
What's new
==========

- New notifications indiciator dot on toolbar
- Fixed mentions/tagging
- Gave dave a new swarm look
- Persist some more settings
- Allow sorting thread replies newest first in options
- Show full created date format on selected notes
- Show client name on selected notes
- Higher quality media
- Increase media viewer transition animation
- Fix some ui glitches when replying
- Fix gpu crash on adrendo devices (some samsung galaxy tablets)

Fernando López Guevara (14):
      feat(note): show full created date format on selected notes
      feat(notedeck): add cross-platform URI opener
      feat(settings): allow sorting thread replies newest first
      feat(settings): persist settings to storage
      feat(settings): show note full date
      fix(media): add spacing
      fix(note-content): avoid empty text blocks
      fix(settings): use localization
      refactor(settings): add settings sections methods
      settings: use timed serializer, handle zoom properly, use custom text style for note body font size, added font size slider, added preview note
      update i18n comments for source client options
      Update crates/notedeck/src/persist/settings_handler.rs

Terry Yiu (2):
      Import Spanish translations
      Fix localization issues and export strings for translation

William Casarin (31):
      add NotedeckOptions and feature flags, add notebook feature
      android: fix build
      chrome: remove duplication in app setup
      columns: clean up flags, refactor content rendering
      columns: fix double reference
      dave: switch to use standard vertex/index buffers
      evolve dave into a swarm
      init notebook
      lint: fix format issue
      make clippy happy
      media: less blurry media
      mediaviewer: decrease transition anim from 500ms to 300ms
      note/ui: fix reply line when replying in narrow mode
      note: small doc fix
      note: turn off full date view for previews
      notebook: draw edges and arrows
      notebook: fix heights of nodes
      notebook: fix node sizes
      notebook: move ui code into its own file
      notebook: remove redundant closure
      perf: a few micro optimizations
      post: set client tag to Damus Android on android
      refactor: collapse client label settings; drop CLI/settings toggles
      remove explicit loop continue
      ui/note: fix extra padding in block renderer
      ui/note: fix indented actionbar in non-wide mode
      ui/note: fix reply description item spacing
      ui/note: fix width instabilities because of spacing_mut
      ui/note: slightly more spacing between blocks
      ui: keep original design on non-narrow

kernelkind (12):
      TMP: update egui for better TextInputState handling
      add `NotesFreshness` to `TimelineTab`
      chrome: method to find whether there are unseen notifications
      extract notifications filter to own method
      fix scroll regression
      insert space after mention selection
      mention-picker: re-add spacing from inner_margin
      mentions: don't lose focus after select mention
      paint unseen indicator
      rename `SearchResultsView` => `MentionPickerView`
      set fresh from `TimelineCache`
      use unseen notification indicator

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-04 08:41:46 -07:00
William Casarin 603de6bbab evolve dave into a swarm
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 23:07:48 -07:00
William Casarin 571bf35109 dave: switch to use standard vertex/index buffers
Fixes: https://github.com/damus-io/notedeck/issues/902
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 20:17:24 -07:00
William Casarin 0dda26791a perf: a few micro optimizations
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 20:17:16 -07:00
William Casarin 7e73ed2760 ui/note: slightly more spacing between blocks
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 20:17:16 -07:00
William Casarin 2fb9470ee6 note/ui: fix reply line when replying in narrow mode
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:54:04 -07:00
William Casarin af2c556700 post: set client tag to Damus Android on android
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:48:34 -07:00
William Casarin 27df33dc83 ui/note: fix reply description item spacing
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:30:37 -07:00
William Casarin 2edc19fbcc ui/note: fix extra padding in block renderer
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:26:26 -07:00
William Casarin edf0e2498b note: small doc fix
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:18:00 -07:00
William Casarin ad35547582 refactor: collapse client label settings; drop CLI/settings toggles
The "top vs bottom" client label setting was cluttering the UI and
codebase with toggles that added little value. This consolidates client
label handling into one option, removes unused CLI/settings knobs, and
makes NoteView’s API consistent and fluent. Result: fewer knobs, less
branching, and a clearer, more predictable UI.

Now client labels are only shown in one place: selected notes.

- Drop `--show-client` arg in notedeck and `--show-note-client=top|bottom`
  args in notedeck_columns

- Remove `NotedeckOptions::ShowClient` and related CLI parsing

- Delete `ShowSourceClientOption` enum, settings UI, and
  `SettingsAction::SetShowSourceClient`

- Collapse `NoteOptions::{ClientNameTop, ClientNameBottom}` into a single
  `NoteOptions::ClientName`

- Add `NoteOptions::{Framed, UnreadIndicator}`

- Move “framed” and unread indicator into flags (no more ad‑hoc bools)

- Add new NoteView builder methods: `.client_name()`, `.frame()`,
  `.unread_indicator()`, and `.selected_style()`

- CLI flags for showing client labels have been removed

- `ClientNameTop`/`ClientNameBottom` replaced with `ClientName`

- API using `framed` or `show_unread_indicator` booleans must now use
  the new flag setters

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:16:15 -07:00
William Casarin 24f70930eb note: turn off full date view for previews
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 14:53:14 -07:00
William Casarin 5b1bc442d4 Pull spanish translations from terry
Terry Yiu (2):
      Import Spanish translations
      Fix localization issues and export strings for translation
2025-08-03 14:02:43 -07:00
William Casarin 391abe817d columns: clean up flags, refactor content rendering
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 14:02:05 -07:00
William Casarin 30eb2e0258 columns: fix double reference
its not needed

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 14:00:12 -07:00
William Casarin 21fe3527a8 lint: fix format issue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 13:58:14 -07:00
William Casarin 249e166a95 remove explicit loop continue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 10:44:07 -07:00
William Casarin 3f9d030046 Merge remote-tracking branch 'github/pr/1025' 2025-08-03 10:38:38 -07:00
Fernando López Guevara 26ece3bc05 feat(note): show full created date format on selected notes 2025-08-01 08:42:58 -03:00
Fernando López Guevara a64ff3b630 feat(note): created at show full date format 2025-08-01 08:40:10 -03:00
Fernando López Guevara ab84304265 feat(settings): show note full date 2025-08-01 08:38:49 -03:00
64 changed files with 2714 additions and 677 deletions
+3
View File
@@ -18,4 +18,7 @@ export OLLAMA_HOST=http://ollama.jb55.com
# simple todo reminders
export TODO_FILE=TODO
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
2>/dev/null todo.sh ls || :
Generated
+47 -7
View File
@@ -1554,7 +1554,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.2.0"
source = "git+https://github.com/damus-io/egui-nav?rev=3c67eb6298edbff36d46546897cfac33df4f04db#3c67eb6298edbff36d46546897cfac33df4f04db"
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
dependencies = [
"egui",
"egui_extras",
@@ -2336,6 +2336,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -3064,6 +3070,22 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lnsocket"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a373bcde8b65d6db11a0cd0f70dd4a24af854dd7a112b0a51258593c65f48ff"
dependencies = [
"bitcoin",
"hashbrown 0.13.2",
"hex",
"lightning-types",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "lock_api"
version = "0.4.13"
@@ -3478,13 +3500,14 @@ dependencies = [
[[package]]
name = "notedeck"
version = "0.5.9"
version = "0.6.0"
dependencies = [
"base32",
"bech32",
"bincode",
"bitflags 2.9.1",
"blurhash",
"chrono",
"dirs",
"eframe",
"egui",
@@ -3529,8 +3552,9 @@ dependencies = [
[[package]]
name = "notedeck_chrome"
version = "0.5.9"
version = "0.6.0"
dependencies = [
"bitflags 2.9.1",
"eframe",
"egui",
"egui-winit",
@@ -3538,6 +3562,7 @@ dependencies = [
"egui_tabs",
"nostrdb",
"notedeck",
"notedeck_clndash",
"notedeck_columns",
"notedeck_dave",
"notedeck_notebook",
@@ -3557,9 +3582,23 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "notedeck_clndash"
version = "0.6.0"
dependencies = [
"eframe",
"egui",
"lnsocket",
"notedeck",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "notedeck_columns"
version = "0.5.9"
version = "0.6.0"
dependencies = [
"base64 0.22.1",
"bech32",
@@ -3613,7 +3652,7 @@ dependencies = [
[[package]]
name = "notedeck_dave"
version = "0.5.9"
version = "0.6.0"
dependencies = [
"async-openai",
"bytemuck",
@@ -3621,6 +3660,7 @@ dependencies = [
"eframe",
"egui",
"egui-wgpu",
"egui_extras",
"enostr",
"futures",
"hex",
@@ -3637,7 +3677,7 @@ dependencies = [
[[package]]
name = "notedeck_notebook"
version = "0.5.9"
version = "0.6.0"
dependencies = [
"egui",
"jsoncanvas",
@@ -3646,7 +3686,7 @@ dependencies = [
[[package]]
name = "notedeck_ui"
version = "0.5.9"
version = "0.6.0"
dependencies = [
"bitflags 2.9.1",
"eframe",
+6 -3
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
package.version = "0.5.9"
package.version = "0.6.0"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
@@ -8,12 +8,14 @@ members = [
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/notedeck_clndash",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
]
[workspace.dependencies]
opener = "0.8.2"
chrono = "0.4.40"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
@@ -25,7 +27,7 @@ egui = { version = "0.31.1", features = ["serde"] }
egui-wgpu = "0.31.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "3c67eb6298edbff36d46546897cfac33df4f04db" }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "de6e2d51892478fdd516df166f866e64dedbae07" }
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
#egui_virtual_list = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
@@ -47,6 +49,7 @@ nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_clndash = { path = "crates/notedeck_clndash" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="clnlogo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.078823"
inkscape:cx="396.72867"
inkscape:cy="561.25984"
inkscape:window-width="2020"
inkscape:window-height="1420"
inkscape:window-x="270"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
style="display:inline">
<g
id="g4"
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
<path
class="st1"
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
id="path3"
style="fill:#f0d003" />
<path
fill="#fffae6"
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
id="path4" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+50 -8
View File
@@ -45,6 +45,8 @@ Algo_2452 = Algorithmus
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
# Label for zap amount input field
Amount_70f0 = Menge
# Label for appearance settings section
Appearance_4c7f = Darstellung
# Button to send message to Dave AI assistant
Ask_b7f4 = Fragen
# Placeholder text for Dave AI input field
@@ -59,10 +61,18 @@ Broadcast_fe43 = Senden
Broadcast_Local_7e50 = Lokal senden
# Button label to cancel an action
Cancel_ed3b = Abbrechen
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Abbrechen
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Zwischenspeicher leeren
# Hover text for editable zap amount
Click_to_edit_0414 = Zum Bearbeiten anklicken
# Column title for note composition
Compose_Note_c094 = Notiz erstellen
# Label for configure relays, settings section
Configure_relays_d156 = Relays konfigurieren
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Bestätigen
# Button label to confirm an action
Confirm_f8a6 = Bestätigen
# Status label for connected relay
@@ -88,19 +98,19 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
# Copy the text content of the note to clipboard
Copy_Text_f81c = Text kopieren
# Relative time in days
count_d_b9be = { $count }Tg.
count_d_b9be = { $count }T
# Relative time in hours
count_h_3ecb = { $count }Std.
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }Min.
count_m_b41e = { $count }min
# Relative time in months
count_mo_7aba = { $count }Mon.
count_mo_7aba = { $count }M
# Relative time in seconds
count_s_aa26 = { $count }Sek.
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }Wo.
count_w_7468 = { $count }W
# Relative time in years
count_y_9408 = { $count }J.
count_y_9408 = { $count }J
# Button to create a new account
Create_Account_6994 = Konto erstellen
# Button label to create a new deck
@@ -111,6 +121,8 @@ Custom_a69e = Benutzerdefiniert
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for Theme Dark, Appearance settings section
Dark_85fe = Dunkel
# Label for deck name input field
Deck_name_cd32 = Deck-Name
# Label for decks section in side panel
@@ -151,12 +163,16 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
# Label for find user button
Find_User_bd12 = Profil finden
# Label for font size, Appearance settings section
Font_size_dd73 = Schriftgröße:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Startseite
# Label for deck icon selection
Icon_b0ab = Symbol
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Bildcache Größe:
# Title for individual user column
Individual_b776 = Individuell
# Error message for invalid zap amount
@@ -177,8 +193,12 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
# Label for language, Appearance settings section
Language_e264 = Sprache:
# Title for last note per user column
Last_Note_per_User_17ad = Letzte Notiz pro Profil
# Label for Theme Light, Appearance settings section
Light_7475 = Hell
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
# Login page title
@@ -216,11 +236,15 @@ Notifications_d673 = Benachrichtigungen
# Title for notifications column
Notifications_ef56 = Benachrichtigungen
# Relative time for very recent events (less than 3 seconds)
now_2181 = Jetzt
now_2181 = Gerade eben
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = An
# Button label to open email client
Open_Email_25e9 = E-Mail öffnen
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
# Label for others settings section
Others_7267 = Andere
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
# Error message for missing deck name
@@ -267,6 +291,10 @@ replying_to_a_note_e0bc = Antwort auf eine Notiz
Repost_this_note_8e56 = Diese Notiz teilen
# Label for reposted notes
Reposted_61c8 = Teilen
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Zurücksetzen
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Zurücksetzen
# Heading for support section
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -289,6 +317,8 @@ See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
# Button label to send a zap
Send_1ea4 = Senden
# Column title for app settings
Settings_7a4f = Einstellungen
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
# Button label to sign out of account
@@ -297,6 +327,8 @@ Sign_out_337b = Abmelden
Someone_else_s_Notes_7e5f = Notizen anderer Profile
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Neueste Antworten zuerst sortieren:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
# Description for hashtags column
@@ -315,10 +347,14 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Ben
Step_1_8656 = Schritt 1
# Step 2 label in support instructions
Step_2_d08d = Schritt 2
# Label for storage settings section
Storage_ed65 = Speicher
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
# Support email address
Support_email_44d9 = E-Mail Support:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
# Hover text for light mode toggle button
@@ -327,6 +363,8 @@ Switch_to_light_mode_72ce = Zum Hellmodus wechseln
Tap_to_Load_4b05 = Zum Laden antippen
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
# Label for theme, Appearance settings section
Theme_4aac = Design:
# Column title for note thread view
Thread_0f20 = Unterhaltung
# Link text for thread references
@@ -341,6 +379,8 @@ Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das ak
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
# Profile username field label
Username_daa7 = Benutzername
# Label for view folder button, Storage settings section
View_folder_9742 = Ordner anzeigen
# Column title for wallet management
Wallet_5e50 = Wallet
# Hint for deck name input field
@@ -359,6 +399,8 @@ Your_Notifications_080d = Deine Benachrichtigungen
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zappe diese Notiz
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoomstufe:
# Pluralized strings
-12
View File
@@ -79,9 +79,6 @@ Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Bottom
# Broadcast the note to all connected relays
Broadcast_fe43 = Broadcast
@@ -247,9 +244,6 @@ Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Hide
# Title for Home column
Home_8c19 = Home
@@ -493,9 +487,6 @@ 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:
# Label for Source client, others settings section
Source_client_fb2b = Source client:
# 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
@@ -556,9 +547,6 @@ Thread_0f20 = Thread
# Link text for thread references
thread_ad1f = thread
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Top
# Title for universe column
Universe_e01e = Universe
-12
View File
@@ -79,9 +79,6 @@ Banner_52ef = {"["}Bàññér{"]"}
# Beta version label
BETA_8e5d = {"["}BÉTÀ{"]"}
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = {"["}Bóttóm{"]"}
# Broadcast the note to all connected relays
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
@@ -247,9 +244,6 @@ Font_size_dd73 = {"["}Fóñt sízé:{"]"}
# Title for hashtags column
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
# Option in settings section to hide the source client label in note display
Hide_281d = {"["}Hídé{"]"}
# Title for Home column
Home_8c19 = {"["}Hómé{"]"}
@@ -493,9 +487,6 @@ Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtí
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = {"["}Sórt réplíés ñéwést fírst:{"]"}
# Label for Source client, others settings section
Source_client_fb2b = {"["}Sóúrçé çlíéñt:{"]"}
# 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{"]"}
@@ -556,9 +547,6 @@ Thread_0f20 = {"["}Thréàd{"]"}
# Link text for thread references
thread_ad1f = {"["}thréàd{"]"}
# Option in settings section to show the source client label at the top of the note
Top_6aeb = {"["}Tóp{"]"}
# Title for universe column
Universe_e01e = {"["}Úñívérsé{"]"}
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Parte inferior
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
@@ -163,10 +161,10 @@ Enter_your_key_0fca = Ingresa tu clave
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Ocultar
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
@@ -237,6 +235,8 @@ Notifications_d673 = Notificaciones
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = respondiendo a una nota
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configuración
# Label for Show source client, others settings section
Show_source_client_9e31 = Mostrar cliente de origen
# 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
@@ -325,6 +325,8 @@ Sign_out_337b = Cerrar sesión
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
@@ -349,6 +351,8 @@ Storage_ed65 = Almacenamiento
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
@@ -363,8 +367,6 @@ Theme_4aac = Tema:
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Parte superior
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Parte inferior
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
@@ -163,10 +161,10 @@ Enter_your_key_0fca = Ingresa tu clave
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Ocultar
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
@@ -237,6 +235,8 @@ Notifications_d673 = Notificaciones
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = respondiendo a una nota
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configuración
# Label for Show source client, others settings section
Show_source_client_9e31 = Mostrar cliente de origen
# 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
@@ -325,6 +325,8 @@ Sign_out_337b = Cerrar sesión
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
@@ -349,6 +351,8 @@ Storage_ed65 = Almacenamiento
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
@@ -363,8 +367,6 @@ Theme_4aac = Tema:
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Parte superior
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
Banner_52ef = Bannière
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = En bas
# Broadcast the note to all connected relays
Broadcast_fe43 = Diffusion
# Broadcast the note only to local network relays
@@ -163,10 +161,10 @@ Enter_your_key_0fca = Entrez votre clé
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
# Label for find user button
Find_User_bd12 = Trouver un utilisateur
# Label for font size, Appearance settings section
Font_size_dd73 = Taille du texte :
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Masquer
# Title for Home column
Home_8c19 = Accueil
# Label for deck icon selection
@@ -237,6 +235,8 @@ Notifications_d673 = Notifications
Notifications_ef56 = Notifications
# Relative time for very recent events (less than 3 seconds)
now_2181 = maintenant
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Activé
# Button label to open email client
Open_Email_25e9 = Ouvrir Email
# Instruction to open email client
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = répondre à une note
Repost_this_note_8e56 = Republier cette note
# Label for reposted notes
Reposted_61c8 = Republier
# Label for reset 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
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
Send_1ea4 = Envoyer
# Column title for app settings
Settings_7a4f = Paramètres
# Label for Show source client, others settings section
Show_source_client_9e31 = Afficher le client source
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
# Button label to sign out of account
@@ -325,6 +325,8 @@ Sign_out_337b = Se déconnecter
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Trier les réponses les plus récentes en premier :
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
# Description for hashtags column
@@ -349,6 +351,8 @@ Storage_ed65 = Stockage
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
# Support email address
Support_email_44d9 = Adresse email de l'assistance :
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Passer en mode sombre
# Hover text for light mode toggle button
@@ -363,8 +367,6 @@ Theme_4aac = Thème :
Thread_0f20 = Fil
# Link text for thread references
thread_ad1f = fil
# Option in settings section to show the source client label at the top of the note
Top_6aeb = En haut
# Title for universe column
Universe_e01e = Universel
# Column title for universe feed
+410
View File
@@ -0,0 +1,410 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 概要
# Column title for account management
Accounts_f018 = アカウント
# Button label to add a relay
Add_269d = 追加
# Label for add column button
Add_47df = 追加
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = このアカウントでのみ使用される別のウォレットを追加
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = 続行するにはウォレットを追加してください
# Button label to add a new account
Add_account_1cfc = アカウントを追加
# Column title for adding new account
Add_Account_d06c = アカウントの追加
# Column title for adding algorithm column
Add_Algo_Column_0d75 = アルゴカラムの追加
# Column title for adding new column
Add_Column_c764 = カラムの追加
# Column title for adding new deck
Add_Deck_fabf = デッキの追加
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = 外部通知カラムの追加
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = ハッシュタグカラムの追加
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = 最後の投稿カラムの追加
# Column title for adding notifications column
Add_Notifications_Column_79f8 = 外部通知カラムの追加
# Button label to add a relay
Add_relay_269d = リレーを追加
# Button label to add a wallet
Add_Wallet_d1be = ウォレットを追加
# Title for algorithmic feeds column
Algo_2452 = アルゴ
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 投稿の発見に役立つアルゴリズムフィードです
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外観
# Button to send message to Dave AI assistant
Ask_b7f4 = 質問
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Dave に何でも質問してみましょう…
# Profile banner URL field label
Banner_52ef = バナー
# Beta version label
BETA_8e5d = ベータ
# Broadcast the note to all connected relays
Broadcast_fe43 = ブロードキャスト
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = ローカルにブロードキャスト
# Button label to cancel an action
Cancel_ed3b = キャンセル
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = キャンセル
# Label for clear cache button, Storage settings section
Clear_cache_dccb = キャッシュを消去
# Hover text for editable zap amount
Click_to_edit_0414 = クリックして編集
# Column title for note composition
Compose_Note_c094 = メモの作成
# Label for configure relays, settings section
Configure_relays_d156 = リレーを設定
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 決定
# Button label to confirm an action
Confirm_f8a6 = 決定
# Status label for connected relay
Connected_f8cc = 接続済
# Status label for connecting relay
Connecting_6b7e = 接続中…
# Title for contact list column
Contact_List_f85a = フォロイーリスト
# Column title for contact lists
Contacts_7533 = フォロー
# Column title for last notes per contact
Contacts__last_notes_3f84 = フォロー (最後の投稿)
# Button label to copy logs
Copy_a688 = コピー
# Button to copy media link to clipboard
Copy_Link_dc7c = リンクをコピー
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = 投稿 ID をコピー
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = 投稿の JSON をコピー
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = 公開鍵をコピー
# Copy the text content of the note to clipboard
Copy_Text_f81c = テキストをコピー
# Relative time in days
count_d_b9be = { $count }日
# Relative time in hours
count_h_3ecb = { $count }時間
# Relative time in minutes
count_m_b41e = { $count }分
# Relative time in months
count_mo_7aba = { $count }ヶ月
# Relative time in seconds
count_s_aa26 = { $count }秒
# Relative time in weeks
count_w_7468 = { $count }週間
# Relative time in years
count_y_9408 = { $count }年
# Button to create a new account
Create_Account_6994 = アカウントを作成
# Button label to create a new deck
Create_Deck_16b7 = デッキを作成
# Column title for custom timelines
Custom_a69e = カスタマイズ
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap 金額をカスタマイズ
# Column title for support page
Damus_Support_27c0 = Damus サポート
# Label for Theme Dark, Appearance settings section
Dark_85fe = ダーク
# Label for deck name input field
Deck_name_cd32 = デッキ名
# Label for decks section in side panel
DECKS_1fad = デッキ
# Label for default zap amount input
Default_amount_per_zap_399d = Zap ごとのデフォルトの金額:
# Name of the default deck feed
Default_Deck_fcca = 既定のデッキ
# Button label to delete a deck
Delete_Deck_bb29 = デッキを削除
# Tooltip for deleting a column
Delete_this_column_8d5a = このカラムを削除します
# Button label to delete a wallet
Delete_Wallet_d1d4 = ウォレットを削除
# Profile display name field label
Display_name_f9d9 = 表示名
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" が識別に使用されます
# Column title for editing deck
Edit_Deck_4018 = デッキの編集
# Button label to edit a deck
Edit_Deck_fd93 = デッキを編集
# Button label to edit user profile
Edit_Profile_49e6 = プロファイルを編集
# Column title for profile editing
Edit_Profile_8ad4 = プロファイルの編集
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 必要なハッシュタグをここに入力してください (複数スペースで区切る場合)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = ここにリレーを入力してください
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = ユーザーの鍵 (npub, hex, nip05) を入力してください...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = 鍵を入力してください
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 公開鍵 (npub)、nostr アドレス (例: { $address })、秘密鍵 (nsec) を入力してください。 投稿、返信などを行うには秘密鍵を入力する必要があります。
# Label for find user button
Find_User_bd12 = ユーザーを探す
# Label for font size, Appearance settings section
Font_size_dd73 = フォントサイズ:
# Title for hashtags column
Hashtags_f8e0 = ハッシュタグ
# Title for Home column
Home_8c19 = ホーム
# Label for deck icon selection
Icon_b0ab = アイコン
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 画像キャッシュのサイズ:
# Title for individual user column
Individual_b776 = 個人用
# Error message for invalid zap amount
Invalid_amount_6630 = 無効な金額です
# Error message for invalid key input
Invalid_key_4726 = 無効な鍵です。
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = 無効な NWC URI です
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 投稿と返信を記録します
# Label for language, Appearance settings section
Language_e264 = 言語:
# Title for last note per user column
Last_Note_per_User_17ad = ユーザーごとの最後の投稿
# Label for Theme Light, Appearance settings section
Light_7475 = ライト
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ライトニングネットワークアドレス (lud16)
# Login page title
Login_9eef = ログイン
# Login button text
Login_now___let_s_do_this_5630 = 今すぐログイン — レッツゴー!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = フォローしていない人のメディアです
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = このカラムを別の位置に移動します
# Title for the user's deck
My_Deck_4ac5 = あなたのデッキ
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nostr は初めてですか?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Nostr アドレス (NIP-05)
# Default username when profile is not available
nostrich_df29 = ノス民
# Status label for disconnected relay
Not_Connected_6292 = 未接続
# Link text for note references
note_cad6 = 投稿
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck はベータ製品です。問題が発生した場合はサポートに問い合わせてください。
# Filter label for notes only view
Notes_03fb = 投稿
# Label for notes-only filter
Notes_60d2 = 投稿
# Filter label for notes and replies view
Notes___Replies_1ec2 = 投稿 & 返信
# Label for notes and replies filter
Notes___Replies_6e3b = 投稿 & 返信
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = たった今
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 有効
# Button label to open email client
Open_Email_25e9 = メールを開く
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = デフォルトのメールクライアントを開いて、Damus チームのヘルプを表示しましょう。
# Label for others settings section
Others_7267 = その他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = ここに NWC の URI を貼り付けてください...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = デッキの名前を作成してください。
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = デッキの名前を作成してアイコンを選択してください。
# Error message for missing deck icon
Please_select_an_icon_655b = アイコンを選択してください。
# Button label to post a note
Post_now_8a49 = すぐに投稿
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 下のボタンを押して、最新のログをシステムのクリップボードにコピーします。その後、メールに貼り付けてください。
# Profile picture URL field label
Profile_picture_81ff = プロフィール写真
# Column title for quote composition
Quote_475c = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 不明な投稿の引用です
# Label for read-only profile mode
Read_only_82ff = 読み取り専用
# Column title for relay management
Relays_9d89 = リレー
# Label for relay list section
Relays_ad5e = リレー
# Column title for reply composition
Reply_3bf1 = 返信
# Hover text for reply button
Reply_to_this_note_f5de = この投稿に返信
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = 不明な投稿に返信しています
# Fallback template for replying to user
replying_to__user_15ab = { $user } に返信
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = 誰かのスレッドで { $user } に返信
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = { $user }の { $note } の { $thread_user }の { $thread } に返信
# Template for replying to user's note
replying_to__user__s__note_ccba = { $user }の { $note } に返信
# Template for replying to root thread
replying_to__user__s__thread_444d = { $user }の { $thread } に返信
# Fallback text when reply note is not found
replying_to_a_note_e0bc = 投稿に返信
# Hover text for repost button
Repost_this_note_8e56 = このメモを再投稿
# Label for reposted notes
Reposted_61c8 = 再投稿
# Label for reset note body font size, Appearance settings section
Reset_4e60 = リセット
# Label for reset zoom level, Appearance settings section
Reset_62d4 = リセット
# Heading for support section
Running_into_a_bug_1796 = バグに遭遇しましたか?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 変更を保存
# Column title for search page
Search_c573 = 検索
# Placeholder for search notes input field
Search_notes_42a6 = 投稿を検索しましょう...
# Search in progress message
Searching_for___query_5d18 = 「{ $query }」を検索中
# Description for Home column
See_notes_from_your_contacts_ac16 = フォローしている人の投稿を表示
# Description for universe column
See_the_whole_nostr_universe_7694 = 全ユニバースを表示します
# Button label to send a zap
Send_1ea4 = 送信
# Column title for app settings
Settings_7a4f = 設定
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 一覧から各ユーザーの最後の投稿を表示する
# Button label to sign out of account
Sign_out_337b = サインアウト
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = 他の人の投稿
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 他の人の通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 最新の返信を最初に並べ替え:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = フォローリストにある各ユーザーの最後の投稿を取得します
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = 特定のハッシュタグで最新の情報を受け取ります
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = 通知とメンションの最新の情報を受け取ります
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 他のユーザーの投稿と返信の最新の情報を受け取ります
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 投稿と返信の最新の情報を受け取ります
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = あなたの通知とメンションの最新の情報を受け取ります
# Step 1 label in support instructions
Step_1_8656 = ステップ 1
# Step 2 label in support instructions
Step_2_d08d = ステップ 2
# Label for storage settings section
Storage_ed65 = ストレージ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 他のユーザー投稿の購読
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 投稿の購読
# Support email address
Support_email_44d9 = サポートメール:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = ダークモードに切り替える
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = ライトモードに切り替える
# Button text to load blurred media
Tap_to_Load_4b05 = タップして読み込む
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI アシスタントトライアルが終了しました: (テストしていただきありがとうございます! Zap 対応デイブは近日公開予定です!
# Label for theme, Appearance settings section
Theme_4aac = テーマ:
# Column title for note thread view
Thread_0f20 = スレッド
# Link text for thread references
thread_ad1f = スレッド
# Title for universe column
Universe_e01e = ユニバース
# Column title for universe feed
Universe_ffaa = ユニバース
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = このウォレットを現在のアカウントにのみ使用する
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $domain }" の "{ $username }" が識別に使用されます
# Profile username field label
Username_daa7 = ユーザー名
# Label for view folder button, Storage settings section
View_folder_9742 = フォルダを表示
# Column title for wallet management
Wallet_5e50 = ウォレット
# Hint for deck name input field
We_recommend_short_names_083e = 短い名前を推奨しています
# Profile website field label
Website_7980 = Web サイト
# Placeholder for note input field
Write_a_banger_note_here_bad2 = アツい一言をどうぞ...
# Placeholder text for key input field
Your_key_here_81bd = ここに鍵を入力...
# Title for your notes column
Your_Notes_f6db = 投稿
# Title for your notifications column
Your_Notifications_080d = 通知
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = この投稿に Zap
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 拡大率:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] { $query } の結果を '{ $count }' 件取得しました
*[other] ' { $query } の結果を '{ $count }' 件取得しました
}
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = Perguntar ao Dave
Banner_52ef = Destaque
# Beta version label
BETA_8e5d = Beta
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Abaixo
# Broadcast the note to all connected relays
Broadcast_fe43 = Encaminhar
# Broadcast the note only to local network relays
@@ -163,10 +161,10 @@ Enter_your_key_0fca = Sua chave aqui
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Pesquisar usuário
# Label for font size, Appearance settings section
Font_size_dd73 = Tamanho da letra
# Title for hashtags column
Hashtags_f8e0 = #
# Option in settings section to hide the source client label in note display
Hide_281d = Ocultar
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
@@ -237,6 +235,8 @@ Notifications_d673 = Notificações
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = Agora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = Ligar
# Button label to open email client
Open_Email_25e9 = Abrir E-mail
# Instruction to open email client
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = Respondendo nota
Repost_this_note_8e56 = Republicar nota
# Label for reposted notes
Reposted_61c8 = Publicada
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Redefinir
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Resetar
# Heading for support section
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Label for Show source client, others settings section
Show_source_client_9e31 = Mostrar cliente de origem
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
# Button label to sign out of account
@@ -325,6 +325,8 @@ Sign_out_337b = Sair
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes primeiro:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
# Description for hashtags column
@@ -349,6 +351,8 @@ Storage_ed65 = Armazenamento
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
# Support email address
Support_email_44d9 = E-mail de suporte
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para modo escuro
# Hover text for light mode toggle button
@@ -363,8 +367,6 @@ Theme_4aac = Tema:
Thread_0f20 = Fio
# Link text for thread references
thread_ad1f = Fio
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Topo
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
+410
View File
@@ -0,0 +1,410 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = 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 }'
}
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = ถามเดฟได้ทุกเรื่อง.
Banner_52ef = ภาพปก
# Beta version label
BETA_8e5d = เบต้า
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = ด้านล่าง
# Broadcast the note to all connected relays
Broadcast_fe43 = เผยแพร่
# Broadcast the note only to local network relays
@@ -165,10 +163,10 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
# Label for find user button
Find_User_bd12 = ค้นหาผู้ใช้
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
# Title for hashtags column
Hashtags_f8e0 = แฮชแท็ก
# Option in settings section to hide the source client label in note display
Hide_281d = ซ่อน
# Title for Home column
Home_8c19 = หน้าแรก
# Label for deck icon selection
@@ -239,6 +237,8 @@ Notifications_d673 = การแจ้งเตือน
Notifications_ef56 = การแจ้งเตือน
# Relative time for very recent events (less than 3 seconds)
now_2181 = เมื่อสักครู่
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
# Button label to open email client
Open_Email_25e9 = เปิดอีเมล
# Instruction to open email client
@@ -291,6 +291,8 @@ replying_to_a_note_e0bc = ตอบกลับโน้ต
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
# Label for reposted notes
Reposted_61c8 = รีโพสต์แล้ว
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
# Label for reset zoom level, Appearance settings section
Reset_62d4 = รีเซ็ต
# Heading for support section
@@ -317,8 +319,6 @@ See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ท
Send_1ea4 = ส่ง
# Column title for app settings
Settings_7a4f = การตั้งค่า
# Label for Show source client, others settings section
Show_source_client_9e31 = แสดงไคลเอนต์ต้นทาง
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
# Button label to sign out of account
@@ -327,6 +327,8 @@ Sign_out_337b = ออกจากระบบ
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 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
@@ -351,6 +353,8 @@ Storage_ed65 = พื้นที่จัดเก็บ
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
# Support email address
Support_email_44d9 = Support email:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
# Hover text for light mode toggle button
@@ -365,8 +369,6 @@ Theme_4aac = ธีม:
Thread_0f20 = เธรด
# Link text for thread references
thread_ad1f = เธรด
# Option in settings section to show the source client label at the top of the note
Top_6aeb = ด้านบน
# Title for universe column
Universe_e01e = จักรวาล
# Column title for universe feed
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = 向 Dave 提问任何问题…
Banner_52ef = 横幅
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = 底部
# Broadcast the note to all connected relays
Broadcast_fe43 = 广播
# Broadcast the note only to local network relays
@@ -163,10 +161,10 @@ Enter_your_key_0fca = 请输入你的密钥
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥(npub)、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
# Label for find user button
Find_User_bd12 = 查找用户
# Label for font size, Appearance settings section
Font_size_dd73 = 字体大小:
# Title for hashtags column
Hashtags_f8e0 = 标签
# Option in settings section to hide the source client label in note display
Hide_281d = 隐藏
# Title for Home column
Home_8c19 = 主页
# Label for deck icon selection
@@ -237,6 +235,8 @@ Notifications_d673 = 通知
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 刚刚
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 开启
# Button label to open email client
Open_Email_25e9 = 打开电子邮箱
# Instruction to open email client
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = 正在回复笔记
Repost_this_note_8e56 = 转发此笔记
# Label for reposted notes
Reposted_61c8 = 已转发
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
Send_1ea4 = 发送
# Column title for app settings
Settings_7a4f = 设置
# Label for Show source client, others settings section
Show_source_client_9e31 = 显示来源客户端
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
# Button label to sign out of account
@@ -325,6 +325,8 @@ Sign_out_337b = 登出
Someone_else_s_Notes_7e5f = 其他人的笔记
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回复:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 获取你的联系人列表中每个用户的最新一条笔记
# Description for hashtags column
@@ -349,6 +351,8 @@ Storage_ed65 = 存储
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
# Support email address
Support_email_44d9 = 支持电子邮件:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切换到暗色模式
# Hover text for light mode toggle button
@@ -363,8 +367,6 @@ Theme_4aac = 主题:
Thread_0f20 = 帖子
# Link text for thread references
thread_ad1f = 帖子
# Option in settings section to show the source client label at the top of the note
Top_6aeb = 顶部
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
+10 -8
View File
@@ -55,8 +55,6 @@ Ask_dave_anything_33d1 = 向 Dave 提問任何問題...
Banner_52ef = 橫幅
# Beta version label
BETA_8e5d = 測試版
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = 底部
# Broadcast the note to all connected relays
Broadcast_fe43 = 廣播
# Broadcast the note only to local network relays
@@ -163,10 +161,10 @@ Enter_your_key_0fca = 請輸入你的密鑰
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰(npub)、nostr 地址(如 { $address })、或私鑰(nsec)。你必須輸入你的私鑰才能發貼、回覆等等。
# Label for find user button
Find_User_bd12 = 查找用戶
# Label for font size, Appearance settings section
Font_size_dd73 = 字體大小:
# Title for hashtags column
Hashtags_f8e0 = 標籤
# Option in settings section to hide the source client label in note display
Hide_281d = 隱藏
# Title for Home column
Home_8c19 = 主頁
# Label for deck icon selection
@@ -237,6 +235,8 @@ Notifications_d673 = 通知
Notifications_ef56 = 通知
# Relative time for very recent events (less than 3 seconds)
now_2181 = 剛剛
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = 開啟
# Button label to open email client
Open_Email_25e9 = 打開電子郵箱
# Instruction to open email client
@@ -289,6 +289,8 @@ replying_to_a_note_e0bc = 正在回覆筆記
Repost_this_note_8e56 = 轉發此筆記
# Label for reposted notes
Reposted_61c8 = 已轉發
# Label for reset note body font size, Appearance settings section
Reset_4e60 = 重置
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
@@ -315,8 +317,6 @@ See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
Send_1ea4 = 發送
# Column title for app settings
Settings_7a4f = 設置
# Label for Show source client, others settings section
Show_source_client_9e31 = 顯示來源客戶端
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
# Button label to sign out of account
@@ -325,6 +325,8 @@ Sign_out_337b = 登出
Someone_else_s_Notes_7e5f = 其他人的筆記
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = 其他人的通知
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = 按最新排序回覆:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = 獲取你的聯繫人列表中每個用戶的最新一條筆記
# Description for hashtags column
@@ -349,6 +351,8 @@ Storage_ed65 = 儲存
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
# Support email address
Support_email_44d9 = 支持電子郵件:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切換到暗色模式
# Hover text for light mode toggle button
@@ -363,8 +367,6 @@ Theme_4aac = 主題:
Thread_0f20 = 串文
# Link text for thread references
thread_ad1f = 串文
# Option in settings section to show the source client label at the top of the note
Top_6aeb = 頂部
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
+1 -1
View File
@@ -135,7 +135,7 @@ pub fn setup_multicast_relay(
std::thread::spawn(move || {
let mut events = Events::with_capacity(1);
loop {
if let Err(err) = poll.poll(&mut events, Some(Duration::from_millis(100))) {
if let Err(err) = poll.poll(&mut events, None) {
error!("multicast socket poll error: {err}. ending multicast poller.");
return;
}
+1
View File
@@ -49,6 +49,7 @@ once_cell = { workspace = true }
md5 = { workspace = true }
bitflags = { workspace = true }
regex = "1"
chrono = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
+1 -1
View File
@@ -296,7 +296,7 @@ impl Notedeck {
.cloned()
.collect();
if !completely_unrecognized.is_empty() {
let err = format!("Unrecognized arguments: {:?}", completely_unrecognized);
let err = format!("Unrecognized arguments: {completely_unrecognized:?}");
tracing::error!("{}", &err);
return Err(Error::Generic(err));
}
+2 -2
View File
@@ -124,10 +124,10 @@ impl Args {
res.options.set(NotedeckOptions::UseKeystore, true);
} else if arg == "--relay-debug" {
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--show-client" {
res.options.set(NotedeckOptions::ShowClient, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
} else if arg == "--clndash" {
res.options.set(NotedeckOptions::FeatureClnDash, true);
} else {
unrecognized_args.insert(arg.clone());
}
+17 -1
View File
@@ -11,11 +11,13 @@ const DE: LanguageIdentifier = langid!("de");
const ES_419: LanguageIdentifier = langid!("es-419");
const ES_ES: LanguageIdentifier = langid!("es-ES");
const FR: LanguageIdentifier = langid!("fr");
const JA: LanguageIdentifier = langid!("ja");
const PT_BR: LanguageIdentifier = langid!("pt-BR");
const PT_PT: LanguageIdentifier = langid!("pt-PT");
const TH: LanguageIdentifier = langid!("th");
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
const NUM_FTLS: usize = 10;
const NUM_FTLS: usize = 12;
const EN_US_NATIVE_NAME: &str = "English (US)";
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
@@ -23,7 +25,9 @@ const DE_NATIVE_NAME: &str = "Deutsch";
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
const ES_ES_NATIVE_NAME: &str = "Español (España)";
const FR_NATIVE_NAME: &str = "Français";
const JA_NATIVE_NAME: &str = "日本語";
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
const PT_PT_NATIVE_NAME: &str = "Português (Portugal)";
const TH_NATIVE_NAME: &str = "ภาษาไทย";
const ZH_CN_NATIVE_NAME: &str = "简体中文";
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
@@ -58,10 +62,18 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
identifier: FR,
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
},
StaticBundle {
identifier: JA,
ftl: include_str!("../../../../assets/translations/ja/main.ftl"),
},
StaticBundle {
identifier: PT_BR,
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
},
StaticBundle {
identifier: PT_PT,
ftl: include_str!("../../../../assets/translations/pt-PT/main.ftl"),
},
StaticBundle {
identifier: TH,
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
@@ -113,7 +125,9 @@ impl Default for Localization {
ES_419.clone(),
ES_ES.clone(),
FR.clone(),
JA.clone(),
PT_BR.clone(),
PT_PT.clone(),
TH.clone(),
ZH_CN.clone(),
ZH_TW.clone(),
@@ -126,7 +140,9 @@ impl Default for Localization {
(ES_419, ES_419_NATIVE_NAME.to_owned()),
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
(FR, FR_NATIVE_NAME.to_owned()),
(JA, JA_NATIVE_NAME.to_owned()),
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
(PT_PT, PT_PT_NATIVE_NAME.to_owned()),
(TH, TH_NATIVE_NAME.to_owned()),
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
+9 -1
View File
@@ -1,5 +1,6 @@
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;
@@ -464,6 +465,7 @@ impl Images {
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)?;
@@ -485,7 +487,13 @@ impl Images {
MediaCacheType::Gif => &mut self.gifs,
};
ensure_latest_texture_from_cache(ui, url, &mut self.gif_states, &mut cache.textures_cache)
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 {
+1
View File
@@ -80,6 +80,7 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
pub use style::NotedeckTextStyle;
pub use theme::ColorTheme;
pub use time::time_ago_since;
pub use time::time_format;
pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
+109 -66
View File
@@ -3,14 +3,18 @@ use std::{
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)?;
@@ -18,7 +22,102 @@ pub fn ensure_latest_texture_from_cache(
return None;
};
Some(ensure_latest_texture(ui, url, gifs, img))
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(
@@ -26,6 +125,7 @@ pub fn ensure_latest_texture(
url: &str,
gifs: &mut GifStateMap,
img: &mut TexturedImage,
animation_mode: AnimationMode,
) -> TextureHandle {
match img {
TexturedImage::Static(handle) => handle.clone(),
@@ -45,77 +145,20 @@ pub fn ensure_latest_texture(
}
}
let now = Instant::now();
let (texture, maybe_new_state, request_next_repaint) = match gifs.get(url) {
Some(prev_state) => {
let should_advance =
now - prev_state.last_frame_rendered >= prev_state.last_frame_duration;
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
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 = SystemTime::now().checked_add(frame.delay);
(
&frame.texture,
Some(GifState {
last_frame_rendered: now,
last_frame_duration: frame.delay,
next_frame_time,
last_frame_index: maybe_new_index,
}),
next_frame_time,
)
}
None => {
let (tex, state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (&frame.texture, None),
None => (&animation.first_frame.texture, None),
};
(tex, state, prev_state.next_frame_time)
}
}
} else {
let (tex, state) = match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (&frame.texture, None),
None => (&animation.first_frame.texture, None),
};
(tex, state, prev_state.next_frame_time)
}
}
None => (
&animation.first_frame.texture,
Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
next_frame_time: None,
last_frame_index: 0,
}),
None,
),
};
if let Some(new_state) = maybe_new_state {
if let Some(new_state) = next_state.maybe_new_state {
gifs.insert(url.to_owned(), new_state);
}
if let Some(req) = request_next_repaint {
tracing::trace!("requesting repaint for {url} after {req:?}");
// 24fps for gif is fine
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(41));
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);
}
}
texture.clone()
next_state.texture
}
}
}
+18
View File
@@ -12,3 +12,21 @@ pub use blur::{
};
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)
}
}
+6 -1
View File
@@ -24,7 +24,11 @@ pub enum NoteAction {
Profile(Pubkey),
/// User has clicked a note link
Note { note_id: NoteId, preview: bool },
Note {
note_id: NoteId,
preview: bool,
scroll_offset: f32,
},
/// User has selected some context option
Context(ContextSelection),
@@ -44,6 +48,7 @@ impl NoteAction {
NoteAction::Note {
note_id: id,
preview: false,
scroll_offset: 0.0,
}
}
}
+3 -3
View File
@@ -20,15 +20,15 @@ bitflags! {
/// Use keystore?
const UseKeystore = 1 << 4;
/// Show client on notes?
const ShowClient = 1 << 5;
/// Simulate is_compiled_as_mobile ?
const Mobile = 1 << 6;
// ===== Feature Flags ======
/// Is notebook enabled?
const FeatureNotebook = 1 << 32;
/// Is clndash enabled?
const FeatureClnDash = 1 << 33;
}
}
+14 -4
View File
@@ -1,12 +1,22 @@
#[cfg(target_os = "android")]
pub mod android;
const VIRT_HEIGHT: i32 = 400;
#[cfg(target_os = "android")]
pub fn virtual_keyboard_height() -> i32 {
android::virtual_keyboard_height()
pub fn virtual_keyboard_height(virt: bool) -> i32 {
if virt {
VIRT_HEIGHT
} else {
android::virtual_keyboard_height()
}
}
#[cfg(not(target_os = "android"))]
pub fn virtual_keyboard_height() -> i32 {
0
pub fn virtual_keyboard_height(virt: bool) -> i32 {
if virt {
VIRT_HEIGHT
} else {
0
}
}
+1 -2
View File
@@ -13,8 +13,7 @@ pub fn setup_egui_context(
zoom_factor: f32,
) {
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
let is_oled = crate::ui::is_oled();
let is_oled = crate::ui::is_oled(is_mobile);
ctx.options_mut(|o| {
tracing::info!("Loaded theme {:?} from disk", theme);
+9
View File
@@ -1,4 +1,5 @@
use crate::{tr, Localization};
use chrono::DateTime;
use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds
@@ -83,6 +84,14 @@ fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String
}
}
pub fn time_format(_i18n: &mut Localization, timestamp: u64) -> String {
// TODO: format this using the selected locale
DateTime::from_timestamp(timestamp as i64, 0)
.unwrap()
.format("%l:%M %p %b %d, %Y")
.to_string()
}
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
+2 -2
View File
@@ -16,8 +16,8 @@ pub fn is_narrow(ctx: &egui::Context) -> bool {
screen_size.x < NARROW_SCREEN_WIDTH
}
pub fn is_oled() -> bool {
is_compiled_as_mobile()
pub fn is_oled(is_mobile_override: bool) -> bool {
is_mobile_override || is_compiled_as_mobile()
}
#[inline]
+2
View File
@@ -9,6 +9,7 @@ license = "GPLv3"
description = "The nostr browser"
[dependencies]
bitflags = { workspace = true }
eframe = { workspace = true }
egui_tabs = { workspace = true }
egui_extras = { workspace = true }
@@ -17,6 +18,7 @@ notedeck_columns = { workspace = true }
notedeck_ui = { workspace = true }
notedeck_dave = { workspace = true }
notedeck_notebook = { workspace = true }
notedeck_clndash = { workspace = true }
notedeck = { workspace = true }
nostrdb = { workspace = true }
puffin = { workspace = true, optional = true }
@@ -8,7 +8,7 @@
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
android:exported="true"
android:launchMode="singleTask"
>
+3
View File
@@ -1,4 +1,5 @@
use notedeck::{AppAction, AppContext};
use notedeck_clndash::ClnDash;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_notebook::Notebook;
@@ -8,6 +9,7 @@ pub enum NotedeckApp {
Dave(Box<Dave>),
Columns(Box<Damus>),
Notebook(Box<Notebook>),
ClnDash(Box<ClnDash>),
Other(Box<dyn notedeck::App>),
}
@@ -17,6 +19,7 @@ impl notedeck::App for NotedeckApp {
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
NotedeckApp::Other(other) => other.update(ctx, ui),
}
}
+150 -73
View File
@@ -2,6 +2,7 @@
//#[cfg(target_arch = "wasm32")]
//use wasm_bindgen::prelude::*;
use crate::app::NotedeckApp;
use crate::ChromeOptions;
use eframe::CreationContext;
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder};
@@ -17,35 +18,19 @@ use notedeck_columns::{
Damus,
};
use notedeck_dave::{Dave, DaveAvatar};
use notedeck_notebook::Notebook;
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
use std::collections::HashMap;
static ICON_WIDTH: f32 = 40.0;
pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
#[derive(Default)]
pub struct Chrome {
active: i32,
open: bool,
tab_selected: i32,
options: ChromeOptions,
apps: Vec<NotedeckApp>,
#[cfg(feature = "memory")]
show_memory_debug: bool,
}
impl Default for Chrome {
fn default() -> Self {
Self {
active: 0,
tab_selected: 0,
// sidemenu is not open by default on mobile/narrow uis
open: !notedeck::ui::is_compiled_as_mobile(),
apps: vec![],
#[cfg(feature = "memory")]
show_memory_debug: false,
}
}
pub repaint_causes: HashMap<egui::RepaintCause, u64>,
}
/// When you click the toolbar button, these actions
@@ -212,13 +197,17 @@ impl Chrome {
chrome.add_app(NotedeckApp::Notebook(Box::default()));
}
if notedeck.has_option(NotedeckOptions::FeatureClnDash) {
chrome.add_app(NotedeckApp::ClnDash(Box::default()));
}
chrome.set_active(0);
Ok(chrome)
}
pub fn toggle(&mut self) {
self.open = !self.open;
self.options.toggle(ChromeOptions::IsOpen);
}
pub fn add_app(&mut self, app: NotedeckApp) {
@@ -245,16 +234,6 @@ impl Chrome {
None
}
fn get_notebook(&mut self) -> Option<&mut Notebook> {
for app in &mut self.apps {
if let NotedeckApp::Notebook(notebook) = app {
return Some(notebook);
}
}
None
}
fn switch_to_dave(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Dave(_) = app {
@@ -263,14 +242,6 @@ impl Chrome {
}
}
fn switch_to_notebook(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Notebook(_) = app {
self.active = i as i32;
}
}
}
fn switch_to_columns(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Columns(_) = app {
@@ -361,7 +332,9 @@ impl Chrome {
fn amount_open(&self, ui: &mut egui::Ui) -> f32 {
let open_id = egui::Id::new("chrome_open");
let side_panel_width: f32 = 74.0;
ui.ctx().animate_bool(open_id, self.open) * side_panel_width
ui.ctx()
.animate_bool(open_id, self.options.contains(ChromeOptions::IsOpen))
* side_panel_width
}
fn toolbar_height() -> f32 {
@@ -472,12 +445,25 @@ impl Chrome {
fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> {
ui.spacing_mut().item_spacing.x = 0.0;
if notedeck::ui::is_narrow(ui.ctx()) {
if ctx.args.options.contains(NotedeckOptions::Debug)
&& ui.ctx().input(|i| i.key_pressed(egui::Key::Backtick))
{
self.options.toggle(ChromeOptions::VirtualKeyboard);
}
let r = if notedeck::ui::is_narrow(ui.ctx()) {
self.toolbar_chrome(ctx, ui)
} else {
let amt_open = self.amount_open(ui);
self.panel(ctx, StripBuilder::new(ui), amt_open)
};
// virtual keyboard
if self.options.contains(ChromeOptions::VirtualKeyboard) {
virtual_keyboard_ui(ui);
}
r
}
fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
@@ -492,37 +478,37 @@ impl Chrome {
if ui.add(expand_side_panel_button()).clicked() {
//self.active = (self.active + 1) % (self.apps.len() as i32);
self.open = !self.open;
self.options.toggle(ChromeOptions::IsOpen);
}
ui.add_space(4.0);
ui.add(milestone_name(i18n));
ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode;
if columns_button(ui)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
self.active = 0;
}
ui.add_space(32.0);
if let Some(dave) = self.get_dave() {
let rect = dave_sidebar_rect(ui);
let dave_resp = dave_button(dave.avatar_mut(), ui, rect)
.on_hover_cursor(egui::CursorIcon::PointingHand);
if dave_resp.clicked() {
self.switch_to_dave();
}
}
//ui.add_space(32.0);
for (i, app) in self.apps.iter_mut().enumerate() {
let r = match app {
NotedeckApp::Columns(_columns_app) => columns_button(ui),
if let Some(_notebook) = self.get_notebook() {
if notebook_button(ui)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
self.switch_to_notebook();
NotedeckApp::Dave(dave) => {
ui.add_space(24.0);
let rect = dave_sidebar_rect(ui);
dave_button(dave.avatar_mut(), ui, rect)
}
NotedeckApp::ClnDash(_clndash) => clndash_button(ui),
NotedeckApp::Notebook(_notebook) => notebook_button(ui),
NotedeckApp::Other(_other) => {
// app provides its own button rendering ui?
panic!("TODO: implement other apps")
}
};
ui.add_space(4.0);
if r.on_hover_cursor(egui::CursorIcon::PointingHand).clicked() {
self.active = i as i32;
}
}
}
@@ -719,6 +705,17 @@ fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
)
}
fn clndash_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"clndash-button",
24.0,
app_images::cln_image(),
app_images::cln_image(),
ui,
false,
)
}
fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"notebook-button",
@@ -959,7 +956,7 @@ fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
/// The section of the chrome sidebar that starts at the
/// bottom and goes up
fn bottomup_sidebar(
_chrome: &mut Chrome,
chrome: &mut Chrome,
ctx: &mut AppContext,
ui: &mut egui::Ui,
) -> Option<ChromePanelAction> {
@@ -1009,11 +1006,30 @@ fn bottomup_sidebar(
.on_hover_cursor(egui::CursorIcon::PointingHand);
if ctx.args.options.contains(NotedeckOptions::Debug) {
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
ui.weak(format!(
"{:10.1}",
ctx.frame_history.mean_frame_time() * 1e3
));
let r = ui
.weak(format!("{}", ctx.frame_history.fps() as i32))
.union(ui.weak(format!(
"{:10.1}",
ctx.frame_history.mean_frame_time() * 1e3
)))
.on_hover_cursor(egui::CursorIcon::PointingHand);
if r.clicked() {
chrome.options.toggle(ChromeOptions::RepaintDebug);
}
if chrome.options.contains(ChromeOptions::RepaintDebug) {
for cause in ui.ctx().repaint_causes() {
chrome
.repaint_causes
.entry(cause)
.and_modify(|rc| {
*rc += 1;
})
.or_insert(1);
}
repaint_causes_window(ui, &chrome.repaint_causes)
}
#[cfg(feature = "memory")]
{
@@ -1024,14 +1040,14 @@ fn bottomup_sidebar(
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
_chrome.show_memory_debug = !_chrome.show_memory_debug;
chrome.show_memory_debug = !chrome.show_memory_debug;
}
}
if let Some(resident) = mem_use.resident {
ui.weak(format!("{}", format_bytes(resident as f64)));
}
if _chrome.show_memory_debug {
if chrome.show_memory_debug {
egui::Window::new("Memory Debug").show(ui.ctx(), memory_debug_ui);
}
}
@@ -1151,3 +1167,64 @@ pub fn format_bytes(number_of_bytes: f64) -> String {
format!("{:.*} GiB", decimals, number_of_bytes / 30.0_f64.exp2())
}
}
fn repaint_causes_window(ui: &mut egui::Ui, causes: &HashMap<egui::RepaintCause, u64>) {
egui::Window::new("Repaint Causes").show(ui.ctx(), |ui| {
use egui_extras::{Column, TableBuilder};
TableBuilder::new(ui)
.column(Column::auto().at_least(600.0).resizable(true))
.column(Column::auto().at_least(50.0).resizable(true))
.column(Column::auto().at_least(50.0).resizable(true))
.column(Column::remainder())
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("file");
});
header.col(|ui| {
ui.heading("line");
});
header.col(|ui| {
ui.heading("count");
});
header.col(|ui| {
ui.heading("reason");
});
})
.body(|mut body| {
for (cause, hits) in causes.iter() {
body.row(30.0, |mut row| {
row.col(|ui| {
ui.label(cause.file.to_string());
});
row.col(|ui| {
ui.label(format!("{}", cause.line));
});
row.col(|ui| {
ui.label(format!("{hits}"));
});
row.col(|ui| {
ui.label(format!("{}", &cause.reason));
});
});
}
});
});
}
fn virtual_keyboard_ui(ui: &mut egui::Ui) {
let height = notedeck::platform::virtual_keyboard_height(true);
let screen_rect = ui.ctx().screen_rect();
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
let rect = Rect::from_min_max(min, screen_rect.max);
let painter = ui.painter_at(rect);
painter.rect_filled(rect, 0.0, Color32::from_black_alpha(200));
ui.put(rect, |ui: &mut egui::Ui| {
ui.centered_and_justified(|ui| {
ui.label("This is a keyboard");
})
.response
});
}
+2
View File
@@ -5,6 +5,8 @@ mod android;
mod app;
mod chrome;
mod options;
pub use app::NotedeckApp;
pub use chrome::Chrome;
pub use options::ChromeOptions;
+35
View File
@@ -0,0 +1,35 @@
use bitflags::bitflags;
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChromeOptions: u64 {
/// Is the chrome currently open?
const NoOptions = 0;
/// Is the chrome currently open?
const IsOpen = 1 << 0;
/// Are we simulating a virtual keyboard? This is mostly for debugging
/// if we are too lazy to open up a real mobile device with soft
/// keyboard
const VirtualKeyboard = 1 << 1;
/// Are we showing the memory debug window?
const MemoryDebug = 1 << 2;
/// Repaint debug
const RepaintDebug = 1 << 3;
}
}
impl Default for ChromeOptions {
fn default() -> Self {
let mut options = ChromeOptions::NoOptions;
options.set(
ChromeOptions::IsOpen,
!notedeck::ui::is_compiled_as_mobile(),
);
options
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "notedeck_clndash"
edition = "2024"
version.workspace = true
[dependencies]
egui = { workspace = true }
notedeck = { workspace = true }
#notedeck_ui = { workspace = true }
eframe = { workspace = true }
lnsocket = "0.4.0"
tracing = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
+526
View File
@@ -0,0 +1,526 @@
use egui::{Color32, Label, RichText};
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
use lnsocket::{CommandoClient, LNSocket};
use notedeck::{AppAction, AppContext};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::str::FromStr;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
struct Channel {
to_us: i64,
to_them: i64,
original: ListPeerChannel,
}
struct Channels {
max_total_msat: i64,
avail_in: i64,
avail_out: i64,
channels: Vec<Channel>,
}
#[derive(Default)]
pub struct ClnDash {
initialized: bool,
connection_state: ConnectionState,
get_info: Option<String>,
channels: Option<Result<Channels, lnsocket::Error>>,
channel: Option<CommChannel>,
last_summary: Option<Summary>,
}
impl Default for ConnectionState {
fn default() -> Self {
ConnectionState::Dead("uninitialized".to_string())
}
}
struct CommChannel {
req_tx: UnboundedSender<Request>,
event_rx: UnboundedReceiver<Event>,
}
/// Responses from the socket
enum ClnResponse {
GetInfo(Value),
ListPeerChannels(Result<Channels, lnsocket::Error>),
}
#[derive(Deserialize, Serialize)]
struct ListPeerChannel {
short_channel_id: String,
our_reserve_msat: i64,
to_us_msat: i64,
total_msat: i64,
their_reserve_msat: i64,
}
enum ConnectionState {
Dead(String),
Connecting,
Active,
}
#[derive(Eq, PartialEq, Clone, Debug)]
enum Request {
GetInfo,
ListPeerChannels,
}
enum Event {
/// We lost the socket somehow
Ended {
reason: String,
},
Connected,
Response(ClnResponse),
}
impl notedeck::App for ClnDash {
fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
if !self.initialized {
self.connection_state = ConnectionState::Connecting;
self.setup_connection();
self.initialized = true;
}
self.process_events();
self.show(ui);
None
}
}
fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
match state {
ConnectionState::Active => {
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
}
ConnectionState::Connecting => {
ui.add(Label::new(
RichText::new("Connecting").color(Color32::YELLOW),
));
}
ConnectionState::Dead(reason) => {
ui.add(Label::new(
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
));
}
}
}
impl ClnDash {
fn show(&mut self, ui: &mut egui::Ui) {
egui::Frame::new()
.inner_margin(egui::Margin::same(20))
.show(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
connection_state_ui(ui, &self.connection_state);
if let Some(Ok(ch)) = self.channels.as_ref() {
let summary = compute_summary(ch);
summary_cards_ui(ui, &summary, self.last_summary.as_ref());
ui.add_space(8.0);
}
channels_ui(ui, &self.channels);
if let Some(info) = self.get_info.as_ref() {
get_info_ui(ui, info);
}
});
});
}
fn setup_connection(&mut self) {
let (req_tx, mut req_rx) = unbounded_channel::<Request>();
let (event_tx, event_rx) = unbounded_channel::<Event>();
self.channel = Some(CommChannel { req_tx, event_rx });
tokio::spawn(async move {
let key = SecretKey::new(&mut rand::thread_rng());
let their_pubkey = PublicKey::from_str(
"03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71",
)
.unwrap();
let lnsocket =
match LNSocket::connect_and_init(key, their_pubkey, "ln.damus.io:9735").await {
Err(err) => {
let _ = event_tx.send(Event::Ended {
reason: err.to_string(),
});
return;
}
Ok(lnsocket) => {
let _ = event_tx.send(Event::Connected);
lnsocket
}
};
let rune = std::env::var("RUNE").unwrap_or(
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
);
let commando = CommandoClient::spawn(lnsocket, &rune);
loop {
match req_rx.recv().await {
None => {
let _ = event_tx.send(Event::Ended {
reason: "channel dead?".to_string(),
});
break;
}
Some(req) => {
tracing::debug!("calling {req:?}");
match req {
Request::GetInfo => match commando.call("getinfo", json!({})).await {
Ok(v) => {
let _ = event_tx.send(Event::Response(ClnResponse::GetInfo(v)));
}
Err(err) => {
tracing::error!("get_info error {}", err);
}
},
Request::ListPeerChannels => {
let peer_channels =
commando.call("listpeerchannels", json!({})).await;
let channels = peer_channels.map(|v| {
let peer_channels: Vec<ListPeerChannel> =
serde_json::from_value(v["channels"].clone()).unwrap();
to_channels(peer_channels)
});
let _ = event_tx
.send(Event::Response(ClnResponse::ListPeerChannels(channels)));
}
}
}
}
}
});
}
fn process_events(&mut self) {
let Some(channel) = &mut self.channel else {
return;
};
while let Ok(event) = channel.event_rx.try_recv() {
match event {
Event::Ended { reason } => {
self.connection_state = ConnectionState::Dead(reason);
}
Event::Connected => {
self.connection_state = ConnectionState::Active;
let _ = channel.req_tx.send(Request::GetInfo);
let _ = channel.req_tx.send(Request::ListPeerChannels);
}
Event::Response(resp) => match resp {
ClnResponse::ListPeerChannels(chans) => {
if let Some(Ok(prev)) = self.channels.as_ref() {
self.last_summary = Some(compute_summary(prev));
}
self.channels = Some(chans);
}
ClnResponse::GetInfo(value) => {
if let Ok(s) = serde_json::to_string_pretty(&value) {
self.get_info = Some(s);
}
}
},
}
}
}
}
fn get_info_ui(ui: &mut egui::Ui, info: &str) {
ui.horizontal_wrapped(|ui| {
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
});
}
fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
// ---------- numbers ----------
let short_channel_id = &c.original.short_channel_id;
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
// Feel free to switch to log scaling if you have whales:
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
// ---------- colors & style ----------
let out_color = Color32::from_rgb(84, 69, 201); // blue
let in_color = Color32::from_rgb(158, 56, 180); // purple
// Thickness scales with capacity, but keeps a nice minimum
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
let row_h = thickness + 14.0;
// ---------- layout ----------
let (rect, response) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), row_h),
egui::Sense::hover(),
);
let painter = ui.painter_at(rect);
let bar_rect = egui::Rect::from_min_max(
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
);
let corner_radius = (thickness * 0.5) as u8;
let out_radius = egui::CornerRadius {
ne: 0,
nw: corner_radius,
sw: corner_radius,
se: 0,
};
let in_radius = egui::CornerRadius {
ne: corner_radius,
nw: 0,
sw: 0,
se: corner_radius,
};
/*
painter.rect_filled(bar_rect, rounding, track_color);
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
*/
// Split widths
let usable = (c.to_us + c.to_them).max(1) as f32;
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
let split_x = bar_rect.left() + out_w;
// Outbound fill (left)
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
if out_rect.width() > 0.5 {
painter.rect_filled(out_rect, out_radius, out_color);
}
// Inbound fill (right)
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
if in_rect.width() > 0.5 {
painter.rect_filled(in_rect, in_radius, in_color);
}
// Tooltip
response.on_hover_text_at_pointer(format!(
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
human_sat(c.to_us),
human_sat(c.to_them),
human_sat(c.original.total_msat),
));
}
// ---------- helper ----------
fn human_sat(msat: i64) -> String {
let sats = msat / 1000;
if sats >= 1_000_000 {
format!("{:.1}M", sats as f64 / 1_000_000.0)
} else if sats >= 1_000 {
format!("{:.1}k", sats as f64 / 1_000.0)
} else {
sats.to_string()
}
}
fn channels_ui(ui: &mut egui::Ui, channels: &Option<Result<Channels, lnsocket::Error>>) {
match channels {
Some(Ok(channels)) => {
if channels.channels.is_empty() {
ui.label("no channels yet...");
return;
}
for channel in &channels.channels {
channel_ui(ui, channel, channels.max_total_msat);
}
ui.label(format!("available out {}", human_sat(channels.avail_out)));
ui.label(format!("available in {}", human_sat(channels.avail_in)));
}
Some(Err(err)) => {
ui.label(format!("error fetching channels: {err}"));
}
None => {
ui.label("no channels yet...");
}
}
}
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
let mut avail_out: i64 = 0;
let mut avail_in: i64 = 0;
let mut max_total_msat: i64 = 0;
let mut channels: Vec<Channel> = peer_channels
.into_iter()
.map(|c| {
let to_us = (c.to_us_msat - c.our_reserve_msat).max(0);
let to_them_raw = (c.total_msat - c.to_us_msat).max(0);
let to_them = (to_them_raw - c.their_reserve_msat).max(0);
avail_out += to_us;
avail_in += to_them;
if c.total_msat > max_total_msat {
max_total_msat = c.total_msat; // <-- max, not sum
}
Channel {
to_us,
to_them,
original: c,
}
})
.collect();
channels.sort_by(|a, b| {
let a_capacity = a.to_them + a.to_us;
let b_capacity = b.to_them + b.to_us;
a_capacity.partial_cmp(&b_capacity).unwrap().reverse()
});
Channels {
max_total_msat,
avail_out,
avail_in,
channels,
}
}
fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
let old = prev.cloned().unwrap_or_default();
let items: [(&str, String, Option<String>); 6] = [
(
"Total capacity",
human_sat(s.total_msat),
prev.map(|_| delta_str(s.total_msat, old.total_msat)),
),
(
"Avail out",
human_sat(s.avail_out_msat),
prev.map(|_| delta_str(s.avail_out_msat, old.avail_out_msat)),
),
(
"Avail in",
human_sat(s.avail_in_msat),
prev.map(|_| delta_str(s.avail_in_msat, old.avail_in_msat)),
),
("# Channels", s.channel_count.to_string(), None),
("Largest", human_sat(s.largest_msat), None),
(
"Outbound %",
format!("{:.0}%", s.outbound_pct * 100.0),
None,
),
];
// --- responsive columns ---
let min_card = 160.0;
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
egui::Grid::new("summary_grid")
.num_columns(cols)
.min_col_width(min_card)
.spacing(egui::vec2(8.0, 8.0))
.show(ui, |ui| {
let items_len = items.len();
for (i, (t, v, d)) in items.into_iter().enumerate() {
card_cell(ui, t, v, d, min_card);
// End the row when we filled a row worth of cells
if (i + 1) % cols == 0 {
ui.end_row();
}
}
// If the last row wasn't full, close it anyway
if items_len % cols != 0 {
ui.end_row();
}
});
}
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
let weak = ui.visuals().weak_text_color();
egui::Frame::group(ui.style())
.fill(ui.visuals().extreme_bg_color)
.corner_radius(egui::CornerRadius::same(10))
.inner_margin(egui::Margin::same(10))
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.show(ui, |ui| {
ui.set_min_width(min_card);
ui.vertical(|ui| {
ui.add(
egui::Label::new(egui::RichText::new(title).small().color(weak))
.wrap_mode(egui::TextWrapMode::Wrap),
);
ui.add_space(4.0);
ui.add(
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
.wrap_mode(egui::TextWrapMode::Wrap),
);
if let Some(d) = delta {
ui.add_space(2.0);
ui.add(
egui::Label::new(egui::RichText::new(d).small().color(weak))
.wrap_mode(egui::TextWrapMode::Wrap),
);
}
});
ui.set_min_height(20.0);
});
}
#[derive(Clone, Default)]
struct Summary {
total_msat: i64,
avail_out_msat: i64,
avail_in_msat: i64,
channel_count: usize,
largest_msat: i64,
outbound_pct: f32, // fraction of total capacity
}
fn compute_summary(ch: &Channels) -> Summary {
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
let largest_msat: i64 = ch
.channels
.iter()
.map(|c| c.original.total_msat)
.max()
.unwrap_or(0);
let outbound_pct = if total_msat > 0 {
ch.avail_out as f32 / total_msat as f32
} else {
0.0
};
Summary {
total_msat,
avail_out_msat: ch.avail_out,
avail_in_msat: ch.avail_in,
channel_count: ch.channels.len(),
largest_msat,
outbound_pct,
}
}
fn delta_str(new: i64, old: i64) -> String {
let d = new - old;
match d.cmp(&0) {
std::cmp::Ordering::Greater => format!("{}", human_sat(d)),
std::cmp::Ordering::Less => format!("{}", human_sat(-d)),
std::cmp::Ordering::Equal => "·".into(),
}
}
+14 -2
View File
@@ -81,7 +81,11 @@ fn execute_note_action(
.open(ndb, note_cache, txn, pool, &kind)
.map(NotesOpenResult::Timeline);
}
NoteAction::Note { note_id, preview } => 'ex: {
NoteAction::Note {
note_id,
preview,
scroll_offset,
} => 'ex: {
let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id)
else {
tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes()));
@@ -89,7 +93,15 @@ fn execute_note_action(
};
timeline_res = threads
.open(ndb, txn, pool, &thread_selection, preview, col)
.open(
ndb,
txn,
pool,
&thread_selection,
preview,
col,
scroll_offset,
)
.map(NotesOpenResult::Thread);
let route = Route::Thread(thread_selection);
+4 -20
View File
@@ -10,7 +10,7 @@ use crate::{
subscriptions::{SubKind, Subscriptions},
support::Support,
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction},
ui::{self, DesktopSidePanel, SidePanelAction},
view_state::ViewState,
Result,
};
@@ -27,7 +27,6 @@ use notedeck_ui::{
};
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use std::time::Duration;
use tracing::{debug, error, info, trace, warn};
use uuid::Uuid;
@@ -374,7 +373,7 @@ fn render_damus(
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
// We use this for keeping timestamps and things up to date
ui.ctx().request_repaint_after(Duration::from_secs(5));
//ui.ctx().request_repaint_after(Duration::from_secs(5));
app_action
}
@@ -494,14 +493,10 @@ impl Damus {
// cache.add_deck_default(*pk);
//}
};
let settings = &app_context.settings;
let support = Support::new(app_context.path);
let note_options = get_note_options(parsed_args, settings);
let note_options = get_note_options(parsed_args, app_context.settings);
let jobs = JobsCache::default();
let threads = Threads::default();
Self {
@@ -580,7 +575,7 @@ impl Damus {
}
}
fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions {
fn get_note_options(args: ColumnsArgs, settings_handler: &mut SettingsHandler) -> NoteOptions {
let mut note_options = NoteOptions::default();
note_options.set(
@@ -595,17 +590,6 @@ fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler)
NoteOptions::HideMedia,
args.is_flag_set(ColumnsFlag::NoMedia),
);
note_options.set(
NoteOptions::ShowNoteClientTop,
ShowSourceClientOption::Top == settings_handler.show_source_client().into()
|| args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
);
note_options.set(
NoteOptions::ShowNoteClientBottom,
ShowSourceClientOption::Bottom == settings_handler.show_source_client().into()
|| args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
);
note_options.set(
NoteOptions::RepliesNewestFirst,
settings_handler.show_replies_newest_first(),
-6
View File
@@ -11,8 +11,6 @@ pub enum ColumnsFlag {
Textmode,
Scramble,
NoMedia,
ShowNoteClientTop,
ShowNoteClientBottom,
}
pub struct ColumnsArgs {
@@ -54,10 +52,6 @@ impl ColumnsArgs {
res.clear_flag(ColumnsFlag::SinceOptimize);
} else if arg == "--scramble" {
res.set_flag(ColumnsFlag::Scramble);
} else if arg == "--show-note-client=top" {
res.set_flag(ColumnsFlag::ShowNoteClientTop);
} else if arg == "--show-note-client=bottom" {
res.set_flag(ColumnsFlag::ShowNoteClientBottom);
} else if arg == "--no-media" {
res.set_flag(ColumnsFlag::NoMedia);
} else if arg == "--filter" {
+6 -1
View File
@@ -37,6 +37,7 @@ use notedeck::{
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
RelayAction,
};
use notedeck_ui::NoteOptions;
use tracing::error;
/// The result of processing a nav response
@@ -591,6 +592,7 @@ fn render_nav_body(
)
.ui(ui)
.map(RenderNavAction::SettingsAction),
Route::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
txn
@@ -619,13 +621,16 @@ fn render_nav_body(
let action = {
let draft = app.drafts.reply_mut(note.id());
let mut options = app.note_options;
options.set(NoteOptions::Wide, false);
let response = ui::PostReplyView::new(
&mut note_context,
poster,
draft,
&note,
inner_rect,
app.note_options,
options,
&mut app.jobs,
col,
)
+13 -1
View File
@@ -22,11 +22,23 @@ pub struct NewPost {
pub mentions: Vec<Pubkey>,
}
fn client_variant() -> &'static str {
#[cfg(target_os = "android")]
{
"Damus Android"
}
#[cfg(not(target_os = "android"))]
{
"Damus Notedeck"
}
}
fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
builder
.start_tag()
.tag_str("client")
.tag_str("Damus Notedeck")
.tag_str(client_variant())
}
impl NewPost {
+10 -1
View File
@@ -23,6 +23,7 @@ pub struct ThreadNode {
pub prev: ParentState,
pub have_all_ancestors: bool,
pub list: VirtualList,
pub set_scroll_offset: Option<f32>,
}
#[derive(Clone)]
@@ -132,8 +133,14 @@ impl ThreadNode {
prev: parent,
have_all_ancestors: false,
list: VirtualList::new(),
set_scroll_offset: None,
}
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.set_scroll_offset = Some(offset);
self
}
}
#[derive(Default)]
@@ -147,6 +154,7 @@ pub struct Threads {
impl Threads {
/// Opening a thread.
/// Similar to [[super::cache::TimelineCache::open]]
#[allow(clippy::too_many_arguments)]
pub fn open(
&mut self,
ndb: &mut Ndb,
@@ -155,6 +163,7 @@ impl Threads {
thread: &ThreadSelection,
new_scope: bool,
col: usize,
scroll_offset: f32,
) -> Option<NewThreadNotes> {
tracing::info!("Opening thread: {:?}", thread);
let local_sub_filter = if let Some(selected) = &thread.selected_note {
@@ -184,7 +193,7 @@ impl Threads {
RawEntryMut::Vacant(entry) => {
let id = NoteId::new(*selected_note_id);
let node = ThreadNode::new(ParentState::Unknown);
let node = ThreadNode::new(ParentState::Unknown).with_offset(scroll_offset);
entry.insert(id, node);
&local_sub_filter
-1
View File
@@ -26,7 +26,6 @@ pub use preview::{Preview, PreviewApp, PreviewConfig};
pub use profile::ProfileView;
pub use relay::RelayView;
pub use settings::SettingsView;
pub use settings::ShowSourceClientOption;
pub use side_panel::{DesktopSidePanel, SidePanelAction};
pub use thread::ThreadView;
pub use timeline::TimelineView;
+16 -1
View File
@@ -15,6 +15,7 @@ use egui::{
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
use nostrdb::{Ndb, Transaction};
use notedeck::media::gif::ensure_latest_texture;
use notedeck::media::AnimationMode;
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
use notedeck_ui::{
@@ -37,6 +38,7 @@ pub struct PostView<'a, 'd> {
inner_rect: egui::Rect,
note_options: NoteOptions,
jobs: &'a mut JobsCache,
animation_mode: AnimationMode,
}
#[derive(Clone)]
@@ -110,6 +112,11 @@ impl<'a, 'd> PostView<'a, 'd> {
note_options: NoteOptions,
jobs: &'a mut JobsCache,
) -> Self {
let animation_mode = if note_options.contains(NoteOptions::NoAnimations) {
AnimationMode::NoAnimation
} else {
AnimationMode::Continuous { fps: None }
};
PostView {
note_context,
draft,
@@ -117,6 +124,7 @@ impl<'a, 'd> PostView<'a, 'd> {
post_type,
inner_rect,
note_options,
animation_mode,
jobs,
}
}
@@ -129,6 +137,11 @@ impl<'a, 'd> PostView<'a, 'd> {
PostView::id().with("scroll")
}
pub fn animation_mode(mut self, animation_mode: AnimationMode) -> Self {
self.animation_mode = animation_mode;
self
}
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
ui.spacing_mut().item_spacing.x = 12.0;
@@ -492,6 +505,7 @@ impl<'a, 'd> PostView<'a, 'd> {
height,
cur_state,
url,
self.animation_mode,
)
}
to_remove.reverse();
@@ -582,6 +596,7 @@ fn render_post_view_media(
height: u32,
render_state: RenderState,
url: &str,
animation_mode: AnimationMode,
) {
match render_state.texture_state {
notedeck::TextureState::Pending => {
@@ -605,7 +620,7 @@ fn render_post_view_media(
.to_vec();
let texture_handle =
ensure_latest_texture(ui, url, render_state.gifs, renderable_media);
ensure_latest_texture(ui, url, render_state.gifs, renderable_media, animation_mode);
let img_resp = ui.add(
egui::Image::new(&texture_handle)
.max_size(size)
+11 -125
View File
@@ -10,7 +10,6 @@ use notedeck::{
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
};
use notedeck_ui::{NoteOptions, NoteView};
use strum::Display;
use crate::{nav::RouterAction, Damus, Route};
@@ -21,89 +20,9 @@ const MAX_ZOOM: f32 = 3.0;
const ZOOM_STEP: f32 = 0.1;
const RESET_ZOOM: f32 = 1.0;
#[derive(Clone, Copy, PartialEq, Eq, Display)]
pub enum ShowSourceClientOption {
Hide,
Top,
Bottom,
}
impl From<ShowSourceClientOption> for String {
fn from(show_option: ShowSourceClientOption) -> Self {
match show_option {
ShowSourceClientOption::Hide => "hide".to_string(),
ShowSourceClientOption::Top => "top".to_string(),
ShowSourceClientOption::Bottom => "bottom".to_string(),
}
}
}
impl From<NoteOptions> for ShowSourceClientOption {
fn from(note_options: NoteOptions) -> Self {
if note_options.contains(NoteOptions::ShowNoteClientTop) {
ShowSourceClientOption::Top
} else if note_options.contains(NoteOptions::ShowNoteClientBottom) {
ShowSourceClientOption::Bottom
} else {
ShowSourceClientOption::Hide
}
}
}
impl From<String> for ShowSourceClientOption {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"hide" => Self::Hide,
"top" => Self::Top,
"bottom" => Self::Bottom,
_ => Self::Hide, // default fallback
}
}
}
impl ShowSourceClientOption {
pub fn set_note_options(self, note_options: &mut NoteOptions) {
match self {
Self::Hide => {
note_options.set(NoteOptions::ShowNoteClientTop, false);
note_options.set(NoteOptions::ShowNoteClientBottom, false);
}
Self::Bottom => {
note_options.set(NoteOptions::ShowNoteClientTop, false);
note_options.set(NoteOptions::ShowNoteClientBottom, true);
}
Self::Top => {
note_options.set(NoteOptions::ShowNoteClientTop, true);
note_options.set(NoteOptions::ShowNoteClientBottom, false);
}
}
}
fn label(&self, i18n: &mut Localization) -> String {
match self {
Self::Hide => tr!(
i18n,
"Hide",
"Option in settings section to hide the source client label in note display"
),
Self::Top => tr!(
i18n,
"Top",
"Option in settings section to show the source client label at the top of the note"
),
Self::Bottom => tr!(
i18n,
"Bottom",
"Option in settings section to show the source client label at the bottom of the note"
),
}
}
}
pub enum SettingsAction {
SetZoomFactor(f32),
SetTheme(ThemePreference),
SetShowSourceClient(ShowSourceClientOption),
SetLocale(LanguageIdentifier),
SetRepliestNewestFirst(bool),
SetNoteBodyFontSize(f32),
@@ -131,11 +50,6 @@ impl SettingsAction {
ctx.set_zoom_factor(zoom_factor);
settings.set_zoom_factor(zoom_factor);
}
Self::SetShowSourceClient(option) => {
option.set_note_options(&mut app.note_options);
settings.set_show_source_client(option);
}
Self::SetTheme(theme) => {
ctx.set_theme(theme);
settings.set_theme(theme);
@@ -267,6 +181,7 @@ impl<'a> SettingsView<'a> {
});
let txn = Transaction::new(self.note_context.ndb).unwrap();
if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) {
if let Ok(preview_note) =
self.note_context.ndb.get_note_by_id(&txn, note_id.bytes())
@@ -274,17 +189,17 @@ impl<'a> SettingsView<'a> {
notedeck_ui::padding(8.0, ui, |ui| {
if is_narrow(ui.ctx()) {
ui.set_max_width(ui.available_width());
}
NoteView::new(
self.note_context,
&preview_note,
*self.note_options,
self.jobs,
)
.actionbar(false)
.options_button(false)
.show(ui);
NoteView::new(
self.note_context,
&preview_note,
*self.note_options,
self.jobs,
)
.actionbar(false)
.options_button(false)
.show(ui);
}
});
ui.separator();
}
@@ -550,35 +465,6 @@ impl<'a> SettingsView<'a> {
));
}
});
ui.horizontal_wrapped(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Source client:",
"Label for Source client, others settings section",
)));
for option in [
ShowSourceClientOption::Hide,
ShowSourceClientOption::Top,
ShowSourceClientOption::Bottom,
] {
let mut current: ShowSourceClientOption =
self.settings.show_source_client.clone().into();
if ui
.selectable_value(
&mut current,
option,
RichText::new(option.label(self.note_context.i18n))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.changed()
{
action = Some(SettingsAction::SetShowSourceClient(option));
}
}
});
});
action
+24 -6
View File
@@ -52,17 +52,26 @@ impl<'a, 'd> ThreadView<'a, 'd> {
.auto_shrink([false, false])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible);
let offset_id = scroll_id.with(("scroll_offset", self.selected_note_id));
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
scroll_area = scroll_area.vertical_scroll_offset(offset);
if let Some(thread) = self.threads.threads.get_mut(&self.selected_note_id) {
if let Some(new_offset) = thread.set_scroll_offset.take() {
scroll_area = scroll_area.vertical_scroll_offset(new_offset);
}
}
let output = scroll_area.show(ui, |ui| self.notes(ui, &txn));
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
let mut resp = output.inner;
output.inner
if let Some(NoteAction::Note {
note_id: _,
preview: _,
scroll_offset,
}) = &mut resp
{
*scroll_offset = output.state.offset.y;
}
resp
}
fn notes(&mut self, ui: &mut egui::Ui, txn: &Transaction) -> Option<NoteAction> {
@@ -195,6 +204,7 @@ fn strip_note_action(action: NoteAction) -> Option<NoteAction> {
NoteAction::Note {
note_id: _,
preview: false,
scroll_offset: _,
}
) {
return None;
@@ -279,6 +289,12 @@ enum ThreadNoteType {
Reply,
}
impl ThreadNoteType {
fn is_selected(&self) -> bool {
matches!(self, ThreadNoteType::Selected { .. })
}
}
struct ThreadNotes<'a> {
notes: Vec<ThreadNote<'a>>,
selected_index: usize,
@@ -297,6 +313,7 @@ impl<'a> ThreadNote<'a> {
ThreadNoteType::Selected { root: _ } => {
cur_options.set(NoteOptions::Wide, true);
cur_options.set(NoteOptions::SelectableText, true);
cur_options.set(NoteOptions::FullCreatedDate, true);
cur_options
}
ThreadNoteType::Reply => cur_options,
@@ -312,6 +329,7 @@ impl<'a> ThreadNote<'a> {
) -> NoteResponse {
let inner = notedeck_ui::padding(8.0, ui, |ui| {
NoteView::new(note_context, &self.note, self.options(flags), jobs)
.selected_style(self.note_type.is_selected())
.unread_indicator(self.unread_and_have_replies)
.show(ui)
});
+2 -1
View File
@@ -18,8 +18,9 @@ serde_json = { workspace = true }
serde = { workspace = true }
nostrdb = { workspace = true }
hex = { workspace = true }
chrono = "0.4.40"
chrono = { workspace = true }
rand = "0.9.0"
bytemuck = "1.22.0"
futures = "0.3.31"
#reqwest = "0.12.15"
egui_extras = { workspace = true }
+139 -155
View File
@@ -1,13 +1,19 @@
use std::num::NonZeroU64;
use crate::mesh;
use crate::{Quaternion, Vec3};
use eframe::egui_wgpu::{self, wgpu};
use eframe::egui_wgpu::{
self,
wgpu::{self, util::DeviceExt},
};
use egui::{Rect, Response};
use rand::Rng;
use std::borrow::Cow;
pub struct DaveAvatar {
rotation: Quaternion,
rot_dir: Vec3,
logical_time: f32,
}
// Matrix utilities for perspective projection
@@ -53,141 +59,75 @@ fn matrix_multiply(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
result
}
fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
]
}
fn generate_dave_instances(instance_count: u32) -> Vec<mesh::Instance> {
let mut rng = rand::rng();
let mut instances = Vec::with_capacity(instance_count as usize);
// Logo gradient endpoints (01 range)
const C0: [f32; 3] = [53.0 / 255.0, 77.0 / 255.0, 235.0 / 255.0]; // rgb(53, 77, 235)
const C1: [f32; 3] = [229.0 / 255.0, 20.0 / 255.0, 205.0 / 255.0]; // rgb(229, 20, 205)
let golden_angle = std::f32::consts::PI * (3.0 - 5.0_f32.sqrt());
for i in 0..instance_count {
let i_f = (i as f32) + 0.5;
let n = instance_count as f32;
// Fibonacci sphere (unit directions)
let z = 1.0 - (2.0 * i_f) / n;
let r = (1.0 - z * z).sqrt();
let theta = golden_angle * i_f;
// Use base_pos as *direction*; shader will normalize/scale anyway
let base_pos = [r * theta.cos(), z, r * theta.sin()];
let scale = 0.03;
//let scale = scale + scale_var + rng.random::<f32>() * scale; // slightly smaller cubes
let seed = rng.random::<f32>() * 1000.0;
// damus logo gradient
let t_base = (z + 1.0) * 0.5; // 0..1
let t_jitter = (rng.random::<f32>() - 0.5) * 0.06; // ±0.03
let t = (t_base + t_jitter).clamp(0.0, 1.0);
let color = lerp3(C0, C1, t);
instances.push(mesh::Instance {
base_pos,
scale,
seed,
color,
});
}
instances
}
impl DaveAvatar {
pub fn new(wgpu_render_state: &egui_wgpu::RenderState) -> Self {
const BINDING_SIZE: u64 = 256;
let device = &wgpu_render_state.device;
let instance_count: u32 = 256;
let instances = generate_dave_instances(instance_count);
// Create shader module with improved shader code
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cube_shader"),
source: wgpu::ShaderSource::Wgsl(
r#"
struct Uniforms {
model_view_proj: mat4x4<f32>,
model: mat4x4<f32>, // Added model matrix for correct normal transformation
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) normal: vec3<f32>,
@location(1) world_pos: vec3<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
// Define cube vertices (-0.5 to 0.5 in each dimension)
var positions = array<vec3<f32>, 8>(
vec3<f32>(-0.5, -0.5, -0.5), // 0: left bottom back
vec3<f32>(0.5, -0.5, -0.5), // 1: right bottom back
vec3<f32>(-0.5, 0.5, -0.5), // 2: left top back
vec3<f32>(0.5, 0.5, -0.5), // 3: right top back
vec3<f32>(-0.5, -0.5, 0.5), // 4: left bottom front
vec3<f32>(0.5, -0.5, 0.5), // 5: right bottom front
vec3<f32>(-0.5, 0.5, 0.5), // 6: left top front
vec3<f32>(0.5, 0.5, 0.5) // 7: right top front
);
// Define indices for the 12 triangles (6 faces * 2 triangles)
var indices = array<u32, 36>(
// back face (Z-)
0, 2, 1, 1, 2, 3,
// front face (Z+)
4, 5, 6, 5, 7, 6,
// left face (X-)
0, 4, 2, 2, 4, 6,
// right face (X+)
1, 3, 5, 3, 7, 5,
// bottom face (Y-)
0, 1, 4, 1, 5, 4,
// top face (Y+)
2, 6, 3, 3, 6, 7
);
// Define normals for each face
var face_normals = array<vec3<f32>, 6>(
vec3<f32>(0.0, 0.0, -1.0), // back face (Z-)
vec3<f32>(0.0, 0.0, 1.0), // front face (Z+)
vec3<f32>(-1.0, 0.0, 0.0), // left face (X-)
vec3<f32>(1.0, 0.0, 0.0), // right face (X+)
vec3<f32>(0.0, -1.0, 0.0), // bottom face (Y-)
vec3<f32>(0.0, 1.0, 0.0) // top face (Y+)
);
var output: VertexOutput;
// Get vertex from indices
let index = indices[vertex_index];
let position = positions[index];
// Determine which face this vertex belongs to
let face_index = vertex_index / 6u;
// Apply transformations
output.position = uniforms.model_view_proj * vec4<f32>(position, 1.0);
// Transform normal to world space
// Extract the 3x3 rotation part from the 4x4 model matrix
let normal_matrix = mat3x3<f32>(
uniforms.model[0].xyz,
uniforms.model[1].xyz,
uniforms.model[2].xyz
);
output.normal = normalize(normal_matrix * face_normals[face_index]);
// Pass world position for lighting calculations
output.world_pos = (uniforms.model * vec4<f32>(position, 1.0)).xyz;
return output;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Material properties
let material_color = vec3<f32>(1.0, 1.0, 1.0); // White color
let ambient_strength = 0.2;
let diffuse_strength = 0.7;
let specular_strength = 0.2;
let shininess = 20.0;
// Light properties
let light_pos = vec3<f32>(2.0, 2.0, 2.0); // Light positioned diagonally above and to the right
let light_color = vec3<f32>(1.0, 1.0, 1.0); // White light
// View position (camera)
let view_pos = vec3<f32>(0.0, 0.0, 3.0); // Camera position
// Calculate ambient lighting
let ambient = ambient_strength * light_color;
// Calculate diffuse lighting
let normal = normalize(in.normal); // Renormalize the interpolated normal
let light_dir = normalize(light_pos - in.world_pos);
let diff = max(dot(normal, light_dir), 0.0);
let diffuse = diffuse_strength * diff * light_color;
// Calculate specular lighting
let view_dir = normalize(view_pos - in.world_pos);
let reflect_dir = reflect(-light_dir, normal);
let spec = pow(max(dot(view_dir, reflect_dir), 0.0), shininess);
let specular = specular_strength * spec * light_color;
// Combine lighting components
let result = (ambient + diffuse + specular) * material_color;
return vec4<f32>(result, 1.0);
}
"#
.into(),
),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("dave.wgsl"))),
});
// Create uniform buffer for MVP matrix and model matrix
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cube_uniform_buffer"),
size: 128, // Two 4x4 matrices of f32 (2 * 16 * 4 bytes)
size: BINDING_SIZE, // Two 4x4 matrices of f32 (2 * 16 * 4 bytes)
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
@@ -197,11 +137,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
label: Some("cube_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(128),
min_binding_size: NonZeroU64::new(BINDING_SIZE),
},
count: None,
}],
@@ -224,6 +164,24 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
push_constant_ranges: &[],
});
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube_vertices"),
contents: bytemuck::cast_slice(&mesh::CUBE_VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube_instances"),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube_indices"),
contents: bytemuck::cast_slice(&mesh::CUBE_INDICES),
usage: wgpu::BufferUsages::INDEX,
});
// Create render pipeline
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("cube_pipeline"),
@@ -231,7 +189,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[], // No vertex buffer - vertices are in the shader
buffers: &[mesh::Vertex::LAYOUT, mesh::Instance::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
@@ -274,6 +232,10 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
pipeline,
bind_group,
uniform_buffer,
instance_buffer,
vertex_buffer,
index_buffer,
instance_count,
});
let initial_rot = {
@@ -283,7 +245,9 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Apply rotations (order matters)
y_rotation.multiply(&x_rotation)
};
Self {
logical_time: 0.0,
rotation: initial_rot,
rot_dir: Vec3::new(0.0, 0.0, 0.0),
}
@@ -364,7 +328,7 @@ impl DaveAvatar {
}
// Create model matrix from rotation quaternion
let model_matrix = self.rotation.to_matrix4();
let model = self.rotation.to_matrix4();
// Create projection matrix with proper depth range
// Adjust aspect ratio based on rect dimensions
@@ -372,20 +336,37 @@ impl DaveAvatar {
let projection = perspective_matrix(std::f32::consts::PI / 4.0, aspect, 0.1, 100.0);
// Create view matrix (move camera back a bit)
let view_matrix = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -3.0, 1.0,
let camera_pos = [0.0, 0.0, 1.5];
// Right-handed look-at at origin; view is a translate by -camera_pos
let [cx, cy, cz] = camera_pos;
#[rustfmt::skip]
let view = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
-cx, -cy, -cz, 1.0,
];
// Combine matrices: projection * view * model
let mv_matrix = matrix_multiply(&view_matrix, &model_matrix);
let mvp_matrix = matrix_multiply(&projection, &mv_matrix);
let view_proj = matrix_multiply(&projection, &view);
let is_light = if ui.ctx().theme() == egui::Theme::Light {
1.0
} else {
-1.0
};
self.logical_time += ui.ctx().input(|i| i.stable_dt.min(0.1));
// Add paint callback
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
CubeCallback {
mvp_matrix,
model_matrix,
GpuData {
view_proj,
model,
camera_pos,
time: self.logical_time,
is_light: [is_light, 0.0, 0.0, 0.0],
},
));
@@ -394,12 +375,17 @@ impl DaveAvatar {
}
// Callback implementation
struct CubeCallback {
mvp_matrix: [f32; 16], // Model-View-Projection matrix
model_matrix: [f32; 16], // Model matrix for lighting calculations
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct GpuData {
view_proj: [f32; 16], // Model-View-Projection matrix
model: [f32; 16], // Model matrix for lighting calculations
camera_pos: [f32; 3], // xyz
time: f32,
is_light: [f32; 4],
}
impl egui_wgpu::CallbackTrait for CubeCallback {
impl egui_wgpu::CallbackTrait for GpuData {
fn prepare(
&self,
_device: &wgpu::Device,
@@ -410,21 +396,8 @@ impl egui_wgpu::CallbackTrait for CubeCallback {
) -> Vec<wgpu::CommandBuffer> {
let resources: &CubeRenderResources = resources.get().unwrap();
// Create a combined uniform buffer with both matrices
let mut uniform_data = [0.0f32; 32]; // Space for two 4x4 matrices
// Copy MVP matrix to first 16 floats
uniform_data[0..16].copy_from_slice(&self.mvp_matrix);
// Copy model matrix to next 16 floats
uniform_data[16..32].copy_from_slice(&self.model_matrix);
// Update uniform buffer with both matrices
queue.write_buffer(
&resources.uniform_buffer,
0,
bytemuck::cast_slice(&uniform_data),
);
queue.write_buffer(&resources.uniform_buffer, 0, bytemuck::bytes_of(self));
Vec::new()
}
@@ -439,7 +412,14 @@ impl egui_wgpu::CallbackTrait for CubeCallback {
render_pass.set_pipeline(&resources.pipeline);
render_pass.set_bind_group(0, &resources.bind_group, &[]);
render_pass.draw(0..36, 0..1); // 36 vertices for a cube (6 faces * 2 triangles * 3 vertices)
render_pass.set_vertex_buffer(0, resources.vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, resources.instance_buffer.slice(..));
render_pass.set_index_buffer(resources.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(
0..mesh::CUBE_INDICES.len() as u32,
0,
0..resources.instance_count,
);
}
}
@@ -448,4 +428,8 @@ struct CubeRenderResources {
pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
uniform_buffer: wgpu::Buffer,
instance_buffer: wgpu::Buffer,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
instance_count: u32,
}
+1
View File
@@ -0,0 +1 @@
+155
View File
@@ -0,0 +1,155 @@
struct Uniforms {
view_proj: mat4x4<f32>,
model: mat4x4<f32>,
camera_pos: vec3<f32>,
time: f32,
is_light: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
struct VSOut {
@builtin(position) position: vec4<f32>,
@location(0) normal: vec3<f32>,
@location(1) world_pos: vec3<f32>,
@location(2) color: vec3<f32>,
};
// Vertex inputs
@vertex
fn vs_main(
@location(0) in_pos: vec3<f32>,
@location(1) in_normal: vec3<f32>,
@location(2) base_pos: vec3<f32>,
@location(3) scale: f32,
@location(4) seed: f32,
@location(5) color: vec3<f32>,
) -> VSOut {
var out: VSOut;
let t = uniforms.time;
// --- Coherent spherical layout ---
let dir = normalize(base_pos + vec3<f32>(1e-6, 0.0, 0.0)); // avoid NaN if zero
let radius = 0.4;
// Gentle, coherent drift so it breathes
let drift = vec3<f32>(
0.06 * sin(0.9 * t + seed * 1.3),
0.05 * sin(1.1 * t + seed * 2.1),
0.06 * cos(0.7 * t + seed * 0.7)
);
// Final instance position on/near the sphere
//let loose = 0.2 * base_pos + drift;
let tight = dir * radius + drift;
//let tight = dir * radius;
//let coherence = 0.8; // [0..1], or pass as a uniform
//let pos_ws = mix(loose, tight, coherence);
let pos_ws = tight;
// --- Orient cube so its local +Z points outward (along dir) ---
// Build a stable tangent basis
var up = vec3<f32>(0.0, 1.0, 0.0);
if (abs(dot(dir, up)) > 0.92) {
up = vec3<f32>(1.0, 0.0, 0.0);
}
let tangent = normalize(cross(up, dir));
let bitangent = cross(dir, tangent);
// Optional tiny spin around outward axis for sparkle
let spin = 0.9 * t + seed * 0.9;
let cs = cos(spin);
let sn = sin(spin);
let rot_tangent = cs * tangent + sn * bitangent;
let rot_bitangent = -sn * tangent + cs * bitangent;
// Rotation matrix whose columns are the local basis
let R = mat3x3<f32>(rot_tangent, rot_bitangent, dir);
// Scale + orient local vertex + place at spherical position
let local = R * (in_pos * scale);
let world_vec4 = uniforms.model * vec4<f32>(local, 1.0);
let world = world_vec4 + vec4<f32>(pos_ws, 0.0);
out.position = uniforms.view_proj * world;
// Normal from model rotation only (ignoring per-instance rotation for now)
let nmat = mat3x3<f32>(
uniforms.model[0].xyz,
uniforms.model[1].xyz,
uniforms.model[2].xyz
);
out.normal = normalize(nmat * in_normal);
out.world_pos = world.xyz;
out.color = color;
return out;
}
@fragment
fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
// Same lighting as you had, but tint by per-instance color
let material_color = in.color;
let ambient_strength = 0.2;
let diffuse_strength = 0.7;
let specular_strength = 0.2;
let shininess = 20.0;
let light_pos = vec3<f32>(2.0, 2.0, 2.0);
let light_color = vec3<f32>(1.0, 1.0, 1.0);
let view_pos = uniforms.camera_pos;
let n = normalize(in.normal);
let l = normalize(light_pos - in.world_pos);
let v = normalize(view_pos - in.world_pos);
let r = reflect(-l, n);
let ambient = ambient_strength * light_color;
let diffuse = diffuse_strength * max(dot(n, l), 0.0) * light_color;
let specular = specular_strength * pow(max(dot(v, r), 0.0), shininess) * light_color;
let exposure = exp2(1.5);
var color = (ambient + diffuse + specular) * material_color;
// --- Distance-based factor (camera-space distance) ---
let dist = length(view_pos - in.world_pos);
let FADE_NEAR = 1.0; // start ramping here
let FADE_FAR = 2.2; // fully applied by here
let fade = smoothstep(FADE_NEAR, FADE_FAR, dist); // 0..1
// --- Exposure drift with distance (sign flips by mode) ---
// Dark mode target exposure at far: lower; Light mode target at far: higher.
let min_exp = 1.80; // far-end exposure multiplier in dark mode
let max_exp = 1.35; // far-end exposure multiplier in light mode
let darker = mix(1.0, min_exp, fade); // darkens with distance
let brighter = mix(1.0, max_exp, fade); // brightens with distance
let exp_factor = select(darker, brighter, uniforms.is_light.x > 0.0);
// Apply exposure + tonemap
let base_exposure = exp2(1.5);
color = aces_fitted(color * base_exposure * exp_factor);
// --- Optional: fade to background so distant points dissolve away ---
// Background: black in dark mode, white in light mode.
let bg = select(vec3<f32>(0.0), vec3<f32>(1.0), uniforms.is_light.x > 0.0);
// If you want white for BOTH modes instead, use:
// let bg = vec3<f32>(1.0);
color = mix(color, bg, fade);
return vec4<f32>(color, 1.0);
}
// ACES-fit tonemap (keeps highlights nicer than Reinhard)
fn aces_fitted(x: vec3<f32>) -> vec3<f32> {
let a = 2.51;
let b = 0.03;
let c = 2.43;
let d = 0.59;
let e = 0.14;
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3(0.0), vec3(1.0));
}
+10 -5
View File
@@ -27,6 +27,7 @@ pub use vec3::Vec3;
mod avatar;
mod config;
pub(crate) mod mesh;
mod messages;
mod quaternion;
mod tools;
@@ -179,6 +180,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse {
/*
let rect = ui.available_rect_before_wrap();
if let Some(av) = self.avatar.as_mut() {
av.render(rect, ui);
ui.ctx().request_repaint();
}
DaveResponse::default()
*/
DaveUi::new(self.model_config.trial, &self.chat, &mut self.input).ui(
app_ctx,
&mut self.jobs,
@@ -334,11 +344,6 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
impl notedeck::App for Dave {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
/*
self.app
.frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
*/
let mut app_action: Option<AppAction> = None;
// always insert system prompt if we have no context
+94
View File
@@ -0,0 +1,94 @@
use eframe::egui_wgpu::wgpu;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pos: [f32; 3],
normal: [f32; 3],
}
impl Vertex {
pub const ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
0 => Float32x3, // position
1 => Float32x3 // normal
];
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRS,
};
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Instance {
pub base_pos: [f32; 3],
pub scale: f32,
pub seed: f32,
pub color: [f32; 3],
}
impl Instance {
pub const ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
2 => Float32x3, // base_pos
3 => Float32, // scale
4 => Float32, // seed
5 => Float32x3 // color
];
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Instance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRS,
};
}
// 6 faces * 4 verts. Each face has a constant normal.
#[rustfmt::skip]
pub const CUBE_VERTICES: [Vertex; 24] = [
// -Z (back)
Vertex { pos: [-0.5,-0.5,-0.5], normal: [0.0, 0.0,-1.0] },
Vertex { pos: [ 0.5,-0.5,-0.5], normal: [0.0, 0.0,-1.0] },
Vertex { pos: [ 0.5, 0.5,-0.5], normal: [0.0, 0.0,-1.0] },
Vertex { pos: [-0.5, 0.5,-0.5], normal: [0.0, 0.0,-1.0] },
// +Z (front)
Vertex { pos: [-0.5,-0.5, 0.5], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [ 0.5,-0.5, 0.5], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [ 0.5, 0.5, 0.5], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [-0.5, 0.5, 0.5], normal: [0.0, 0.0, 1.0] },
// -X (left)
Vertex { pos: [-0.5,-0.5,-0.5], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [-0.5, 0.5,-0.5], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [-0.5, 0.5, 0.5], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [-0.5,-0.5, 0.5], normal: [-1.0, 0.0, 0.0] },
// +X (right)
Vertex { pos: [ 0.5,-0.5,-0.5], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [ 0.5, 0.5,-0.5], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [ 0.5, 0.5, 0.5], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [ 0.5,-0.5, 0.5], normal: [1.0, 0.0, 0.0] },
// -Y (bottom)
Vertex { pos: [-0.5,-0.5,-0.5], normal: [0.0,-1.0, 0.0] },
Vertex { pos: [-0.5,-0.5, 0.5], normal: [0.0,-1.0, 0.0] },
Vertex { pos: [ 0.5,-0.5, 0.5], normal: [0.0,-1.0, 0.0] },
Vertex { pos: [ 0.5,-0.5,-0.5], normal: [0.0,-1.0, 0.0] },
// +Y (top)
Vertex { pos: [-0.5, 0.5,-0.5], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [-0.5, 0.5, 0.5], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [ 0.5, 0.5, 0.5], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [ 0.5, 0.5,-0.5], normal: [0.0, 1.0, 0.0] },
];
// 6 faces * 2 triangles * 3 indices — all CCW when viewed from the outside
pub const CUBE_INDICES: [u16; 36] = [
// -Z (back) normal (0, 0,-1)
0, 3, 2, 0, 2, 1, // +Z (front) normal (0, 0, 1)
4, 5, 6, 4, 6, 7, // -X (left) normal (-1,0, 0)
8, 11, 10, 8, 10, 9, // +X (right) normal ( 1,0, 0)
12, 13, 14, 12, 14, 15, // -Y (bottom) normal (0,-1, 0)
16, 18, 17, 16, 19, 18, // +Y (top) normal (0, 1, 0)
20, 21, 22, 20, 22, 23,
];
+4
View File
@@ -15,6 +15,10 @@ pub fn accounts_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/accounts.png"))
}
pub fn cln_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/clnlogo.svg"))
}
pub fn add_column_dark_image() -> Image<'static> {
Image::new(include_image!(
"../../../assets/icons/add_column_dark_4x.png"
+2 -2
View File
@@ -56,7 +56,7 @@ pub fn hline_with_width(ui: &egui::Ui, range: egui::Rangef) {
ui.painter().hline(range, resize_y, stroke);
}
pub fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) {
pub fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) -> egui::Response {
let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add(Label::new(RichText::new(s).size(10.0).color(color)).selectable(false));
ui.add(Label::new(RichText::new(s).size(10.0).color(color)).selectable(false))
}
+15 -3
View File
@@ -1,6 +1,6 @@
use bitflags::bitflags;
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
use notedeck::media::{MediaInfo, ViewMediaInfo};
use notedeck::media::{AnimationMode, MediaInfo, ViewMediaInfo};
use notedeck::{ImageType, Images};
bitflags! {
@@ -176,7 +176,12 @@ impl<'a> MediaViewer<'a> {
/// we have image layouts
fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
// fetch image texture
let Some(texture) = images.latest_texture(ui, &media.url, ImageType::Content(None)) else {
let Some(texture) = images.latest_texture(
ui,
&media.url,
ImageType::Content(None),
AnimationMode::NoAnimation,
) else {
tracing::error!("could not get latest texture in first_image_rect");
return Rect::ZERO;
};
@@ -206,7 +211,14 @@ impl<'a> MediaViewer<'a> {
let url = &info.url;
// fetch image texture
let Some(texture) = images.latest_texture(ui, url, ImageType::Content(None)) else {
// we want to continually redraw things in the gallery
let Some(texture) = images.latest_texture(
ui,
url,
ImageType::Content(None),
AnimationMode::Continuous { fps: None }, // media viewer has continuous rendering
) else {
continue;
};
+75 -25
View File
@@ -1,16 +1,17 @@
use super::media::image_carousel;
use crate::{
note::{NoteAction, NoteOptions, NoteResponse, NoteView},
secondary_label,
};
use notedeck::{JobsCache, RenderableMedia};
use egui::{Color32, Hyperlink, Label, RichText};
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
use notedeck::Localization;
use notedeck::{
time_format, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle,
};
use notedeck::{JobsCache, RenderableMedia};
use tracing::warn;
use super::media::image_carousel;
use notedeck::{update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle};
pub struct NoteContents<'a, 'd> {
note_context: &'a mut NoteContext<'d>,
txn: &'a Transaction,
@@ -42,9 +43,6 @@ impl<'a, 'd> NoteContents<'a, 'd> {
impl egui::Widget for &mut NoteContents<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
if self.options.contains(NoteOptions::ShowNoteClientTop) {
render_client(ui, self.note_context.note_cache, self.note);
}
let result = render_note_contents(
ui,
self.note_context,
@@ -53,26 +51,27 @@ impl egui::Widget for &mut NoteContents<'_, '_> {
self.options,
self.jobs,
);
if self.options.contains(NoteOptions::ShowNoteClientBottom) {
render_client(ui, self.note_context.note_cache, self.note);
}
self.action = result.action;
result.response
}
}
#[profiling::function]
fn render_client(ui: &mut egui::Ui, note_cache: &mut NoteCache, note: &Note) {
fn render_client_name(ui: &mut egui::Ui, note_cache: &mut NoteCache, note: &Note, before: bool) {
let cached_note = note_cache.cached_note_or_insert_mut(note.key().unwrap(), note);
match cached_note.client.as_deref() {
Some(client) if !client.is_empty() => {
ui.horizontal(|ui| {
secondary_label(ui, format!("via {client}"));
});
}
_ => return,
let Some(client) = cached_note.client.as_ref() else {
return;
};
if client.is_empty() {
return;
}
if before {
secondary_label(ui, "");
}
secondary_label(ui, format!("via {client}"));
}
/// Render an inline note preview with a border. These are used when
@@ -121,9 +120,52 @@ pub fn render_note_preview(
.show(ui)
}
/// Render note contents and surrounding info (client name, full date timestamp)
fn render_note_contents(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
txn: &Transaction,
note: &Note,
options: NoteOptions,
jobs: &mut JobsCache,
) -> NoteResponse {
let response = render_undecorated_note_contents(ui, note_context, txn, note, options, jobs);
ui.horizontal_wrapped(|ui| {
note_bottom_metadata_ui(
ui,
note_context.i18n,
note_context.note_cache,
note,
options,
);
});
response
}
/// Client name, full timestamp, etc
fn note_bottom_metadata_ui(
ui: &mut egui::Ui,
i18n: &mut Localization,
note_cache: &mut NoteCache,
note: &Note,
options: NoteOptions,
) {
let show_full_date = options.contains(NoteOptions::FullCreatedDate);
if show_full_date {
secondary_label(ui, time_format(i18n, note.created_at()));
}
if options.contains(NoteOptions::ClientName) {
render_client_name(ui, note_cache, note, show_full_date);
}
}
#[allow(clippy::too_many_arguments)]
#[profiling::function]
pub fn render_note_contents<'a>(
fn render_undecorated_note_contents<'a>(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
txn: &Transaction,
@@ -150,6 +192,8 @@ pub fn render_note_contents<'a>(
let mut supported_medias: Vec<RenderableMedia> = vec![];
let response = ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 1.0;
let blocks = if let Ok(blocks) = note_context.ndb.get_blocks_by_key(txn, note_key) {
blocks
} else {
@@ -158,10 +202,11 @@ pub fn render_note_contents<'a>(
return;
};
'block_loop: for block in blocks.iter(note) {
for block in blocks.iter(note) {
match block.blocktype() {
BlockType::MentionBech32 => match block.as_mention().unwrap() {
Mention::Profile(profile) => {
profiling::scope!("profile-block");
let act = crate::Mention::new(
note_context.ndb,
note_context.img_cache,
@@ -176,6 +221,7 @@ pub fn render_note_contents<'a>(
}
Mention::Pubkey(npub) => {
profiling::scope!("pubkey-block");
let act = crate::Mention::new(
note_context.ndb,
note_context.img_cache,
@@ -207,8 +253,9 @@ pub fn render_note_contents<'a>(
},
BlockType::Hashtag => {
profiling::scope!("hashtag-block");
if block.as_str().trim().is_empty() {
continue 'block_loop;
continue;
}
let resp = ui
.colored_label(
@@ -224,6 +271,7 @@ pub fn render_note_contents<'a>(
}
BlockType::Url => {
profiling::scope!("url-block");
let mut found_supported = || -> bool {
let url = block.as_str();
@@ -241,7 +289,7 @@ pub fn render_note_contents<'a>(
if hide_media || !found_supported() {
if block.as_str().trim().is_empty() {
continue 'block_loop;
continue;
}
ui.add(Hyperlink::from_label_and_url(
RichText::new(block.as_str())
@@ -253,6 +301,7 @@ pub fn render_note_contents<'a>(
}
BlockType::Text => {
profiling::scope!("text-block");
// truncate logic
let mut truncate = false;
let block_str = if options.contains(NoteOptions::Truncate)
@@ -273,7 +322,7 @@ pub fn render_note_contents<'a>(
block_str
};
if block_str.trim().is_empty() {
continue 'block_loop;
continue;
}
if options.contains(NoteOptions::ScrambleText) {
ui.add(
@@ -314,6 +363,7 @@ pub fn render_note_contents<'a>(
NoteAction::Note { note_id, .. } => NoteAction::Note {
note_id,
preview: true,
scroll_offset: 0.0,
},
other => other,
})
+20 -2
View File
@@ -13,6 +13,7 @@ use notedeck::{
use crate::NoteOptions;
use notedeck::media::gif::ensure_latest_texture;
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
use notedeck::media::AnimationMode;
use notedeck::media::{MediaInfo, ViewMediaInfo};
use crate::{app_images, AnimationHelper, PulseAlpha};
@@ -82,6 +83,18 @@ pub fn image_carousel(
blur_type,
);
let animation_mode = if note_options.contains(NoteOptions::NoAnimations)
{
AnimationMode::NoAnimation
} else {
// if animations aren't disabled, we cap it at 24fps for gifs in carousels
let fps = match media_type {
MediaCacheType::Gif => Some(24.0),
MediaCacheType::Image => None,
};
AnimationMode::Continuous { fps }
};
let media_response = render_media(
ui,
&mut img_cache.gif_states,
@@ -90,6 +103,7 @@ pub fn image_carousel(
size,
i18n,
note_options.contains(NoteOptions::Wide),
animation_mode,
);
if let Some(action) = media_response.inner {
@@ -324,10 +338,12 @@ fn render_media(
size: egui::Vec2,
i18n: &mut Localization,
is_scaled: bool,
animation_mode: AnimationMode,
) -> egui::InnerResponse<Option<MediaUIAction>> {
match render_state {
MediaRenderState::ActualImage(image) => {
let resp = render_success_media(ui, url, image, gifs, size, i18n, is_scaled);
let resp =
render_success_media(ui, url, image, gifs, size, i18n, is_scaled, animation_mode);
if resp.clicked() {
egui::InnerResponse::new(Some(MediaUIAction::Clicked), resp)
} else {
@@ -559,6 +575,7 @@ pub(crate) fn find_renderable_media<'a>(
}
*/
#[allow(clippy::too_many_arguments)]
fn render_success_media(
ui: &mut egui::Ui,
url: &str,
@@ -567,8 +584,9 @@ fn render_success_media(
size: Vec2,
i18n: &mut Localization,
is_scaled: bool,
animation_mode: AnimationMode,
) -> Response {
let texture = ensure_latest_texture(ui, url, gifs, tex);
let texture = ensure_latest_texture(ui, url, gifs, tex, animation_mode);
let scaled = ScaledTexture::new(&texture, size, is_scaled);
+82 -57
View File
@@ -10,7 +10,7 @@ use crate::{
PulseAlpha, Username,
};
pub use contents::{render_note_contents, render_note_preview, NoteContents};
pub use contents::{render_note_preview, NoteContents};
pub use context::NoteContextButton;
use notedeck::get_current_wallet;
use notedeck::note::ZapTargetAmount;
@@ -39,10 +39,8 @@ pub struct NoteView<'a, 'd> {
note_context: &'a mut NoteContext<'d>,
parent: Option<NoteKey>,
note: &'a nostrdb::Note<'a>,
framed: bool,
flags: NoteOptions,
jobs: &'a mut JobsCache,
show_unread_indicator: bool,
}
pub struct NoteResponse {
@@ -89,13 +87,9 @@ impl<'a, 'd> NoteView<'a, 'd> {
pub fn new(
note_context: &'a mut NoteContext<'d>,
note: &'a nostrdb::Note<'a>,
mut flags: NoteOptions,
flags: NoteOptions,
jobs: &'a mut JobsCache,
) -> Self {
flags.set(NoteOptions::ActionBar, true);
flags.set(NoteOptions::HasNotePreviews, true);
let framed = false;
let parent: Option<NoteKey> = None;
Self {
@@ -103,9 +97,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
parent,
note,
flags,
framed,
jobs,
show_unread_indicator: false,
}
}
@@ -117,86 +109,122 @@ impl<'a, 'd> NoteView<'a, 'd> {
.note_previews(false)
.options_button(true)
.is_preview(true)
.full_date(false)
.client_name(false)
}
pub fn selected_style(self, selected: bool) -> Self {
self.wide(selected)
.full_date(selected)
.client_name(selected)
}
#[inline]
pub fn textmode(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::Textmode, enable);
self
}
#[inline]
pub fn client_name(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::ClientName, enable);
self
}
#[inline]
pub fn full_date(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::FullCreatedDate, enable);
self
}
#[inline]
pub fn actionbar(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::ActionBar, enable);
self
}
#[inline]
pub fn hide_media(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::HideMedia, enable);
self
}
#[inline]
pub fn frame(mut self, enable: bool) -> Self {
self.framed = enable;
self.options_mut().set(NoteOptions::Framed, enable);
self
}
#[inline]
pub fn truncate(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::Truncate, enable);
self
}
#[inline]
pub fn small_pfp(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::SmallPfp, enable);
self
}
#[inline]
pub fn medium_pfp(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::MediumPfp, enable);
self
}
#[inline]
pub fn note_previews(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::HasNotePreviews, enable);
self
}
#[inline]
pub fn selectable_text(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::SelectableText, enable);
self
}
#[inline]
pub fn wide(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::Wide, enable);
self
}
#[inline]
pub fn options_button(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::OptionsButton, enable);
self
}
#[inline]
pub fn unread_indicator(mut self, enable: bool) -> Self {
self.options_mut().set(NoteOptions::UnreadIndicator, enable);
self
}
#[inline]
pub fn options(&self) -> NoteOptions {
self.flags
}
#[inline]
pub fn options_mut(&mut self) -> &mut NoteOptions {
&mut self.flags
}
#[inline]
pub fn parent(mut self, parent: NoteKey) -> Self {
self.parent = Some(parent);
self
}
#[inline]
pub fn is_preview(mut self, is_preview: bool) -> Self {
self.options_mut().set(NoteOptions::IsPreview, is_preview);
self
}
pub fn unread_indicator(mut self, show_unread_indicator: bool) -> Self {
self.show_unread_indicator = show_unread_indicator;
self
}
fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
let txn = self.note.txn().expect("todo: implement non-db notes");
@@ -212,7 +240,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
ui.allocate_rect(rect, Sense::hover());
ui.put(rect, |ui: &mut egui::Ui| {
render_reltime(ui, self.note_context.i18n, self.note.created_at(), false).response
render_notetime(ui, self.note_context.i18n, self.note.created_at(), false)
});
let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
ui.allocate_rect(rect, Sense::hover());
@@ -334,7 +362,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
if self.options().contains(NoteOptions::Textmode) {
NoteResponse::new(self.textmode_ui(ui))
} else if self.framed {
} else if self.options().contains(NoteOptions::Framed) {
egui::Frame::new()
.fill(ui.visuals().noninteractive().weak_bg_fill)
.inner_margin(egui::Margin::same(8))
@@ -362,30 +390,31 @@ impl<'a, 'd> NoteView<'a, 'd> {
i18n: &mut Localization,
note: &Note,
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
show_unread_indicator: bool,
flags: NoteOptions,
) {
let horiz_resp = ui
.horizontal(|ui| {
.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };
ui.add(Username::new(i18n, profile.as_ref().ok(), note.pubkey()).abbreviated(20));
render_reltime(ui, i18n, note.created_at(), true);
let response = ui
.add(Username::new(i18n, profile.as_ref().ok(), note.pubkey()).abbreviated(20));
if !flags.contains(NoteOptions::FullCreatedDate) {
return render_notetime(ui, i18n, note.created_at(), true);
}
response
})
.response;
if !show_unread_indicator {
return;
if flags.contains(NoteOptions::UnreadIndicator) {
let radius = 4.0;
let circle_center = {
let mut center = horiz_resp.rect.right_center();
center.x += radius + 4.0;
center
};
ui.painter()
.circle_filled(circle_center, radius, crate::colors::PINK);
}
let radius = 4.0;
let circle_center = {
let mut center = horiz_resp.rect.right_center();
center.x += radius + 4.0;
center
};
ui.painter()
.circle_filled(circle_center, radius, crate::colors::PINK);
}
fn wide_ui(
@@ -416,7 +445,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
self.note_context.i18n,
self.note,
profile,
self.show_unread_indicator,
self.flags,
);
})
.response
@@ -435,6 +464,8 @@ impl<'a, 'd> NoteView<'a, 'd> {
}
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
note_action = reply_desc(
ui,
txn,
@@ -504,16 +535,10 @@ impl<'a, 'd> NoteView<'a, 'd> {
let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey());
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
NoteView::note_header(
ui,
self.note_context.i18n,
self.note,
profile,
self.show_unread_indicator,
);
NoteView::note_header(ui, self.note_context.i18n, self.note, profile, self.flags);
ui.horizontal_wrapped(|ui| 's: {
ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };
ui.spacing_mut().item_spacing.x = 1.0;
let note_reply = self
.note_context
@@ -862,23 +887,23 @@ fn render_note_actionbar(
}
#[profiling::function]
fn render_reltime(
fn render_notetime(
ui: &mut egui::Ui,
i18n: &mut Localization,
created_at: u64,
before: bool,
) -> egui::InnerResponse<()> {
ui.horizontal(|ui| {
if before {
secondary_label(ui, "");
}
secondary_label(ui, notedeck::time_ago_since(i18n, created_at));
if !before {
secondary_label(ui, "");
}
})
) -> Response {
if before {
secondary_label(
ui,
format!("{}", notedeck::time_ago_since(i18n, created_at)),
)
} else {
secondary_label(
ui,
format!("{}", notedeck::time_ago_since(i18n, created_at)),
)
}
}
fn reply_button(ui: &mut egui::Ui, i18n: &mut Localization, note_key: NoteKey) -> egui::Response {
+15 -4
View File
@@ -22,11 +22,22 @@ bitflags! {
/// Is the content truncated? If the length is over a certain size it
/// will end with a ... and a "Show more" button.
const Truncate = 1 << 11;
/// Show note's client in the note header
const ShowNoteClientTop = 1 << 12;
const ShowNoteClientBottom = 1 << 13;
/// Show note's client in the note content
const ClientName = 1 << 12;
const RepliesNewestFirst = 1 << 14;
const RepliesNewestFirst = 1 << 13;
/// Show note's full created at date at the bottom
const FullCreatedDate = 1 << 14;
/// Note has a framed border
const Framed = 1 << 15;
/// Note has an unread reply indicator
const UnreadIndicator = 1 << 16;
/// no animation override (accessibility)
const NoAnimations = 1 << 17;
}
}
@@ -116,7 +116,7 @@ fn render_text_segments(
let link_color = visuals.hyperlink_color;
for segment in segments {
match segment {
match &segment {
TextSegment::Plain(text) => {
ui.add(
Label::new(RichText::new(text).size(size).color(color)).selectable(selectable),
+20 -2
View File
@@ -3,6 +3,7 @@ use egui::{vec2, InnerResponse, Sense, Stroke, TextureHandle};
use notedeck::get_render_state;
use notedeck::media::gif::ensure_latest_texture;
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
use notedeck::media::AnimationMode;
use notedeck::MediaAction;
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
@@ -12,12 +13,21 @@ pub struct ProfilePic<'cache, 'url> {
size: f32,
sense: Sense,
border: Option<Stroke>,
animation_mode: AnimationMode,
pub action: Option<MediaAction>,
}
impl egui::Widget for &mut ProfilePic<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let inner = render_pfp(ui, self.cache, self.url, self.size, self.border, self.sense);
let inner = render_pfp(
ui,
self.cache,
self.url,
self.size,
self.border,
self.sense,
self.animation_mode,
);
self.action = inner.inner;
@@ -35,6 +45,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
sense,
url,
size,
animation_mode: AnimationMode::Reactive,
border: None,
action: None,
}
@@ -45,6 +56,11 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
self
}
pub fn animation_mode(mut self, mode: AnimationMode) -> Self {
self.animation_mode = mode;
self
}
pub fn border_stroke(ui: &egui::Ui) -> Stroke {
Stroke::new(4.0, ui.visuals().panel_fill)
}
@@ -109,6 +125,7 @@ fn render_pfp(
ui_size: f32,
border: Option<Stroke>,
sense: Sense,
animation_mode: AnimationMode,
) -> InnerResponse<Option<MediaAction>> {
// We will want to downsample these so it's not blurry on hi res displays
let img_size = 128u32;
@@ -141,7 +158,8 @@ fn render_pfp(
)
}
notedeck::TextureState::Loaded(textured_image) => {
let texture_handle = ensure_latest_texture(ui, url, cur_state.gifs, textured_image);
let texture_handle =
ensure_latest_texture(ui, url, cur_state.gifs, textured_image, animation_mode);
egui::InnerResponse::new(None, pfp_image(ui, &texture_handle, ui_size, border, sense))
}