119 Commits

Author SHA1 Message Date
tyiu c1d3be4c07 WIP add system locale detection
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-08-01 13:20:35 -04:00
William Casarin 6a08d4b1b2 ui/note: fix width instabilities because of spacing_mut
TODO: get rid of all spacing_mut in the codebase

Fixes: 9ff5753bca ("settings: use timed serializer, handle zoom properly...")
2025-07-31 17:54:53 -07:00
William Casarin d6d7e4c35e android: fix build
Fixes: dac786e60f ("chrome: remove duplication in app setup")
2025-07-31 17:29:06 -07:00
William Casarin c3499729f2 Merge notification dot by kernel
kernelkind (6):
      extract notifications filter to own method
      add `NotesFreshness` to `TimelineTab`
      set fresh from `TimelineCache`
      chrome: method to find whether there are unseen notifications
      paint unseen indicator
      use unseen notification indicator

Changelog-Added: Add notification dot on toolbar
2025-07-31 17:18:20 -07:00
William Casarin dac786e60f chrome: remove duplication in app setup
Also move debug warning to chrome so that headless
notedeck apps don't hit that.
2025-07-31 17:07:51 -07:00
kernelkind 41aa2db3c7 use unseen notification indicator
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:08:08 -04:00
kernelkind 10225158e5 paint unseen indicator
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:36 -04:00
kernelkind 557608db9b chrome: method to find whether there are unseen notifications
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:35 -04:00
kernelkind 8697a5cb0a set fresh from TimelineCache
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:28 -04:00
kernelkind 7aca39aae8 add NotesFreshness to TimelineTab
necessary for notifications indicator

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:24 -04:00
kernelkind aa467b9be0 extract notifications filter to own method
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-31 19:07:13 -04:00
William Casarin 09eeb57bd9 Merge notebook (feature gated)
This merges the experimental notebook app, which can be
enabled with --notebook.

We also switch to bitflags for notedeck options
2025-07-31 16:06:25 -07:00
William Casarin b1a5dd6cab add NotedeckOptions and feature flags, add notebook feature
This switches from bools to flags in our Args struct. We also add
notebook as an optional feature flag (--notebook) since its not ready.
2025-07-31 16:03:13 -07:00
William Casarin d12e5b363c notebook: move ui code into its own file
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:23 -07:00
William Casarin cc8bafddff notebook: remove redundant closure
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:23 -07:00
William Casarin 3766308ce6 notebook: fix node sizes
make sure we always allocate the correct amount of space,
even if we use less.

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:23 -07:00
William Casarin 17f72f6127 notebook: draw edges and arrows
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:08:22 -07:00
William Casarin f592015c0c notebook: fix heights of nodes
some nodes can overflow their contents, so let's use a scroll view to
fix

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:07:51 -07:00
William Casarin 1ab4eeb48c init notebook
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-31 15:07:50 -07:00
William Casarin a8c6baeacb make clippy happy 2025-07-31 11:55:39 -07:00
William Casarin a896a6ecfa Merge remote-tracking branch 'fernando/feat/persist_settings' 2025-07-31 11:48:57 -07:00
Fernando López Guevara f282363748 feat(notedeck): add cross-platform URI opener 2025-07-30 16:27:51 -07:00
William Casarin ba76b20ad2 Merge tagging fixes from kernel
Fixes the following:
1. space added after mention
2. can scroll the mention picker
3. don't lose focus of textedit after mention selection

kernelkind (6):
      rename `SearchResultsView` => `MentionPickerView`
      fix scroll regression
      mention-picker: re-add spacing from inner_margin
      mentions: don't lose focus after select mention
      TMP: update egui for better TextInputState handling
      insert space after mention selection

Fixes: https://github.com/damus-io/notedeck/issues/985
Fixes: https://github.com/damus-io/notedeck/issues/728
Fixes: https://github.com/damus-io/notedeck/issues/986

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-30 16:23:12 -07:00
kernelkind b04f50a9f6 insert space after mention selection
closes: https://github.com/damus-io/notedeck/issues/985

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 18:11:27 -04:00
kernelkind 233be47659 TMP: update egui for better TextInputState handling
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:12 -04:00
kernelkind 173972f920 mentions: don't lose focus after select mention
Closes: https://github.com/damus-io/notedeck/issues/728

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:09 -04:00
kernelkind 31ec21ea02 mention-picker: re-add spacing from inner_margin
shouldn't do this in Frame, for some reason that captures the drag

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:05 -04:00
kernelkind d3d8d7be4b fix scroll regression
Closes: https://github.com/damus-io/notedeck/issues/986

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:45:02 -04:00
kernelkind 09dc101c1b rename SearchResultsView => MentionPickerView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-30 17:44:51 -04:00
Fernando López Guevara 261477339b Update crates/notedeck/src/persist/settings_handler.rs
Co-authored-by: Terry Yiu <963907+tyiu@users.noreply.github.com>
2025-07-30 08:34:47 -03:00
Fernando López Guevara 9ff5753bca settings: use timed serializer, handle zoom properly, use custom text style for note body font size, added font size slider, added preview note 2025-07-29 21:43:26 -03:00
Fernando López Guevara b9e2fe5dd1 fix(media): add spacing 2025-07-29 21:38:04 -03:00
Fernando López Guevara d1a9e0020e fix(note-content): avoid empty text blocks
(cherry picked from commit baa7031c25d0f3d3e8952f49f6625252413559a3)
2025-07-29 21:34:28 -03:00
Fernando López Guevara 1163dd8461 feat(settings): persist settings to storage 2025-07-29 21:33:05 -03:00
Fernando López Guevara 692f4889cf update i18n comments for source client options
Co-authored-by: Terry Yiu <963907+tyiu@users.noreply.github.com>
2025-07-29 21:31:36 -03:00
Fernando López Guevara f2153f53dc feat(settings): allow sorting thread replies newest first 2025-07-29 21:30:35 -03:00
Fernando López Guevara 40764d7368 fix(settings): use localization 2025-07-29 21:22:39 -03:00
Fernando López Guevara be720c0f76 fix(settings): use localization 2025-07-29 21:21:06 -03:00
Fernando López Guevara 5848f1c355 refactor(settings): add settings sections methods 2025-07-29 21:09:33 -03:00
Fernando López Guevara 0dcf70bc15 feat(settings): persist settings to storage 2025-07-29 21:02:18 -03:00
William Casarin 0fc8e70180 ui/note: fix indented actionbar in non-wide mode
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
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 13:58:17 -07:00
William Casarin 2de6851fbd mediaviewer: decrease transition anim from 500ms to 300ms
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 13:45:22 -07:00
William Casarin f57d582307 ui: keep original design on non-narrow
Changed my mind

This reverts commit 6e81b98d2f.
This reverts commit 217f1e45da.
2025-07-29 13:25:19 -07:00
William Casarin 09e608ca75 media: less blurry media
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 13:17:52 -07:00
William Casarin 2bd636ce0a v0.5.9 - Better Media!
- Persist settings to storage
- New fullscreen media viewer with panning and zoom
- Changed note rendering to use the full screen width
- Fixed more wrapping issues
- Fixed crash on large images
- Fix nwc copy/paste
- Portugese translations
- Show locale language names instead of identifier

Fernando López Guevara (5):
      feat(settings): persist settings to storage
      fix(columns): render wide notes on narrow screen
      fix(media): edge-to-edge image display on narrow screen
      fix(media): use ScaledTexture
      fix(note_actionbar): add invisible label to stabilize section width ¯\_(ツ)_/¯

Terry Yiu (5):
      Add human-readable names to locales in settings
      Add Portuguese (Brazil) language and translations
      Export strings for translation
      Import translations
      Internationalize ShowNoteClientOptions labels

William Casarin (19):
      Fullscreen MediaViewer refactor
      images: always resize large images
      media: change is_narrow logic to is_scaled
      media/viewer: click anywhere to close
      media/viewer: fix broken culling
      media/viewer: fix flicker on escape-close
      media/viewer: fullscreen transition animations
      media/viewer: handle click-to-close interactions
      media/viewer: provide image-click provenance
      media/viewer: slower animation
      note/options: made wide the default
      threads: disable wide in threads
      ui/note: fix another reply_desc wrapping issue
      ui/note: simplify weird hack and make note of it
      ui/settings: fix small double clone nit
      ui/wallet: small refactor to use return instead of break
      wallet: fix nwc copy/paste

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 12:24:17 -07:00
William Casarin 79bf6cf126 media/viewer: fix flicker on escape-close
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 12:22:04 -07:00
Fernando López Guevara b8207106d7 feat(settings): persist settings to storage 2025-07-29 11:41:06 -07:00
William Casarin 5280028a82 media/viewer: fix broken culling
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 11:03:42 -07:00
William Casarin f4a6e8f9bb media: change is_narrow logic to is_scaled
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:59:27 -07:00
William Casarin 83fd6de076 Merge remote-tracking branch 'github/pr/1032' 2025-07-29 10:46:55 -07:00
William Casarin b80a0ab0f1 ui/settings: fix small double clone nit
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:32:39 -07:00
William Casarin e437a0db1c Merge Portuguese translations by terry #1036
Terry Yiu (5):
      Export strings for translation
      Add human-readable names to locales in settings
      Internationalize ShowNoteClientOptions labels
      Import translations
      Add Portuguese (Brazil) language and translations
2025-07-29 10:26:21 -07:00
William Casarin 6e81b98d2f note/options: made wide the default
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:24:38 -07:00
William Casarin 217f1e45da Revert "fix(columns): render wide notes on narrow screen"
We're just gonna make it default

This reverts commit 0f00dcf7a7.
2025-07-29 10:22:57 -07:00
William Casarin 96e0366787 threads: disable wide in threads
Since it breaks the reply line rendering

Fixes: 0f00dcf7a7 ("fix(columns): render wide notes on narrow screen")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:18:20 -07:00
William Casarin 2a85ee562c ui/note: simplify weird hack and make note of it
Fixes: https://github.com/damus-io/notedeck/issues/842
Fixes: f2e01f0e40 ("fix(note_actionbar): add invisible label to stabilize section width ¯\_(ツ)_/¯")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-29 10:13:43 -07:00
William Casarin 1fabd347ca Merge remote-tracking branch 'github/pr/1031' 2025-07-29 10:08:43 -07:00
William Casarin 0087fe7dff media/viewer: slower animation
so you can actually see whats going on

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 16:37:53 -07:00
William Casarin 51f7744149 media/viewer: fullscreen transition animations
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 16:12:29 -07:00
William Casarin 6d393c9c37 media/viewer: provide image-click provenance
We will be using this for transitions

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 14:19:03 -07:00
tyiu 39e932c674 Add Portuguese (Brazil) language and translations
Changelog-Added: Added Portuguese (Brazil) language and translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:02 -04:00
tyiu 6919460d18 Import translations
Changelog-Changed: Imported translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
tyiu bf58fdce1f Internationalize ShowNoteClientOptions labels
Changelog-Fixed: Internationalize ShowNoteClientOptions labels
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
tyiu 419102959f Add human-readable names to locales in settings
Changelog-Added: Added human-readable names to locales in settings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
tyiu 9bcbcae688 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-28 16:38:01 -04:00
William Casarin 5c8ab0ce07 media/viewer: handle click-to-close interactions
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 12:19:45 -07:00
William Casarin 590ffa0680 media/viewer: click anywhere to close
this should help mobile ...

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 12:10:00 -07:00
William Casarin 3d18db8fd2 Fullscreen MediaViewer refactor
- Moved media related logic into notedeck instead of the ui crate,
  since they pertain to Images/ImageCache based systems

- Made RenderableMedia owned to make it less of a nightmware
  to work with and the perf should be negligible

- Added a ImageMetadata cache to Images. This is referenced
  whenever we encounter an image so we don't have to
  redo the work all of the time

- Relpaced our ad-hoc, hand(vibe?)-coded panning and zoom logic
  with the Scene widget, which is explicitly designed for
  this use case

- Extracted and detangle fullscreen media rendering from inside of note
  rendering.  We instead let the application decide what action they
  want to perform when note media is clicked on.

- We add an on_view_media action to MediaAction for the application to
  handle. The Columns app uses this toggle a FullscreenMedia app
  option bits whenever we get a MediaAction::ViewMedis(urls).

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-28 08:57:57 -07:00
Fernando López Guevara 661acb3a12 fix(media): use ScaledTexture 2025-07-25 16:35:22 -03:00
Fernando López Guevara 8306003f6f fix(media): edge-to-edge image display on narrow screen 2025-07-25 16:17:45 -03:00
William Casarin 96ab4ee681 ui/note: fix another reply_desc wrapping issue
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
Fixes: https://github.com/damus-io/notedeck/issues/892
Changelog-Fixed: Fix another wrapping issue
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 12:12:25 -07:00
William Casarin 2524ff1061 wallet: fix nwc copy/paste
Fixes: https://github.com/damus-io/notedeck/issues/1012
Changelog-Fixed: Fix NWC copy/paste
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 12:09:15 -07:00
William Casarin eb0ab75e87 ui/wallet: small refactor to use return instead of break
we don't need this weird break syntax when we're in a closure

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 12:04:38 -07:00
William Casarin 009b4cf6b0 images: always resize large images
Fixes: https://github.com/damus-io/notedeck/issues/451
Fixes: https://linear.app/damus/issue/DECK-556/resize-images-to-device-screen-size
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-25 10:52:27 -07:00
Fernando López Guevara f2e01f0e40 fix(note_actionbar): add invisible label to stabilize section width ¯\_(ツ)_/¯ 2025-07-25 12:13:39 -03:00
William Casarin c891f8585d v0.5.8
we got back swipe on everything now fam

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 15:45:45 -07:00
William Casarin 2648967d7b lockfile: fixup
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 15:37:33 -07:00
William Casarin 438dbb2397 Merge drag to nav back on all views by kernel #1035
HE FUCKING DID IT LADS

Made a small tweak on the merge commit to update url to
damus-io/egui-nav upstream

William Casarin (2):
      Merge drag to nav back on all views by kernel #1035

kernelkind (9):
      TMP: update egui-nav
      refactor scrolling for post, reply & quote views
      enforce scroll_id for `ThreadView`
      add `scroll_id` for all views with vertical scroll
      add `DragSwitch`
      use `DragSwitch` in `Column`
      get scroll id for `Route`
      add `route_uses_frame`
      use `DragSwitch` to allow dragging anywhere in navigation
2025-07-24 15:28:30 -07:00
kernelkind 2bd139ef9e use DragSwitch to allow dragging anywhere in navigation
instead of just the top header when there is a vertical scroll

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:54:36 -04:00
kernelkind cda0a68854 add route_uses_frame
need to know this to get the correct drag id

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:54:32 -04:00
kernelkind a555707f67 get scroll id for Route
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:54:11 -04:00
kernelkind 1601914b8b use DragSwitch in Column
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:43 -04:00
kernelkind aac0f54991 add DragSwitch
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:40 -04:00
kernelkind 8960b3f052 add scroll_id for all views with vertical scroll
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:35 -04:00
kernelkind 6db6cf7b7a enforce scroll_id for ThreadView
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:28 -04:00
kernelkind 0bc32272d2 refactor scrolling for post, reply & quote views
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:25 -04:00
kernelkind b05d39cc81 TMP: update egui-nav
need this to make drag switching work

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-24 17:53:12 -04:00
William Casarin 7a83483758 nip10: switch to NoteReply instead of handrolled logic
Cc: kernelkind
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 13:32:19 -07:00
William Casarin 1a3112d8ef Merge remote-tracking branch 'github/pr/1027' 2025-07-24 12:29:11 -07:00
William Casarin c1d0ea1901 Merge remote-tracking branch 'github/jb55-deck-733-profile-sidebar-action-should-route-in-the-active-column' 2025-07-24 12:28:06 -07:00
William Casarin db6103d448 router: fix router selection
Many times we get the router selection wrong. This fixes that

Changelog-Fixed: Fix some routing issues when routing from the Chrome
Fixes: https://github.com/damus-io/notedeck/issues/1024
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 12:11:19 -07:00
Fernando López Guevara 0f00dcf7a7 fix(columns): render wide notes on narrow screen 2025-07-24 15:57:42 -03:00
William Casarin 8f63546524 ui: wrap reply description
This is similar to our fix in:

- Fixes: ee85b754dd ("Fix text wrapping issues")

Where removing the ui.horizontal call fixes subsequent main wrap layout
issues. It's still not clear to me where wrap state is getting mutated
where it would affect subsequent ui calls...

Fixes: https://github.com/damus-io/notedeck/issues/892
Changelog-Fixed: Fixed wrapping issues in Notes & Replies timeslines
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 09:11:12 -07:00
William Casarin 90975180f5 ui/replydesc: quick TextSegment cleanup/optimize
most a micro-optimize + cleanup

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-24 09:03:47 -07:00
Jakub Gladysz bd9a78b305 Do not crash on unknown arg
Signed-off-by: Jakub Gladysz <jakub.gladysz@protonmail.com>
2025-07-24 11:02:43 +03:00
William Casarin 4e27c1f491 v0.5.7
Whats new
=========

- Swipe nav on full screen media
- Made action buttons bigger
- Fix zap button not appearing
- Allow removal of Damoose account
- Profile is now clickable from side bar

- New settings view:
  * Resize zoom level
  * Clear cache
  * Change locale

- Localization support
  * German
  * Spanish
  * French
  * Chinese
  * Thai

Log
---

Fernando López Guevara (3):
      feat(full-screen-media): add swipe navigation
      feat(settings): add settings view
      fix(columns): prevent crash when switching to account with no columns

Terry Yiu (9):
      Add Fluent-based localization manager and add script to export source strings for translations
      Add French, German, Simplified Chinese, and Traditional Chinese translations
      Add Spanish (Latin America and Spain) translations
      Add Thai translations
      Add localization documentation to notedeck DEVELOPER.md
      Clean up time_ago_since, add tests, and internationalize strings
      Fix export_source_strings.py to adjust for tr! and tr_plural! macro signature changes
      Internationalize user-facing strings and export them for translations
      Update Chinese, French, and German translations

William Casarin (15):
      args: add --locale option
      debug: add startup query debug log
      fix missing zap button
      fix one missing home string
      gitignore: remove cache
      i18n: always have en-XA available
      i18n: disable bidi for tests
      i18n: disable broken tests for now
      i18n: make localization context non-global
      media/trust: always show if its yourself
      ripgrep: add ignore file for ftl files
      settings: fix route to relay
      ui/note: make buttons larger
      ui/note: small refactor to use returns instead of break
      wallet: remove unused flag in note context

kernelkind (14):
      add ChromePanelAction::Profile & use for pfp
      add new Accounts button to chrome sidebar
      allow removal of Damoose account
      appease clippy
      bugfix: properly sub to new selected acc after removal of selected
      bugfix: unsubscribe all decks when log out account
      bugfix: unsubscribe from timelines on deck deletion
      expose `AccountCache::falback`
      fix: sometimes most recent contacts list wasn't used
      make `UserAccount` cloneable
      move select account logic to own method
      use `NwcError` instead of nwc::Error
      use saturating sub

name                                                  added  removed  commits
kernelkind <kernelkind@gmail.com>                     +328   -50      14
Fernando López Guevara <fernando.lguevara@gmail.com>  +802   -36      3
William Casarin <jb55@jb55.com>                       +1603  -1297    15
Terry Yiu <git@tyiu.xyz>                              +7547  -1024    9

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 13:03:28 -07:00
William Casarin f9f8b3fe1b Merge remote-tracking branch 'github/pr/1023' 2025-07-23 12:31:51 -07:00
William Casarin 5ddd8660a3 settings: fix route to relay
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 12:29:09 -07:00
William Casarin fe30704496 Merge remote-tracking branch 'fernando/feat/settings-view' 2025-07-23 12:00:29 -07:00
William Casarin e997f1bf68 ui/note: make buttons larger
Changelog-Changed: Make buttons larger
Fixes: https://github.com/damus-io/notedeck/issues/879
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 11:49:06 -07:00
William Casarin ff0428550b fix missing zap button
Changelog-Fixed: Fix missing zap button
Fixes: 397bfce817 ("add `Accounts` to `NoteContext`")
Fixes: https://github.com/damus-io/notedeck/issues/1021
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 11:49:03 -07:00
Fernando López Guevara da6ede5f69 feat(settings): add settings view 2025-07-23 15:33:17 -03:00
William Casarin 56cbf68ea5 ui/note: small refactor to use returns instead of break
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 09:39:05 -07:00
William Casarin ebf31abafa wallet: remove unused flag in note context
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 09:36:38 -07:00
William Casarin e317c57769 ripgrep: add ignore file for ftl files
Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-23 09:36:21 -07:00
William Casarin f722a58d66 Merge new Accounts button to chrome sidebar by kernel #994
kernelkind (3):
      use saturating sub
      add new Accounts button to chrome sidebar
      add ChromePanelAction::Profile & use for pfp
2025-07-23 09:13:49 -07:00
William Casarin ffcd38ef96 Merge prevent crash when switching cols from fernando #997
Fernando López Guevara (1):
      fix(columns): prevent crash when switching to account with no columns
2025-07-23 09:10:21 -07:00
William Casarin 088704a768 Merge media swipe nav from fernando #1010
Fernando López Guevara (1):
      feat(full-screen-media): add swipe navigation
2025-07-23 09:09:04 -07:00
William Casarin 10eedc0ca6 Merge contact list fixes by kernel #998
kernelkind (2):
      appease clippy
      fix: sometimes most recent contacts list wasn't used
2025-07-23 08:54:12 -07:00
tyiu b285be97a1 Add Thai translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-23 00:15:01 -04:00
tyiu 7321e82800 Add Spanish (Latin America and Spain) translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 21:24:07 -04:00
tyiu e686afed1c Update Chinese, French, and German translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-22 21:23:35 -04:00
Fernando López Guevara ed38c75193 feat(full-screen-media): add swipe navigation 2025-07-18 13:46:25 -03:00
kernelkind fdef74c353 fix: sometimes most recent contacts list wasn't used
`ndb::poll_for_notes` appears to give notes as they arrive. We
need to make sure we only use the most recent for contacts

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 18:17:56 -04:00
kernelkind 030e4226f8 appease clippy
```
error: large size difference between variants
   --> crates/notedeck_columns/src/column.rs:249:1
    |
249 | / pub enum IntermediaryRoute {
250 | |     Timeline(Timeline),
    | |     ------------------ the largest variant contains at least 280 bytes
251 | |     Route(Route),
    | |     ------------ the second-largest variant contains at least 72 bytes
252 | | }
    | |_^ the entire enum is at least 280 bytes
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
    = note: `-D clippy::large-enum-variant` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::large_enum_variant)]`
help: consider boxing the large fields to reduce the total size of the enum
    |
250 -     Timeline(Timeline),
250 +     Timeline(Box<Timeline>),
    |

error: could not compile `notedeck_columns` (lib) due to 1 previous error
```

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 18:17:53 -04:00
Fernando López Guevara 508d8dc0ba fix(columns): prevent crash when switching to account with no columns 2025-07-17 19:09:10 -03:00
kernelkind 34afa755b8 add ChromePanelAction::Profile & use for pfp
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 15:23:41 -04:00
kernelkind 45490c918d add new Accounts button to chrome sidebar
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 15:23:32 -04:00
kernelkind a31fdd3ed2 use saturating sub
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 15:23:15 -04:00
128 changed files with 7645 additions and 2846 deletions
-33
View File
@@ -1,33 +0,0 @@
name: Crowdin Download Translations
on:
repository_dispatch:
types: [ crowdin-translation-complete ]
permissions:
contents: write
pull-requests: write
jobs:
crowdin-download:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Crowdin download translations
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
localization_branch_name: crowdin-translations
create_pull_request: true
pull_request_title: 'Crowdin Translations'
pull_request_body: 'Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
-67
View File
@@ -1,67 +0,0 @@
name: Crowdin Upload & Sync
on:
push:
branches: [ master ]
permissions:
contents: write
pull-requests: write
jobs:
crowdin-upload:
runs-on: ubuntu-latest
steps:
- name: Fetch crowdin-translations branch
run: |
git fetch origin crowdin-translations:crowdin-translations || true
- name: Checkout crowdin-translations branch
run: git checkout crowdin-translations || git checkout -b crowdin-translations
- name: Rebase master onto crowdin-translations
run: git rebase master
- name: Fail if rebase conflicts occurred
run: |
if [ -d .git/rebase-merge ] || [ -d .git/rebase-apply ]; then
echo "❌ Rebase conflict detected! Please resolve conflicts in the crowdin-translations branch manually."
exit 1
fi
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Run export_source_strings.py
run: python3 scripts/export_source_strings.py
- name: Check for changes in main.ftl
id: check_diff
run: |
git diff --quiet assets/translations/en-US/main.ftl assets/translations/en-XA/main.ftl || echo "changed=true" >> $GITHUB_OUTPUT
- name: Commit changes to crowdin-translations
if: steps.check_diff.outputs.changed == 'true'
run: |
git add assets/translations/en-US/main.ftl assets/translations/en-XA/main.ftl
git commit -m "Update source strings from export_source_strings.py" || true
git push --force origin crowdin-translations
- name: Crowdin upload sources
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: false
localization_branch_name: crowdin-translations
create_pull_request: true
pull_request_title: 'Crowdin Translations'
pull_request_body: 'Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+4
View File
@@ -19,3 +19,7 @@ queries/damus-notifs.json
.direnv/
scripts/macos_build_secrets.sh
/tags
.zed
.lsp
.idea
local.properties
+1
View File
@@ -0,0 +1 @@
*.ftl
Generated
+298 -66
View File
@@ -765,6 +765,25 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
dependencies = [
"objc-sys",
]
[[package]]
name = "block2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
dependencies = [
"block-sys",
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.5.1"
@@ -802,6 +821,17 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"serde",
]
[[package]]
name = "built"
version = "0.7.7"
@@ -978,6 +1008,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@@ -1233,6 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@@ -1378,20 +1410,26 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecolor"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"bytemuck",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
"serde",
]
[[package]]
name = "eframe"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"ahash",
"bytemuck",
@@ -1427,24 +1465,25 @@ dependencies = [
[[package]]
name = "egui"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"accesskit",
"ahash",
"backtrace",
"bitflags 2.9.1",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
"epaint",
"log",
"nohash-hasher",
"profiling",
"serde",
"similar",
]
[[package]]
name = "egui-wgpu"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"ahash",
"bytemuck",
@@ -1463,7 +1502,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"ahash",
"arboard",
@@ -1481,7 +1520,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"ahash",
"egui",
@@ -1498,7 +1537,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"ahash",
"bytemuck",
@@ -1515,7 +1554,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.2.0"
source = "git+https://github.com/damus-io/egui-nav?rev=111de8ac40b5d18df53e9691eb18a50d49cb31d8#111de8ac40b5d18df53e9691eb18a50d49cb31d8"
source = "git+https://github.com/damus-io/egui-nav?rev=3c67eb6298edbff36d46546897cfac33df4f04db#3c67eb6298edbff36d46546897cfac33df4f04db"
dependencies = [
"egui",
"egui_extras",
@@ -1577,7 +1616,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
[[package]]
name = "emath"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"bytemuck",
"serde",
@@ -1595,7 +1634,7 @@ version = "0.3.0"
dependencies = [
"bech32",
"ewebsock",
"hashbrown",
"hashbrown 0.15.4",
"hex",
"mio",
"nostr 0.37.0",
@@ -1675,13 +1714,13 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
dependencies = [
"ab_glyph",
"ahash",
"bytemuck",
"ecolor",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)",
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
"epaint_default_fonts",
"log",
"nohash-hasher",
@@ -1693,7 +1732,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
[[package]]
name = "equator"
@@ -2269,7 +2308,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.9.1",
"gpu-descriptor-types",
"hashbrown",
"hashbrown 0.15.4",
]
[[package]]
@@ -2291,6 +2330,12 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -2335,6 +2380,17 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "hex_color"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
dependencies = [
"arrayvec",
"rand 0.8.5",
"serde",
]
[[package]]
name = "hex_lit"
version = "0.1.1"
@@ -2496,6 +2552,16 @@ dependencies = [
"cc",
]
[[package]]
name = "icrate"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
dependencies = [
"block2 0.4.0",
"objc2 0.5.2",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
@@ -2654,6 +2720,17 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.9.0"
@@ -2661,7 +2738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.4",
"serde",
]
@@ -2733,25 +2810,6 @@ dependencies = [
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -2868,6 +2926,19 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsoncanvas"
version = "0.1.6"
source = "git+https://github.com/jb55/jsoncanvas?rev=ae60f96e4d022cf037e086b793cacc3225bc14e5#ae60f96e4d022cf037e086b793cacc3225bc14e5"
dependencies = [
"hex_color",
"serde",
"serde_json",
"serde_with",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -3190,7 +3261,7 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
"indexmap",
"indexmap 2.9.0",
"log",
"rustc-hash 1.1.0",
"spirv",
@@ -3304,6 +3375,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "normpath"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "nostr"
version = "0.37.0"
@@ -3383,8 +3463,8 @@ dependencies = [
[[package]]
name = "nostrdb"
version = "0.7.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=a307f5d3863b5319c728b2782959839b8df544cb#a307f5d3863b5319c728b2782959839b8df544cb"
version = "0.8.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3#2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3"
dependencies = [
"bindgen",
"cc",
@@ -3398,21 +3478,24 @@ dependencies = [
[[package]]
name = "notedeck"
version = "0.5.6"
version = "0.5.9"
dependencies = [
"base32",
"bech32",
"bincode",
"bitflags 2.9.1",
"blurhash",
"dirs",
"eframe",
"egui",
"egui-winit",
"egui_extras",
"ehttp",
"enostr",
"fluent",
"fluent-langneg",
"fluent-resmgr",
"hashbrown",
"hashbrown 0.15.4",
"hex",
"image",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3434,6 +3517,7 @@ dependencies = [
"sha2",
"strum",
"strum_macros",
"sys-locale",
"tempfile",
"thiserror 2.0.12",
"tokenator",
@@ -3446,7 +3530,7 @@ dependencies = [
[[package]]
name = "notedeck_chrome"
version = "0.5.6"
version = "0.5.9"
dependencies = [
"eframe",
"egui",
@@ -3457,6 +3541,7 @@ dependencies = [
"notedeck",
"notedeck_columns",
"notedeck_dave",
"notedeck_notebook",
"notedeck_ui",
"profiling",
"puffin",
@@ -3475,7 +3560,7 @@ dependencies = [
[[package]]
name = "notedeck_columns"
version = "0.5.6"
version = "0.5.9"
dependencies = [
"base64 0.22.1",
"bech32",
@@ -3490,16 +3575,16 @@ dependencies = [
"egui_virtual_list",
"ehttp",
"enostr",
"hashbrown",
"hashbrown 0.15.4",
"hex",
"human_format",
"image",
"indexmap",
"indexmap 2.9.0",
"nostrdb",
"notedeck",
"notedeck_ui",
"oot_bitset",
"open",
"opener",
"poll-promise",
"pretty_assertions",
"profiling",
@@ -3507,6 +3592,7 @@ dependencies = [
"puffin_egui",
"rfd",
"rmpv",
"robius-open",
"security-framework 2.11.1",
"serde",
"serde_derive",
@@ -3528,7 +3614,7 @@ dependencies = [
[[package]]
name = "notedeck_dave"
version = "0.5.6"
version = "0.5.9"
dependencies = [
"async-openai",
"bytemuck",
@@ -3550,19 +3636,27 @@ dependencies = [
"tracing",
]
[[package]]
name = "notedeck_notebook"
version = "0.5.9"
dependencies = [
"egui",
"jsoncanvas",
"notedeck",
]
[[package]]
name = "notedeck_ui"
version = "0.5.6"
version = "0.5.9"
dependencies = [
"bitflags 2.9.1",
"blurhash",
"eframe",
"egui",
"egui-winit",
"egui_extras",
"ehttp",
"enostr",
"hashbrown",
"hashbrown 0.15.4",
"image",
"nostrdb",
"notedeck",
@@ -3992,14 +4086,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.3.2"
name = "opener"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
"bstr",
"normpath",
"windows-sys 0.59.0",
]
[[package]]
@@ -4103,12 +4197,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -4397,7 +4485,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
dependencies = [
"egui",
"egui_extras",
"indexmap",
"indexmap 2.9.0",
"natord",
"once_cell",
"parking_lot",
@@ -4725,6 +4813,26 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -4916,6 +5024,30 @@ dependencies = [
"rmp",
]
[[package]]
name = "robius-android-env"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef"
dependencies = [
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ndk-context",
]
[[package]]
name = "robius-open"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243e2abbc8c1ca8ddc283056d4675b67e452fd527c3741c5318642da37840ff3"
dependencies = [
"cfg-if",
"icrate",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"objc2 0.5.2",
"robius-android-env",
"windows 0.54.0",
]
[[package]]
name = "roxmltree"
version = "0.19.0"
@@ -5062,6 +5194,30 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -5215,7 +5371,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"indexmap",
"indexmap 2.9.0",
"itoa",
"memchr",
"ryu",
@@ -5254,6 +5410,38 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars 0.9.0",
"schemars 1.0.4",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -5315,6 +5503,12 @@ dependencies = [
"quote",
]
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "simplecss"
version = "0.2.2"
@@ -5528,6 +5722,15 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
dependencies = [
"libc",
]
[[package]]
name = "sysinfo"
version = "0.30.13"
@@ -5848,7 +6051,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6631,7 +6834,7 @@ dependencies = [
"bitflags 2.9.1",
"cfg_aliases",
"document-features",
"indexmap",
"indexmap 2.9.0",
"log",
"naga",
"once_cell",
@@ -6752,6 +6955,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core 0.54.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"
@@ -6771,6 +6984,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
@@ -6847,6 +7070,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
+16 -10
View File
@@ -1,17 +1,19 @@
[workspace]
resolver = "2"
package.version = "0.5.6"
package.version = "0.5.9"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
"crates/notedeck_columns",
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
]
[workspace.dependencies]
opener = "0.8.2"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
@@ -23,7 +25,7 @@ egui = { version = "0.31.1", features = ["serde"] }
egui-wgpu = "0.31.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "111de8ac40b5d18df53e9691eb18a50d49cb31d8" }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "3c67eb6298edbff36d46546897cfac33df4f04db" }
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
#egui_virtual_list = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
@@ -41,16 +43,17 @@ md5 = "0.7.0"
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
nwc = "0.39.0"
mio = { version = "1.0.3", features = ["os-poll", "net"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "a307f5d3863b5319c728b2782959839b8df544cb" }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b80b98f1db6a03a1b88ca699bfa3" }
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
notedeck_ui = { path = "crates/notedeck_ui" }
tokenator = { path = "crates/tokenator" }
once_cell = "1.19.0"
open = "5.3.0"
robius-open = "0.1"
poll-promise = { version = "0.3.0", features = ["tokio"] }
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
@@ -66,6 +69,7 @@ tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tempfile = "3.13.0"
unic-langid = { version = "0.9.6", features = ["macros"] }
sys-locale = "0.3"
url = "2.5.2"
urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4"] }
@@ -81,6 +85,8 @@ hashbrown = "0.15.2"
openai-api-rs = "6.0.3"
re_memory = "0.23.4"
oot_bitset = "0.1.1"
blurhash = "0.2.3"
[profile.small]
inherits = 'release'
@@ -98,12 +104,12 @@ strip = true # Strip symbols from binary*
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" }
egui = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
root_dir=$PWD
cargo ndk --target arm64-v8a -o ./crates/notedeck_chrome/android/app/src/main/jniLibs/ build --profile release
cd ./crates/notedeck_chrome/android
./gradlew build && ./gradlew installDebug
cd $root_dir
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+4 -66
View File
@@ -6,9 +6,7 @@
# Regular strings
# Profile about/bio field label
About_00c0 = Über
# Display name for account management
Accounts_e233 = Konten
About_00c0 = Über mich
# Column title for account management
Accounts_f018 = Konten
# Button label to add a relay
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = Wallet hinzufügen um fortzufahren
Add_account_1cfc = Konto hinzufügen
# Column title for adding new account
Add_Account_d06c = Konto hinzufügen
# Display name for adding account
Add_Account_d715 = Konto hinzufügen
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Algorithmus-Spalte hinzufügen
# Display name for adding column
Add_Column_c6ff = Spalte hinzufügen
# Column title for adding new column
Add_Column_c764 = Spalte hinzufügen
# Display name for adding deck
Add_Deck_6e5f = Deck hinzufügen
# Column title for adding new deck
Add_Deck_fabf = Deck hinzufügen
# Column title for adding external notifications column
@@ -69,8 +61,6 @@ Broadcast_Local_7e50 = Lokal senden
Cancel_ed3b = Abbrechen
# Hover text for editable zap amount
Click_to_edit_0414 = Zum Bearbeiten anklicken
# Display name for note composition
Compose_Note_ad11 = Notiz erstellen
# Column title for note composition
Compose_Note_c094 = Notiz erstellen
# Button label to confirm an action
@@ -83,8 +73,6 @@ Connecting_6b7e = Verbinde...
Contact_List_f85a = Kontaktliste
# Column title for contact lists
Contacts_7533 = Kontakte
# Timeline kind label for contact lists
Contacts_8b98 = Kontakte
# Column title for last notes per contact
Contacts__last_notes_3f84 = Kontakte (letzte Notizen)
# Button label to copy logs
@@ -100,7 +88,7 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
# Copy the text content of the note to clipboard
Copy_Text_f81c = Text kopieren
# Relative time in days
count_d_b9be = { $count }T.
count_d_b9be = { $count }Tg.
# Relative time in hours
count_h_3ecb = { $count }Std.
# Relative time in minutes
@@ -119,12 +107,8 @@ Create_Account_6994 = Konto erstellen
Create_Deck_16b7 = Deck erstellen
# Column title for custom timelines
Custom_a69e = Benutzerdefiniert
# Display name for custom timelines
Custom_cb4f = Benutzerdefiniert
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
# Display name for zap customization
Customize_Zap_Amount_ed29 = Zap-Betrag anpassen
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for deck name input field
@@ -147,14 +131,10 @@ Display_name_f9d9 = Anzeigename
domain___will_be_used_for_identification_b67e = "{ $domain }" wird zur Identifikation verwendet
# Column title for editing deck
Edit_Deck_4018 = Deck bearbeiten
# Display name for editing deck
Edit_Deck_c9ba = Deck bearbeiten
# Button label to edit a deck
Edit_Deck_fd93 = Deck bearbeiten
# Button label to edit user profile
Edit_Profile_49e6 = Profil bearbeiten
# Display name for profile editing
Edit_Profile_6699 = Profil bearbeiten
# Column title for profile editing
Edit_Profile_8ad4 = Profil bearbeiten
# Placeholder for hashtag input field
@@ -171,14 +151,8 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
# Label for find user button
Find_User_bd12 = Profil finden
# Timeline kind label for hashtag feeds
Hashtag_a0ab = Hashtag
# Display name for hashtag feeds
Hashtags_617e = Hashtags
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Display name for home feed
Home_3efc = Startseite
# Title for Home column
Home_8c19 = Startseite
# Label for deck icon selection
@@ -205,10 +179,6 @@ k_5K_f7e6 = 5K
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
# Title for last note per user column
Last_Note_per_User_17ad = Letzte Notiz pro Profil
# Timeline kind label for last notes per pubkey
Last_Notes_aefe = Letzte Notizen
# Display name for last notes per contact
Last_Per_Pubkey__Contact_33ce = Zuletzt pro Pubkey (Kontakt)
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
# Login page title
@@ -241,10 +211,6 @@ Notes_60d2 = Notizen
Notes___Replies_1ec2 = Notizen & Antworten
# Label for notes and replies filter
Notes___Replies_6e3b = Notizen & Antworten
# Timeline kind label for notifications
Notifications_6228 = Benachrichtigungen
# Display name for notifications
Notifications_8029 = Benachrichtigungen
# Column title for notifications
Notifications_d673 = Benachrichtigungen
# Title for notifications column
@@ -267,30 +233,20 @@ Please_select_an_icon_655b = Bitte wählen ein Symbol aus.
Post_now_8a49 = Jetzt veröffentlichen
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Drücke die Schaltfläche unten, um deine neuesten Protokolle in die Zwischenablage deines Systems zu kopieren. Dann füge sie in deine E-Mail ein.
# Display name for user profiles
Profile_2478 = Profil
# Timeline kind label for user profiles
Profile_9027 = Profil
# Profile picture URL field label
Profile_picture_81ff = Profilbild
# Column title for quote composition
Quote_475c = Zitat
# Display name for quote composition
Quote_a38e = Zitat
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Zitat von unbekannter Notiz
# Label for read-only profile mode
Read_only_82ff = Nur Lesezugriff
# Display name for relay management
Relays_7335 = Relays
# Column title for relay management
Relays_9d89 = Relays
# Label for relay list section
Relays_ad5e = Relays
# Column title for reply composition
Reply_3bf1 = Antwort
# Display name for reply composition
Reply_b40f = Antworten
# Hover text for reply button
Reply_to_this_note_f5de = Auf diese Notiz antworten
# Error message when reply note cannot be found
@@ -321,12 +277,6 @@ sats_e5ec = Sats
Save_6f7c = Speichern
# Button label to save profile changes
Save_changes_00db = Änderungen speichern
# Display name for search results
Search_0aa0 = Suche
# Display name for search page
Search_4503 = Suche
# Timeline kind label for search results
Search_a0b8 = Suche
# Column title for search page
Search_c573 = Suche
# Placeholder for search notes input field
@@ -369,8 +319,6 @@ Step_2_d08d = Schritt 2
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
# Display name for support page
Support_a4b4 = Support
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
# Hover text for light mode toggle button
@@ -380,17 +328,9 @@ Tap_to_Load_4b05 = Zum Laden antippen
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
# Column title for note thread view
Thread_0f20 = Unterhaltungen
# Display name for thread view
Thread_9957 = Unterhaltungen
Thread_0f20 = Unterhaltung
# Link text for thread references
thread_ad1f = Unterhaltungen
# Generic timeline kind label
Timeline_b0fc = Timeline
# Timeline kind label for universe feed
Universe_0a3e = Weltraum
# Display name for universe feed
Universe_d47e = Weltraum
thread_ad1f = Unterhaltung
# Title for universe column
Universe_e01e = Weltraum
# Column title for universe feed
@@ -403,8 +343,6 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
Username_daa7 = Benutzername
# Column title for wallet management
Wallet_5e50 = Wallet
# Display name for wallet management
Wallet_cdca = Wallet
# Hint for deck name input field
We_recommend_short_names_083e = Wir empfehlen kurze Namen
# Profile website field label
+60
View File
@@ -64,6 +64,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in no
# Label for zap amount input field
Amount_70f0 = Amount
# Label for appearance settings section
Appearance_4c7f = Appearance
# Button to send message to Dave AI assistant
Ask_b7f4 = Ask
@@ -76,6 +79,9 @@ Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Bottom
# Broadcast the note to all connected relays
Broadcast_fe43 = Broadcast
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = Broadcast Local
# Button label to cancel an action
Cancel_ed3b = Cancel
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancel
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Clear cache
# Hover text for editable zap amount
Click_to_edit_0414 = Click to edit
# Column title for note composition
Compose_Note_c094 = Compose Note
# Label for configure relays, settings section
Configure_relays_d156 = Configure relays
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirm
# Button label to confirm an action
Confirm_f8a6 = Confirm
@@ -163,6 +181,9 @@ Customize_Zap_Amount_cfc4 = Customize Zap Amount
# Column title for support page
Damus_Support_27c0 = Damus Support
# Label for Theme Dark, Appearance settings section
Dark_85fe = Dark
# Label for deck name input field
Deck_name_cd32 = Deck name
@@ -223,12 +244,18 @@ Find_User_bd12 = Find User
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Option in settings section to hide the source client label in note display
Hide_281d = Hide
# Title for Home column
Home_8c19 = Home
# Label for deck icon selection
Icon_b0ab = Icon
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Image cache size:
# Title for individual user column
Individual_b776 = Individual
@@ -259,9 +286,15 @@ k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
# Label for language, Appearance settings section
Language_e264 = Language:
# Title for last note per user column
Last_Note_per_User_17ad = Last Note per User
# Label for Theme Light, Appearance settings section
Light_7475 = Light
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Lightning network address (lud16)
@@ -325,6 +358,9 @@ Open_Email_25e9 = Open Email
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Open your default email client to get help from the Damus team
# Label for others settings section
Others_7267 = Others
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Paste your NWC URI here...
@@ -394,6 +430,9 @@ Repost_this_note_8e56 = Repost this note
# Label for reposted notes
Reposted_61c8 = Reposted
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Reset
# Heading for support section
Running_into_a_bug_1796 = Running into a bug?
@@ -427,6 +466,12 @@ See_the_whole_nostr_universe_7694 = See the whole nostr universe
# Button label to send a zap
Send_1ea4 = Send
# Column title for app settings
Settings_7a4f = Settings
# Label for Show source client, others settings section
Show_source_client_9e31 = Show source client
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
@@ -466,6 +511,9 @@ Step_1_8656 = Step 1
# Step 2 label in support instructions
Step_2_d08d = Step 2
# Label for storage settings section
Storage_ed65 = Storage
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
@@ -484,12 +532,18 @@ Tap_to_Load_4b05 = Tap to Load
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!
# Label for theme, Appearance settings section
Theme_4aac = Theme:
# Column title for note thread view
Thread_0f20 = Thread
# Link text for thread references
thread_ad1f = thread
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Top
# Title for universe column
Universe_e01e = Universe
@@ -505,6 +559,9 @@ username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at
# Profile username field label
Username_daa7 = Username
# Label for view folder button, Storage settings section
View_folder_9742 = View folder:
# Column title for wallet management
Wallet_5e50 = Wallet
@@ -532,6 +589,9 @@ Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap this note
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Zoom Level:
# Pluralized strings
# Search results count
+60
View File
@@ -64,6 +64,9 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds
# Label for zap amount input field
Amount_70f0 = {"["}Àmóúñt{"]"}
# Label for appearance settings section
Appearance_4c7f = {"["}Àppéàràñçé{"]"}
# Button to send message to Dave AI assistant
Ask_b7f4 = {"["}Àsk{"]"}
@@ -76,6 +79,9 @@ Banner_52ef = {"["}Bàññér{"]"}
# Beta version label
BETA_8e5d = {"["}BÉTÀ{"]"}
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = {"["}Bóttóm{"]"}
# Broadcast the note to all connected relays
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
@@ -85,12 +91,24 @@ Broadcast_Local_7e50 = {"["}Bróàdçàst Lóçàl{"]"}
# Button label to cancel an action
Cancel_ed3b = {"["}Çàñçél{"]"}
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = {"["}Çàñçél{"]"}
# Label for clear cache button, Storage settings section
Clear_cache_dccb = {"["}Çléàr çàçhé{"]"}
# Hover text for editable zap amount
Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"}
# Column title for note composition
Compose_Note_c094 = {"["}Çómpósé Ñóté{"]"}
# Label for configure relays, settings section
Configure_relays_d156 = {"["}Çóñfígúré rélàys{"]"}
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = {"["}Çóñfírm{"]"}
# Button label to confirm an action
Confirm_f8a6 = {"["}Çóñfírm{"]"}
@@ -163,6 +181,9 @@ Customize_Zap_Amount_cfc4 = {"["}Çústómízé Zàp Àmóúñt{"]"}
# Column title for support page
Damus_Support_27c0 = {"["}Dàmús Súppórt{"]"}
# Label for Theme Dark, Appearance settings section
Dark_85fe = {"["}Dàrk{"]"}
# Label for deck name input field
Deck_name_cd32 = {"["}Déçk ñàmé{"]"}
@@ -223,12 +244,18 @@ Find_User_bd12 = {"["}Fíñd Úsér{"]"}
# Title for hashtags column
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
# Option in settings section to hide the source client label in note display
Hide_281d = {"["}Hídé{"]"}
# Title for Home column
Home_8c19 = {"["}Hómé{"]"}
# Label for deck icon selection
Icon_b0ab = {"["}Íçóñ{"]"}
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = {"["}Ímàgé çàçhé sízé:{"]"}
# Title for individual user column
Individual_b776 = {"["}Íñdívídúàl{"]"}
@@ -259,9 +286,15 @@ k_5K_f7e6 = {"["}5K{"]"}
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
# Label for language, Appearance settings section
Language_e264 = {"["}Làñgúàgé:{"]"}
# Title for last note per user column
Last_Note_per_User_17ad = {"["}Làst Ñóté pér Úsér{"]"}
# Label for Theme Light, Appearance settings section
Light_7475 = {"["}Líght{"]"}
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"}
@@ -325,6 +358,9 @@ Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = {"["}Ópéñ yóúr défàúlt émàíl çlíéñt tó gét hélp fróm thé Dàmús téàm{"]"}
# Label for others settings section
Others_7267 = {"["}Óthérs{"]"}
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"}
@@ -394,6 +430,9 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
# Label for reposted notes
Reposted_61c8 = {"["}Répóstéd{"]"}
# Label for reset zoom level, Appearance settings section
Reset_62d4 = {"["}Rését{"]"}
# Heading for support section
Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"}
@@ -427,6 +466,12 @@ See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérs
# Button label to send a zap
Send_1ea4 = {"["}Séñd{"]"}
# Column title for app settings
Settings_7a4f = {"["}Séttíñgs{"]"}
# Label for Show source client, others settings section
Show_source_client_9e31 = {"["}Shów sóúrçé çlíéñt{"]"}
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
@@ -466,6 +511,9 @@ Step_1_8656 = {"["}Stép 1{"]"}
# Step 2 label in support instructions
Step_2_d08d = {"["}Stép 2{"]"}
# Label for storage settings section
Storage_ed65 = {"["}Stóràgé{"]"}
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé élsé's ñótés{"]"}
@@ -484,12 +532,18 @@ Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"}
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"}
# Label for theme, Appearance settings section
Theme_4aac = {"["}Thémé:{"]"}
# Column title for note thread view
Thread_0f20 = {"["}Thréàd{"]"}
# Link text for thread references
thread_ad1f = {"["}thréàd{"]"}
# Option in settings section to show the source client label at the top of the note
Top_6aeb = {"["}Tóp{"]"}
# Title for universe column
Universe_e01e = {"["}Úñívérsé{"]"}
@@ -505,6 +559,9 @@ username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username
# Profile username field label
Username_daa7 = {"["}Úsérñàmé{"]"}
# Label for view folder button, Storage settings section
View_folder_9742 = {"["}Víéw fóldér:{"]"}
# Column title for wallet management
Wallet_5e50 = {"["}Wàllét{"]"}
@@ -532,6 +589,9 @@ Zap_16b4 = {"["}Zàp{"]"}
# Hover text for zap button
Zap_this_note_42b2 = {"["}Zàp thís ñóté{"]"}
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = {"["}Zóóm Lévél:{"]"}
# Pluralized strings
# Search results count
+368
View File
@@ -0,0 +1,368 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Información
# Column title for account management
Accounts_f018 = Cuentas
# Button label to add a relay
Add_269d = Agregar
# Label for add column button
Add_47df = Agregar
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Agregar una billetera diferente que solo se utilizará para esta cuenta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Agregar una billetera para continuar
# Button label to add a new account
Add_account_1cfc = Agregar cuenta
# Column title for adding new account
Add_Account_d06c = Agregar cuenta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Agregar columna algorítmica
# Column title for adding new column
Add_Column_c764 = Agregar columna
# Column title for adding new deck
Add_Deck_fabf = Agregar deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Agregar columna de notificaciones externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Agregar columna de notificaciones
# Button label to add a relay
Add_relay_269d = Agregar relé
# Button label to add a wallet
Add_Wallet_d1be = Agregar billetera
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
# Label for zap amount input field
Amount_70f0 = Cantidad
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar enlace
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }mes
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }sem
# Relative time in years
count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Crear cuenta
# Button label to create a new deck
Create_Deck_16b7 = Crear deck
# Column title for custom timelines
Custom_a69e = Personalizado
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for deck name input field
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
# Name of the default deck feed
Default_Deck_fcca = Deck predeterminado
# Button label to delete a deck
Delete_Deck_bb29 = Eliminar deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar billetera
# Profile display name field label
Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Column title for editing deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
Edit_Deck_fd93 = Editar deck
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Ingresa el relé aquí
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Ingresa tu clave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Ícono
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Cantidad no válida
# Error message for invalid key input
Invalid_key_4726 = Clave no válida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI no válido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100.000
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10.000
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20.000
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50.000
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
Login_9eef = Inicio de sesión
# Login button text
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = No conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas y respuestas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas y respuestas
# Column title for notifications
Notifications_d673 = Notificaciones
# Title for notifications column
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un ícono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un ícono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
# Label for read-only profile mode
Read_only_82ff = Solo lectura
# Column title for relay management
Relays_9d89 = Relés
# Label for relay list section
Relays_ad5e = Relés
# Column title for reply composition
Reply_3bf1 = Respuesta
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconocida
# Fallback template for replying to user
replying_to__user_15ab = respondiendo a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondiendo a una nota
# Hover text for repost button
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Heading for support section
Running_into_a_bug_1796 = ¿Encontraste un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar cambios
# Column title for search page
Search_c573 = Búsqueda
# Placeholder for search notes input field
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
Sign_out_337b = Cerrar sesión
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
# Step 1 label in support instructions
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Cambiar a modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para cargar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar esta billetera solo para la cuenta actual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
# Profile username field label
Username_daa7 = Nombre de usuario
# Column title for wallet management
Wallet_5e50 = Billetera
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nombres cortos
# Profile website field label
Website_7980 = Sitio web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
# Placeholder text for key input field
Your_key_here_81bd = Tu clave aquí...
# Title for your notes column
Your_Notes_f6db = Tus notas
# Title for your notifications column
Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar un zap a esta nota
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}
+368
View File
@@ -0,0 +1,368 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Información
# Column title for account management
Accounts_f018 = Cuentas
# Button label to add a relay
Add_269d = Añadir
# Label for add column button
Add_47df = Añadir
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Añadir un monedero diferente que solo se utilizará para esta cuenta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Añadir un monedero para continuar
# Button label to add a new account
Add_account_1cfc = Añadir cuenta
# Column title for adding new account
Add_Account_d06c = Añadir cuenta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Añadir columna algorítmica
# Column title for adding new column
Add_Column_c764 = Añadir columna
# Column title for adding new deck
Add_Deck_fabf = Añadir deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Añadir columna de notificaciones externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Añadir columna de notificaciones
# Button label to add a relay
Add_relay_269d = Añadir relé
# Button label to add a wallet
Add_Wallet_d1be = Añadir monedero
# Title for algorithmic feeds column
Algo_2452 = Algo
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
# Label for zap amount input field
Amount_70f0 = Cantidad
# Button to send message to Dave AI assistant
Ask_b7f4 = Preguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Pregúntale cualquier cosa a Dave...
# Profile banner URL field label
Banner_52ef = Banner
# Beta version label
BETA_8e5d = BETA
# Broadcast the note to all connected relays
Broadcast_fe43 = Transmitir
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Transmitir localmente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Hover text for editable zap amount
Click_to_edit_0414 = Haz clic para editar
# Column title for note composition
Compose_Note_c094 = Redactar nota
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectado
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contactos
# Column title for contact lists
Contacts_7533 = Contactos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contactos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar enlace
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }mes
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }sem
# Relative time in years
count_y_9408 = { $count }a
# Button to create a new account
Create_Account_6994 = Crear cuenta
# Button label to create a new deck
Create_Deck_16b7 = Crear deck
# Column title for custom timelines
Custom_a69e = Personalizado
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
# Column title for support page
Damus_Support_27c0 = Ayuda de Damus
# Label for deck name input field
Deck_name_cd32 = Nombre del deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = Cantidad predeterminada por zap:
# Name of the default deck feed
Default_Deck_fcca = Deck predeterminado
# Button label to delete a deck
Delete_Deck_bb29 = Eliminar deck
# Tooltip for deleting a column
Delete_this_column_8d5a = Eliminar esta columna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Eliminar monedero
# Profile display name field label
Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Column title for editing deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
Edit_Deck_fd93 = Editar deck
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Ingresa aquí los hashtags deseados (si son varios, sepáralos con un espacio)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Ingresa el relé aquí
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Ingresa la clave del usuario (npub, hex, nip05) aquí...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Ingresa tu clave
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Buscar usuario
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
Home_8c19 = Inicio
# Label for deck icon selection
Icon_b0ab = Icono
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Cantidad no válida
# Error message for invalid key input
Invalid_key_4726 = Clave no válida.
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI no válido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100.000
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10.000
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20.000
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50.000
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# Title for last note per user column
Last_Note_per_User_17ad = Última nota por usuario
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
# Login page title
Login_9eef = Inicio de sesión
# Login button text
Login_now___let_s_do_this_5630 = Inicia sesión ahora, ¡manos a la obra!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que no sigues
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi deck
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Dirección de Nostr (identidad NIP-05)
# Default username when profile is not available
nostrich_df29 = nostrich
# Status label for disconnected relay
Not_Connected_6292 = No conectado
# Link text for note references
note_cad6 = nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck es un producto en fase beta. Es posible que haya errores, así que ponte en contacto con nosotros si tienes algún problema.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas y respuestas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas y respuestas
# Column title for notifications
Notifications_d673 = Notificaciones
# Title for notifications column
Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Crea un nombre para el Deck.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Crea un nombre para el deck y selecciona un icono.
# Error message for missing deck icon
Please_select_an_icon_655b = Selecciona un icono.
# Button label to post a note
Post_now_8a49 = Publicar ahora
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Cita de nota desconocida
# Label for read-only profile mode
Read_only_82ff = Solo lectura
# Column title for relay management
Relays_9d89 = Relés
# Label for relay list section
Relays_ad5e = Relés
# Column title for reply composition
Reply_3bf1 = Respuesta
# Hover text for reply button
Reply_to_this_note_f5de = Responder a esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder a nota desconocida
# Fallback template for replying to user
replying_to__user_15ab = respondiendo a { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = respondiendo a { $user } en la conversación de alguien
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondiendo a { $note } de { $user } en { $thread } de { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = respondiendo a { $note } de { $user }
# Template for replying to root thread
replying_to__user__s__thread_444d = respondiendo a { $thread } de { $user }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = respondiendo a una nota
# Hover text for repost button
Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Heading for support section
Running_into_a_bug_1796 = ¿Has encontrado un error?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Guardar
# Button label to save profile changes
Save_changes_00db = Guardar cambios
# Column title for search page
Search_c573 = Búsqueda
# Placeholder for search notes input field
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
# Button label to sign out of account
Sign_out_337b = Cerrar sesión
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantente al día con un hashtag específico
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Mantente al día con notificaciones y menciones
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantente al día con las notas y respuestas de otra persona
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantente al día con las notificaciones y menciones de otra persona
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantente al día con las notas y respuestas de alguien
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con tus notificaciones y menciones
# Step 1 label in support instructions
Step_1_8656 = Paso 1
# Step 2 label in support instructions
Step_2_d08d = Paso 2
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Cambiar a modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toca para cargar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
# Column title for note thread view
Thread_0f20 = Conversación
# Link text for thread references
thread_ad1f = conversación
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Usar este monedero solo para la cuenta actual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
# Profile username field label
Username_daa7 = Nombre de usuario
# Column title for wallet management
Wallet_5e50 = Monedero
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nombres cortos
# Profile website field label
Website_7980 = Sitio web
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escribe aquí una nota impactante...
# Placeholder text for key input field
Your_key_here_81bd = Tu clave aquí...
# Title for your notes column
Your_Notes_f6db = Tus notas
# Title for your notifications column
Your_Notifications_080d = Tus notificaciones
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Enviar un zap a esta nota
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
}
+40 -62
View File
@@ -7,8 +7,6 @@
# Profile about/bio field label
About_00c0 = A propos
# Display name for account management
Accounts_e233 = Comptes
# Column title for account management
Accounts_f018 = Comptes
# Button label to add a relay
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = Ajouter un portefeuille pour continuer
Add_account_1cfc = Ajouter un compte
# Column title for adding new account
Add_Account_d06c = Ajouter un compte
# Display name for adding account
Add_Account_d715 = Ajouter un compte
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Ajouter une colonne Algo
# Display name for adding column
Add_Column_c6ff = Ajouter une colonne
# Column title for adding new column
Add_Column_c764 = Ajouter une colonne
# Display name for adding deck
Add_Deck_6e5f = Ajouter un deck
# Column title for adding new deck
Add_Deck_fabf = Ajouter un deck
# Column title for adding external notifications column
@@ -53,6 +45,8 @@ Algo_2452 = Algo
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Des fils algorithmiques pour faciliter la découverte de notes
# Label for zap amount input field
Amount_70f0 = Montant
# Label for appearance settings section
Appearance_4c7f = Apparence
# Button to send message to Dave AI assistant
Ask_b7f4 = Demander
# Placeholder text for Dave AI input field
@@ -61,18 +55,26 @@ Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
Banner_52ef = Bannière
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = En bas
# Broadcast the note to all connected relays
Broadcast_fe43 = Diffusion
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Diffusion locale
# Button label to cancel an action
Cancel_ed3b = Annuler
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Annuler
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Vider le cache
# Hover text for editable zap amount
Click_to_edit_0414 = Cliquer pour modifier
# Display name for note composition
Compose_Note_ad11 = Ecrire une note
# Column title for note composition
Compose_Note_c094 = Ecrire une note
# Label for configure relays, settings section
Configure_relays_d156 = Configurer les relais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmer
# Button label to confirm an action
Confirm_f8a6 = Confirmer
# Status label for connected relay
@@ -83,8 +85,6 @@ Connecting_6b7e = Connexion...
Contact_List_f85a = Liste de contacts
# Column title for contact lists
Contacts_7533 = Contacts
# Timeline kind label for contact lists
Contacts_8b98 = Contacts
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contacts (dernières notes)
# Button label to copy logs
@@ -119,14 +119,12 @@ Create_Account_6994 = Créer un compte
Create_Deck_16b7 = Créer un deck
# Column title for custom timelines
Custom_a69e = Personnaliser
# Display name for custom timelines
Custom_cb4f = Personnaliser
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personnaliser le montant du Zap
# Display name for zap customization
Customize_Zap_Amount_ed29 = Personnaliser le montant du Zap
# Column title for support page
Damus_Support_27c0 = Assistance Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = Sombre
# Label for deck name input field
Deck_name_cd32 = Nom du deck
# Label for decks section in side panel
@@ -147,14 +145,10 @@ Display_name_f9d9 = Nom d'utilisateur
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
# Column title for editing deck
Edit_Deck_4018 = Modifier le deck
# Display name for editing deck
Edit_Deck_c9ba = Modifier le deck
# Button label to edit a deck
Edit_Deck_fd93 = Modifier le deck
# Button label to edit user profile
Edit_Profile_49e6 = Modifier le profil
# Display name for profile editing
Edit_Profile_6699 = Modifier le profil
# Column title for profile editing
Edit_Profile_8ad4 = Modifier le profil
# Placeholder for hashtag input field
@@ -169,18 +163,16 @@ Enter_your_key_0fca = Entrez votre clé
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
# Label for find user button
Find_User_bd12 = Trouver un utilisateur
# Timeline kind label for hashtag feeds
Hashtag_a0ab = Hashtag
# Display name for hashtag feeds
Hashtags_617e = Hashtags
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Display name for home feed
Home_3efc = Accueil
# Option in settings section to hide the source client label in note display
Hide_281d = Masquer
# Title for Home column
Home_8c19 = Accueil
# Label for deck icon selection
Icon_b0ab = Icone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Taille du cache des images :
# Title for individual user column
Individual_b776 = Individuel
# Error message for invalid zap amount
@@ -201,12 +193,12 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
# Label for language, Appearance settings section
Language_e264 = Langue :
# Title for last note per user column
Last_Note_per_User_17ad = Dernière note par utilisateur
# Timeline kind label for last notes per pubkey
Last_Notes_aefe = Dernières notes
# Display name for last notes per contact
Last_Per_Pubkey__Contact_33ce = Dernière par Pubkey (Contact)
# Label for Theme Light, Appearance settings section
Light_7475 = Clair
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Adresse réseau Lightning (lud16)
# Login page title
@@ -239,10 +231,6 @@ Notes_60d2 = Notes
Notes___Replies_1ec2 = Notes & Réponses
# Label for notes and replies filter
Notes___Replies_6e3b = Notes & Réponses
# Timeline kind label for notifications
Notifications_6228 = Notifications
# Display name for notifications
Notifications_8029 = Notifications
# Column title for notifications
Notifications_d673 = Notifications
# Title for notifications column
@@ -253,6 +241,8 @@ now_2181 = maintenant
Open_Email_25e9 = Ouvrir Email
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Ouvrez votre service d'email par défaut pour obtenir de l'aide de l'équipe Damus
# Label for others settings section
Others_7267 = Autres
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Collez ici votre NWC URI...
# Error message for missing deck name
@@ -265,30 +255,20 @@ Please_select_an_icon_655b = Veuillez choisir une icône.
Post_now_8a49 = Publier maintenant
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Cliquez sur le bouton ci-dessous pour copier vos données les plus récentes dans le presse-papiers de votre système. Collez-les ensuite dans votre courrier électronique.
# Display name for user profiles
Profile_2478 = Profil
# Timeline kind label for user profiles
Profile_9027 = Profil
# Profile picture URL field label
Profile_picture_81ff = Photo de profil
# Column title for quote composition
Quote_475c = Citation
# Display name for quote composition
Quote_a38e = Citation
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citation d'une note inconnue
# Label for read-only profile mode
Read_only_82ff = En lecture seule
# Display name for relay management
Relays_7335 = Relais
# Column title for relay management
Relays_9d89 = Relais
# Label for relay list section
Relays_ad5e = Relais
# Column title for reply composition
Reply_3bf1 = Répondre
# Display name for reply composition
Reply_b40f = Répondre
# Hover text for reply button
Reply_to_this_note_f5de = Répondre à cette note
# Error message when reply note cannot be found
@@ -309,6 +289,8 @@ replying_to_a_note_e0bc = répondre à une note
Repost_this_note_8e56 = Republier cette note
# Label for reposted notes
Reposted_61c8 = Republier
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Réinitialiser
# Heading for support section
Running_into_a_bug_1796 = Vous rencontrez un problème ?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -319,12 +301,6 @@ sats_e5ec = sats
Save_6f7c = Enregistrer
# Button label to save profile changes
Save_changes_00db = Enregistrer les modifications
# Display name for search results
Search_0aa0 = Recherche
# Display name for search page
Search_4503 = Rechercher
# Timeline kind label for search results
Search_a0b8 = Recherche
# Column title for search page
Search_c573 = Rechercher
# Placeholder for search notes input field
@@ -337,6 +313,10 @@ See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
# Button label to send a zap
Send_1ea4 = Envoyer
# Column title for app settings
Settings_7a4f = Paramètres
# Label for Show source client, others settings section
Show_source_client_9e31 = Afficher le client source
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
# Button label to sign out of account
@@ -363,12 +343,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Restez informé pour
Step_1_8656 = Etape 1
# Step 2 label in support instructions
Step_2_d08d = Etape 2
# Label for storage settings section
Storage_ed65 = Stockage
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
# Display name for support page
Support_a4b4 = Assistance
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Passer en mode sombre
# Hover text for light mode toggle button
@@ -377,18 +357,14 @@ Switch_to_light_mode_72ce = Passer en mode clair
Tap_to_Load_4b05 = Appuyer pour charger
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La période d'essai de l'assistant IA Dave Nostr est terminée :(. Merci de l'avoir testé ! Un Dave compatible-Zap sera bientôt disponible !
# Label for theme, Appearance settings section
Theme_4aac = Thème :
# Column title for note thread view
Thread_0f20 = Fil
# Display name for thread view
Thread_9957 = Fil
# Link text for thread references
thread_ad1f = fil
# Generic timeline kind label
Timeline_b0fc = Chronologie
# Timeline kind label for universe feed
Universe_0a3e = Universel
# Display name for universe feed
Universe_d47e = Universel
# Option in settings section to show the source client label at the top of the note
Top_6aeb = En haut
# Title for universe column
Universe_e01e = Universel
# Column title for universe feed
@@ -399,10 +375,10 @@ Use_this_wallet_for_the_current_account_only_61dc = Utiliser ce portefeuille pou
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" à "{ $domain }" sera utilisé pour l'identification
# Profile username field label
Username_daa7 = Nom d'utilisateur
# Label for view folder button, Storage settings section
View_folder_9742 = Voir le dossier :
# Column title for wallet management
Wallet_5e50 = Portefeuille
# Display name for wallet management
Wallet_cdca = Portefeuille
# Hint for deck name input field
We_recommend_short_names_083e = Nous recommandons des noms courts
# Profile website field label
@@ -419,6 +395,8 @@ Your_Notifications_080d = Vos notifications
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap cette note
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Niveau de zoom :
# Pluralized strings
+408
View File
@@ -0,0 +1,408 @@
# Main translation file for Notedeck
# This file contains common UI strings used throughout the application
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
# Regular strings
# Profile about/bio field label
About_00c0 = Sobre
# Column title for account management
Accounts_f018 = Contas
# Button label to add a relay
Add_269d = Transmitir
# Label for add column button
Add_47df = Adicionar coluna
# Button label to add a different wallet
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar outra carteira a ser usada apenas nesta conta
# Error message for missing wallet
Add_a_wallet_to_continue_d170 = Obrigatório adicionar carteira
# Button label to add a new account
Add_account_1cfc = Adicionar conta nova aqui
# Column title for adding new account
Add_Account_d06c = Adicionar nova conta
# Column title for adding algorithm column
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
# Column title for adding new column
Add_Column_c764 = Adicionar coluna
# Column title for adding new deck
Add_Deck_fabf = Adicionar aba
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = Adicionar coluna de #
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar última coluna de notas
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
Add_relay_269d = Adicionar transmissão
# Button label to add a wallet
Add_Wallet_d1be = Adicionar carteira
# Title for algorithmic feeds column
Algo_2452 = Algoritmos
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algoritmos para pesquisar notas
# Label for zap amount input field
Amount_70f0 = Valor
# Label for appearance settings section
Appearance_4c7f = Aparência
# Button to send message to Dave AI assistant
Ask_b7f4 = Perguntar
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = Perguntar ao Dave
# Profile banner URL field label
Banner_52ef = Destaque
# Beta version label
BETA_8e5d = Beta
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = Abaixo
# Broadcast the note to all connected relays
Broadcast_fe43 = Encaminhar
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = Encaminhar especificamente
# Button label to cancel an action
Cancel_ed3b = Cancelar
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = Cancelar
# Label for clear cache button, Storage settings section
Clear_cache_dccb = Limpar cache
# Hover text for editable zap amount
Click_to_edit_0414 = Editar valor
# Column title for note composition
Compose_Note_c094 = Compor nota
# Label for configure relays, settings section
Configure_relays_d156 = Configurar canais
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = Confirmar
# Button label to confirm an action
Confirm_f8a6 = Confirmar
# Status label for connected relay
Connected_f8cc = Conectar
# Status label for connecting relay
Connecting_6b7e = Conectando...
# Title for contact list column
Contact_List_f85a = Lista de contatos
# Column title for contact lists
Contacts_7533 = Contatos
# Column title for last notes per contact
Contacts__last_notes_3f84 = Contatos (últimas notas)
# Button label to copy logs
Copy_a688 = Copiar
# Button to copy media link to clipboard
Copy_Link_dc7c = Copiar link
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar nota "JSON"
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copiar texto
# Relative time in days
count_d_b9be = { $count }D
# Relative time in hours
count_h_3ecb = { $count }H
# Relative time in minutes
count_m_b41e = { $count }M
# Relative time in months
count_mo_7aba = { $count }Mes
# Relative time in seconds
count_s_aa26 = { $count }S
# Relative time in weeks
count_w_7468 = { $count }Sem
# Relative time in years
count_y_9408 = { $count }A
# Button to create a new account
Create_Account_6994 = Criar conta
# Button label to create a new deck
Create_Deck_16b7 = Criar aba
# Column title for custom timelines
Custom_a69e = Personalizar
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = Personalizar valor do ZAP
# Column title for support page
Damus_Support_27c0 = Ajuda
# Label for Theme Dark, Appearance settings section
Dark_85fe = Modo escuro
# Label for deck name input field
Deck_name_cd32 = Nome da aba
# Label for decks section in side panel
DECKS_1fad = ABAS
# Label for default zap amount input
Default_amount_per_zap_399d = Valor padrão de ZAP
# Name of the default deck feed
Default_Deck_fcca = Nome padrão de abas
# Button label to delete a deck
Delete_Deck_bb29 = Deletar aba
# Tooltip for deleting a column
Delete_this_column_8d5a = Deletar esta coluna
# Button label to delete a wallet
Delete_Wallet_d1d4 = Deletar carteira
# Profile display name field label
Display_name_f9d9 = Nome de exibição
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será utilizado para identificação
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
Edit_Deck_fd93 = Editar
# Button label to edit user profile
Edit_Profile_49e6 = Editar perfil
# Column title for profile editing
Edit_Profile_8ad4 = Editar perfil
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Digite as # desejadas aqui (para múltiplos espaços separados)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = Insira a retransmissão aqui
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = Digite a chave do usuário (npub, hex, nip05) aqui...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = Sua chave aqui
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
# Label for find user button
Find_User_bd12 = Pesquisar usuário
# Title for hashtags column
Hashtags_f8e0 = #
# Option in settings section to hide the source client label in note display
Hide_281d = Ocultar
# Title for Home column
Home_8c19 = Início
# Label for deck icon selection
Icon_b0ab = Ícone
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = Tamanho do cache de imagem:
# Title for individual user column
Individual_b776 = Individual
# Error message for invalid zap amount
Invalid_amount_6630 = Quantia inválida
# Error message for invalid key input
Invalid_key_4726 = Chave inválida
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI Inválido
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100 mil
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10 mil
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20 mil
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50 mil
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5 mil
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanhe suas notas e respostas
# Label for language, Appearance settings section
Language_e264 = Idioma
# Title for last note per user column
Last_Note_per_User_17ad = Última Nota por Usuário
# Label for Theme Light, Appearance settings section
Light_7475 = Modo claro
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = Endereço de rede de eletrização (lud16)
# Login page title
Login_9eef = Entrar
# Login button text
Login_now___let_s_do_this_5630 = Entrar agora! Vamos nessa!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = Conteúdo de pessoas que você não segue
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = Mover esta coluna
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Novo no Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (Identidade NIP-05)
# Default username when profile is not available
nostrich_df29 = Nostrich
# Status label for disconnected relay
Not_Connected_6292 = Desconectado
# Link text for note references
note_cad6 = Nota
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere erros e entre em contato conosco quando tiver problemas.
# Filter label for notes only view
Notes_03fb = Notas
# Label for notes-only filter
Notes_60d2 = Notas
# Filter label for notes and replies view
Notes___Replies_1ec2 = Notas e respostas
# Label for notes and replies filter
Notes___Replies_6e3b = Notas e respostas
# Column title for notifications
Notifications_d673 = Notificações
# Title for notifications column
Notifications_ef56 = Notificações
# Relative time for very recent events (less than 3 seconds)
now_2181 = Agora
# Button label to open email client
Open_Email_25e9 = Abrir E-mail
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abra o seu cliente de e-mail padrão para obter ajuda do time Damus
# Label for others settings section
Others_7267 = Outros
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = Cole seu URI NWC aqui...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = Por favor, crie um nome para a aba.
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Por favor, crie um nome para a aba e selecione um ícone.
# Error message for missing deck icon
Please_select_an_icon_655b = Favor selecionar um ícone.
# Button label to post a note
Post_now_8a49 = Postar
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Clique abaixo para copiar seus registros mais recentes para a área de transferência do seu sistema. Em seguida, cole-os no seu E-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
# Label for read-only profile mode
Read_only_82ff = Modo leitura
# Column title for relay management
Relays_9d89 = Canais
# Label for relay list section
Relays_ad5e = Canais
# Column title for reply composition
Reply_3bf1 = Responder
# Hover text for reply button
Reply_to_this_note_f5de = Responder esta nota
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = Responder nota desconhecida
# Fallback template for replying to user
replying_to__user_15ab = Respondendo { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = Respondendo { $user } no tópico de alguém
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = Resposta { $user }de { $note } em { $thread_user }' { $thread }
# Template for replying to user's note
replying_to__user__s__note_ccba = Respondendo { $user }de { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = Respondendo { $user }de { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = Respondendo nota
# Hover text for repost button
Repost_this_note_8e56 = Republicar nota
# Label for reposted notes
Reposted_61c8 = Publicada
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Resetar
# Heading for support section
Running_into_a_bug_1796 = Precisa de ajuda?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = Salvar
# Button label to save profile changes
Save_changes_00db = Salvo
# Column title for search page
Search_c573 = Pesquisar
# Placeholder for search notes input field
Search_notes_42a6 = Pesquisar notas...
# Search in progress message
Searching_for___query_5d18 = Pesquisando por '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
# Description for universe column
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
Settings_7a4f = Configurações
# Label for Show source client, others settings section
Show_source_client_9e31 = Mostrar cliente de origem
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
# Button label to sign out of account
Sign_out_337b = Sair
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = Notas de outra pessoa
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = Mantenha-se atualizado com uma certa hashtag
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = Ficar atualizado com notificações e menções
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Mantenha-se atualizado com as notas e respostas de alguém
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Mantenha-se atualizado com as notificações e menções de alguém
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Mantenha-se atualizado com as notas e respostas de alguém
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantenha-se atualizado com suas notificações e menções
# Step 1 label in support instructions
Step_1_8656 = Passo 1
# Step 2 label in support instructions
Step_2_d08d = Passo 2
# Label for storage settings section
Storage_ed65 = Armazenamento
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Mudar para modo escuro
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = Mudar para modo claro
# Button text to load blurred media
Tap_to_Load_4b05 = Toque para carregar
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nostr terminou :(. Obrigado por testar! Em breve teremos Dave habilitado para Zap
# Label for theme, Appearance settings section
Theme_4aac = Tema:
# Column title for note thread view
Thread_0f20 = Fio
# Link text for thread references
thread_ad1f = Fio
# Option in settings section to show the source client label at the top of the note
Top_6aeb = Topo
# Title for universe column
Universe_e01e = Universo
# Column title for universe feed
Universe_ffaa = Universo
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = Use esta carteira apenas para a conta atual
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username }" em "{ $domain }" será usado para identificação
# Profile username field label
Username_daa7 = Usuário
# Label for view folder button, Storage settings section
View_folder_9742 = Visualizar pasta:
# Column title for wallet management
Wallet_5e50 = Carteira
# Hint for deck name input field
We_recommend_short_names_083e = Recomendamos nomes pequenos
# Profile website field label
Website_7980 = Site
# Placeholder for note input field
Write_a_banger_note_here_bad2 = Escreva uma nota criativa aqui.
# Placeholder text for key input field
Your_key_here_81bd = Sua chave aqui...
# Title for your notes column
Your_Notes_f6db = Suas notas
# Title for your notifications column
Your_Notifications_080d = Suas notificações
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap esta nota
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = Nível de zoom:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] Obteve um resultado { $count } para '{ $query }'
*[other] Obteve { $count } resultados para '{ $query }'
}
+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 = เพิ่ม Deck
# Column title for adding external notifications column
Add_External_Notifications_Column_41ae = เพิ่มคอลัมน์การแจ้งเตือนภายนอก
# Column title for adding hashtag column
Add_Hashtag_Column_ebf4 = เพิ่มคอลัมน์แฮชแท็ก
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = เพิ่มคอลัมน์โน้ตล่าสุด
# Column title for adding notifications column
Add_Notifications_Column_79f8 = เพิ่มคอลัมน์การแจ้งเตือน
# Button label to add a relay
Add_relay_269d = เพิ่มรีเลย์
# Button label to add a wallet
Add_Wallet_d1be = เพิ่มวอลเล็ต
# Title for algorithmic feeds column
Algo_2452 = อัลกอฯ
# Description for algorithmic feeds column
Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลกอริทึมที่ช่วยในการค้นหาโน้ต
# Label for zap amount input field
Amount_70f0 = จำนวน
# Label for appearance settings section
Appearance_4c7f = รูปลักษณ์
# Button to send message to Dave AI assistant
Ask_b7f4 = ถาม
# Placeholder text for Dave AI input field
Ask_dave_anything_33d1 = ถามเดฟได้ทุกเรื่อง...
# Profile banner URL field label
Banner_52ef = ภาพปก
# Beta version label
BETA_8e5d = เบต้า
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = ด้านล่าง
# Broadcast the note to all connected relays
Broadcast_fe43 = เผยแพร่
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = เผยแพร่เฉพาะที่
# Button label to cancel an action
Cancel_ed3b = ยกเลิก
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = ยกเลิก
# Label for clear cache button, Storage settings section
Clear_cache_dccb = ล้างแคช
# Hover text for editable zap amount
Click_to_edit_0414 = คลิกเพื่อแก้ไข
# Column title for note composition
Compose_Note_c094 = เขียนโน้ต
# Label for configure relays, settings section
Configure_relays_d156 = กำหนดค่ารีเลย์
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = ยืนยัน
# Button label to confirm an action
Confirm_f8a6 = ยืนยัน
# Status label for connected relay
Connected_f8cc = เชื่อมต่อแล้ว
# Status label for connecting relay
Connecting_6b7e = กำลังเชื่อมต่อ...
# Title for contact list column
Contact_List_f85a = รายชื่อผู้ติดต่อ
# Column title for contact lists
Contacts_7533 = ผู้ติดต่อ
# Column title for last notes per contact
Contacts__last_notes_3f84 = ผู้ติดต่อ (โน้ตล่าสุด)
# Button label to copy logs
Copy_a688 = คัดลอก
# Button to copy media link to clipboard
Copy_Link_dc7c = คัดลอกลิงก์
# Copy the unique note identifier to clipboard
Copy_Note_ID_6b45 = คัดลอก ID โน้ต
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = คัดลอก Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = คัดลอกข้อความ
# Relative time in days
count_d_b9be = { $count }d
# Relative time in hours
count_h_3ecb = { $count }h
# Relative time in minutes
count_m_b41e = { $count }m
# Relative time in months
count_mo_7aba = { $count }mo
# Relative time in seconds
count_s_aa26 = { $count }s
# Relative time in weeks
count_w_7468 = { $count }w
# Relative time in years
count_y_9408 = { $count }y
# Button to create a new account
Create_Account_6994 = สร้างบัญชี
# Button label to create a new deck
Create_Deck_16b7 = สร้าง Deck
# Column title for custom timelines
Custom_a69e = กำหนดเอง
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = กำหนดจำนวน Zap
# Column title for support page
Damus_Support_27c0 = ฝ่ายสนับสนุน Damus
# Label for Theme Dark, Appearance settings section
Dark_85fe = มืด
# Label for deck name input field
Deck_name_cd32 = ชื่อ Deck
# Label for decks section in side panel
DECKS_1fad = DECKS
# Label for default zap amount input
Default_amount_per_zap_399d = ยอด Zap เริ่มต้น
# Name of the default deck feed
Default_Deck_fcca = Deck หลัก
# Button label to delete a deck
Delete_Deck_bb29 = ลบ Deck
# Tooltip for deleting a column
Delete_this_column_8d5a = ลบคอลัมน์นี้
# Button label to delete a wallet
Delete_Wallet_d1d4 = ลบวอลเล็ต
# Profile display name field label
Display_name_f9d9 = ชื่อที่แสดง
# Domain identification message
domain___will_be_used_for_identification_b67e = { $domain } จะใช้สำหรับการระบุตัวตน
# Column title for editing deck
Edit_Deck_4018 = แก้ไข Deck
# Button label to edit a deck
Edit_Deck_fd93 = แก้ไข Deck
# Button label to edit user profile
Edit_Profile_49e6 = แก้ไขโปรไฟล์
# Column title for profile editing
Edit_Profile_8ad4 = แก้ไขโปรไฟล์
# Placeholder for hashtag input field
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = ใส่แฮชแท็กที่ต้องการ (หากมีหลายอัน ให้คั่นด้วยการเว้นวรรค)
# Placeholder for relay input field
Enter_the_relay_here_1c8b = ใส่รีเลย์ที่นี่
# Hint text to prompt entering the user's public key.
Enter_the_user_s_key__npub__hex__nip05__here_650c = ใส่คีย์ของผู้ใช้ (npub, hex, nip05)...
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
Enter_your_key_0fca = ใส่คีย์ของคุณ
# Instructions for entering Nostr credentials
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 =
โปรดใส่คีย์สาธารณะ (npub), ที่อยู่ Nostr (เช่น { $address }) หรือคีย์ส่วนตัว (nsec)
คุณจำเป็นต้องใส่คีย์ส่วนตัวเพื่อทำการโพสต์, ตอบกลับ และอื่นๆ
# Label for find user button
Find_User_bd12 = ค้นหาผู้ใช้
# Title for hashtags column
Hashtags_f8e0 = แฮชแท็ก
# Option in settings section to hide the source client label in note display
Hide_281d = ซ่อน
# Title for Home column
Home_8c19 = หน้าแรก
# Label for deck icon selection
Icon_b0ab = ไอคอน
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = ขนาดแคชรูปภาพ:
# Title for individual user column
Individual_b776 = ปัจเจคบุคคล
# Error message for invalid zap amount
Invalid_amount_6630 = จำนวนเงินไม่ถูกต้อง
# Error message for invalid key input
Invalid_key_4726 = คีย์ไม่ถูกต้อง
# Error message for invalid Nostr Wallet Connect URI
Invalid_NWC_URI_031b = NWC URI ไม่ถูกต้อง
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
k_100K_686c = 100K
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
k_10K_f7e6 = 10K
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
k_20K_4977 = 20K
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
k_50K_c2dc = 50K
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
# Label for language, Appearance settings section
Language_e264 = ภาษา:
# Title for last note per user column
Last_Note_per_User_17ad = โน้ตล่าสุดของผู้ใช้แต่ละคน
# Label for Theme Light, Appearance settings section
Light_7475 = สว่าง
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = ที่อยู่ Lightning Network (lud16)
# Login page title
Login_9eef = เข้าสู่ระบบ
# Login button text
Login_now___let_s_do_this_5630 = เข้าสู่ระบบเลย — มาเริ่มกัน!
# Text shown on blurred media from unfollowed users
Media_from_someone_you_don_t_follow_5611 = สื่อจากคนที่คุณไม่ได้ติดตาม
# Tooltip for moving a column
Moves_this_column_to_another_position_0d4b = ย้ายคอลัมน์นี้ไปยังตำแหน่งอื่น
# Title for the user's deck
My_Deck_4ac5 = Deck ของฉัน
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = มือใหม่สำหรับ Nostr?
# NIP-05 identity field label
Nostr_address__NIP-05_identity_74a2 = ที่อยู่ Nostr (NIP-05)
# Default username when profile is not available
nostrich_df29 = นกม่วง
# Status label for disconnected relay
Not_Connected_6292 = ไม่ได้เชื่อมต่อ
# Link text for note references
note_cad6 = โน้ต
# Beta product warning message
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck เป็นผลิตภัณฑ์รุ่นเบต้า ซึ่งอาจมีข้อบกพร่องเกิดขึ้นได้ โปรดติดต่อเราเมื่อคุณพบปัญหา
# Filter label for notes only view
Notes_03fb = โน้ต
# Label for notes-only filter
Notes_60d2 = โน้ต
# Filter label for notes and replies view
Notes___Replies_1ec2 = โน้ตและการตอบกลับ
# Label for notes and replies filter
Notes___Replies_6e3b = โน้ตและการตอบกลับ
# Column title for notifications
Notifications_d673 = การแจ้งเตือน
# Title for notifications column
Notifications_ef56 = การแจ้งเตือน
# Relative time for very recent events (less than 3 seconds)
now_2181 = เมื่อสักครู่
# Button label to open email client
Open_Email_25e9 = เปิดอีเมล
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = เปิดโปรแกรมอีเมลของคุณเพื่อรับความช่วยเหลือจากทีม Damus
# Label for others settings section
Others_7267 = อื่นๆ
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = วาง NWC URI ของคุณที่นี่...
# Error message for missing deck name
Please_create_a_name_for_the_deck_38e7 = กรุณาตั้งชื่อ Deck
# Error message for missing deck name and icon
Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาตั้งชื่อ Deck และเลือกไอคอน
# Error message for missing deck icon
Please_select_an_icon_655b = กรุณาเลือกไอคอน
# Button label to post a note
Post_now_8a49 = โพสต์เลย
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ
# Profile picture URL field label
Profile_picture_81ff = รูปโปรไฟล์
# Column title for quote composition
Quote_475c = อ้างอิง
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = อ้างอิงโน้ตที่ไม่รู้จัก
# Label for read-only profile mode
Read_only_82ff = อ่านอย่างเดียว
# Column title for relay management
Relays_9d89 = รีเลย์
# Label for relay list section
Relays_ad5e = รีเลย์
# Column title for reply composition
Reply_3bf1 = ตอบกลับ
# Hover text for reply button
Reply_to_this_note_f5de = ตอบกลับโน้ตนี้
# Error message when reply note cannot be found
Reply_to_unknown_note_4401 = ตอบกลับโน้ตที่ไม่รู้จัก
# Fallback template for replying to user
replying_to__user_15ab = ตอบกลับ { $user }
# Template for replying to user in unknown thread
replying_to__user__in_someone_s_thread_e148 = ตอบกลับ { $user } ในเธรดของผู้อื่น
# Template for replying to note in different user's thread
replying_to__user__s__note__in__thread_user__s__thread_daa8 = ตอบกลับโน้ต { $note } ของ { $user } ในเธรด { $thread } ของ { $thread_user }
# Template for replying to user's note
replying_to__user__s__note_ccba = ตอบกลับโน้ต { $user } ใน { $note }
# Template for replying to root thread
replying_to__user__s__thread_444d = ตอบกลับ { $user } ใน { $thread }
# Fallback text when reply note is not found
replying_to_a_note_e0bc = ตอบกลับโน้ต
# Hover text for repost button
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
# Label for reposted notes
Reposted_61c8 = รีโพสต์แล้ว
# Label for reset zoom level, Appearance settings section
Reset_62d4 = รีเซ็ต
# Heading for support section
Running_into_a_bug_1796 = พบปัญหาในการใช้งานใช่ไหม?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
SATS_45d7 = SATS
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
sats_e5ec = sats
# Button to save default zap amount
Save_6f7c = บันทึก
# Button label to save profile changes
Save_changes_00db = บันทึกการเปลี่ยนแปลง
# Column title for search page
Search_c573 = ค้นหา
# Placeholder for search notes input field
Search_notes_42a6 = ค้นหาโน้ต...
# Search in progress message
Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
# Description for Home column
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
# Description for universe column
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
# Button label to send a zap
Send_1ea4 = ส่ง
# Column title for app settings
Settings_7a4f = การตั้งค่า
# Label for Show source client, others settings section
Show_source_client_9e31 = แสดงไคลเอนต์ต้นทาง
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
# Button label to sign out of account
Sign_out_337b = ออกจากระบบ
# Title for someone else's notes column
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
# Description for hashtags column
Stay_up_to_date_with_a_certain_hashtag_88e3 = ติดตามความเคลื่อนไหวของแฮชแท็ก
# Description for notifications column
Stay_up_to_date_with_notifications_and_mentions_6f4e = ติดตามการแจ้งเตือนและการกล่าวถึง
# Description for someone else's notes column
Stay_up_to_date_with_someone_else_s_notes___replies_464c = ติดตามโน้ตและการตอบกลับของผู้อื่น
# Description for someone else's notifications column
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = ติดตามการแจ้งเตือนและการกล่าวถึงของผู้อื่น
# Description for individual user column
Stay_up_to_date_with_someone_s_notes___replies_aa78 = ติดตามโน้ตและการตอบกลับของผู้อื่น
# Description for your notifications column
Stay_up_to_date_with_your_notifications_and_mentions_e73e = ติดตามการแจ้งเตือนและการกล่าวถึงของคุณ
# Step 1 label in support instructions
Step_1_8656 = ขั้นตอนที่ 1
# Step 2 label in support instructions
Step_2_d08d = ขั้นตอนที่ 2
# Label for storage settings section
Storage_ed65 = พื้นที่จัดเก็บ
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
# Hover text for light mode toggle button
Switch_to_light_mode_72ce = เปลี่ยนเป็นโหมดสว่าง
# Button text to load blurred media
Tap_to_Load_4b05 = แตะเพื่อโหลด
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = ช่วงทดลองใช้ผู้ช่วย AI 'Dave Nostr' ได้สิ้นสุดลงแล้ว :( ขอบคุณที่ร่วมทดสอบ! Dave ที่รองรับการ Zap กำลังจะมาเร็วๆ นี้!
# Label for theme, Appearance settings section
Theme_4aac = ธีม:
# Column title for note thread view
Thread_0f20 = เธรด
# Link text for thread references
thread_ad1f = เธรด
# Option in settings section to show the source client label at the top of the note
Top_6aeb = ด้านบน
# Title for universe column
Universe_e01e = จักรวาล
# Column title for universe feed
Universe_ffaa = จักรวาล
# Checkbox label for using wallet only for current account
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
# Username and domain identification message
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" ที่ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
# Profile username field label
Username_daa7 = ชื่อผู้ใช้
# Label for view folder button, Storage settings section
View_folder_9742 = ดูโฟลเดอร์:
# Column title for wallet management
Wallet_5e50 = วอลเล็ต
# Hint for deck name input field
We_recommend_short_names_083e = เราแนะนำให้ใช้ชื่อสั้นๆ
# Profile website field label
Website_7980 = เว็บไซต์
# Placeholder for note input field
Write_a_banger_note_here_bad2 = เขียนโน้ตปังๆ ที่นี่...
# Placeholder text for key input field
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
# Title for your notes column
Your_Notes_f6db = โน้ตของคุณ
# Title for your notifications column
Your_Notifications_080d = การแจ้งเตือนของคุณ
# Heading for zap (tip) action
Zap_16b4 = Zap
# Hover text for zap button
Zap_this_note_42b2 = Zap โน้ตนี้
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = ระดับการซูม:
# Pluralized strings
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[one] ผลการค้นหา '{ $query }': พบ { $count } รายการ
*[other] ผลการค้นหา '{ $query }': พบ { $count } รายการ
}
+40 -62
View File
@@ -7,8 +7,6 @@
# Profile about/bio field label
About_00c0 = 关于
# Display name for account management
Accounts_e233 = 帐户
# Column title for account management
Accounts_f018 = 帐户
# Button label to add a relay
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = 添加钱包以继续
Add_account_1cfc = 添加帐户
# Column title for adding new account
Add_Account_d06c = 添加帐户
# Display name for adding account
Add_Account_d715 = 添加帐户
# Column title for adding algorithm column
Add_Algo_Column_0d75 = 添加算法列
# Display name for adding column
Add_Column_c6ff = 添加列
# Column title for adding new column
Add_Column_c764 = 添加列
# Display name for adding deck
Add_Deck_6e5f = 添加仪表板
# Column title for adding new deck
Add_Deck_fabf = 添加仪表板
# Column title for adding external notifications column
@@ -53,6 +45,8 @@ Algo_2452 = 算法
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用于帮助发现笔记的算法源
# Label for zap amount input field
Amount_70f0 = 金额
# Label for appearance settings section
Appearance_4c7f = 外观
# Button to send message to Dave AI assistant
Ask_b7f4 = 询问
# Placeholder text for Dave AI input field
@@ -61,18 +55,26 @@ Ask_dave_anything_33d1 = 向 Dave 提问任何问题…
Banner_52ef = 横幅
# Beta version label
BETA_8e5d = BETA
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = 底部
# Broadcast the note to all connected relays
Broadcast_fe43 = 广播
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = 仅广播至本地中继
# Button label to cancel an action
Cancel_ed3b = 取消
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = 取消
# Label for clear cache button, Storage settings section
Clear_cache_dccb = 清除缓存
# Hover text for editable zap amount
Click_to_edit_0414 = 点击以编辑
# Display name for note composition
Compose_Note_ad11 = 撰写笔记
# Column title for note composition
Compose_Note_c094 = 撰写笔记
# Label for configure relays, settings section
Configure_relays_d156 = 配置中继器
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 确认
# Button label to confirm an action
Confirm_f8a6 = 确认
# Status label for connected relay
@@ -83,8 +85,6 @@ Connecting_6b7e = 正在连接...
Contact_List_f85a = 联系人列表
# Column title for contact lists
Contacts_7533 = 联系人
# Timeline kind label for contact lists
Contacts_8b98 = 联系人
# Column title for last notes per contact
Contacts__last_notes_3f84 = 联系人(最新笔记)
# Button label to copy logs
@@ -119,14 +119,12 @@ Create_Account_6994 = 创建帐户
Create_Deck_16b7 = 创建仪表板
# Column title for custom timelines
Custom_a69e = 自定义
# Display name for custom timelines
Custom_cb4f = 自定义
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = 自定义打闪金额
# Display name for zap customization
Customize_Zap_Amount_ed29 = 自定义打闪金额
# Column title for support page
Damus_Support_27c0 = 达摩支持
# Label for Theme Dark, Appearance settings section
Dark_85fe = 暗色
# Label for deck name input field
Deck_name_cd32 = 仪表板名称
# Label for decks section in side panel
@@ -147,14 +145,10 @@ Display_name_f9d9 = 显示名称
domain___will_be_used_for_identification_b67e = "{ $domain }" 将用于身份识别
# Column title for editing deck
Edit_Deck_4018 = 编辑仪表板
# Display name for editing deck
Edit_Deck_c9ba = 编辑仪表板
# Button label to edit a deck
Edit_Deck_fd93 = 编辑仪表板
# Button label to edit user profile
Edit_Profile_49e6 = 编辑个人档案
# Display name for profile editing
Edit_Profile_6699 = 编辑个人档案
# Column title for profile editing
Edit_Profile_8ad4 = 编辑个人档案
# Placeholder for hashtag input field
@@ -169,18 +163,16 @@ Enter_your_key_0fca = 请输入你的密钥
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥(npub)、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
# Label for find user button
Find_User_bd12 = 查找用户
# Timeline kind label for hashtag feeds
Hashtag_a0ab = 标签
# Display name for hashtag feeds
Hashtags_617e = 标签
# Title for hashtags column
Hashtags_f8e0 = 标签
# Display name for home feed
Home_3efc = 主页
# Option in settings section to hide the source client label in note display
Hide_281d = 隐藏
# Title for Home column
Home_8c19 = 主页
# Label for deck icon selection
Icon_b0ab = 图标
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 图像缓存大小:
# Title for individual user column
Individual_b776 = 个人
# Error message for invalid zap amount
@@ -201,12 +193,12 @@ k_50K_c2dc = 5万
k_5K_f7e6 = 5千
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 随时查看你的笔记和回复
# Label for language, Appearance settings section
Language_e264 = 语言:
# Title for last note per user column
Last_Note_per_User_17ad = 每个用户的最新笔记
# Timeline kind label for last notes per pubkey
Last_Notes_aefe = 最新笔记
# Display name for last notes per contact
Last_Per_Pubkey__Contact_33ce = 每个公钥(联系人)的最新笔记
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 闪电网络地址(lud16
# Login page title
@@ -239,10 +231,6 @@ Notes_60d2 = 笔记
Notes___Replies_1ec2 = 笔记和回复
# Label for notes and replies filter
Notes___Replies_6e3b = 笔记和回复
# Timeline kind label for notifications
Notifications_6228 = 通知
# Display name for notifications
Notifications_8029 = 通知
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
@@ -253,6 +241,8 @@ now_2181 = 刚刚
Open_Email_25e9 = 打开电子邮箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打开你的默认电子邮件客户端以获得达摩团队的帮助
# Label for others settings section
Others_7267 = 其它
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = 在此粘贴你的 NWC URI...
# Error message for missing deck name
@@ -265,30 +255,20 @@ Please_select_an_icon_655b = 请选择一个图标。
Post_now_8a49 = 立即发布
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 请按下面的按钮将你最近的日志复制到系统剪贴板,然后将其粘贴到你的电子邮件。
# Display name for user profiles
Profile_2478 = 个人资料
# Timeline kind label for user profiles
Profile_9027 = 个人资料
# Profile picture URL field label
Profile_picture_81ff = 头像图片
# Column title for quote composition
Quote_475c = 引用
# Display name for quote composition
Quote_a38e = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 引用未知笔记
# Label for read-only profile mode
Read_only_82ff = 只读
# Display name for relay management
Relays_7335 = 中继器
# Column title for relay management
Relays_9d89 = 中继器
# Label for relay list section
Relays_ad5e = 中继器
# Column title for reply composition
Reply_3bf1 = 回复
# Display name for reply composition
Reply_b40f = 回复
# Hover text for reply button
Reply_to_this_note_f5de = 回复此笔记
# Error message when reply note cannot be found
@@ -309,6 +289,8 @@ replying_to_a_note_e0bc = 正在回复笔记
Repost_this_note_8e56 = 转发此笔记
# Label for reposted notes
Reposted_61c8 = 已转发
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了吗?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -319,12 +301,6 @@ sats_e5ec = 聪
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 保存变更
# Display name for search results
Search_0aa0 = 搜索
# Display name for search page
Search_4503 = 搜索
# Timeline kind label for search results
Search_a0b8 = 搜索
# Column title for search page
Search_c573 = 搜索
# Placeholder for search notes input field
@@ -337,6 +313,10 @@ See_notes_from_your_contacts_ac16 = 查看来自你的联系人的笔记
See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 发送
# Column title for app settings
Settings_7a4f = 设置
# Label for Show source client, others settings section
Show_source_client_9e31 = 显示来源客户端
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
# Button label to sign out of account
@@ -363,12 +343,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 获取你的通知
Step_1_8656 = 第一步
# Step 2 label in support instructions
Step_2_d08d = 第二步
# Label for storage settings section
Storage_ed65 = 存储
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 订阅某人的笔记
# Display name for support page
Support_a4b4 = 获取帮助
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切换到暗色模式
# Hover text for light mode toggle button
@@ -377,18 +357,14 @@ Switch_to_light_mode_72ce = 切换到亮色模式
Tap_to_Load_4b05 = 点击加载
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手试用期已经结束 :(。感谢测试!可打闪付款的 Dave 即将来临!
# Label for theme, Appearance settings section
Theme_4aac = 主题:
# Column title for note thread view
Thread_0f20 = 帖子
# Display name for thread view
Thread_9957 = 帖子
# Link text for thread references
thread_ad1f = 帖子
# Generic timeline kind label
Timeline_b0fc = 时间线
# Timeline kind label for universe feed
Universe_0a3e = 宇宙
# Display name for universe feed
Universe_d47e = 宇宙
# Option in settings section to show the source client label at the top of the note
Top_6aeb = 顶部
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
@@ -399,10 +375,10 @@ Use_this_wallet_for_the_current_account_only_61dc = 此钱包仅限用于当前
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 于 "{ $domain }" 将被用于身份识别
# Profile username field label
Username_daa7 = 用户名
# Label for view folder button, Storage settings section
View_folder_9742 = 查看文件夹:
# Column title for wallet management
Wallet_5e50 = 钱包
# Display name for wallet management
Wallet_cdca = 钱包
# Hint for deck name input field
We_recommend_short_names_083e = 我们推荐使用简短的名称
# Profile website field label
@@ -419,6 +395,8 @@ Your_Notifications_080d = 你的通知
Zap_16b4 = 打闪
# Hover text for zap button
Zap_this_note_42b2 = 打闪此笔记
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 缩放大小:
# Pluralized strings
+40 -62
View File
@@ -7,8 +7,6 @@
# Profile about/bio field label
About_00c0 = 關於
# Display name for account management
Accounts_e233 = 帳戶
# Column title for account management
Accounts_f018 = 帳戶
# Button label to add a relay
@@ -23,16 +21,10 @@ Add_a_wallet_to_continue_d170 = 添加錢包以繼續
Add_account_1cfc = 新增帳戶
# Column title for adding new account
Add_Account_d06c = 新增帳戶
# Display name for adding account
Add_Account_d715 = 新增帳戶
# Column title for adding algorithm column
Add_Algo_Column_0d75 = 添加算法列
# Display name for adding column
Add_Column_c6ff = 添加列
# Column title for adding new column
Add_Column_c764 = 添加列
# Display name for adding deck
Add_Deck_6e5f = 添加儀表板
# Column title for adding new deck
Add_Deck_fabf = 添加儀表板
# Column title for adding external notifications column
@@ -53,6 +45,8 @@ Algo_2452 = 算法
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 用於幫助發現筆記的算法源
# Label for zap amount input field
Amount_70f0 = 金額
# Label for appearance settings section
Appearance_4c7f = 外觀
# Button to send message to Dave AI assistant
Ask_b7f4 = 詢問
# Placeholder text for Dave AI input field
@@ -61,18 +55,26 @@ Ask_dave_anything_33d1 = 向 Dave 提問任何問題...
Banner_52ef = 橫幅
# Beta version label
BETA_8e5d = 測試版
# Option in settings section to show the source client label at the bottom of the note
Bottom_33c8 = 底部
# Broadcast the note to all connected relays
Broadcast_fe43 = 廣播
# Broadcast the note only to local network relays
Broadcast_Local_7e50 = 僅廣播至本地中繼
# Button label to cancel an action
Cancel_ed3b = 取消
# Label for cancel clear cache, Storage settings section
Cancel_fd8b = 取消
# Label for clear cache button, Storage settings section
Clear_cache_dccb = 清除快取
# Hover text for editable zap amount
Click_to_edit_0414 = 點擊編輯
# Display name for note composition
Compose_Note_ad11 = 撰寫筆記
# Column title for note composition
Compose_Note_c094 = 撰寫筆記
# Label for configure relays, settings section
Configure_relays_d156 = 配置中繼器
# Label for confirm clear cache, Storage settings section
Confirm_9d9d = 確認
# Button label to confirm an action
Confirm_f8a6 = 確認
# Status label for connected relay
@@ -83,8 +85,6 @@ Connecting_6b7e = 正在連接 ...
Contact_List_f85a = 聯絡人列表
# Column title for contact lists
Contacts_7533 = 聯絡人
# Timeline kind label for contact lists
Contacts_8b98 = 聯絡人
# Column title for last notes per contact
Contacts__last_notes_3f84 = 聯絡人(最新筆記)
# Button label to copy logs
@@ -119,14 +119,12 @@ Create_Account_6994 = 創建帳戶
Create_Deck_16b7 = 創建儀表板
# Column title for custom timelines
Custom_a69e = 自訂
# Display name for custom timelines
Custom_cb4f = 自訂
# Column title for zap amount customization
Customize_Zap_Amount_cfc4 = 自訂打閃金額
# Display name for zap customization
Customize_Zap_Amount_ed29 = 自訂打閃金額
# Column title for support page
Damus_Support_27c0 = 達摩支持
# Label for Theme Dark, Appearance settings section
Dark_85fe = 暗色
# Label for deck name input field
Deck_name_cd32 = 儀表板名稱
# Label for decks section in side panel
@@ -147,14 +145,10 @@ Display_name_f9d9 = 顯示名稱
domain___will_be_used_for_identification_b67e = "{ $domain }" 將用於身份識別
# Column title for editing deck
Edit_Deck_4018 = 編輯儀表板
# Display name for editing deck
Edit_Deck_c9ba = 編輯儀表板
# Button label to edit a deck
Edit_Deck_fd93 = 編輯儀表板
# Button label to edit user profile
Edit_Profile_49e6 = 編輯個人檔案
# Display name for profile editing
Edit_Profile_6699 = 編輯個人檔案
# Column title for profile editing
Edit_Profile_8ad4 = 編輯個人檔案
# Placeholder for hashtag input field
@@ -169,18 +163,16 @@ Enter_your_key_0fca = 請輸入你的密鑰
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰(npub)、nostr 地址(如 { $address })、或私鑰(nsec)。你必須輸入你的私鑰才能發貼、回覆等等。
# Label for find user button
Find_User_bd12 = 查找用戶
# Timeline kind label for hashtag feeds
Hashtag_a0ab = 標籤
# Display name for hashtag feeds
Hashtags_617e = 標籤
# Title for hashtags column
Hashtags_f8e0 = 標籤
# Display name for home feed
Home_3efc = 主頁
# Option in settings section to hide the source client label in note display
Hide_281d = 隱藏
# Title for Home column
Home_8c19 = 主頁
# Label for deck icon selection
Icon_b0ab = 圖標
# Label for Image cache size, Storage settings section
Image_cache_size_3004 = 圖像快取大小:
# Title for individual user column
Individual_b776 = 個人
# Error message for invalid zap amount
@@ -201,12 +193,12 @@ k_50K_c2dc = 5萬
k_5K_f7e6 = 5千
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = 隨時查看你的筆記和回覆
# Label for language, Appearance settings section
Language_e264 = 語言:
# Title for last note per user column
Last_Note_per_User_17ad = 每個用戶的最新筆記
# Timeline kind label for last notes per pubkey
Last_Notes_aefe = 最新筆記
# Display name for last notes per contact
Last_Per_Pubkey__Contact_33ce = 每個公鑰(聯繫人)的最新筆記
# Label for Theme Light, Appearance settings section
Light_7475 = 亮色
# Bitcoin Lightning network address field label
Lightning_network_address__lud16_ea51 = 閃電網絡地址(lud16
# Login page title
@@ -239,10 +231,6 @@ Notes_60d2 = 筆記
Notes___Replies_1ec2 = 筆記和回覆
# Label for notes and replies filter
Notes___Replies_6e3b = 筆記和回覆
# Timeline kind label for notifications
Notifications_6228 = 通知
# Display name for notifications
Notifications_8029 = 通知
# Column title for notifications
Notifications_d673 = 通知
# Title for notifications column
@@ -253,6 +241,8 @@ now_2181 = 剛剛
Open_Email_25e9 = 打開電子郵箱
# Instruction to open email client
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = 打開你的默認電子郵件客戶端以獲得達摩團隊的幫助
# Label for others settings section
Others_7267 = 其他
# Placeholder text for NWC URI input
Paste_your_NWC_URI_here_b471 = 在此貼上你的 NWC URI...
# Error message for missing deck name
@@ -265,30 +255,20 @@ Please_select_an_icon_655b = 請選擇一個圖標。
Post_now_8a49 = 立即發布
# Instruction for copying logs
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = 請按下面的按鈕將你最近的日誌複製到剪貼板,然後將其粘貼到你的電子郵件。
# Display name for user profiles
Profile_2478 = 個人檔案
# Timeline kind label for user profiles
Profile_9027 = 個人檔案
# Profile picture URL field label
Profile_picture_81ff = 頭像圖片
# Column title for quote composition
Quote_475c = 引用
# Display name for quote composition
Quote_a38e = 引用
# Error message when quote note cannot be found
Quote_of_unknown_note_e4f0 = 引用未知筆記
# Label for read-only profile mode
Read_only_82ff = 只讀
# Display name for relay management
Relays_7335 = 中繼器
# Column title for relay management
Relays_9d89 = 中繼器
# Label for relay list section
Relays_ad5e = 中繼器
# Column title for reply composition
Reply_3bf1 = 回覆
# Display name for reply composition
Reply_b40f = 回覆
# Hover text for reply button
Reply_to_this_note_f5de = 回覆此筆記
# Error message when reply note cannot be found
@@ -309,6 +289,8 @@ replying_to_a_note_e0bc = 正在回覆筆記
Repost_this_note_8e56 = 轉發此筆記
# Label for reposted notes
Reposted_61c8 = 已轉發
# Label for reset zoom level, Appearance settings section
Reset_62d4 = 重置
# Heading for support section
Running_into_a_bug_1796 = 遇到故障了嗎?
# Label for satoshis (Bitcoin unit) for custom zap amount input field
@@ -319,12 +301,6 @@ sats_e5ec = 聰
Save_6f7c = 保存
# Button label to save profile changes
Save_changes_00db = 保存變更
# Display name for search results
Search_0aa0 = 搜索
# Display name for search page
Search_4503 = 搜索
# Timeline kind label for search results
Search_a0b8 = 搜索
# Column title for search page
Search_c573 = 搜索
# Placeholder for search notes input field
@@ -337,6 +313,10 @@ See_notes_from_your_contacts_ac16 = 查看來自你的聯繫人的筆記
See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
# Button label to send a zap
Send_1ea4 = 發送
# Column title for app settings
Settings_7a4f = 設置
# Label for Show source client, others settings section
Show_source_client_9e31 = 顯示來源客戶端
# Description for last note per user column
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
# Button label to sign out of account
@@ -363,12 +343,12 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = 獲取你的通知
Step_1_8656 = 第一步
# Step 2 label in support instructions
Step_2_d08d = 第二步
# Label for storage settings section
Storage_ed65 = 儲存
# Column title for subscribing to external user
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = 訂閱某人的筆記
# Display name for support page
Support_a4b4 = 獲取幫助
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = 切換到暗色模式
# Hover text for light mode toggle button
@@ -377,18 +357,14 @@ Switch_to_light_mode_72ce = 切換到亮色模式
Tap_to_Load_4b05 = 點擊加載
# Message shown when Dave trial period has ended
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI 助手試用期已經結束 :(。感謝測試!可打閃付款的 Dave 即將來臨!
# Label for theme, Appearance settings section
Theme_4aac = 主題:
# Column title for note thread view
Thread_0f20 = 串文
# Display name for thread view
Thread_9957 = 串文
# Link text for thread references
thread_ad1f = 串文
# Generic timeline kind label
Timeline_b0fc = 時間線
# Timeline kind label for universe feed
Universe_0a3e = 宇宙
# Display name for universe feed
Universe_d47e = 宇宙
# Option in settings section to show the source client label at the top of the note
Top_6aeb = 頂部
# Title for universe column
Universe_e01e = 宇宙
# Column title for universe feed
@@ -399,10 +375,10 @@ Use_this_wallet_for_the_current_account_only_61dc = 此錢包僅限用於當前
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" 於 "{ $domain }" 將被用於身份識別
# Profile username field label
Username_daa7 = 用戶名
# Label for view folder button, Storage settings section
View_folder_9742 = 查看文件夾:
# Column title for wallet management
Wallet_5e50 = 錢包
# Display name for wallet management
Wallet_cdca = 錢包
# Hint for deck name input field
We_recommend_short_names_083e = 我們推薦使用簡短的名稱
# Profile website field label
@@ -419,6 +395,8 @@ Your_Notifications_080d = 你的通知
Zap_16b4 = 打閃
# Hover text for zap button
Zap_this_note_42b2 = 打閃此筆記
# Label for zoom level, Appearance settings section
Zoom_Level_29a8 = 縮放大小:
# Pluralized strings
+4
View File
@@ -9,11 +9,13 @@ nostrdb = { workspace = true }
jni = { workspace = true }
url = { workspace = true }
strum = { workspace = true }
blurhash = { workspace = true }
strum_macros = { workspace = true }
dirs = { workspace = true }
enostr = { workspace = true }
nostr = { workspace = true }
egui = { workspace = true }
egui_extras = { workspace = true }
eframe = { workspace = true }
image = { workspace = true }
base32 = { workspace = true }
@@ -43,8 +45,10 @@ fluent = { workspace = true }
fluent-resmgr = { workspace = true }
fluent-langneg = { workspace = true }
unic-langid = { workspace = true }
sys-locale = { workspace = true }
once_cell = { workspace = true }
md5 = { workspace = true }
bitflags = { workspace = true }
regex = "1"
[dev-dependencies]
+4
View File
@@ -207,6 +207,10 @@ impl Accounts {
self.cache.selected_mut()
}
pub fn get_selected_wallet(&self) -> Option<&ZapWallet> {
self.cache.selected().wallet.as_ref()
}
pub fn get_selected_wallet_mut(&mut self) -> Option<&mut ZapWallet> {
self.cache.selected_mut().wallet.as_mut()
}
+21 -1
View File
@@ -15,6 +15,7 @@ pub enum ContactState {
Received {
contacts: HashSet<Pubkey>,
note_key: NoteKey,
timestamp: u64,
},
}
@@ -57,6 +58,7 @@ impl Contacts {
ContactState::Received {
contacts,
note_key: _,
timestamp: _,
} => {
if contacts.contains(other_pubkey) {
IsFollowing::Yes
@@ -82,6 +84,18 @@ impl Contacts {
}
};
if let ContactState::Received {
contacts: _,
note_key: _,
timestamp,
} = self.get_state()
{
if *timestamp > note.created_at() {
// the current contact list is more up to date than the one we just received. ignore it.
return;
}
}
update_state(&mut self.state, &note, *key);
}
@@ -96,11 +110,17 @@ fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
*state = ContactState::Received {
contacts: get_contacts_owned(note),
note_key: key,
timestamp: note.created_at(),
};
}
ContactState::Received { contacts, note_key } => {
ContactState::Received {
contacts,
note_key,
timestamp,
} => {
update_contacts(contacts, note);
*note_key = key;
*timestamp = note.created_at();
}
};
}
+1 -2
View File
@@ -1,12 +1,11 @@
use std::collections::BTreeSet;
use crate::{AccountData, RelaySpec};
use enostr::{Keypair, Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
use tracing::{debug, error, info};
use url::Url;
use crate::{AccountData, RelaySpec};
#[derive(Clone)]
pub(crate) struct AccountRelayData {
pub filter: Filter,
+79 -21
View File
@@ -1,13 +1,14 @@
use crate::account::FALLBACK_PUBKEY;
use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, ZoomHandler};
use crate::persist::{AppSizeHandler, SettingsHandler};
use crate::wallet::GlobalWallet;
use crate::zaps::Zaps;
use crate::Error;
use crate::JobPool;
use crate::NotedeckOptions;
use crate::{
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
UnknownIds,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
};
use egui::Margin;
use egui::ThemePreference;
@@ -19,6 +20,7 @@ use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub enum AppAction {
Note(NoteAction),
@@ -40,9 +42,8 @@ pub struct Notedeck {
global_wallet: GlobalWallet,
path: DataPath,
args: Args,
theme: ThemeHandler,
settings: SettingsHandler,
app: Option<Rc<RefCell<dyn App>>>,
zoom: ZoomHandler,
app_size: AppSizeHandler,
unrecognized_args: BTreeSet<String>,
clipboard: Clipboard,
@@ -99,10 +100,18 @@ impl eframe::App for Notedeck {
render_notedeck(self, ctx);
self.zoom.try_save_zoom_factor(ctx);
self.settings.update_batch(|settings| {
settings.zoom_factor = ctx.zoom_factor();
settings.locale = self.i18n.get_current_locale().to_string();
settings.theme = if ctx.style().visuals.dark_mode {
ThemePreference::Dark
} else {
ThemePreference::Light
};
});
self.app_size.try_save_app_size(ctx);
if self.args.relay_debug {
if self.args.options.contains(NotedeckOptions::RelayDebug) {
if self.pool.debug.is_none() {
self.pool.use_debug();
}
@@ -159,10 +168,11 @@ impl Notedeck {
1024usize * 1024usize * 1024usize * 1024usize
};
let theme = ThemeHandler::new(&path);
let settings = SettingsHandler::new(&path).load();
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
let keystore = if parsed_args.use_keystore {
let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
let keys_path = path.path(DataPathType::Keys);
let selected_key_path = path.path(DataPathType::SelectedKey);
Some(AccountStorage::new(
@@ -213,12 +223,8 @@ impl Notedeck {
let img_cache = Images::new(img_cache_dir);
let note_cache = NoteCache::default();
let zoom = ZoomHandler::new(&path);
let app_size = AppSizeHandler::new(&path);
if let Some(z) = zoom.get_zoom_factor() {
ctx.set_zoom_factor(z);
}
let app_size = AppSizeHandler::new(&path);
// migrate
if let Err(e) = img_cache.migrate_v0() {
@@ -231,15 +237,22 @@ impl Notedeck {
// Initialize localization
let mut i18n = Localization::new();
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
settings.locale().parse();
if setting_locale.is_ok() {
if let Err(err) = i18n.set_locale(setting_locale.unwrap()) {
error!("{err}");
}
}
if let Some(locale) = &parsed_args.locale {
if let Err(err) = i18n.set_locale(locale.to_owned()) {
error!("{err}");
}
}
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
Self {
ndb,
img_cache,
@@ -250,9 +263,8 @@ impl Notedeck {
global_wallet,
path: path.clone(),
args: parsed_args,
theme,
settings,
app: None,
zoom,
app_size,
unrecognized_args,
frame_history: FrameHistory::default(),
@@ -263,6 +275,44 @@ impl Notedeck {
}
}
/// Setup egui context
pub fn setup(&self, ctx: &egui::Context) {
// Initialize global i18n context
//crate::i18n::init_global_i18n(i18n.clone());
crate::setup::setup_egui_context(
ctx,
self.args.options,
self.theme(),
self.note_body_font_size(),
self.zoom_factor(),
);
}
/// ensure we recognized all the arguments
pub fn check_args(&self, other_app_args: &BTreeSet<String>) -> Result<(), Error> {
let completely_unrecognized: Vec<String> = self
.unrecognized_args()
.intersection(other_app_args)
.cloned()
.collect();
if !completely_unrecognized.is_empty() {
let err = format!("Unrecognized arguments: {:?}", completely_unrecognized);
tracing::error!("{}", &err);
return Err(Error::Generic(err));
}
Ok(())
}
#[inline]
pub fn options(&self) -> NotedeckOptions {
self.args.options
}
pub fn has_option(&self, option: NotedeckOptions) -> bool {
self.options().contains(option)
}
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
self.set_app(app);
self
@@ -279,7 +329,7 @@ impl Notedeck {
global_wallet: &mut self.global_wallet,
path: &self.path,
args: &self.args,
theme: &mut self.theme,
settings: &mut self.settings,
clipboard: &mut self.clipboard,
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
@@ -297,7 +347,15 @@ impl Notedeck {
}
pub fn theme(&self) -> ThemePreference {
self.theme.load()
self.settings.theme()
}
pub fn note_body_font_size(&self) -> f32 {
self.settings.note_body_font_size()
}
pub fn zoom_factor(&self) -> f32 {
self.settings.zoom_factor()
}
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
+14 -26
View File
@@ -1,23 +1,15 @@
use std::collections::BTreeSet;
use crate::NotedeckOptions;
use enostr::{Keypair, Pubkey, SecretKey};
use tracing::error;
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub struct Args {
pub relays: Vec<String>,
pub is_mobile: Option<bool>,
pub locale: Option<LanguageIdentifier>,
pub show_note_client: bool,
pub keys: Vec<Keypair>,
pub light: bool,
pub debug: bool,
pub relay_debug: bool,
/// Enable when running tests so we don't panic on app startup
pub tests: bool,
pub use_keystore: bool,
pub options: NotedeckOptions,
pub dbpath: Option<String>,
pub datapath: Option<String>,
}
@@ -28,14 +20,8 @@ impl Args {
let mut unrecognized_args = BTreeSet::new();
let mut res = Args {
relays: vec![],
is_mobile: None,
keys: vec![],
light: false,
show_note_client: false,
debug: false,
relay_debug: false,
tests: false,
use_keystore: true,
options: NotedeckOptions::default(),
dbpath: None,
datapath: None,
locale: None,
@@ -47,9 +33,9 @@ impl Args {
let arg = &args[i];
if arg == "--mobile" {
res.is_mobile = Some(true);
res.options.set(NotedeckOptions::Mobile, true);
} else if arg == "--light" {
res.light = true;
res.options.set(NotedeckOptions::LightTheme, true);
} else if arg == "--locale" {
i += 1;
let Some(locale) = args.get(i) else {
@@ -68,11 +54,11 @@ impl Args {
}
}
} else if arg == "--dark" {
res.light = false;
res.options.set(NotedeckOptions::LightTheme, false);
} else if arg == "--debug" {
res.debug = true;
res.options.set(NotedeckOptions::Debug, true);
} else if arg == "--testrunner" {
res.tests = true;
res.options.set(NotedeckOptions::Tests, true);
} else if arg == "--pub" || arg == "--npub" {
i += 1;
let pubstr = if let Some(next_arg) = args.get(i) {
@@ -135,11 +121,13 @@ impl Args {
};
res.relays.push(relay.clone());
} else if arg == "--no-keystore" {
res.use_keystore = false;
res.options.set(NotedeckOptions::UseKeystore, true);
} else if arg == "--relay-debug" {
res.relay_debug = true;
} else if arg == "--show-note-client" {
res.show_note_client = true;
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--show-client" {
res.options.set(NotedeckOptions::ShowClient, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
} else {
unrecognized_args.insert(arg.clone());
}
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
UnknownIds,
};
use egui_winit::clipboard::Clipboard;
@@ -20,7 +20,7 @@ pub struct AppContext<'a> {
pub global_wallet: &'a mut GlobalWallet,
pub path: &'a DataPath,
pub args: &'a Args,
pub theme: &'a mut ThemeHandler,
pub settings: &'a mut SettingsHandler,
pub clipboard: &'a mut Clipboard,
pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory,
+152
View File
@@ -1,4 +1,9 @@
use crate::{ui, NotedeckTextStyle};
use egui::FontData;
use egui::FontDefinitions;
use egui::FontTweak;
use std::collections::BTreeMap;
use std::sync::Arc;
pub enum NamedFontFamily {
Medium,
@@ -31,6 +36,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
NotedeckTextStyle::Button => 13.0,
NotedeckTextStyle::Small => 12.0,
NotedeckTextStyle::Tiny => 10.0,
NotedeckTextStyle::NoteBody => 16.0,
}
}
@@ -46,6 +52,7 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
NotedeckTextStyle::Button => 13.0,
NotedeckTextStyle::Small => 12.0,
NotedeckTextStyle::Tiny => 10.0,
NotedeckTextStyle::NoteBody => 13.0,
}
}
@@ -56,3 +63,148 @@ pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32
desktop_font_size(text_style)
}
}
// Use gossip's approach to font loading. This includes japanese fonts
// for rending stuff from japanese users.
pub fn setup_fonts(ctx: &egui::Context) {
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
let mut families = BTreeMap::new();
font_data.insert(
"Onest".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
))),
);
font_data.insert(
"OnestMedium".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
))),
);
font_data.insert(
"DejaVuSans".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
))),
);
font_data.insert(
"OnestBold".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
))),
);
/*
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
font_data.insert(
"DejaVuSans".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
);
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
*/
font_data.insert(
"Inconsolata".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/Inconsolata-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.22, // This font is smaller than DejaVuSans
y_offset_factor: -0.18, // and too low
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
font_data.insert(
"NotoSansCJK".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
))),
);
font_data.insert(
"NotoSansThai".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansThai-Regular.ttf"
))),
);
// Some good looking emojis. Use as first priority:
font_data.insert(
"NotoEmoji".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoEmoji-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.1, // make them a touch larger
y_offset_factor: 0.0,
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
let base_fonts = vec![
"DejaVuSans".to_owned(),
"NotoEmoji".to_owned(),
"NotoSansCJK".to_owned(),
"NotoSansThai".to_owned(),
];
let mut proportional = vec!["Onest".to_owned()];
proportional.extend(base_fonts.clone());
let mut medium = vec!["OnestMedium".to_owned()];
medium.extend(base_fonts.clone());
let mut mono = vec!["Inconsolata".to_owned()];
mono.extend(base_fonts.clone());
let mut bold = vec!["OnestBold".to_owned()];
bold.extend(base_fonts.clone());
let emoji = vec!["NotoEmoji".to_owned()];
families.insert(egui::FontFamily::Proportional, proportional);
families.insert(egui::FontFamily::Monospace, mono);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
medium,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
bold,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
emoji,
);
tracing::debug!("fonts: {:?}", families);
let defs = FontDefinitions {
font_data,
families,
};
ctx.set_fonts(defs);
}
+292 -24
View File
@@ -3,15 +3,31 @@ use fluent::{FluentArgs, FluentBundle, FluentResource};
use fluent_langneg::negotiate_languages;
use std::borrow::Cow;
use std::collections::HashMap;
use sys_locale;
use unic_langid::{langid, LanguageIdentifier};
const EN_XA: LanguageIdentifier = langid!("en-XA");
const EN_US: LanguageIdentifier = langid!("en-US");
const EN_XA: LanguageIdentifier = langid!("en-XA");
const DE: LanguageIdentifier = langid!("de");
const FR: LanguageIdentifier = langid!("FR");
const ZH_CN: LanguageIdentifier = langid!("ZH_CN");
const ZH_TW: LanguageIdentifier = langid!("ZH_TW");
const NUM_FTLS: usize = 6;
const ES_419: LanguageIdentifier = langid!("es-419");
const ES_ES: LanguageIdentifier = langid!("es-ES");
const FR: LanguageIdentifier = langid!("fr");
const PT_BR: LanguageIdentifier = langid!("pt-BR");
const TH: LanguageIdentifier = langid!("th");
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
const NUM_FTLS: usize = 10;
const EN_US_NATIVE_NAME: &str = "English (US)";
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
const DE_NATIVE_NAME: &str = "Deutsch";
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
const ES_ES_NATIVE_NAME: &str = "Español (España)";
const FR_NATIVE_NAME: &str = "Français";
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
const TH_NATIVE_NAME: &str = "ภาษาไทย";
const ZH_CN_NATIVE_NAME: &str = "简体中文";
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
struct StaticBundle {
identifier: LanguageIdentifier,
@@ -31,10 +47,26 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
identifier: DE,
ftl: include_str!("../../../../assets/translations/de/main.ftl"),
},
StaticBundle {
identifier: ES_419,
ftl: include_str!("../../../../assets/translations/es-419/main.ftl"),
},
StaticBundle {
identifier: ES_ES,
ftl: include_str!("../../../../assets/translations/es-ES/main.ftl"),
},
StaticBundle {
identifier: FR,
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
},
StaticBundle {
identifier: PT_BR,
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
},
StaticBundle {
identifier: TH,
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
},
StaticBundle {
identifier: ZH_CN,
ftl: include_str!("../../../../assets/translations/zh-CN/main.ftl"),
@@ -55,6 +87,8 @@ pub struct Localization {
available_locales: Vec<LanguageIdentifier>,
/// Fallback locale
fallback_locale: LanguageIdentifier,
/// Native names for locales
locale_native_names: HashMap<LanguageIdentifier, String>,
/// Cached string results per locale (only for strings without arguments)
string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
@@ -68,24 +102,50 @@ pub struct Localization {
impl Default for Localization {
fn default() -> Self {
// Default to English (US)
let default_locale = &EN_US;
let fallback_locale = default_locale.to_owned();
// Build available locales list
let available_locales = vec![
EN_US.clone(),
EN_XA.clone(),
DE.clone(),
ES_419.clone(),
ES_ES.clone(),
FR.clone(),
PT_BR.clone(),
TH.clone(),
ZH_CN.clone(),
ZH_TW.clone(),
];
let locale_native_names = HashMap::from([
(EN_US, EN_US_NATIVE_NAME.to_owned()),
(EN_XA, EN_XA_NATIVE_NAME.to_owned()),
(DE, DE_NATIVE_NAME.to_owned()),
(ES_419, ES_419_NATIVE_NAME.to_owned()),
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
(FR, FR_NATIVE_NAME.to_owned()),
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
(TH, TH_NATIVE_NAME.to_owned()),
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
]);
// Detect system locale and find best match
let current_locale = Self::negotiate_system_locale_with_preferences(&available_locales);
// Fallback locale is always EN_US
let fallback_locale = EN_US.clone();
tracing::info!(
"Localization initialized - Selected locale: {}, Fallback: {}",
current_locale,
fallback_locale
);
Self {
current_locale: default_locale.to_owned(),
current_locale,
available_locales,
fallback_locale,
locale_native_names,
use_isolating: true,
normalized_key_cache: HashMap::new(),
string_cache: HashMap::new(),
@@ -108,6 +168,150 @@ impl Localization {
}
}
/// Extract just the language and region from locale string (e.g., "fr-FR-u-mu-celsius" -> "fr-FR")
fn extract_language_region(locale_str: &str) -> String {
// Split by '-' and analyze the parts
let parts: Vec<&str> = locale_str.split('-').collect();
if parts.len() >= 2 {
// Check if the second part looks like a region
let second_part = parts[1];
if (second_part.len() >= 2) {
format!("{}-{}", parts[0], parts[1])
} else {
// Second part is not a region, probably an extension (e.g., "u", "t", "x")
// Just return the language part
parts[0].to_string()
}
} else {
// Only one part, return as is
locale_str.to_string()
}
}
/// Negotiate the best locale from all system preferences against available locales
fn negotiate_system_locale_with_preferences(
available_locales: &[LanguageIdentifier],
) -> LanguageIdentifier {
// Get all system preferred locales in descending order
let mut system_locales: Vec<String> = sys_locale::get_locales().collect();
if system_locales.is_empty() {
tracing::info!("No system locales detected, using fallback: en-US");
return EN_US.clone();
}
tracing::info!("System preferred locales: {:?}", system_locales);
// If we only got one locale, it might be that the system only returns the primary locale
// In this case, we can try to add common fallbacks based on the detected locale
if system_locales.len() == 1 {
let primary = &system_locales[0];
// Try to parse the primary locale, handling extensions
let primary_lang = if let Ok(locale) = primary.parse::<LanguageIdentifier>() {
locale.language.as_str().to_string()
} else {
// If parsing fails, try extracting language-region
// let stripped = Self::extract_language_region(primary);
// if let Ok(locale) = stripped.parse::<LanguageIdentifier>() {
// locale.language.as_str().to_string()
// } else {
tracing::info!("Could not parse primary locale: {}", primary);
"unknown".to_string()
// }
};
tracing::info!(
"Only one system locale detected: {} (language: {})",
primary,
primary_lang
);
// Add common fallbacks for the detected language
match primary_lang.as_str() {
"uk" => {
// For Ukrainian, add common fallbacks
system_locales.push("es-ES".to_string());
system_locales.push("en-US".to_string());
tracing::info!("Added fallbacks for Ukrainian: {:?}", system_locales);
}
"es" => {
// For Spanish, add English fallback
system_locales.push("en-US".to_string());
tracing::info!("Added fallback for Spanish: {:?}", system_locales);
}
_ => {
// For other languages, add English fallback
system_locales.push("en-US".to_string());
tracing::info!("Added fallback for {}: {:?}", primary_lang, system_locales);
}
}
}
// Convert system locale strings to LanguageIdentifiers, handling extensions
let mut parsed_system_locales = Vec::new();
for locale_str in system_locales {
// Try to parse the locale string directly first
if let Ok(locale) = locale_str.parse::<LanguageIdentifier>() {
parsed_system_locales.push(locale);
continue;
}
// If parsing fails, try extracting just language-region
// let stripped_locale = Self::extract_language_region(&locale_str);
// if let Ok(locale) = stripped_locale.parse::<LanguageIdentifier>() {
// parsed_system_locales.push(locale);
// continue;
// }
tracing::info!("Failed to parse locale string: {}", locale_str);
}
if parsed_system_locales.is_empty() {
tracing::info!("No valid system locales parsed, using fallback: en-US");
return EN_US.clone();
}
// First try exact matches with fluent_langneg
let fallback = &EN_US;
let negotiated = negotiate_languages(
&parsed_system_locales,
available_locales,
Some(fallback),
fluent_langneg::NegotiationStrategy::Filtering,
);
if let Some(result) = negotiated.first() {
tracing::info!(
"Exact match found: {} from preferences: {:?}",
result,
parsed_system_locales
);
return (*result).clone();
}
// If no exact match, try language-only fallbacks
tracing::info!("No exact matches found, trying language-only fallbacks");
for system_locale in &parsed_system_locales {
let system_lang = system_locale.language.as_str();
// Look for any available locale with the same language
for available_locale in available_locales {
if available_locale.language.as_str() == system_lang {
tracing::debug!(
"Language match found: {} (system: {})",
available_locale,
system_locale
);
return available_locale.clone();
}
}
}
tracing::info!("No language matches found, using fallback: en-US");
EN_US.clone()
}
/// Gets a localized string by its ID
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
self.get_cached_string(id, None)
@@ -373,6 +577,10 @@ impl Localization {
&self.fallback_locale
}
pub fn get_locale_native_name(&self, locale: &LanguageIdentifier) -> Option<&str> {
self.locale_native_names.get(locale).map(|s| s.as_str())
}
/// Gets cache statistics for monitoring performance
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
let mut total_strings = 0;
@@ -403,20 +611,6 @@ impl Localization {
Ok(())
}
/// Negotiates the best locale from a list of preferred locales
pub fn negotiate_locale(&self, preferred: &[LanguageIdentifier]) -> LanguageIdentifier {
let available = self.available_locales.clone();
let negotiated = negotiate_languages(
preferred,
&available,
Some(&self.fallback_locale),
fluent_langneg::NegotiationStrategy::Filtering,
);
negotiated
.first()
.map_or(self.fallback_locale.clone(), |v| (*v).clone())
}
}
/// Statistics about cache usage
@@ -429,6 +623,80 @@ pub struct CacheStats {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_language_region() {
// Test that we extract just language and region from various locale formats
// Test locales with extensions
let unicode_locale = "fr-FR-u-mu-celsius";
let extracted = Localization::extract_language_region(unicode_locale);
assert_eq!(extracted, "fr-FR");
let transformed_locale = "en-US-t-0-abc123";
let extracted = Localization::extract_language_region(transformed_locale);
assert_eq!(extracted, "en-US");
let private_locale = "de-DE-x-phonebk";
let extracted = Localization::extract_language_region(private_locale);
assert_eq!(extracted, "de-DE");
// Test simple locale (no extensions)
let simple_locale = "en-US";
let extracted = Localization::extract_language_region(simple_locale);
assert_eq!(extracted, "en-US");
// Test language-only locale
let lang_only = "en";
let extracted = Localization::extract_language_region(lang_only);
assert_eq!(extracted, "en");
// Test language with extensions (no region)
let lang_with_extensions = "fr-u-mu-celsius";
let extracted = Localization::extract_language_region(lang_with_extensions);
assert_eq!(extracted, "fr");
// Test language with other extension types (no region)
let lang_with_t_ext = "en-t-0-abc123";
let extracted = Localization::extract_language_region(lang_with_t_ext);
assert_eq!(extracted, "en");
let lang_with_x_ext = "de-x-phonebk";
let extracted = Localization::extract_language_region(lang_with_x_ext);
assert_eq!(extracted, "de");
// Test locale with numeric region code
let numeric_region = "es-419-u-mu-celsius";
let extracted = Localization::extract_language_region(numeric_region);
assert_eq!(extracted, "es-419");
// Test locale with 3-letter region code
let three_letter_region = "en-USA-t-0-abc123";
let extracted = Localization::extract_language_region(three_letter_region);
assert_eq!(extracted, "en-USA");
// Test locale with 2-letter region code
let two_letter_region = "fr-FR-u-mu-celsius";
let extracted = Localization::extract_language_region(two_letter_region);
assert_eq!(extracted, "fr-FR");
// Test complex locale with multiple parts
let complex_locale = "zh-CN-u-ca-chinese-x-private";
let extracted = Localization::extract_language_region(complex_locale);
assert_eq!(extracted, "zh-CN");
// Verify that extracted locales can be parsed
let test_cases = ["fr-FR", "en-US", "de-DE", "en", "zh-CN"];
for extracted in test_cases {
if let Ok(locale) = extracted.parse::<LanguageIdentifier>() {
tracing::info!("Successfully parsed extracted locale: {}", locale);
} else {
tracing::error!("Failed to parse extracted locale: {}", extracted);
panic!("Should parse locale after extraction");
}
}
}
//
// TODO(jb55): write tests that work, i broke all these during the refacto
+170 -2
View File
@@ -1,4 +1,9 @@
use crate::media::gif::ensure_latest_texture_from_cache;
use crate::media::images::ImageType;
use crate::urls::{UrlCache, UrlMimes};
use crate::ImageMetadata;
use crate::ObfuscationType;
use crate::RenderableMedia;
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
@@ -7,9 +12,11 @@ use poll_promise::Promise;
use egui::ColorImage;
use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::fs::{self, create_dir_all, File};
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use std::{io, thread};
use hex::ToHex;
use sha2::Digest;
@@ -19,7 +26,7 @@ use tracing::warn;
#[derive(Default)]
pub struct TexturesCache {
cache: hashbrown::HashMap<String, TextureStateInternal>,
pub cache: hashbrown::HashMap<String, TextureStateInternal>,
}
impl TexturesCache {
@@ -139,6 +146,12 @@ pub enum TextureState<'a> {
Loaded(&'a mut TexturedImage),
}
impl<'a> TextureState<'a> {
pub fn is_loaded(&self) -> bool {
matches!(self, Self::Loaded(_))
}
}
impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
fn from(value: &'a mut TextureStateInternal) -> Self {
match value {
@@ -220,6 +233,7 @@ pub struct MediaCache {
pub cache_dir: path::PathBuf,
pub textures_cache: TexturesCache,
pub cache_type: MediaCacheType,
pub cache_size: Arc<Mutex<Option<u64>>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
@@ -231,10 +245,29 @@ pub enum MediaCacheType {
impl MediaCache {
pub fn new(parent_dir: &Path, cache_type: MediaCacheType) -> Self {
let cache_dir = parent_dir.join(Self::rel_dir(cache_type));
let cache_dir_clone = cache_dir.clone();
let cache_size = Arc::new(Mutex::new(None));
let cache_size_clone = Arc::clone(&cache_size);
thread::spawn(move || {
let mut last_checked = Instant::now() - Duration::from_secs(999);
loop {
// check cache folder size every 60 s
if last_checked.elapsed() >= Duration::from_secs(60) {
let size = compute_folder_size(&cache_dir_clone);
*cache_size_clone.lock().unwrap() = Some(size);
last_checked = Instant::now();
}
thread::sleep(Duration::from_secs(5));
}
});
Self {
cache_dir,
textures_cache: TexturesCache::default(),
cache_type,
cache_size,
}
}
@@ -331,8 +364,14 @@ impl MediaCache {
);
}
}
Ok(())
}
fn clear(&mut self) {
self.textures_cache.cache.clear();
*self.cache_size.try_lock().unwrap() = Some(0);
}
}
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
@@ -349,10 +388,33 @@ fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
.expect("Failed to create RgbaImage from ColorImage")
}
fn compute_folder_size<P: AsRef<Path>>(path: P) -> u64 {
fn walk(path: &Path) -> u64 {
let mut size = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
size += metadata.len();
} else if metadata.is_dir() {
size += walk(&path);
}
}
}
}
size
}
walk(path.as_ref())
}
pub struct Images {
pub base_path: path::PathBuf,
pub static_imgs: MediaCache,
pub gifs: MediaCache,
pub urls: UrlMimes,
/// cached imeta data
pub metadata: HashMap<String, ImageMetadata>,
pub gif_states: GifStateMap,
}
@@ -360,10 +422,12 @@ impl Images {
/// path to directory to place [`MediaCache`]s
pub fn new(path: path::PathBuf) -> Self {
Self {
base_path: path.clone(),
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
gifs: MediaCache::new(&path, MediaCacheType::Gif),
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
gif_states: Default::default(),
metadata: Default::default(),
}
}
@@ -372,6 +436,58 @@ impl Images {
self.gifs.migrate_v0()
}
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
Self::find_renderable_media(&mut self.urls, &self.metadata, url)
}
pub fn find_renderable_media(
urls: &mut UrlMimes,
imeta: &HashMap<String, ImageMetadata>,
url: &str,
) -> Option<RenderableMedia> {
let media_type = crate::urls::supported_mime_hosted_at_url(urls, url)?;
let obfuscation_type = match imeta.get(url) {
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
None => ObfuscationType::Default,
};
Some(RenderableMedia {
url: url.to_string(),
media_type,
obfuscation_type,
})
}
pub fn latest_texture(
&mut self,
ui: &mut egui::Ui,
url: &str,
img_type: ImageType,
) -> Option<TextureHandle> {
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
let cache_dir = self.get_cache(cache_type).cache_dir.clone();
let is_loaded = self
.get_cache_mut(cache_type)
.textures_cache
.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
})
.is_loaded();
if !is_loaded {
return None;
}
let cache = match cache_type {
MediaCacheType::Image => &mut self.static_imgs,
MediaCacheType::Gif => &mut self.gifs,
};
ensure_latest_texture_from_cache(ui, url, &mut self.gif_states, &mut cache.textures_cache)
}
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
match cache_type {
MediaCacheType::Image => &self.static_imgs,
@@ -385,6 +501,26 @@ impl Images {
MediaCacheType::Gif => &mut self.gifs,
}
}
pub fn clear_folder_contents(&mut self) -> io::Result<()> {
for entry in fs::read_dir(self.base_path.clone())? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
}
self.urls.cache.clear();
self.static_imgs.clear();
self.gifs.clear();
self.gif_states.clear();
Ok(())
}
}
pub type GifStateMap = HashMap<String, GifState>;
@@ -395,3 +531,35 @@ pub struct GifState {
pub next_frame_time: Option<SystemTime>,
pub last_frame_index: usize,
}
pub struct LatestTexture {
pub texture: TextureHandle,
pub request_next_repaint: Option<SystemTime>,
}
pub fn get_render_state<'a>(
ctx: &egui::Context,
images: &'a mut Images,
cache_type: MediaCacheType,
url: &str,
img_type: ImageType,
) -> RenderState<'a> {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
});
RenderState {
texture_state,
gifs: &mut images.gif_states,
}
}
pub struct RenderState<'a> {
pub texture_state: TextureState<'a>,
pub gifs: &'a mut GifStateMap,
}
+1
View File
@@ -23,6 +23,7 @@ impl JobPool {
pub fn new(num_threads: usize) -> Self {
let (tx, rx) = mpsc::channel::<Job>();
// TODO(jb55) why not mpmc here !???
let arc_rx = Arc::new(Mutex::new(rx));
for _ in 0..num_threads {
let arc_rx_clone = arc_rx.clone();
@@ -1,6 +1,6 @@
use crate::JobPool;
use egui::TextureHandle;
use hashbrown::{hash_map::RawEntryMut, HashMap};
use notedeck::JobPool;
use poll_promise::Promise;
#[derive(Default)]
+17 -4
View File
@@ -12,16 +12,20 @@ mod frame_history;
pub mod i18n;
mod imgcache;
mod job_pool;
mod jobs;
pub mod media;
mod muted;
pub mod name;
pub mod note;
mod notecache;
mod options;
mod persist;
pub mod platform;
pub mod profile;
pub mod relay_debug;
pub mod relayspec;
mod result;
mod setup;
pub mod storage;
mod style;
pub mod theme;
@@ -47,10 +51,18 @@ pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use imgcache::{
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
TexturedImage, TexturesCache,
};
pub use job_pool::JobPool;
pub use jobs::{
BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
};
pub use media::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
};
pub use muted::{MuteFun, Muted};
pub use name::NostrName;
pub use note::{
@@ -58,6 +70,7 @@ pub use note::{
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
};
pub use notecache::{CachedNote, NoteCache};
pub use options::NotedeckOptions;
pub use persist::*;
pub use profile::get_profile_url;
pub use relay_debug::RelayDebugView;
@@ -72,8 +85,8 @@ pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction,
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
pub use user_account::UserAccount;
pub use wallet::{
get_current_wallet, get_wallet_for, GlobalWallet, Wallet, WalletError, WalletType,
WalletUIState, ZapWallet,
get_current_wallet, get_current_wallet_mut, get_wallet_for, GlobalWallet, Wallet, WalletError,
WalletType, WalletUIState, ZapWallet,
};
pub use zaps::{
get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget,
+127
View File
@@ -0,0 +1,127 @@
use crate::{Images, MediaCacheType, TexturedImage};
use poll_promise::Promise;
/// Tracks where media was on the screen so that
/// we can do fun animations when opening the
/// Media Viewer
#[derive(Debug, Clone)]
pub struct MediaInfo {
/// The original screen position where it
/// was rendered from. This is not where
/// it should be rendered in the scene.
pub original_position: egui::Rect,
pub url: String,
}
/// Contains various information for when a user
/// clicks a piece of media. It contains the current
/// location on screen for each piece of media.
///
/// Viewers can use this to smoothly transition from
/// the timeline to the viewer
#[derive(Debug, Clone, Default)]
pub struct ViewMediaInfo {
pub clicked_index: usize,
pub medias: Vec<MediaInfo>,
}
impl ViewMediaInfo {
pub fn clicked_media(&self) -> &MediaInfo {
&self.medias[self.clicked_index]
}
}
/// Actions generated by media ui interactions
pub enum MediaAction {
/// An image was clicked on in a carousel, we have
/// the opportunity to open into a fullscreen media viewer
/// with a list of url values
ViewMedias(ViewMediaInfo),
FetchImage {
url: String,
cache_type: MediaCacheType,
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
},
DoneLoading {
url: String,
cache_type: MediaCacheType,
},
}
impl std::fmt::Debug for MediaAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ViewMedias(ViewMediaInfo {
clicked_index,
medias,
}) => f
.debug_struct("ViewMedias")
.field("clicked_index", clicked_index)
.field("media", medias)
.finish(),
Self::FetchImage {
url,
cache_type,
no_pfp_promise,
} => f
.debug_struct("FetchNoPfpImage")
.field("url", url)
.field("cache_type", cache_type)
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
.finish(),
Self::DoneLoading { url, cache_type } => f
.debug_struct("DoneLoading")
.field("url", url)
.field("cache_type", cache_type)
.finish(),
}
}
}
impl MediaAction {
/// Handle view media actions
pub fn on_view_media(&self, handler: impl FnOnce(&ViewMediaInfo)) {
if let MediaAction::ViewMedias(view_medias) = self {
handler(view_medias)
}
}
/// Default processing logic for Media Actions. We don't handle ViewMedias here since
/// this may be app specific ?
pub fn process_default_media_actions(self, images: &mut Images) {
match self {
MediaAction::ViewMedias(_urls) => {
// NOTE(jb55): don't assume we want to show a fullscreen
// media viewer we can use on_view_media for that. We
// also don't want to have a notedeck_ui dependency in
// the notedeck lib (MediaViewerState)
//
// In general our notedeck crate should be pretty
// agnostic to functionallity in general unless it low
// level like image rendering.
//
//mview_state.set_urls(urls);
}
MediaAction::FetchImage {
url,
cache_type,
no_pfp_promise: promise,
} => {
images
.get_cache_mut(cache_type)
.textures_cache
.insert_pending(&url, promise);
}
MediaAction::DoneLoading { url, cache_type } => {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
cache.textures_cache.move_to_loaded(&url);
}
}
}
}
@@ -5,8 +5,8 @@ use nostrdb::Note;
use crate::jobs::{Job, JobError, JobParamsOwned};
#[derive(Clone)]
pub struct Blur<'a> {
pub blurhash: &'a str,
pub struct ImageMetadata {
pub blurhash: String,
pub dimensions: Option<PixelDimensions>, // width and height in pixels
}
@@ -44,7 +44,7 @@ impl PointDimensions {
}
}
impl Blur<'_> {
impl ImageMetadata {
pub fn scaled_pixel_dimensions(
&self,
ui: &egui::Ui,
@@ -75,9 +75,8 @@ impl Blur<'_> {
}
}
pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
let mut blurs = HashMap::new();
/// Find blurhashes in image metadata and update our cache
pub fn update_imeta_blurhashes(note: &Note, blurs: &mut HashMap<String, ImageMetadata>) {
for tag in note.tags() {
let mut tag_iter = tag.into_iter();
if tag_iter
@@ -93,13 +92,11 @@ pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
continue;
};
blurs.insert(url, blur);
blurs.insert(url.to_string(), blur);
}
}
blurs
}
fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
fn find_blur(tag_iter: nostrdb::TagIter<'_>) -> Option<(String, ImageMetadata)> {
let mut url = None;
let mut blurhash = None;
let mut dims = None;
@@ -138,21 +135,21 @@ fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
});
Some((
url,
Blur {
blurhash,
url.to_string(),
ImageMetadata {
blurhash: blurhash.to_string(),
dimensions,
},
))
}
#[derive(Clone)]
pub enum ObfuscationType<'a> {
Blurhash(Blur<'a>),
pub enum ObfuscationType {
Blurhash(ImageMetadata),
Default,
}
pub(crate) fn compute_blurhash(
pub fn compute_blurhash(
params: Option<JobParamsOwned>,
dims: PixelDimensions,
) -> Result<Job, JobError> {
@@ -185,9 +182,9 @@ fn generate_blurhash_texturehandle(
url: &str,
width: u32,
height: u32,
) -> notedeck::Result<egui::TextureHandle> {
) -> Result<egui::TextureHandle, crate::Error> {
let bytes = blurhash::decode(blurhash, width, height, 1.0)
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
.map_err(|e| crate::Error::Generic(e.to_string()))?;
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
Ok(ctx.load_texture(url, img, Default::default()))
@@ -3,37 +3,32 @@ use std::{
time::{Instant, SystemTime},
};
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
use egui::TextureHandle;
use notedeck::{GifState, GifStateMap, TexturedImage};
pub struct LatextTexture<'a> {
pub texture: &'a TextureHandle,
pub request_next_repaint: Option<SystemTime>,
}
/// This is necessary because other repaint calls can effectively steal our repaint request.
/// So we must keep on requesting to repaint at our desired time to ensure our repaint goes through.
/// See [`egui::Context::request_repaint_after`]
pub fn handle_repaint<'a>(ui: &egui::Ui, latest: LatextTexture<'a>) -> &'a TextureHandle {
if let Some(_repaint) = latest.request_next_repaint {
// 24fps for gif is fine
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(41));
}
latest.texture
}
#[must_use = "caller should pass the return value to `gif::handle_repaint`"]
pub fn retrieve_latest_texture<'a>(
pub fn ensure_latest_texture_from_cache(
ui: &egui::Ui,
url: &str,
gifs: &'a mut GifStateMap,
cached_image: &'a mut TexturedImage,
) -> LatextTexture<'a> {
match cached_image {
TexturedImage::Static(texture) => LatextTexture {
texture,
request_next_repaint: None,
},
gifs: &mut GifStateMap,
textures: &mut TexturesCache,
) -> Option<TextureHandle> {
let tstate = textures.cache.get_mut(url)?;
let TextureState::Loaded(img) = tstate.into() else {
return None;
};
Some(ensure_latest_texture(ui, url, gifs, img))
}
pub fn ensure_latest_texture(
ui: &egui::Ui,
url: &str,
gifs: &mut GifStateMap,
img: &mut TexturedImage,
) -> TextureHandle {
match img {
TexturedImage::Static(handle) => handle.clone(),
TexturedImage::Animated(animation) => {
if let Some(receiver) = &animation.receiver {
loop {
@@ -115,12 +110,12 @@ pub fn retrieve_latest_texture<'a>(
if let Some(req) = request_next_repaint {
tracing::trace!("requesting repaint for {url} after {req:?}");
// 24fps for gif is fine
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(41));
}
LatextTexture {
texture,
request_next_repaint,
}
texture.clone()
}
}
}
+475
View File
@@ -0,0 +1,475 @@
use crate::{Animation, ImageFrame, MediaCache, MediaCacheType, TextureFrame, TexturedImage};
use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
use image::codecs::gif::GifDecoder;
use image::imageops::FilterType;
use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
use poll_promise::Promise;
use std::collections::VecDeque;
use std::io::Cursor;
use std::path::PathBuf;
use std::path::{self, Path};
use std::sync::mpsc;
use std::sync::mpsc::SyncSender;
use std::thread;
use std::time::Duration;
use tokio::fs;
// NOTE(jb55): chatgpt wrote this because I was too dumb to
pub fn aspect_fill(
ui: &mut egui::Ui,
sense: Sense,
texture_id: egui::TextureId,
aspect_ratio: f32,
) -> egui::Response {
let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
let frame_ratio = frame.width() / frame.height();
let (width, height) = if frame_ratio > aspect_ratio {
// Frame is wider than the content
(frame.width(), frame.width() / aspect_ratio)
} else {
// Frame is taller than the content
(frame.height() * aspect_ratio, frame.height())
};
let content_rect = Rect::from_min_size(
frame.min
+ egui::vec2(
(frame.width() - width) / 2.0,
(frame.height() - height) / 2.0,
),
egui::vec2(width, height),
);
// Set the clipping rectangle to the frame
//let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
//ui.set_clip_rect(frame);
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
// Draw the texture within the calculated rect, potentially clipping it
painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
painter.image(texture_id, content_rect, uv, Color32::WHITE);
// Restore the original clipping rectangle
//ui.set_clip_rect(clip_rect);
response
}
#[profiling::function]
pub fn round_image(image: &mut ColorImage) {
// The radius to the edge of of the avatar circle
let edge_radius = image.size[0] as f32 / 2.0;
let edge_radius_squared = edge_radius * edge_radius;
for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
// y coordinate
let uy = pixnum / image.size[0];
let y = uy as f32;
let y_offset = edge_radius - y;
// x coordinate
let ux = pixnum % image.size[0];
let x = ux as f32;
let x_offset = edge_radius - x;
// The radius to this pixel (may be inside or outside the circle)
let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
// If inside of the avatar circle
if pixel_radius_squared <= edge_radius_squared {
// squareroot to find how many pixels we are from the edge
let pixel_radius: f32 = pixel_radius_squared.sqrt();
let distance = edge_radius - pixel_radius;
// If we are within 1 pixel of the edge, we should fade, to
// antialias the edge of the circle. 1 pixel from the edge should
// be 100% of the original color, and right on the edge should be
// 0% of the original color.
if distance <= 1.0 {
*pixel = Color32::from_rgba_premultiplied(
(pixel.r() as f32 * distance) as u8,
(pixel.g() as f32 * distance) as u8,
(pixel.b() as f32 * distance) as u8,
(pixel.a() as f32 * distance) as u8,
);
}
} else {
// Outside of the avatar circle
*pixel = Color32::TRANSPARENT;
}
}
}
/// If the image's longest dimension is greater than max_edge, downscale
fn resize_image_if_too_big(
image: image::DynamicImage,
max_edge: u32,
filter: FilterType,
) -> image::DynamicImage {
// if we have no size hint, resize to something reasonable
let w = image.width();
let h = image.height();
let long = w.max(h);
if long > max_edge {
let scale = max_edge as f32 / long as f32;
let new_w = (w as f32 * scale).round() as u32;
let new_h = (h as f32 * scale).round() as u32;
image.resize(new_w, new_h, filter)
} else {
image
}
}
///
/// Process an image, resizing so we don't blow up video memory or even crash
///
/// For profile pictures, make them round and small to fit the size hint
/// For everything else, either:
///
/// - resize to the size hint
/// - keep the size if the longest dimension is less than MAX_IMG_LENGTH
/// - resize if any larger, using [`resize_image_if_too_big`]
///
#[profiling::function]
fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
const MAX_IMG_LENGTH: u32 = 2048;
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
match imgtyp {
ImageType::Content(size_hint) => {
let image = match size_hint {
None => resize_image_if_too_big(image, MAX_IMG_LENGTH, FILTER_TYPE),
Some((w, h)) => image.resize(w, h, FILTER_TYPE),
};
let image_buffer = image.into_rgba8();
ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
)
}
ImageType::Profile(size) => {
// Crop square
let smaller = image.width().min(image.height());
if image.width() > smaller {
let excess = image.width() - smaller;
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
} else if image.height() > smaller {
let excess = image.height() - smaller;
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
}
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
let mut color_image = ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
image_buffer.as_flat_samples().as_slice(),
);
round_image(&mut color_image);
color_image
}
}
}
#[profiling::function]
fn parse_img_response(
response: ehttp::Response,
imgtyp: ImageType,
) -> Result<ColorImage, crate::Error> {
let content_type = response.content_type().unwrap_or_default();
let size_hint = match imgtyp {
ImageType::Profile(size) => SizeHint::Size(size, size),
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
ImageType::Content(None) => SizeHint::default(),
};
if content_type.starts_with("image/svg") {
profiling::scope!("load_svg");
let mut color_image =
egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
round_image(&mut color_image);
Ok(color_image)
} else if content_type.starts_with("image/") {
profiling::scope!("load_from_memory");
let dyn_image = image::load_from_memory(&response.bytes)?;
Ok(process_image(imgtyp, dyn_image))
} else {
Err(format!("Expected image, found content-type {content_type:?}").into())
}
}
fn fetch_img_from_disk(
ctx: &egui::Context,
url: &str,
path: &path::Path,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let ctx = ctx.clone();
let url = url.to_owned();
let path = path.to_owned();
Promise::spawn_async(async move {
Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
})
}
async fn async_fetch_img_from_disk(
ctx: egui::Context,
url: String,
path: &path::Path,
cache_type: MediaCacheType,
) -> Result<TexturedImage, crate::Error> {
match cache_type {
MediaCacheType::Image => {
let data = fs::read(path).await?;
let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image)?;
let img = buffer_to_color_image(
image_buffer.as_flat_samples_u8(),
image_buffer.width(),
image_buffer.height(),
);
Ok(TexturedImage::Static(ctx.load_texture(
&url,
img,
Default::default(),
)))
}
MediaCacheType::Gif => {
let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
generate_gif(ctx, url, path, gif_bytes, false, |i| {
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
})
}
}
}
fn generate_gif(
ctx: egui::Context,
url: String,
path: &path::Path,
data: Vec<u8>,
write_to_disk: bool,
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
) -> Result<TexturedImage, crate::Error> {
let decoder = {
let reader = Cursor::new(data.as_slice());
GifDecoder::new(reader)?
};
let (tex_input, tex_output) = mpsc::sync_channel(4);
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
let (inp, out) = mpsc::sync_channel(4);
(Some(inp), Some(out))
} else {
(None, None)
};
let mut frames: VecDeque<Frame> = decoder
.into_frames()
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
.map_err(|e| crate::Error::Generic(e.to_string()))?;
let first_frame = frames.pop_front().map(|frame| {
generate_animation_frame(
&ctx,
&url,
0,
frame,
maybe_encoder_input.as_ref(),
process_to_egui,
)
});
let cur_url = url.clone();
thread::spawn(move || {
for (index, frame) in frames.into_iter().enumerate() {
let texture_frame = generate_animation_frame(
&ctx,
&cur_url,
index,
frame,
maybe_encoder_input.as_ref(),
process_to_egui,
);
if tex_input.send(texture_frame).is_err() {
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
break;
}
}
});
if let Some(encoder_output) = maybe_encoder_output {
let path = path.to_owned();
thread::spawn(move || {
let mut imgs = Vec::new();
while let Ok(img) = encoder_output.recv() {
imgs.push(img);
}
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
tracing::error!("Could not write gif to disk: {e}");
}
});
}
first_frame.map_or_else(
|| {
Err(crate::Error::Generic(
"first frame not found for gif".to_owned(),
))
},
|first_frame| {
Ok(TexturedImage::Animated(Animation {
other_frames: Default::default(),
receiver: Some(tex_output),
first_frame,
}))
},
)
}
fn generate_animation_frame(
ctx: &egui::Context,
url: &str,
index: usize,
frame: image::Frame,
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
) -> TextureFrame {
let delay = Duration::from(frame.delay());
let img = DynamicImage::ImageRgba8(frame.into_buffer());
let color_img = process_to_egui(img);
if let Some(sender) = maybe_encoder_input {
if let Err(e) = sender.send(ImageFrame {
delay,
image: color_img.clone(),
}) {
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
}
}
TextureFrame {
delay,
texture: ctx.load_texture(format!("{url}{index}"), color_img, Default::default()),
}
}
fn buffer_to_color_image(
samples: Option<FlatSamples<&[u8]>>,
width: u32,
height: u32,
) -> ColorImage {
// TODO(jb55): remove unwrap here
let flat_samples = samples.unwrap();
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
}
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>, crate::Error> {
std::fs::read(path).map_err(|e| crate::Error::Generic(e.to_string()))
}
/// Controls type-specific handling
#[derive(Debug, Clone, Copy)]
pub enum ImageType {
/// Profile Image (size)
Profile(u32),
/// Content Image with optional size hint
Content(Option<(u32, u32)>),
}
pub fn fetch_img(
img_cache_path: &Path,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let key = MediaCache::key(url);
let path = img_cache_path.join(key);
if path.exists() {
fetch_img_from_disk(ctx, url, &path, cache_type)
} else {
fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
}
// TODO: fetch image from local cache
}
fn fetch_img_from_net(
cache_path: &path::Path,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
cache_type: MediaCacheType,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(url);
let ctx = ctx.clone();
let cloned_url = url.to_owned();
let cache_path = cache_path.to_owned();
ehttp::fetch(request, move |response| {
let handle = response.map_err(crate::Error::Generic).and_then(|resp| {
match cache_type {
MediaCacheType::Image => {
let img = parse_img_response(resp, imgtyp);
img.map(|img| {
let texture_handle =
ctx.load_texture(&cloned_url, img.clone(), Default::default());
// write to disk
std::thread::spawn(move || {
MediaCache::write(&cache_path, &cloned_url, img)
});
TexturedImage::Static(texture_handle)
})
}
MediaCacheType::Gif => {
let gif_bytes = resp.bytes;
generate_gif(
ctx.clone(),
cloned_url,
&cache_path,
gif_bytes,
true,
move |img| process_image(imgtyp, img),
)
}
}
});
sender.send(Some(handle)); // send the results back to the UI thread.
ctx.request_repaint();
});
promise
}
pub fn fetch_no_pfp_promise(
ctx: &Context,
cache: &MediaCache,
) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
crate::media::images::fetch_img(
&cache.cache_dir,
ctx,
crate::profile::no_pfp_url(),
ImageType::Profile(128),
MediaCacheType::Image,
)
}
+1
View File
@@ -0,0 +1 @@
+14
View File
@@ -0,0 +1,14 @@
pub mod action;
pub mod blur;
pub mod gif;
pub mod images;
pub mod imeta;
pub mod renderable;
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
pub use blur::{
compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
PointDimensions,
};
pub use images::ImageType;
pub use renderable::RenderableMedia;
+9
View File
@@ -0,0 +1,9 @@
use super::ObfuscationType;
use crate::MediaCacheType;
/// Media that is prepared for rendering. Use [`Images::get_renderable_media`] to get these
pub struct RenderableMedia {
pub url: String,
pub media_type: MediaCacheType,
pub obfuscation_type: ObfuscationType,
}
+1 -61
View File
@@ -1,8 +1,7 @@
use super::context::ContextSelection;
use crate::{zaps::NoteZapTargetOwned, Images, MediaCacheType, TexturedImage};
use crate::{zaps::NoteZapTargetOwned, MediaAction};
use egui::Vec2;
use enostr::{NoteId, Pubkey};
use poll_promise::Promise;
#[derive(Debug)]
pub struct ScrollInfo {
@@ -61,62 +60,3 @@ pub struct ZapTargetAmount {
pub target: NoteZapTargetOwned,
pub specified_msats: Option<u64>, // if None use default amount
}
pub enum MediaAction {
FetchImage {
url: String,
cache_type: MediaCacheType,
no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
},
DoneLoading {
url: String,
cache_type: MediaCacheType,
},
}
impl std::fmt::Debug for MediaAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FetchImage {
url,
cache_type,
no_pfp_promise,
} => f
.debug_struct("FetchNoPfpImage")
.field("url", url)
.field("cache_type", cache_type)
.field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
.finish(),
Self::DoneLoading { url, cache_type } => f
.debug_struct("DoneLoading")
.field("url", url)
.field("cache_type", cache_type)
.finish(),
}
}
}
impl MediaAction {
pub fn process(self, images: &mut Images) {
match self {
MediaAction::FetchImage {
url,
cache_type,
no_pfp_promise: promise,
} => {
images
.get_cache_mut(cache_type)
.textures_cache
.insert_pending(&url, promise);
}
MediaAction::DoneLoading { url, cache_type } => {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
cache.textures_cache.move_to_loaded(&url);
}
}
}
}
+3 -2
View File
@@ -1,10 +1,11 @@
mod action;
mod context;
pub use action::{MediaAction, NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
use crate::Accounts;
use crate::GlobalWallet;
use crate::JobPool;
use crate::Localization;
use crate::UnknownIds;
@@ -20,6 +21,7 @@ use std::fmt;
pub struct NoteContext<'d> {
pub ndb: &'d Ndb,
pub accounts: &'d Accounts,
pub global_wallet: &'d GlobalWallet,
pub i18n: &'d mut Localization,
pub img_cache: &'d mut Images,
pub note_cache: &'d mut NoteCache,
@@ -28,7 +30,6 @@ pub struct NoteContext<'d> {
pub job_pool: &'d mut JobPool,
pub unknown_ids: &'d mut UnknownIds,
pub clipboard: &'d mut egui_winit::clipboard::Clipboard,
pub current_account_has_wallet: bool,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
+39
View File
@@ -0,0 +1,39 @@
use bitflags::bitflags;
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NotedeckOptions: u64 {
// ===== Settings ======
/// Are we on light theme?
const LightTheme = 1 << 0;
/// Debug controls, fps stats
const Debug = 1 << 1;
/// Show relay debug window?
const RelayDebug = 1 << 2;
/// Are we running as tests?
const Tests = 1 << 3;
/// Use keystore?
const UseKeystore = 1 << 4;
/// Show client on notes?
const ShowClient = 1 << 5;
/// Simulate is_compiled_as_mobile ?
const Mobile = 1 << 6;
// ===== Feature Flags ======
/// Is notebook enabled?
const FeatureNotebook = 1 << 32;
}
}
impl Default for NotedeckOptions {
fn default() -> Self {
NotedeckOptions::UseKeystore
}
}
+4 -4
View File
@@ -1,9 +1,9 @@
mod app_size;
mod theme_handler;
mod settings_handler;
mod token_handler;
mod zoom;
pub use app_size::AppSizeHandler;
pub use theme_handler::ThemeHandler;
pub use settings_handler::Settings;
pub use settings_handler::SettingsHandler;
pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE;
pub use token_handler::TokenHandler;
pub use zoom::ZoomHandler;
@@ -0,0 +1,253 @@
use crate::{
storage::delete_file, timed_serializer::TimedSerializer, DataPath, DataPathType, Directory,
};
use egui::ThemePreference;
use serde::{Deserialize, Serialize};
use tracing::{error, info};
const THEME_FILE: &str = "theme.txt";
const ZOOM_FACTOR_FILE: &str = "zoom_level.json";
const SETTINGS_FILE: &str = "settings.json";
const DEFAULT_THEME: ThemePreference = ThemePreference::Dark;
const DEFAULT_LOCALE: &str = "en-US";
const DEFAULT_ZOOM_FACTOR: f32 = 1.0;
const DEFAULT_SHOW_SOURCE_CLIENT: &str = "hide";
const DEFAULT_SHOW_REPLIES_NEWEST_FIRST: bool = false;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 13.0;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 16.0;
fn deserialize_theme(serialized_theme: &str) -> Option<ThemePreference> {
match serialized_theme {
"dark" => Some(ThemePreference::Dark),
"light" => Some(ThemePreference::Light),
"system" => Some(ThemePreference::System),
_ => None,
}
}
#[derive(Serialize, Deserialize, PartialEq, Clone)]
pub struct Settings {
pub theme: ThemePreference,
pub locale: String,
pub zoom_factor: f32,
pub show_source_client: String,
pub show_replies_newest_first: bool,
pub note_body_font_size: f32,
}
impl Default for Settings {
fn default() -> Self {
Self {
theme: DEFAULT_THEME,
locale: DEFAULT_LOCALE.to_string(),
zoom_factor: DEFAULT_ZOOM_FACTOR,
show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(),
show_replies_newest_first: DEFAULT_SHOW_REPLIES_NEWEST_FIRST,
note_body_font_size: DEFAULT_NOTE_BODY_FONT_SIZE,
}
}
}
pub struct SettingsHandler {
directory: Directory,
serializer: TimedSerializer<Settings>,
current_settings: Option<Settings>,
}
impl SettingsHandler {
fn read_from_theme_file(&self) -> Option<ThemePreference> {
match self.directory.get_file(THEME_FILE.to_string()) {
Ok(contents) => deserialize_theme(contents.trim()),
Err(_) => None,
}
}
fn read_from_zomfactor_file(&self) -> Option<f32> {
match self.directory.get_file(ZOOM_FACTOR_FILE.to_string()) {
Ok(contents) => serde_json::from_str::<f32>(&contents).ok(),
Err(_) => None,
}
}
fn migrate_to_settings_file(&mut self) -> bool {
let mut settings = Settings::default();
let mut migrated = false;
// if theme.txt exists migrate
if let Some(theme_from_file) = self.read_from_theme_file() {
info!("migrating theme preference from theme.txt file");
_ = delete_file(&self.directory.file_path, THEME_FILE.to_string());
settings.theme = theme_from_file;
migrated = true;
} else {
info!("theme.txt file not found, using default theme");
};
// if zoom_factor.txt exists migrate
if let Some(zom_factor) = self.read_from_zomfactor_file() {
info!("migrating theme preference from zom_factor file");
_ = delete_file(&self.directory.file_path, ZOOM_FACTOR_FILE.to_string());
settings.zoom_factor = zom_factor;
migrated = true;
} else {
info!("zoom_factor.txt exists migrate file not found, using default zoom factor");
};
if migrated {
self.current_settings = Some(settings);
self.try_save_settings();
}
migrated
}
pub fn new(path: &DataPath) -> Self {
let directory = Directory::new(path.path(DataPathType::Setting));
let serializer =
TimedSerializer::new(path, DataPathType::Setting, "settings.json".to_owned());
Self {
directory,
serializer,
current_settings: None,
}
}
pub fn load(mut self) -> Self {
if self.migrate_to_settings_file() {
return self;
}
match self.directory.get_file(SETTINGS_FILE.to_string()) {
Ok(contents_str) => {
// Parse JSON content
match serde_json::from_str::<Settings>(&contents_str) {
Ok(settings) => {
self.current_settings = Some(settings);
}
Err(_) => {
error!("Invalid settings format. Using defaults");
self.current_settings = Some(Settings::default());
}
}
}
Err(_) => {
error!("Could not read settings. Using defaults");
self.current_settings = Some(Settings::default());
}
}
self
}
pub(crate) fn try_save_settings(&mut self) {
let settings = self.get_settings_mut().clone();
self.serializer.try_save(settings);
}
pub fn get_settings_mut(&mut self) -> &mut Settings {
if self.current_settings.is_none() {
self.current_settings = Some(Settings::default());
}
self.current_settings.as_mut().unwrap()
}
pub fn set_theme(&mut self, theme: ThemePreference) {
self.get_settings_mut().theme = theme;
self.try_save_settings();
}
pub fn set_locale<S>(&mut self, locale: S)
where
S: Into<String>,
{
self.get_settings_mut().locale = locale.into();
self.try_save_settings();
}
pub fn set_zoom_factor(&mut self, zoom_factor: f32) {
self.get_settings_mut().zoom_factor = zoom_factor;
self.try_save_settings();
}
pub fn set_show_source_client<S>(&mut self, option: S)
where
S: Into<String>,
{
self.get_settings_mut().show_source_client = option.into();
self.try_save_settings();
}
pub fn set_show_replies_newest_first(&mut self, value: bool) {
self.get_settings_mut().show_replies_newest_first = value;
self.try_save_settings();
}
pub fn set_note_body_font_size(&mut self, value: f32) {
self.get_settings_mut().note_body_font_size = value;
self.try_save_settings();
}
pub fn update_batch<F>(&mut self, update_fn: F)
where
F: FnOnce(&mut Settings),
{
let settings = self.get_settings_mut();
update_fn(settings);
self.try_save_settings();
}
pub fn update_settings(&mut self, new_settings: Settings) {
self.current_settings = Some(new_settings);
self.try_save_settings();
}
pub fn theme(&self) -> ThemePreference {
self.current_settings
.as_ref()
.map(|s| s.theme)
.unwrap_or(DEFAULT_THEME)
}
pub fn locale(&self) -> String {
self.current_settings
.as_ref()
.map(|s| s.locale.clone())
.unwrap_or_else(|| DEFAULT_LOCALE.to_string())
}
pub fn zoom_factor(&self) -> f32 {
self.current_settings
.as_ref()
.map(|s| s.zoom_factor)
.unwrap_or(DEFAULT_ZOOM_FACTOR)
}
pub fn show_source_client(&self) -> String {
self.current_settings
.as_ref()
.map(|s| s.show_source_client.to_string())
.unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string())
}
pub fn show_replies_newest_first(&self) -> bool {
self.current_settings
.as_ref()
.map(|s| s.show_replies_newest_first)
.unwrap_or(DEFAULT_SHOW_REPLIES_NEWEST_FIRST)
}
pub fn is_loaded(&self) -> bool {
self.current_settings.is_some()
}
pub fn note_body_font_size(&self) -> f32 {
self.current_settings
.as_ref()
.map(|s| s.note_body_font_size)
.unwrap_or(DEFAULT_NOTE_BODY_FONT_SIZE)
}
}
@@ -1,76 +0,0 @@
use egui::ThemePreference;
use tracing::{error, info};
use crate::{storage, DataPath, DataPathType, Directory};
pub struct ThemeHandler {
directory: Directory,
fallback_theme: ThemePreference,
}
const THEME_FILE: &str = "theme.txt";
impl ThemeHandler {
pub fn new(path: &DataPath) -> Self {
let directory = Directory::new(path.path(DataPathType::Setting));
let fallback_theme = ThemePreference::Dark;
Self {
directory,
fallback_theme,
}
}
pub fn load(&self) -> ThemePreference {
match self.directory.get_file(THEME_FILE.to_owned()) {
Ok(contents) => match deserialize_theme(contents) {
Some(theme) => theme,
None => {
error!(
"Could not deserialize theme. Using fallback {:?} instead",
self.fallback_theme
);
self.fallback_theme
}
},
Err(e) => {
error!(
"Could not read {} file: {:?}\nUsing fallback {:?} instead",
THEME_FILE, e, self.fallback_theme
);
self.fallback_theme
}
}
}
pub fn save(&self, theme: ThemePreference) {
match storage::write_file(
&self.directory.file_path,
THEME_FILE.to_owned(),
&theme_to_serialized(&theme),
) {
Ok(_) => info!(
"Successfully saved {:?} theme change to {}",
theme, THEME_FILE
),
Err(_) => error!("Could not save {:?} theme change to {}", theme, THEME_FILE),
}
}
}
fn theme_to_serialized(theme: &ThemePreference) -> String {
match theme {
ThemePreference::Dark => "dark",
ThemePreference::Light => "light",
ThemePreference::System => "system",
}
.to_owned()
}
fn deserialize_theme(serialized_theme: String) -> Option<ThemePreference> {
match serialized_theme.as_str() {
"dark" => Some(ThemePreference::Dark),
"light" => Some(ThemePreference::Light),
"system" => Some(ThemePreference::System),
_ => None,
}
}
-26
View File
@@ -1,26 +0,0 @@
use crate::{DataPath, DataPathType};
use egui::Context;
use crate::timed_serializer::TimedSerializer;
pub struct ZoomHandler {
serializer: TimedSerializer<f32>,
}
impl ZoomHandler {
pub fn new(path: &DataPath) -> Self {
let serializer =
TimedSerializer::new(path, DataPathType::Setting, "zoom_level.json".to_owned());
Self { serializer }
}
pub fn try_save_zoom_factor(&mut self, ctx: &Context) {
let cur_zoom_level = ctx.zoom_factor();
self.serializer.try_save(cur_zoom_level);
}
pub fn get_zoom_factor(&self) -> Option<f32> {
self.serializer.get_item()
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
debug!("updating virtual keyboard height {}", height);
// Convert and store atomically
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
KEYBOARD_HEIGHT.store(height, Ordering::SeqCst);
}
/// Gets the current Android virtual keyboard height. Useful for transforming
+47
View File
@@ -0,0 +1,47 @@
use crate::fonts;
use crate::theme;
use crate::NotedeckOptions;
use crate::NotedeckTextStyle;
use egui::FontId;
use egui::ThemePreference;
pub fn setup_egui_context(
ctx: &egui::Context,
options: NotedeckOptions,
theme: ThemePreference,
note_body_font_size: f32,
zoom_factor: f32,
) {
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
let is_oled = crate::ui::is_oled();
ctx.options_mut(|o| {
tracing::info!("Loaded theme {:?} from disk", theme);
o.theme_preference = theme;
});
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
fonts::setup_fonts(ctx);
if crate::ui::is_compiled_as_mobile() {
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
}
egui_extras::install_image_loaders(ctx);
ctx.options_mut(|o| {
o.input_options.max_click_duration = 0.4;
});
ctx.all_styles_mut(|style| crate::theme::add_custom_style(is_mobile, style));
ctx.set_zoom_factor(zoom_factor);
let mut style = (*ctx.style()).clone();
style.text_styles.insert(
NotedeckTextStyle::NoteBody.text_style(),
FontId::proportional(note_body_font_size),
);
ctx.set_style(style);
}
+3
View File
@@ -15,6 +15,7 @@ pub enum NotedeckTextStyle {
Button,
Small,
Tiny,
NoteBody,
}
impl NotedeckTextStyle {
@@ -29,6 +30,7 @@ impl NotedeckTextStyle {
Self::Button => TextStyle::Button,
Self::Small => TextStyle::Small,
Self::Tiny => TextStyle::Name("Tiny".into()),
Self::NoteBody => TextStyle::Name("NoteBody".into()),
}
}
@@ -43,6 +45,7 @@ impl NotedeckTextStyle {
Self::Button => FontFamily::Proportional,
Self::Small => FontFamily::Proportional,
Self::Tiny => FontFamily::Proportional,
Self::NoteBody => FontFamily::Proportional,
}
}
+160 -4
View File
@@ -1,7 +1,35 @@
use egui::{
style::{Selection, WidgetVisuals, Widgets},
Color32, CornerRadius, Stroke, Visuals,
};
use crate::{fonts, NotedeckTextStyle};
use egui::style::Interaction;
use egui::style::Selection;
use egui::style::WidgetVisuals;
use egui::style::Widgets;
use egui::Color32;
use egui::CornerRadius;
use egui::FontId;
use egui::Stroke;
use egui::Style;
use egui::Visuals;
use strum::IntoEnumIterator;
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
// BACKGROUNDS
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
pub struct ColorTheme {
// VISUALS
@@ -86,3 +114,131 @@ pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
..default
}
}
pub fn desktop_dark_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: DARKER_BG,
extreme_bg_color: DARK_ISH_BG,
text_color: Color32::WHITE,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: DARK_ISH_BG,
window_stroke_color: DARK_BG,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: DARK_ISH_BG,
noninteractive_weak_bg_fill: DARK_BG,
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: SEMI_DARKER_BG,
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
inactive_weak_bg_fill: SEMI_DARK_BG,
}
}
pub fn mobile_dark_color_theme() -> ColorTheme {
ColorTheme {
panel_fill: Color32::BLACK,
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
..desktop_dark_color_theme()
}
}
pub fn light_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: Color32::WHITE,
extreme_bg_color: LIGHTER_GRAY,
text_color: BLACK,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: Color32::WHITE,
window_stroke_color: DARKER_GRAY,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: Color32::WHITE,
noninteractive_weak_bg_fill: LIGHTER_GRAY,
noninteractive_bg_stroke_color: LIGHT_GRAY,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
inactive_bg_fill: LIGHTER_GRAY,
inactive_weak_bg_fill: LIGHTER_GRAY,
}
}
/// Create custom text sizes for any FontSizes
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
let font_size = if is_mobile {
fonts::mobile_font_size
} else {
fonts::desktop_font_size
};
style.text_styles = NotedeckTextStyle::iter()
.map(|text_style| {
(
text_style.text_style(),
FontId::new(font_size(&text_style), text_style.font_family()),
)
})
.collect();
style.interaction = Interaction {
tooltip_delay: 0.1,
show_tooltips_only_when_still: false,
..Interaction::default()
};
// debug: show callstack for the current widget on hover if all
// modifier keys are pressed down.
/*
#[cfg(feature = "debug-widget-callstack")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-widget-callstack` feature requires a debug build, \
release builds are unsupported."
);
style.debug.debug_on_hover_with_all_modifiers = true;
}
// debug: show an overlay on all interactive widgets
#[cfg(feature = "debug-interactive-widgets")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-interactive-widgets` feature requires a debug build, \
release builds are unsupported."
);
style.debug.show_interactive_widgets = true;
}
*/
}
pub fn light_mode() -> Visuals {
create_themed_visuals(crate::theme::light_color_theme(), Visuals::light())
}
pub fn dark_mode(is_oled: bool) -> Visuals {
create_themed_visuals(
if is_oled {
mobile_dark_color_theme()
} else {
desktop_dark_color_theme()
},
Visuals::dark(),
)
}
+8 -8
View File
@@ -2,16 +2,16 @@ use crate::debouncer::Debouncer;
use crate::{storage, DataPath, DataPathType, Directory};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::info; // Adjust this import path as needed
use tracing::info;
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> {
directory: Directory,
file_name: String,
debouncer: Debouncer,
saved_item: Option<T>,
}
impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
let directory = Directory::new(path.path(path_type));
let delay = Duration::from_millis(1000);
@@ -30,11 +30,11 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
self
}
// returns whether successful
/// Returns whether it actually wrote the new value
pub fn try_save(&mut self, cur_item: T) -> bool {
if self.debouncer.should_act() {
if let Some(saved_item) = self.saved_item {
if saved_item != cur_item {
if let Some(ref saved_item) = self.saved_item {
if *saved_item != cur_item {
return self.save(cur_item);
}
} else {
@@ -45,8 +45,8 @@ impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize
}
pub fn get_item(&self) -> Option<T> {
if self.saved_item.is_some() {
return self.saved_item;
if let Some(ref item) = self.saved_item {
return Some(item.clone());
}
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
+12 -1
View File
@@ -1,8 +1,19 @@
use crate::NotedeckTextStyle;
pub const NARROW_SCREEN_WIDTH: f32 = 550.0;
pub fn richtext_small<S>(text: S) -> egui::RichText
where
S: Into<String>,
{
egui::RichText::new(text).text_style(NotedeckTextStyle::Small.text_style())
}
/// Determine if the screen is narrow. This is useful for detecting mobile
/// contexts, but with the nuance that we may also have a wide android tablet.
pub fn is_narrow(ctx: &egui::Context) -> bool {
let screen_size = ctx.input(|c| c.screen_rect().size());
screen_size.x < 550.0
screen_size.x < NARROW_SCREEN_WIDTH
}
pub fn is_oled() -> bool {
+11
View File
@@ -68,6 +68,17 @@ impl UrlCache {
}
}
}
pub fn clear(&mut self) {
if self.from_disk_promise.is_none() {
let cache = self.cache.clone();
std::thread::spawn(move || {
if let Ok(mut locked_cache) = cache.write() {
locked_cache.clear();
}
});
}
}
}
fn merge_cache(cur_cache: Arc<RwLock<UrlsToMime>>, from_disk: UrlsToMime) {
+12 -1
View File
@@ -24,7 +24,7 @@ pub fn get_wallet_for<'a>(
global_wallet.wallet.as_ref()
}
pub fn get_current_wallet<'a>(
pub fn get_current_wallet_mut<'a>(
accounts: &'a mut Accounts,
global_wallet: &'a mut GlobalWallet,
) -> Option<&'a mut ZapWallet> {
@@ -35,6 +35,17 @@ pub fn get_current_wallet<'a>(
Some(wallet)
}
pub fn get_current_wallet<'a>(
accounts: &'a Accounts,
global_wallet: &'a GlobalWallet,
) -> Option<&'a ZapWallet> {
let Some(wallet) = accounts.get_selected_wallet() else {
return global_wallet.wallet.as_ref();
};
Some(wallet)
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum WalletType {
Auto,
+7
View File
@@ -16,6 +16,7 @@ egui = { workspace = true }
notedeck_columns = { workspace = true }
notedeck_ui = { workspace = true }
notedeck_dave = { workspace = true }
notedeck_notebook = { workspace = true }
notedeck = { workspace = true }
nostrdb = { workspace = true }
puffin = { workspace = true, optional = true }
@@ -63,6 +64,12 @@ short_description = "The nostr browser"
identifier = "com.damus.notedeck"
icon = ["assets/app_icon.icns"]
[package.metadata.android.manifest.queries]
intent = [
{ action = ["android.intent.action.MAIN"] },
]
[package.metadata.android]
package = "com.damus.app"
apk_name = "Notedeck"
@@ -23,9 +23,16 @@
</activity>
</application>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<uses-feature android:name="android.hardware.vulkan.level"
android:required="true"
android:version="1" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+3 -28
View File
@@ -2,10 +2,8 @@
//use egui_android::run_android;
use egui_winit::winit::platform::android::activity::AndroidApp;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use crate::{app::NotedeckApp, chrome::Chrome, setup::setup_chrome};
use crate::chrome::Chrome;
use notedeck::Notedeck;
#[no_mangle]
@@ -67,31 +65,8 @@ pub async fn android_main(app: AndroidApp) {
Box::new(move |cc| {
let ctx = &cc.egui_ctx;
let mut notedeck = Notedeck::new(ctx, path, &app_args);
setup_chrome(ctx, &notedeck.args(), notedeck.theme());
let context = &mut notedeck.app_context();
let dave = Dave::new(cc.wgpu_render_state.as_ref());
let columns = Damus::new(context, &app_args);
let mut chrome = Chrome::new();
// ensure we recognized all the arguments
let completely_unrecognized: Vec<String> = notedeck
.unrecognized_args()
.intersection(columns.unrecognized_args())
.cloned()
.collect();
assert!(
completely_unrecognized.is_empty(),
"unrecognized args: {:?}",
completely_unrecognized
);
chrome.add_app(NotedeckApp::Columns(columns));
chrome.add_app(NotedeckApp::Dave(dave));
// test dav
chrome.set_active(0);
notedeck.setup(ctx);
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
notedeck.set_app(chrome);
Ok(Box::new(notedeck))
+5 -2
View File
@@ -1,11 +1,13 @@
use notedeck::{AppAction, AppContext};
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_notebook::Notebook;
#[allow(clippy::large_enum_variant)]
pub enum NotedeckApp {
Dave(Dave),
Columns(Damus),
Dave(Box<Dave>),
Columns(Box<Damus>),
Notebook(Box<Notebook>),
Other(Box<dyn notedeck::App>),
}
@@ -14,6 +16,7 @@ impl notedeck::App for NotedeckApp {
match self {
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
NotedeckApp::Other(other) => other.update(ctx, ui),
}
}
+272 -22
View File
@@ -2,16 +2,22 @@
//#[cfg(target_arch = "wasm32")]
//use wasm_bindgen::prelude::*;
use crate::app::NotedeckApp;
use eframe::CreationContext;
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction};
use notedeck::Error;
use notedeck::{
tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
tr, App, AppAction, AppContext, Localization, Notedeck, NotedeckOptions, NotedeckTextStyle,
UserAccount, WalletType,
};
use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
column::SelectionResult,
timeline::{kind::ListKind, TimelineKind},
Damus,
};
use notedeck_dave::{Dave, DaveAvatar};
use notedeck_notebook::Notebook;
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
static ICON_WIDTH: f32 = 40.0;
@@ -58,6 +64,7 @@ pub enum ChromePanelAction {
Wallet,
Toolbar(ToolbarAction),
SaveTheme(ThemePreference),
Profile(notedeck::enostr::Pubkey),
}
impl ChromePanelAction {
@@ -111,10 +118,8 @@ impl ChromePanelAction {
fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
match self {
Self::SaveTheme(theme) => {
ui.ctx().options_mut(|o| {
o.theme_preference = *theme;
});
ctx.theme.save(*theme);
ui.ctx().set_theme(*theme);
ctx.settings.set_theme(*theme);
}
Self::Toolbar(toolbar_action) => match toolbar_action {
@@ -150,7 +155,7 @@ impl ChromePanelAction {
}
Self::Settings => {
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Relays);
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Settings);
}
Self::Wallet => {
@@ -160,13 +165,56 @@ impl ChromePanelAction {
notedeck_columns::Route::Wallet(WalletType::Auto),
);
}
Self::Profile(pk) => {
columns_route_to_profile(pk, chrome, ctx, ui);
}
}
}
}
/// Some people have been running notedeck in debug, let's catch that!
fn stop_debug_mode(options: NotedeckOptions) {
if !options.contains(NotedeckOptions::Tests)
&& cfg!(debug_assertions)
&& !options.contains(NotedeckOptions::Debug)
{
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
println!(
"It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want."
);
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
println!("---------------------------------");
panic!();
}
}
impl Chrome {
pub fn new() -> Self {
Chrome::default()
/// Create a new chrome with the default app setup
pub fn new_with_apps(
cc: &CreationContext,
app_args: &[String],
notedeck: &mut Notedeck,
) -> Result<Self, Error> {
stop_debug_mode(notedeck.options());
let context = &mut notedeck.app_context();
let dave = Dave::new(cc.wgpu_render_state.as_ref());
let columns = Damus::new(context, app_args);
let mut chrome = Chrome::default();
notedeck.check_args(columns.unrecognized_args())?;
chrome.add_app(NotedeckApp::Columns(Box::new(columns)));
chrome.add_app(NotedeckApp::Dave(Box::new(dave)));
if notedeck.has_option(NotedeckOptions::FeatureNotebook) {
chrome.add_app(NotedeckApp::Notebook(Box::default()));
}
chrome.set_active(0);
Ok(chrome)
}
pub fn toggle(&mut self) {
@@ -197,6 +245,16 @@ impl Chrome {
None
}
fn get_notebook(&mut self) -> Option<&mut Notebook> {
for app in &mut self.apps {
if let NotedeckApp::Notebook(notebook) = app {
return Some(notebook);
}
}
None
}
fn switch_to_dave(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Dave(_) = app {
@@ -205,6 +263,14 @@ impl Chrome {
}
}
fn switch_to_notebook(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Notebook(_) = app {
self.active = i as i32;
}
}
}
fn switch_to_columns(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Columns(_) = app {
@@ -321,7 +387,12 @@ impl Chrome {
});
strip.cell(|ui| {
if let Some(action) = self.toolbar(ui) {
let pk = ctx.accounts.get_selected_account().key.pubkey;
let unseen_notification =
unseen_notification(self.get_columns_app(), ctx.ndb, pk);
if let Some(action) = self.toolbar(ui, unseen_notification) {
got_action = Some(ChromePanelAction::Toolbar(action))
}
});
@@ -330,7 +401,7 @@ impl Chrome {
got_action
}
fn toolbar(&mut self, ui: &mut egui::Ui) -> Option<ToolbarAction> {
fn toolbar(&mut self, ui: &mut egui::Ui, unseen_notification: bool) -> Option<ToolbarAction> {
use egui_tabs::{TabColor, Tabs};
let rect = ui.available_rect_before_wrap();
@@ -374,7 +445,9 @@ impl Chrome {
action = Some(ToolbarAction::Dave);
}
}
} else if index == 2 && notifications_button(ui, btn_size).clicked() {
} else if index == 2
&& notifications_button(ui, btn_size, unseen_notification).clicked()
{
action = Some(ToolbarAction::Notifications);
}
@@ -426,14 +499,12 @@ impl Chrome {
ui.add(milestone_name(i18n));
ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode;
{
if columns_button(ui)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
self.active = 0;
}
}
ui.add_space(32.0);
if let Some(dave) = self.get_dave() {
@@ -444,8 +515,50 @@ impl Chrome {
self.switch_to_dave();
}
}
//ui.add_space(32.0);
if let Some(_notebook) = self.get_notebook() {
if notebook_button(ui)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
self.switch_to_notebook();
}
}
}
}
fn unseen_notification(
columns: Option<&mut Damus>,
ndb: &nostrdb::Ndb,
current_pk: notedeck::enostr::Pubkey,
) -> bool {
let Some(columns) = columns else {
return false;
};
let Some(tl) = columns
.timeline_cache
.get_mut(&TimelineKind::Notifications(current_pk))
else {
return false;
};
let freshness = &mut tl.current_view_mut().freshness;
freshness.update(|timestamp_last_viewed| {
let filter = notedeck_columns::timeline::kind::notifications_filter(&current_pk)
.since_mut(timestamp_last_viewed);
let txn = Transaction::new(ndb).expect("txn");
let Some(res) = ndb.query(&txn, &[filter], 1).ok() else {
return false;
};
!res.is_empty()
});
freshness.has_unseen()
}
impl notedeck::App for Chrome {
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
@@ -500,6 +613,7 @@ fn expanding_button(
light_img: egui::Image,
dark_img: egui::Image,
ui: &mut egui::Ui,
unseen_indicator: bool,
) -> egui::Response {
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let img = if ui.visuals().dark_mode {
@@ -511,16 +625,34 @@ fn expanding_button(
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
let cur_img_size = helper.scale_1d_pos(img_size);
img.paint_at(
ui,
helper
let paint_rect = helper
.get_animation_rect()
.shrink((max_size - cur_img_size) / 2.0),
);
.shrink((max_size - cur_img_size) / 2.0);
img.paint_at(ui, paint_rect);
if unseen_indicator {
paint_unseen_indicator(ui, paint_rect, helper.scale_1d_pos(3.0));
}
helper.take_animation_response()
}
fn paint_unseen_indicator(ui: &mut egui::Ui, rect: egui::Rect, radius: f32) {
let center = rect.center();
let top_right = rect.right_top();
let distance = center.distance(top_right);
let midpoint = {
let mut cur = center;
cur.x += distance / 2.0;
cur.y -= distance / 2.0;
cur
};
let painter = ui.painter_at(rect);
painter.circle_filled(midpoint, radius, notedeck_ui::colors::PINK);
}
fn support_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"help-button",
@@ -528,6 +660,7 @@ fn support_button(ui: &mut egui::Ui) -> egui::Response {
app_images::help_light_image(),
app_images::help_dark_image(),
ui,
false,
)
}
@@ -538,16 +671,18 @@ fn settings_button(ui: &mut egui::Ui) -> egui::Response {
app_images::settings_light_image(),
app_images::settings_dark_image(),
ui,
false,
)
}
fn notifications_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
fn notifications_button(ui: &mut egui::Ui, size: f32, unseen_indicator: bool) -> egui::Response {
expanding_button(
"notifications-button",
size,
app_images::notifications_light_image(),
app_images::notifications_dark_image(),
ui,
unseen_indicator,
)
}
@@ -558,6 +693,7 @@ fn home_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
app_images::home_light_image(),
app_images::home_dark_image(),
ui,
false,
)
}
@@ -568,6 +704,29 @@ fn columns_button(ui: &mut egui::Ui) -> egui::Response {
app_images::columns_image(),
app_images::columns_image(),
ui,
false,
)
}
fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"accounts-button",
24.0,
app_images::accounts_image().tint(ui.visuals().text_color()),
app_images::accounts_image(),
ui,
false,
)
}
fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"notebook-button",
40.0,
app_images::algo_image(),
app_images::algo_image(),
ui,
false,
)
}
@@ -681,11 +840,12 @@ fn chrome_handle_app_action(
ctx.global_wallet,
ctx.zaps,
ctx.img_cache,
&mut columns.view_state,
ui,
);
if let Some(action) = m_action {
let col = cols.column_mut(0);
let col = cols.selected_mut();
action.process(&mut col.router, &mut col.sheet_router);
}
@@ -693,6 +853,60 @@ fn chrome_handle_app_action(
}
}
fn columns_route_to_profile(
pk: &notedeck::enostr::Pubkey,
chrome: &mut Chrome,
ctx: &mut AppContext,
ui: &mut egui::Ui,
) {
chrome.switch_to_columns();
let Some(columns) = chrome.get_columns_app() else {
return;
};
let cols = columns
.decks_cache
.active_columns_mut(ctx.i18n, ctx.accounts)
.unwrap();
let router = cols.get_selected_router();
if router.routes().iter().any(|r| {
matches!(
r,
notedeck_columns::Route::Timeline(TimelineKind::Profile(_))
)
}) {
router.go_back();
return;
}
let txn = Transaction::new(ctx.ndb).unwrap();
let m_action = notedeck_columns::actionbar::execute_and_process_note_action(
notedeck::NoteAction::Profile(*pk),
ctx.ndb,
cols,
0,
&mut columns.timeline_cache,
&mut columns.threads,
ctx.note_cache,
ctx.pool,
&txn,
ctx.unknown_ids,
ctx.accounts,
ctx.global_wallet,
ctx.zaps,
ctx.img_cache,
&mut columns.view_state,
ui,
);
if let Some(action) = m_action {
let col = cols.selected_mut();
action.process(&mut col.router, &mut col.sheet_router);
}
}
fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let helper = AnimationHelper::new(ui, "pfp-button", egui::vec2(max_size, max_size));
@@ -708,6 +922,38 @@ fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
ui.put(helper.get_animation_rect(), &mut widget);
helper.take_animation_response()
// let selected = ctx.accounts.cache.selected();
// pfp_resp.context_menu(|ui| {
// for (pk, account) in &ctx.accounts.cache {
// let profile = ctx.ndb.get_profile_by_pubkey(&txn, pk).ok();
// let is_selected = *pk == selected.key.pubkey;
// let has_nsec = account.key.secret_key.is_some();
// let profile_peview_view = {
// let max_size = egui::vec2(ui.available_width(), 77.0);
// let resp = ui.allocate_response(max_size, egui::Sense::click());
// ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| {
// ui.add(
// &mut ProfilePic::new(ctx.img_cache, get_profile_url(profile.as_ref()))
// .size(24.0),
// )
// })
// };
// // if let Some(op) = profile_peview_view {
// // return_op = Some(match op {
// // ProfilePreviewAction::SwitchTo => AccountsViewResponse::SelectAccount(*pk),
// // ProfilePreviewAction::RemoveAccount => AccountsViewResponse::RemoveAccount(*pk),
// // });
// // }
// }
// // if ui.menu_image_button(image, add_contents).clicked() {
// // // ui.ctx().copy_text(url.to_owned());
// // ui.close_menu();
// // }
// });
}
/// The section of the chrome sidebar that starts at the
@@ -720,6 +966,7 @@ fn bottomup_sidebar(
ui.add_space(8.0);
let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand);
let accounts_resp = accounts_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
let theme_action = match ui.ctx().theme() {
@@ -761,7 +1008,7 @@ fn bottomup_sidebar(
.add(wallet_button())
.on_hover_cursor(egui::CursorIcon::PointingHand);
if ctx.args.debug {
if ctx.args.options.contains(NotedeckOptions::Debug) {
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
ui.weak(format!(
"{:10.1}",
@@ -791,6 +1038,9 @@ fn bottomup_sidebar(
}
if pfp_resp.clicked() {
let pk = ctx.accounts.get_selected_account().key.pubkey;
Some(ChromePanelAction::Profile(pk))
} else if accounts_resp.clicked() {
Some(ChromePanelAction::Account)
} else if settings_resp.clicked() {
Some(ChromePanelAction::Settings)
-151
View File
@@ -1,151 +0,0 @@
use egui::{FontData, FontDefinitions, FontTweak};
use std::collections::BTreeMap;
use std::sync::Arc;
use tracing::debug;
use notedeck::fonts::NamedFontFamily;
// Use gossip's approach to font loading. This includes japanese fonts
// for rending stuff from japanese users.
pub fn setup_fonts(ctx: &egui::Context) {
let mut font_data: BTreeMap<String, Arc<FontData>> = BTreeMap::new();
let mut families = BTreeMap::new();
font_data.insert(
"Onest".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
))),
);
font_data.insert(
"OnestMedium".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
))),
);
font_data.insert(
"DejaVuSans".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
))),
);
font_data.insert(
"OnestBold".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
))),
);
/*
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
font_data.insert(
"DejaVuSans".to_owned(),
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
);
font_data.insert(
"DejaVuSansBold".to_owned(),
FontData::from_static(include_bytes!(
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
)),
);
*/
font_data.insert(
"Inconsolata".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/Inconsolata-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.22, // This font is smaller than DejaVuSans
y_offset_factor: -0.18, // and too low
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
font_data.insert(
"NotoSansCJK".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
))),
);
font_data.insert(
"NotoSansThai".to_owned(),
Arc::new(FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoSansThai-Regular.ttf"
))),
);
// Some good looking emojis. Use as first priority:
font_data.insert(
"NotoEmoji".to_owned(),
Arc::new(
FontData::from_static(include_bytes!(
"../../../assets/fonts/NotoEmoji-Regular.ttf"
))
.tweak(FontTweak {
scale: 1.1, // make them a touch larger
y_offset_factor: 0.0,
y_offset: 0.0,
baseline_offset_factor: 0.0,
}),
),
);
let base_fonts = vec![
"DejaVuSans".to_owned(),
"NotoEmoji".to_owned(),
"NotoSansCJK".to_owned(),
"NotoSansThai".to_owned(),
];
let mut proportional = vec!["Onest".to_owned()];
proportional.extend(base_fonts.clone());
let mut medium = vec!["OnestMedium".to_owned()];
medium.extend(base_fonts.clone());
let mut mono = vec!["Inconsolata".to_owned()];
mono.extend(base_fonts.clone());
let mut bold = vec!["OnestBold".to_owned()];
bold.extend(base_fonts.clone());
let emoji = vec!["NotoEmoji".to_owned()];
families.insert(egui::FontFamily::Proportional, proportional);
families.insert(egui::FontFamily::Monospace, mono);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
medium,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
bold,
);
families.insert(
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
emoji,
);
debug!("fonts: {:?}", families);
let defs = FontDefinitions {
font_data,
families,
};
ctx.set_fonts(defs);
}
-2
View File
@@ -1,6 +1,4 @@
pub mod fonts;
pub mod setup;
pub mod theme;
#[cfg(target_os = "android")]
mod android;
+5 -41
View File
@@ -10,12 +10,7 @@ static GLOBAL: AccountingAllocator<std::alloc::System> =
AccountingAllocator::new(std::alloc::System);
use notedeck::{DataPath, DataPathType, Notedeck};
use notedeck_chrome::{
setup::{generate_native_options, setup_chrome},
Chrome, NotedeckApp,
};
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_chrome::{setup::generate_native_options, Chrome};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
@@ -91,29 +86,8 @@ async fn main() {
let ctx = &cc.egui_ctx;
let mut notedeck = Notedeck::new(ctx, base_path, &args);
let mut chrome = Chrome::new();
let columns = Damus::new(&mut notedeck.app_context(), &args);
let dave = Dave::new(cc.wgpu_render_state.as_ref());
setup_chrome(ctx, notedeck.args(), notedeck.theme());
// ensure we recognized all the arguments
let completely_unrecognized: Vec<String> = notedeck
.unrecognized_args()
.intersection(columns.unrecognized_args())
.cloned()
.collect();
assert!(
completely_unrecognized.is_empty(),
"unrecognized args: {completely_unrecognized:?}"
);
chrome.add_app(NotedeckApp::Columns(columns));
chrome.add_app(NotedeckApp::Dave(dave));
chrome.set_active(0);
notedeck.setup(ctx);
let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
notedeck.set_app(chrome);
Ok(Box::new(notedeck))
@@ -147,7 +121,8 @@ pub fn main() {
#[cfg(test)]
mod tests {
use super::{Damus, Notedeck};
use super::Notedeck;
use notedeck_columns::Damus;
use std::path::{Path, PathBuf};
fn create_tmp_dir() -> PathBuf {
@@ -212,17 +187,6 @@ mod tests {
let mut app_ctx = notedeck.app_context();
let app = Damus::new(&mut app_ctx, &args);
// ensure we recognized all the arguments
let completely_unrecognized: Vec<String> = unrecognized_args
.intersection(app.unrecognized_args())
.cloned()
.collect();
assert!(
completely_unrecognized.is_empty(),
"unrecognized args: {:?}",
completely_unrecognized
);
assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2);
let tl1 = app
+7 -1
View File
@@ -38,7 +38,13 @@ impl PreviewRunner {
"unrecognized args: {:?}",
notedeck.unrecognized_args()
);
setup_chrome(ctx, notedeck.args(), notedeck.theme());
setup_chrome(
ctx,
notedeck.args(),
notedeck.theme(),
notedeck.note_body_font_size(),
notedeck.zoom_factor(),
);
notedeck.set_app(PreviewApp::new(preview));
-49
View File
@@ -1,55 +1,6 @@
use crate::{fonts, theme};
use eframe::NativeOptions;
use egui::ThemePreference;
use notedeck::{AppSizeHandler, DataPath};
use notedeck_ui::app_images;
use tracing::info;
pub fn setup_chrome(ctx: &egui::Context, args: &notedeck::Args, theme: ThemePreference) {
let is_mobile = args
.is_mobile
.unwrap_or(notedeck::ui::is_compiled_as_mobile());
let is_oled = notedeck::ui::is_oled();
// Some people have been running notedeck in debug, let's catch that!
if !args.tests && cfg!(debug_assertions) && !args.debug {
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
println!("---------------------------------");
panic!();
}
ctx.options_mut(|o| {
info!("Loaded theme {:?} from disk", theme);
o.theme_preference = theme;
});
ctx.set_visuals_of(egui::Theme::Dark, theme::dark_mode(is_oled));
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
setup_cc(ctx, is_mobile);
}
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) {
fonts::setup_fonts(ctx);
if notedeck::ui::is_compiled_as_mobile() {
ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2);
}
//ctx.set_pixels_per_point(1.0);
//
//
//ctx.tessellation_options_mut(|to| to.feathering = false);
egui_extras::install_image_loaders(ctx);
ctx.options_mut(|o| {
o.input_options.max_click_duration = 0.4;
});
ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style));
}
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
-149
View File
@@ -1,149 +0,0 @@
use egui::{style::Interaction, Color32, FontId, Style, Visuals};
use notedeck::{ColorTheme, NotedeckTextStyle};
use strum::IntoEnumIterator;
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
// BACKGROUNDS
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
pub fn desktop_dark_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: DARKER_BG,
extreme_bg_color: DARK_ISH_BG,
text_color: Color32::WHITE,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: DARK_ISH_BG,
window_stroke_color: DARK_BG,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: DARK_ISH_BG,
noninteractive_weak_bg_fill: DARK_BG,
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: SEMI_DARKER_BG,
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
inactive_weak_bg_fill: SEMI_DARK_BG,
}
}
pub fn mobile_dark_color_theme() -> ColorTheme {
ColorTheme {
panel_fill: Color32::BLACK,
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
..desktop_dark_color_theme()
}
}
pub fn light_color_theme() -> ColorTheme {
ColorTheme {
// VISUALS
panel_fill: Color32::WHITE,
extreme_bg_color: LIGHTER_GRAY,
text_color: BLACK,
err_fg_color: RED_700,
warn_fg_color: ORANGE_700,
hyperlink_color: PURPLE,
selection_color: PURPLE_ALT,
// WINDOW
window_fill: Color32::WHITE,
window_stroke_color: DARKER_GRAY,
// NONINTERACTIVE WIDGET
noninteractive_bg_fill: Color32::WHITE,
noninteractive_weak_bg_fill: LIGHTER_GRAY,
noninteractive_bg_stroke_color: LIGHT_GRAY,
noninteractive_fg_stroke_color: GRAY_SECONDARY,
// INACTIVE WIDGET
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
inactive_bg_fill: LIGHTER_GRAY,
inactive_weak_bg_fill: LIGHTER_GRAY,
}
}
pub fn light_mode() -> Visuals {
notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light())
}
pub fn dark_mode(is_oled: bool) -> Visuals {
notedeck::theme::create_themed_visuals(
if is_oled {
mobile_dark_color_theme()
} else {
desktop_dark_color_theme()
},
Visuals::dark(),
)
}
/// Create custom text sizes for any FontSizes
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
let font_size = if is_mobile {
notedeck::fonts::mobile_font_size
} else {
notedeck::fonts::desktop_font_size
};
style.text_styles = NotedeckTextStyle::iter()
.map(|text_style| {
(
text_style.text_style(),
FontId::new(font_size(&text_style), text_style.font_family()),
)
})
.collect();
style.interaction = Interaction {
tooltip_delay: 0.1,
show_tooltips_only_when_still: false,
..Interaction::default()
};
// debug: show callstack for the current widget on hover if all
// modifier keys are pressed down.
#[cfg(feature = "debug-widget-callstack")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-widget-callstack` feature requires a debug build, \
release builds are unsupported."
);
style.debug.debug_on_hover_with_all_modifiers = true;
}
// debug: show an overlay on all interactive widgets
#[cfg(feature = "debug-interactive-widgets")]
{
#[cfg(not(debug_assertions))]
compile_error!(
"The `debug-interactive-widgets` feature requires a debug build, \
release builds are unsupported."
);
style.debug.show_interactive_widgets = true;
}
}
+2 -1
View File
@@ -11,6 +11,7 @@ description = "A tweetdeck-style notedeck app"
crate-type = ["lib", "cdylib"]
[dependencies]
opener = { workspace = true }
rmpv = { workspace = true }
bech32 = { workspace = true }
notedeck = { workspace = true }
@@ -31,7 +32,7 @@ image = { workspace = true }
indexmap = { workspace = true }
nostrdb = { workspace = true }
notedeck_ui = { workspace = true }
open = { workspace = true }
robius-open = { workspace = true }
poll-promise = { workspace = true }
puffin = { workspace = true, optional = true }
puffin_egui = { workspace = true, optional = true }
+15 -1
View File
@@ -8,6 +8,7 @@ use crate::{
},
ThreadSelection, TimelineCache, TimelineKind,
},
view_state::ViewState,
};
use enostr::{NoteId, Pubkey, RelayPool};
@@ -16,6 +17,7 @@ use notedeck::{
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
};
use notedeck_ui::media::MediaViewerFlags;
use tracing::error;
pub struct NewNotes {
@@ -51,6 +53,7 @@ fn execute_note_action(
global_wallet: &mut GlobalWallet,
zaps: &mut Zaps,
images: &mut Images,
view_state: &mut ViewState,
router_type: RouterType,
ui: &mut egui::Ui,
col: usize,
@@ -153,7 +156,16 @@ fn execute_note_action(
}
},
NoteAction::Media(media_action) => {
media_action.process(images);
media_action.on_view_media(|medias| {
view_state.media_viewer.media_info = medias.clone();
tracing::debug!("on_view_media {:?}", &medias);
view_state
.media_viewer
.flags
.set(MediaViewerFlags::Open, true);
});
media_action.process_default_media_actions(images)
}
}
@@ -180,6 +192,7 @@ pub fn execute_and_process_note_action(
global_wallet: &mut GlobalWallet,
zaps: &mut Zaps,
images: &mut Images,
view_state: &mut ViewState,
ui: &mut egui::Ui,
) -> Option<RouterAction> {
let router_type = {
@@ -204,6 +217,7 @@ pub fn execute_and_process_note_action(
global_wallet,
zaps,
images,
view_state,
router_type,
ui,
col,
+110 -46
View File
@@ -10,19 +10,21 @@ use crate::{
subscriptions::{SubKind, Subscriptions},
support::Support,
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
ui::{self, DesktopSidePanel, SidePanelAction},
ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction},
view_state::ViewState,
Result,
};
use egui_extras::{Size, StripBuilder};
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
use nostrdb::Transaction;
use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
Localization, UnknownIds,
Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds,
};
use notedeck_ui::{
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
NoteOptions,
};
use notedeck_ui::{jobs::JobsCache, NoteOptions};
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use std::time::Duration;
@@ -38,6 +40,7 @@ pub enum DamusState {
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
pub struct Damus {
state: DamusState,
pub decks_cache: DecksCache,
pub view_state: ViewState,
pub drafts: Drafts,
@@ -77,9 +80,7 @@ fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
columns.select_left();
}
egui::Key::BrowserBack | egui::Key::Escape => {
if let Some(column) = columns.selected_mut() {
column.router_mut().go_back();
}
columns.get_selected_router().go_back();
}
_ => {}
}
@@ -360,18 +361,54 @@ fn render_damus(
app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
damus
.note_options
.set(NoteOptions::Wide, is_narrow(ui.ctx()));
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
render_damus_mobile(damus, app_ctx, ui)
} else {
render_damus_desktop(damus, app_ctx, ui)
};
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
// We use this for keeping timestamps and things up to date
ui.ctx().request_repaint_after(Duration::from_secs(5));
app_action
}
/// Present a fullscreen media viewer if the FullscreenMedia AppOptions flag is set. This is
/// typically set by image carousels using a MediaAction's on_view_media callback when
/// an image is clicked
fn fullscreen_media_viewer_ui(
ui: &mut egui::Ui,
state: &mut MediaViewerState,
img_cache: &mut Images,
) {
if !state.should_show(ui) {
if state.scene_rect.is_some() {
// if we shouldn't show yet we will have a scene
// rect, then we should clear it for next time
tracing::debug!("fullscreen_media_viewer_ui: resetting scene rect");
state.scene_rect = None;
}
return;
}
let resp = MediaViewer::new(state).fullscreen(true).ui(img_cache, ui);
if resp.clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
fullscreen_media_close(state);
}
}
/// Close the fullscreen media player. This also resets the scene_rect state
fn fullscreen_media_close(state: &mut MediaViewerState) {
state.flags.set(MediaViewerFlags::Open, false);
}
/*
fn determine_key_storage_type() -> KeyStorageType {
#[cfg(target_os = "macos")]
@@ -393,47 +430,58 @@ fn determine_key_storage_type() -> KeyStorageType {
impl Damus {
/// Called once before the first frame.
pub fn new(ctx: &mut AppContext<'_>, args: &[String]) -> Self {
pub fn new(app_context: &mut AppContext<'_>, args: &[String]) -> Self {
// arg parsing
let (parsed_args, unrecognized_args) =
ColumnsArgs::parse(args, Some(ctx.accounts.selected_account_pubkey()));
ColumnsArgs::parse(args, Some(app_context.accounts.selected_account_pubkey()));
let account = ctx.accounts.selected_account_pubkey_bytes();
let account = app_context.accounts.selected_account_pubkey_bytes();
let mut timeline_cache = TimelineCache::default();
let mut options = AppOptions::default();
let tmp_columns = !parsed_args.columns.is_empty();
options.set(AppOptions::TmpColumns, tmp_columns);
options.set(
AppOptions::Debug,
app_context.args.options.contains(NotedeckOptions::Debug),
);
options.set(
AppOptions::SinceOptimize,
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
);
let decks_cache = if tmp_columns {
info!("DecksCache: loading from command line arguments");
let mut columns: Columns = Columns::new();
let txn = Transaction::new(ctx.ndb).unwrap();
let txn = Transaction::new(app_context.ndb).unwrap();
for col in &parsed_args.columns {
let timeline_kind = col.clone().into_timeline_kind();
if let Some(add_result) = columns.add_new_timeline_column(
&mut timeline_cache,
&txn,
ctx.ndb,
ctx.note_cache,
ctx.pool,
app_context.ndb,
app_context.note_cache,
app_context.pool,
&timeline_kind,
) {
add_result.process(
ctx.ndb,
ctx.note_cache,
app_context.ndb,
app_context.note_cache,
&txn,
&mut timeline_cache,
ctx.unknown_ids,
app_context.unknown_ids,
);
}
}
columns_to_decks_cache(ctx.i18n, columns, account)
} else if let Some(decks_cache) =
crate::storage::load_decks_cache(ctx.path, ctx.ndb, &mut timeline_cache, ctx.i18n)
{
columns_to_decks_cache(app_context.i18n, columns, account)
} else if let Some(decks_cache) = crate::storage::load_decks_cache(
app_context.path,
app_context.ndb,
&mut timeline_cache,
app_context.i18n,
) {
info!(
"DecksCache: loading from disk {}",
crate::storage::DECKS_CACHE_FILE
@@ -441,35 +489,16 @@ impl Damus {
decks_cache
} else {
info!("DecksCache: creating new with demo configuration");
DecksCache::new_with_demo_config(&mut timeline_cache, ctx)
//for (pk, _) in &ctx.accounts.cache {
DecksCache::new_with_demo_config(&mut timeline_cache, app_context)
//for (pk, _) in &app_context.accounts.cache {
// cache.add_deck_default(*pk);
//}
};
let settings = &app_context.settings;
let support = Support::new(ctx.path);
let mut note_options = NoteOptions::default();
note_options.set(
NoteOptions::Textmode,
parsed_args.is_flag_set(ColumnsFlag::Textmode),
);
note_options.set(
NoteOptions::ScrambleText,
parsed_args.is_flag_set(ColumnsFlag::Scramble),
);
note_options.set(
NoteOptions::HideMedia,
parsed_args.is_flag_set(ColumnsFlag::NoMedia),
);
note_options.set(
NoteOptions::ShowNoteClient,
parsed_args.is_flag_set(ColumnsFlag::ShowNoteClient),
);
options.set(AppOptions::Debug, ctx.args.debug);
options.set(
AppOptions::SinceOptimize,
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
);
let support = Support::new(app_context.path);
let note_options = get_note_options(parsed_args, settings);
let jobs = JobsCache::default();
@@ -551,6 +580,39 @@ impl Damus {
}
}
fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions {
let mut note_options = NoteOptions::default();
note_options.set(
NoteOptions::Textmode,
args.is_flag_set(ColumnsFlag::Textmode),
);
note_options.set(
NoteOptions::ScrambleText,
args.is_flag_set(ColumnsFlag::Scramble),
);
note_options.set(
NoteOptions::HideMedia,
args.is_flag_set(ColumnsFlag::NoMedia),
);
note_options.set(
NoteOptions::ShowNoteClientTop,
ShowSourceClientOption::Top == settings_handler.show_source_client().into()
|| args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
);
note_options.set(
NoteOptions::ShowNoteClientBottom,
ShowSourceClientOption::Bottom == settings_handler.show_source_client().into()
|| args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
);
note_options.set(
NoteOptions::RepliesNewestFirst,
settings_handler.show_replies_newest_first(),
);
note_options
}
/*
fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
let stroke = ui.style().interact(&response).fg_stroke;
@@ -572,6 +634,7 @@ fn render_damus_mobile(
let mut app_action: Option<AppAction> = None;
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
if !app.columns(app_ctx.accounts).columns().is_empty() {
let r = nav::render_nav(
active_col,
@@ -662,6 +725,7 @@ fn should_show_compose_button(decks: &DecksCache, accounts: &Accounts) -> bool {
Route::Reply(_) => false,
Route::Quote(_) => false,
Route::Relays => false,
Route::Settings => false,
Route::ComposeNote => false,
Route::AddColumn(_) => false,
Route::EditProfile(_) => false,
+6 -3
View File
@@ -11,7 +11,8 @@ pub enum ColumnsFlag {
Textmode,
Scramble,
NoMedia,
ShowNoteClient,
ShowNoteClientTop,
ShowNoteClientBottom,
}
pub struct ColumnsArgs {
@@ -53,8 +54,10 @@ impl ColumnsArgs {
res.clear_flag(ColumnsFlag::SinceOptimize);
} else if arg == "--scramble" {
res.set_flag(ColumnsFlag::Scramble);
} else if arg == "--show-note-client" {
res.set_flag(ColumnsFlag::ShowNoteClient);
} else if arg == "--show-note-client=top" {
res.set_flag(ColumnsFlag::ShowNoteClientTop);
} else if arg == "--show-note-client=bottom" {
res.set_flag(ColumnsFlag::ShowNoteClientBottom);
} else if arg == "--no-media" {
res.set_flag(ColumnsFlag::NoMedia);
} else if arg == "--filter" {
+19 -9
View File
@@ -1,5 +1,6 @@
use crate::{
actionbar::TimelineOpenResult,
drag::DragSwitch,
route::{Route, Router, SingletonRouter},
timeline::{Timeline, TimelineCache, TimelineKind},
};
@@ -13,6 +14,7 @@ use tracing::warn;
pub struct Column {
pub router: Router<Route>,
pub sheet_router: SingletonRouter<Route>,
pub drag: DragSwitch,
}
impl Column {
@@ -21,6 +23,7 @@ impl Column {
Column {
router,
sheet_router: SingletonRouter::default(),
drag: DragSwitch::default(),
}
}
@@ -156,11 +159,9 @@ impl Columns {
// Get the first router in the columns if there are columns present.
// Otherwise, create a new column picker and return the router
pub fn get_first_router(&mut self) -> &mut Router<Route> {
if self.columns.is_empty() {
self.new_column_picker();
}
self.columns[0].router_mut()
pub fn get_selected_router(&mut self) -> &mut Router<Route> {
self.ensure_column();
self.selected_mut().router_mut()
}
#[inline]
@@ -181,16 +182,25 @@ impl Columns {
Some(&self.columns[self.selected as usize])
}
#[inline]
pub fn selected_mut(&mut self) -> Option<&mut Column> {
// TODO(jb55): switch to non-empty container for columns?
fn ensure_column(&mut self) {
if self.columns.is_empty() {
return None;
self.new_column_picker();
}
Some(&mut self.columns[self.selected as usize])
}
/// Get the selected column. If you're looking to route something
/// and you're not sure which one to choose, use this one
#[inline]
pub fn selected_mut(&mut self) -> &mut Column {
self.ensure_column();
assert!(self.selected < self.columns.len() as i32);
&mut self.columns[self.selected as usize]
}
#[inline]
pub fn column_mut(&mut self, ind: usize) -> &mut Column {
self.ensure_column();
&mut self.columns[ind]
}
+1 -1
View File
@@ -35,7 +35,7 @@ impl DecksCache {
accounts: &notedeck::Accounts,
) -> Option<&mut Column> {
self.active_columns_mut(i18n, accounts)
.and_then(|ad| ad.selected_mut())
.map(|ad| ad.selected_mut())
}
pub fn selected_column(&self, accounts: &notedeck::Accounts) -> Option<&Column> {
+103
View File
@@ -0,0 +1,103 @@
#[derive(Default, Clone, Debug)]
pub struct DragSwitch {
state: Option<DragState>,
}
#[derive(Clone, Debug)]
struct DragState {
start_pos: egui::Pos2,
cur_direction: DragDirection,
}
impl DragSwitch {
/// should call BEFORE both drag directions get rendered
pub fn update(&mut self, horizontal: egui::Id, vertical: egui::Id, ctx: &egui::Context) {
let horiz_being_dragged = ctx.is_being_dragged(horizontal);
let vert_being_dragged = ctx.is_being_dragged(vertical);
if !horiz_being_dragged && !vert_being_dragged {
self.state = None;
return;
}
let Some(state) = &mut self.state else {
return;
};
let Some(cur_pos) = ctx.pointer_interact_pos() else {
return;
};
let dx = (state.start_pos.x - cur_pos.x).abs();
let dy = (state.start_pos.y - cur_pos.y).abs();
let new_direction = if dx > dy {
DragDirection::Horizontal
} else {
DragDirection::Vertical
};
if new_direction == DragDirection::Horizontal
&& state.cur_direction == DragDirection::Vertical
{
// drag is occuring mostly in the horizontal direction
ctx.set_dragged_id(horizontal);
let new_dir = DragDirection::Horizontal;
state.cur_direction = new_dir;
} else if new_direction == DragDirection::Vertical
&& state.cur_direction == DragDirection::Horizontal
{
// drag is occuring mostly in the vertical direction
let new_dir = DragDirection::Vertical;
state.cur_direction = new_dir;
ctx.set_dragged_id(vertical);
}
}
/// should call AFTER both drag directions rendered
pub fn check_for_drag_start(
&mut self,
ctx: &egui::Context,
horizontal: egui::Id,
vertical: egui::Id,
) {
let Some(drag_id) = ctx.drag_started_id() else {
return;
};
let cur_direction = if drag_id == horizontal {
DragDirection::Horizontal
} else if drag_id == vertical {
DragDirection::Vertical
} else {
return;
};
let Some(cur_pos) = ctx.pointer_interact_pos() else {
return;
};
self.state = Some(DragState {
start_pos: cur_pos,
cur_direction,
});
}
}
#[derive(Debug, PartialEq, Clone)]
enum DragDirection {
Horizontal,
Vertical,
}
pub fn get_drag_id(ui: &egui::Ui, scroll_id: egui::Id) -> egui::Id {
ui.id().with(egui::Id::new(scroll_id)).with("area")
}
// unfortunately a Frame makes a new id for the Ui
pub fn get_drag_id_through_frame(ui: &egui::Ui, scroll_id: egui::Id) -> egui::Id {
ui.id()
.with(egui::Id::new("child"))
.with(egui::Id::new(scroll_id))
.with("area")
}
+1
View File
@@ -12,6 +12,7 @@ pub mod column;
mod deck_state;
mod decks;
mod draft;
mod drag;
mod key_parsing;
pub mod login_manager;
mod media_upload;
+2 -2
View File
@@ -11,7 +11,7 @@ use sha2::{Digest, Sha256};
use url::Url;
use crate::Error;
use notedeck_ui::images::fetch_binary_from_disk;
use notedeck::media::images::fetch_binary_from_disk;
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
@@ -143,7 +143,7 @@ pub fn nip96_upload(
Err(e) => {
return Promise::from_ready(Err(Error::Generic(format!(
"could not read contents of file to upload: {e}"
))))
))));
}
};
@@ -474,12 +474,10 @@ impl TimelineSub {
let before = self.state.clone();
's: {
match &mut self.state {
SubState::NoSub { dependers } => {
*dependers -= 1;
}
SubState::NoSub { dependers } => *dependers = dependers.saturating_sub(1),
SubState::LocalOnly { local, dependers } => {
if *dependers > 1 {
*dependers -= 1;
*dependers = dependers.saturating_sub(1);
break 's;
}
@@ -492,7 +490,7 @@ impl TimelineSub {
}
SubState::RemoteOnly { remote, dependers } => {
if *dependers > 1 {
*dependers -= 1;
*dependers = dependers.saturating_sub(1);
break 's;
}
@@ -502,7 +500,7 @@ impl TimelineSub {
}
SubState::Unified { unified, dependers } => {
if *dependers > 1 {
*dependers -= 1;
*dependers = dependers.saturating_sub(1);
break 's;
}
+159 -34
View File
@@ -4,25 +4,28 @@ use crate::{
column::ColumnsAction,
deck_state::DeckState,
decks::{Deck, DecksAction, DecksCache},
drag::{get_drag_id, get_drag_id_through_frame},
options::AppOptions,
profile::{ProfileAction, SaveProfileChanges},
route::{Route, Router, SingletonRouter},
timeline::{
route::{render_thread_route, render_timeline_route},
TimelineCache,
TimelineCache, TimelineKind,
},
ui::{
self,
add_column::render_add_column_routes,
add_column::{render_add_column_routes, AddColumnView},
column::NavTitle,
configure_deck::ConfigureDeckView,
edit_deck::{EditDeckResponse, EditDeckView},
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType},
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
profile::EditProfileView,
search::{FocusState, SearchView},
settings::SettingsAction,
support::SupportView,
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
RelayView,
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
TimelineView,
},
Damus,
};
@@ -31,8 +34,8 @@ use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, P
use enostr::ProfileState;
use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{
get_current_default_msats, get_current_wallet, tr, ui::is_narrow, Accounts, AppContext,
NoteAction, NoteContext, RelayAction,
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
RelayAction,
};
use tracing::error;
@@ -60,6 +63,7 @@ pub enum RenderNavAction {
SwitchingAction(SwitchingAction),
WalletAction(WalletAction),
RelayAction(RelayAction),
SettingsAction(SettingsAction),
}
pub enum SwitchingAction {
@@ -454,6 +458,7 @@ fn process_render_nav_action(
ctx.global_wallet,
ctx.zaps,
ctx.img_cache,
&mut app.view_state,
ui,
)
}
@@ -480,6 +485,9 @@ fn process_render_nav_action(
.process_relay_action(ui.ctx(), ctx.pool, action);
None
}
RenderNavAction::SettingsAction(action) => {
action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx())
}
};
if let Some(action) = router_action {
@@ -497,13 +505,12 @@ fn process_render_nav_action(
fn render_nav_body(
ui: &mut egui::Ui,
app: &mut Damus,
ctx: &mut AppContext<'_>,
ctx: &mut AppContext,
top: &Route,
depth: usize,
col: usize,
inner_rect: egui::Rect,
) -> Option<RenderNavAction> {
let current_account_has_wallet = get_current_wallet(ctx.accounts, ctx.global_wallet).is_some();
let mut note_context = NoteContext {
ndb: ctx.ndb,
accounts: ctx.accounts,
@@ -514,8 +521,8 @@ fn render_nav_body(
job_pool: ctx.job_pool,
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
current_account_has_wallet,
i18n: ctx.i18n,
global_wallet: ctx.global_wallet,
};
match top {
Route::Timeline(kind) => {
@@ -538,6 +545,8 @@ fn render_nav_body(
scroll_to_top,
);
app.timeline_cache.set_fresh(kind);
// always clear the scroll_to_top request
if scroll_to_top {
app.options.remove(AppOptions::ScrollToTop);
@@ -573,6 +582,15 @@ fn render_nav_body(
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
.ui(ui)
.map(RenderNavAction::RelayAction),
Route::Settings => SettingsView::new(
ctx.settings.get_settings_mut(),
&mut note_context,
&mut app.note_options,
&mut app.jobs,
)
.ui(ui)
.map(RenderNavAction::SettingsAction),
Route::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
txn
@@ -596,15 +614,12 @@ fn render_nav_body(
return None;
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
let poster = ctx.accounts.selected_filled()?;
let action = {
let draft = app.drafts.reply_mut(note.id());
let response = egui::ScrollArea::vertical()
.show(ui, |ui| {
ui::PostReplyView::new(
let response = ui::PostReplyView::new(
&mut note_context,
poster,
draft,
@@ -612,11 +627,9 @@ fn render_nav_body(
inner_rect,
app.note_options,
&mut app.jobs,
col,
)
.id_source(id)
.show(ui)
})
.inner;
.show(ui);
response.action
};
@@ -637,14 +650,10 @@ fn render_nav_body(
return None;
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
let poster = ctx.accounts.selected_filled()?;
let draft = app.drafts.quote_mut(note.id());
let response = egui::ScrollArea::vertical()
.show(ui, |ui| {
crate::ui::note::QuoteRepostView::new(
let response = crate::ui::note::QuoteRepostView::new(
&mut note_context,
poster,
draft,
@@ -652,11 +661,9 @@ fn render_nav_body(
inner_rect,
app.note_options,
&mut app.jobs,
col,
)
.id_source(id)
.show(ui)
})
.inner;
.show(ui);
response.action.map(Into::into)
}
@@ -736,7 +743,7 @@ fn render_nav_body(
new_deck_state.clear();
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_first_router()
.get_selected_router()
.go_back();
}
resp
@@ -767,7 +774,7 @@ fn render_nav_body(
}
}
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_first_router()
.get_selected_router()
.go_back();
}
@@ -842,7 +849,7 @@ fn render_nav_body(
}
};
WalletView::new(state, ctx.i18n)
WalletView::new(state, ctx.i18n, ctx.clipboard)
.ui(ui)
.map(RenderNavAction::WalletAction)
}
@@ -929,13 +936,50 @@ pub fn render_nav(
}
};
let nav_response = Nav::new(
&app.columns(ctx.accounts)
let routes = app
.columns(ctx.accounts)
.column(col)
.router()
.routes()
.clone(),
)
.clone();
let nav = Nav::new(&routes).id_source(egui::Id::new(("nav", col)));
let drag_ids = 's: {
let Some(top_route) = &routes.last().cloned() else {
break 's None;
};
let Some(scroll_id) = get_scroll_id(
top_route,
app.columns(ctx.accounts)
.column(col)
.router()
.routes()
.len(),
&app.timeline_cache,
col,
) else {
break 's None;
};
let vertical_drag_id = if route_uses_frame(top_route) {
get_drag_id_through_frame(ui, scroll_id)
} else {
get_drag_id(ui, scroll_id)
};
let horizontal_drag_id = nav.drag_id(ui);
let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.column_mut(col)
.drag;
drag.update(horizontal_drag_id, vertical_drag_id, ui.ctx());
Some((horizontal_drag_id, vertical_drag_id))
};
let nav_response = nav
.navigating(
app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
@@ -948,7 +992,6 @@ pub fn render_nav(
.router_mut()
.returning,
)
.id_source(egui::Id::new(("nav", col)))
.show_mut(ui, |ui, render_type, nav| match render_type {
NavUiType::Title => NavTitle::new(
ctx.ndb,
@@ -971,5 +1014,87 @@ pub fn render_nav(
}
});
if let Some((horizontal_drag_id, vertical_drag_id)) = drag_ids {
let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.column_mut(col)
.drag;
drag.check_for_drag_start(ui.ctx(), horizontal_drag_id, vertical_drag_id);
}
RenderNavResponse::new(col, NotedeckNavResponse::Nav(Box::new(nav_response)))
}
fn get_scroll_id(
top: &Route,
depth: usize,
timeline_cache: &TimelineCache,
col: usize,
) -> Option<egui::Id> {
match top {
Route::Timeline(timeline_kind) => match timeline_kind {
TimelineKind::List(_)
| TimelineKind::Search(_)
| TimelineKind::Algo(_)
| TimelineKind::Notifications(_)
| TimelineKind::Universe
| TimelineKind::Hashtag(_)
| TimelineKind::Generic(_) => {
TimelineView::scroll_id(timeline_cache, timeline_kind, col)
}
TimelineKind::Profile(pubkey) => {
if depth > 1 {
Some(ProfileView::scroll_id(col, pubkey))
} else {
TimelineView::scroll_id(timeline_cache, timeline_kind, col)
}
}
},
Route::Thread(thread_selection) => Some(ThreadView::scroll_id(
thread_selection.selected_or_root(),
col,
)),
Route::Accounts(accounts_route) => match accounts_route {
crate::accounts::AccountsRoute::Accounts => Some(AccountsView::scroll_id()),
crate::accounts::AccountsRoute::AddAccount => None,
},
Route::Reply(note_id) => Some(PostReplyView::scroll_id(col, note_id.bytes())),
Route::Quote(note_id) => Some(QuoteRepostView::scroll_id(col, note_id.bytes())),
Route::Relays => Some(RelayView::scroll_id()),
Route::ComposeNote => Some(PostView::scroll_id()),
Route::AddColumn(add_column_route) => Some(AddColumnView::scroll_id(add_column_route)),
Route::EditProfile(_) => Some(EditProfileView::scroll_id()),
Route::Support => None,
Route::NewDeck => Some(ConfigureDeckView::scroll_id()),
Route::Search => Some(SearchView::scroll_id()),
Route::EditDeck(_) => None,
Route::Wallet(_) => None,
Route::CustomizeZapAmount(_) => None,
Route::Settings => None,
}
}
/// Does the corresponding View for the route use a egui::Frame to wrap the ScrollArea?
/// TODO(kernelkind): this is quite hacky...
fn route_uses_frame(route: &Route) -> bool {
match route {
Route::Accounts(accounts_route) => match accounts_route {
crate::accounts::AccountsRoute::Accounts => true,
crate::accounts::AccountsRoute::AddAccount => false,
},
Route::Relays => true,
Route::Timeline(_) => false,
Route::Thread(_) => false,
Route::Reply(_) => false,
Route::Quote(_) => false,
Route::Settings => false,
Route::ComposeNote => false,
Route::AddColumn(_) => false,
Route::EditProfile(_) => false,
Route::Support => false,
Route::NewDeck => false,
Route::Search => false,
Route::EditDeck(_) => false,
Route::Wallet(_) => false,
Route::CustomizeZapAmount(_) => false,
}
}
+57 -13
View File
@@ -1,4 +1,8 @@
use egui::{text::LayoutJob, TextBuffer, TextFormat};
use egui::{
text::{CCursor, CCursorRange, LayoutJob},
text_edit::TextEditOutput,
TextBuffer, TextEdit, TextFormat,
};
use enostr::{FullKeypair, Pubkey};
use nostrdb::{Note, NoteBuilder, NoteReply};
use std::{
@@ -270,6 +274,36 @@ impl Default for PostBuffer {
}
}
/// New cursor index (indexed by characters) after operation is performed
#[must_use = "must call MentionSelectedResponse::process"]
pub struct MentionSelectedResponse {
pub next_cursor_index: usize,
}
impl MentionSelectedResponse {
pub fn process(&self, ctx: &egui::Context, text_edit_output: &TextEditOutput) {
let text_edit_id = text_edit_output.response.id;
let Some(mut before_state) = TextEdit::load_state(ctx, text_edit_id) else {
return;
};
let mut new_cursor = text_edit_output
.galley
.from_ccursor(CCursor::new(self.next_cursor_index));
new_cursor.ccursor.prefer_next_row = true;
before_state
.cursor
.set_char_range(Some(CCursorRange::one(CCursor::new(
self.next_cursor_index,
))));
ctx.memory_mut(|mem| mem.request_focus(text_edit_id));
TextEdit::store_state(ctx, text_edit_id, before_state);
}
}
impl PostBuffer {
pub fn get_new_mentions_key(&mut self) -> usize {
let prev = self.mentions_key;
@@ -319,15 +353,21 @@ impl PostBuffer {
mention_key: usize,
full_name: &str,
pk: Pubkey,
) {
if let Some(info) = self.mentions.get(&mention_key) {
let text_start_index = info.start_index + 1;
self.delete_char_range(text_start_index..info.end_index);
self.insert_text(full_name, text_start_index);
self.select_full_mention(mention_key, pk);
} else {
) -> Option<MentionSelectedResponse> {
let Some(info) = self.mentions.get(&mention_key) else {
error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions);
}
return None;
};
let text_start_index = info.start_index + 1; // increment by one to exclude the mention indicator, '@'
self.delete_char_range(text_start_index..info.end_index);
let text_chars_inserted = self.insert_text(full_name, text_start_index);
self.select_full_mention(mention_key, pk);
let space_chars_inserted = self.insert_text(" ", text_start_index + text_chars_inserted);
Some(MentionSelectedResponse {
next_cursor_index: text_start_index + text_chars_inserted + space_chars_inserted,
})
}
pub fn delete_mention(&mut self, mention_key: usize) {
@@ -919,7 +959,7 @@ mod tests {
buf.select_mention_and_replace_name(0, "jb55", JB55());
assert_eq!(buf.as_str(), "@jb55 ");
buf.insert_text(" test", 5);
buf.insert_text("test", 6);
assert_eq!(buf.as_str(), "@jb55 test");
assert_eq!(buf.mentions.len(), 1);
@@ -1201,16 +1241,20 @@ mod tests {
buf.insert_text("@jb", 0);
buf.select_mention_and_replace_name(0, "jb55", JB55());
buf.insert_text(" test ", 5);
buf.insert_text("test ", 6);
assert_eq!(buf.as_str(), "@jb55 test ");
buf.insert_text("@kernel", 11);
buf.select_mention_and_replace_name(1, "KernelKind", KK());
buf.insert_text(" test", 22);
assert_eq!(buf.as_str(), "@jb55 test @KernelKind ");
buf.insert_text("test", 23);
assert_eq!(buf.as_str(), "@jb55 test @KernelKind test");
assert_eq!(buf.mentions.len(), 2);
buf.insert_text(" ", 5);
buf.insert_text("@els", 6);
assert_eq!(buf.as_str(), "@jb55 @elstest @KernelKind test");
assert_eq!(buf.mentions.len(), 3);
assert_eq!(buf.mentions.get(&2).unwrap().bounds(), 6..10);
buf.select_mention_and_replace_name(2, "elsat", JB55());
+1
View File
@@ -144,6 +144,7 @@ fn send_kind_3_event(ndb: &Ndb, pool: &mut RelayPool, accounts: &Accounts, actio
let ContactState::Received {
contacts: _,
note_key,
timestamp: _,
} = accounts.get_selected_account().data.contacts.get_state()
else {
return;
+18
View File
@@ -19,6 +19,7 @@ pub enum Route {
Reply(NoteId),
Quote(NoteId),
Relays,
Settings,
ComposeNote,
AddColumn(AddColumnRoute),
EditProfile(Pubkey),
@@ -47,6 +48,10 @@ impl Route {
Route::Relays
}
pub fn settings() -> Self {
Route::Settings
}
pub fn thread(thread_selection: ThreadSelection) -> Self {
Route::Thread(thread_selection)
}
@@ -110,6 +115,9 @@ impl Route {
Route::Relays => {
writer.write_token("relay");
}
Route::Settings => {
writer.write_token("settings");
}
Route::ComposeNote => {
writer.write_token("compose");
}
@@ -169,6 +177,12 @@ impl Route {
Ok(Route::Relays)
})
},
|p| {
p.parse_all(|p| {
p.parse_token("settings")?;
Ok(Route::Settings)
})
},
|p| {
p.parse_all(|p| {
p.parse_token("quote")?;
@@ -250,6 +264,9 @@ impl Route {
Route::Relays => {
ColumnTitle::formatted(tr!(i18n, "Relays", "Column title for relay management"))
}
Route::Settings => {
ColumnTitle::formatted(tr!(i18n, "Settings", "Column title for app settings"))
}
Route::Accounts(amr) => match amr {
AccountsRoute::Accounts => ColumnTitle::formatted(tr!(
i18n,
@@ -555,6 +572,7 @@ impl fmt::Display for Route {
write!(f, "{}", tr!("Quote", "Display name for quote composition"))
}
Route::Relays => write!(f, "{}", tr!("Relays", "Display name for relay management")),
Route::Settings => write!(f, "{}", tr!("Settings", "Display name for settings management")),
Route::Accounts(amr) => match amr {
AccountsRoute::Accounts => write!(
f,
+1 -1
View File
@@ -28,7 +28,7 @@ impl Support {
}
static MAX_LOG_LINES: usize = 500;
static SUPPORT_EMAIL: &str = "support@damus.io";
pub static SUPPORT_EMAIL: &str = "support+notedeck@damus.io";
static EMAIL_TEMPLATE: &str = concat!("version ", env!("CARGO_PKG_VERSION"), "\nCommit hash: ", env!("GIT_COMMIT_HASH"), "\n\nDescribe the bug you have encountered:\n<-- your statement here -->\n\n===== Paste your log below =====\n\n");
impl Support {
@@ -221,6 +221,14 @@ impl TimelineCache {
pub fn num_timelines(&self) -> usize {
self.timelines.len()
}
pub fn set_fresh(&mut self, kind: &TimelineKind) {
let Some(tl) = self.get_mut(kind) else {
return;
};
tl.current_view_mut().freshness.set_fresh();
}
}
/// Look for new thread notes since our last fetch
+12 -10
View File
@@ -471,11 +471,9 @@ impl TimelineKind {
},
// TODO: still need to update this to fetch likes, zaps, etc
TimelineKind::Notifications(pubkey) => FilterState::ready(vec![Filter::new()
.pubkeys([pubkey.bytes()])
.kinds([1])
.limit(default_limit())
.build()]),
TimelineKind::Notifications(pubkey) => {
FilterState::ready(vec![notifications_filter(pubkey)])
}
TimelineKind::Hashtag(hashtag) => {
let filters = hashtag
@@ -573,11 +571,7 @@ impl TimelineKind {
)),
TimelineKind::Notifications(pk) => {
let notifications_filter = Filter::new()
.pubkeys([pk.bytes()])
.kinds([1])
.limit(default_limit())
.build();
let notifications_filter = notifications_filter(&pk);
Some(Timeline::new(
TimelineKind::notifications(pk),
@@ -628,6 +622,14 @@ impl TimelineKind {
}
}
pub fn notifications_filter(pk: &Pubkey) -> Filter {
Filter::new()
.pubkeys([pk.bytes()])
.kinds([1])
.limit(default_limit())
.build()
}
#[derive(Debug)]
pub struct TitleNeedsDb<'a> {
kind: &'a TimelineKind,
+108 -2
View File
@@ -8,6 +8,7 @@ use crate::{
use notedeck::{
contacts::hybrid_contacts_filter,
debouncer::Debouncer,
filter::{self, HybridFilter},
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
NoteCache, NoteRef, UnknownIds,
@@ -16,8 +17,11 @@ use notedeck::{
use egui_virtual_list::VirtualList;
use enostr::{PoolRelay, Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
use std::cell::RefCell;
use std::rc::Rc;
use std::{
cell::RefCell,
time::{Duration, UNIX_EPOCH},
};
use std::{rc::Rc, time::SystemTime};
use tracing::{debug, error, info, warn};
@@ -103,6 +107,7 @@ pub struct TimelineTab {
pub selection: i32,
pub filter: ViewFilter,
pub list: Rc<RefCell<VirtualList>>,
pub freshness: NotesFreshness,
}
impl TimelineTab {
@@ -138,6 +143,7 @@ impl TimelineTab {
selection,
filter,
list,
freshness: NotesFreshness::default(),
}
}
@@ -607,6 +613,7 @@ pub fn fetch_contact_list(
ContactState::Received {
contacts: _,
note_key: _,
timestamp: _,
} => FilterState::GotRemote(filter::GotRemoteType::Contact),
};
@@ -726,6 +733,7 @@ pub fn is_timeline_ready(
let ContactState::Received {
contacts: _,
note_key,
timestamp: _,
} = accounts.get_selected_account().data.contacts.get_state()
else {
return false;
@@ -778,3 +786,101 @@ pub fn is_timeline_ready(
}
}
}
#[derive(Debug)]
pub struct NotesFreshness {
debouncer: Debouncer,
state: NotesFreshnessState,
}
#[derive(Debug)]
enum NotesFreshnessState {
Fresh {
timestamp_viewed: u64,
},
Stale {
have_unseen: bool,
timestamp_last_viewed: u64,
},
}
impl Default for NotesFreshness {
fn default() -> Self {
Self {
debouncer: Debouncer::new(Duration::from_secs(2)),
state: NotesFreshnessState::Stale {
have_unseen: true,
timestamp_last_viewed: 0,
},
}
}
}
impl NotesFreshness {
pub fn set_fresh(&mut self) {
if !self.debouncer.should_act() {
return;
}
self.state = NotesFreshnessState::Fresh {
timestamp_viewed: timestamp_now(),
};
self.debouncer.bounce();
}
pub fn update(&mut self, check_have_unseen: impl FnOnce(u64) -> bool) {
if !self.debouncer.should_act() {
return;
}
match &self.state {
NotesFreshnessState::Fresh { timestamp_viewed } => {
let Ok(dur) = SystemTime::now()
.duration_since(UNIX_EPOCH + Duration::from_secs(*timestamp_viewed))
else {
return;
};
if dur > Duration::from_secs(2) {
self.state = NotesFreshnessState::Stale {
have_unseen: check_have_unseen(*timestamp_viewed),
timestamp_last_viewed: *timestamp_viewed,
};
}
}
NotesFreshnessState::Stale {
have_unseen,
timestamp_last_viewed,
} => {
if *have_unseen {
return;
}
self.state = NotesFreshnessState::Stale {
have_unseen: check_have_unseen(*timestamp_last_viewed),
timestamp_last_viewed: *timestamp_last_viewed,
};
}
}
self.debouncer.bounce();
}
pub fn has_unseen(&self) -> bool {
match &self.state {
NotesFreshnessState::Fresh {
timestamp_viewed: _,
} => false,
NotesFreshnessState::Stale {
have_unseen,
timestamp_last_viewed: _,
} => *have_unseen,
}
}
}
fn timestamp_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
}
@@ -6,8 +6,8 @@ use crate::{
};
use enostr::Pubkey;
use notedeck::NoteContext;
use notedeck_ui::{jobs::JobsCache, NoteOptions};
use notedeck::{JobsCache, NoteContext};
use notedeck_ui::NoteOptions;
#[allow(clippy::too_many_arguments)]
pub fn render_timeline_route(
@@ -81,14 +81,17 @@ pub fn render_thread_route(
// default truncated everywher eelse
note_options.set(NoteOptions::Truncate, false);
// We need the reply lines in threads
note_options.set(NoteOptions::Wide, false);
ui::ThreadView::new(
threads,
selection.selected_or_root(),
note_options,
note_context,
jobs,
col,
)
.id_source(col)
.ui(ui)
.map(Into::into)
}
+10 -58
View File
@@ -405,32 +405,13 @@ fn direct_replies_filter_non_root(
let tmp_selected = *selected_note_id;
nostrdb::Filter::new()
.kinds([1])
.custom(move |n: nostrdb::Note<'_>| {
for tag in n.tags() {
if tag.count() < 4 {
continue;
.custom(move |note: nostrdb::Note<'_>| {
let reply = nostrdb::NoteReply::new(note.tags());
if reply.is_reply_to_root() {
return false;
}
let Some("e") = tag.get_str(0) else {
continue;
};
let Some(tagged_id) = tag.get_id(1) else {
continue;
};
if *tagged_id != tmp_selected {
// NOTE: if these aren't dereferenced a segfault occurs...
continue;
}
if let Some(data) = tag.get_str(3) {
if data == "reply" {
return true;
}
}
}
false
reply.reply().is_some_and(|r| r.id == &tmp_selected)
})
.event(root_id)
.build()
@@ -448,42 +429,13 @@ fn direct_replies_filter_non_root(
/// let tmp = *root_id;
/// .custom(move |_| { tmp }) // ✅
fn direct_replies_filter_root(root_id: &[u8; 32]) -> nostrdb::Filter {
let tmp_root = *root_id;
let moved_root_id = *root_id;
nostrdb::Filter::new()
.kinds([1])
.custom(move |n: nostrdb::Note<'_>| {
let mut contains_root = false;
for tag in n.tags() {
if tag.count() < 4 {
continue;
}
let Some("e") = tag.get_str(0) else {
continue;
};
if let Some(s) = tag.get_str(3) {
if s == "reply" {
return false;
}
}
let Some(tagged_id) = tag.get_id(1) else {
continue;
};
if *tagged_id != tmp_root {
continue;
}
if let Some(s) = tag.get_str(3) {
if s == "root" {
contains_root = true;
}
}
}
contains_root
.custom(move |note: nostrdb::Note<'_>| {
nostrdb::NoteReply::new(note.tags())
.reply_to_root()
.is_some_and(|r| r.id == &moved_root_id)
})
.event(root_id)
.build()
@@ -52,6 +52,7 @@ impl<'a> AccountsView<'a> {
ui.add_space(8.0);
scroll_area()
.id_salt(AccountsView::scroll_id())
.show(ui, |ui| {
Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n)
})
@@ -59,6 +60,10 @@ impl<'a> AccountsView<'a> {
})
}
pub fn scroll_id() -> egui::Id {
egui::Id::new("accounts")
}
fn show_accounts(
ui: &mut Ui,
accounts: &Accounts,
+7 -2
View File
@@ -64,14 +64,14 @@ enum AddColumnOption {
Individual(PubkeySource),
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default, Hash)]
pub enum AddAlgoRoute {
#[default]
Base,
LastPerPubkey,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub enum AddColumnRoute {
Base,
UndecidedNotification,
@@ -187,8 +187,13 @@ impl<'a> AddColumnView<'a> {
}
}
pub fn scroll_id(route: &AddColumnRoute) -> egui::Id {
egui::Id::new(("add_column", route))
}
pub fn ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
ScrollArea::vertical()
.id_salt(AddColumnView::scroll_id(&AddColumnRoute::Base))
.show(ui, |ui| {
let mut selected_option: Option<AddColumnResponse> = None;
for column_option_data in self.get_base_options(ui) {
@@ -481,6 +481,7 @@ impl<'a> NavTitle<'a> {
Route::AddColumn(_add_col_route) => None,
Route::Support => None,
Route::Relays => None,
Route::Settings => None,
Route::NewDeck => None,
Route::EditDeck(_) => None,
Route::EditProfile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
@@ -33,6 +33,10 @@ impl<'a> ConfigureDeckView<'a> {
self
}
pub fn scroll_id() -> egui::Id {
egui::Id::new("configure-deck")
}
pub fn ui(&mut self, ui: &mut Ui) -> Option<ConfigureDeckResponse> {
let title_font = egui::FontId::new(
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4),
@@ -261,6 +265,7 @@ fn glyph_options_ui(
) -> Option<char> {
let mut selected_glyph = None;
egui::ScrollArea::vertical()
.id_salt(ConfigureDeckView::scroll_id())
.max_height(max_height)
.show(ui, |ui| {
let max_width = ui.available_width();
@@ -11,19 +11,21 @@ use notedeck_ui::{
};
use tracing::error;
pub struct SearchResultsView<'a> {
/// Displays user profiles for the user to pick from.
/// Useful for manually typing a username and selecting the profile desired
pub struct MentionPickerView<'a> {
ndb: &'a Ndb,
txn: &'a Transaction,
img_cache: &'a mut Images,
results: &'a Vec<&'a [u8; 32]>,
}
pub enum SearchResultsResponse {
pub enum MentionPickerResponse {
SelectResult(Option<usize>),
DeleteMention,
}
impl<'a> SearchResultsView<'a> {
impl<'a> MentionPickerView<'a> {
pub fn new(
img_cache: &'a mut Images,
ndb: &'a Ndb,
@@ -38,8 +40,8 @@ impl<'a> SearchResultsView<'a> {
}
}
fn show(&mut self, ui: &mut egui::Ui, width: f32) -> SearchResultsResponse {
let mut search_results_selection = None;
fn show(&mut self, ui: &mut egui::Ui, width: f32) -> MentionPickerResponse {
let mut selection = None;
ui.vertical(|ui| {
for (i, res) in self.results.iter().enumerate() {
let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) {
@@ -54,16 +56,16 @@ impl<'a> SearchResultsView<'a> {
.add(user_result(&profile, self.img_cache, i, width))
.clicked()
{
search_results_selection = Some(i)
selection = Some(i)
}
}
});
SearchResultsResponse::SelectResult(search_results_selection)
MentionPickerResponse::SelectResult(selection)
}
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> SearchResultsResponse {
let widget_id = ui.id().with("search_results");
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> MentionPickerResponse {
let widget_id = ui.id().with("mention_results");
let area_resp = egui::Area::new(widget_id)
.order(egui::Order::Foreground)
.fixed_pos(rect.left_top())
@@ -72,10 +74,10 @@ impl<'a> SearchResultsView<'a> {
let inner_margin_size = 8.0;
egui::Frame::NONE
.fill(ui.visuals().panel_fill)
.inner_margin(inner_margin_size)
.show(ui, |ui| {
let width = rect.width() - (2.0 * inner_margin_size);
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
let close_button_resp = {
let close_button_size = 16.0;
let (close_section_rect, _) = ui.allocate_exact_size(
@@ -95,16 +97,16 @@ impl<'a> SearchResultsView<'a> {
.inner
};
ui.add_space(8.0);
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
let scroll_resp = ScrollArea::vertical()
.max_width(width)
.max_width(rect.width())
.auto_shrink(Vec2b::FALSE)
.show(ui, |ui| self.show(ui, width));
ui.advance_cursor_after_rect(rect);
if close_button_resp {
SearchResultsResponse::DeleteMention
MentionPickerResponse::DeleteMention
} else {
scroll_resp.inner
}
@@ -128,7 +130,18 @@ fn user_result<'a>(
let spacing = 8.0;
let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
let helper = AnimationHelper::new(ui, ("user_result", index), vec2(width, max_image));
let animation_rect = {
let max_width = ui.available_width();
let extra_width = (max_width - width) / 2.0;
let left = ui.cursor().left();
let (rect, _) =
ui.allocate_exact_size(vec2(width + extra_width, max_image), egui::Sense::click());
let (_, right) = rect.split_left_right_at_x(left + extra_width);
right
};
let helper = AnimationHelper::new_from_rect(ui, ("user_result", index), animation_rect);
let icon_rect = {
let r = helper.get_animation_rect();
+4 -1
View File
@@ -5,13 +5,14 @@ pub mod column;
pub mod configure_deck;
pub mod edit_deck;
pub mod images;
pub mod mentions_picker;
pub mod note;
pub mod post;
pub mod preview;
pub mod profile;
pub mod relay;
pub mod search;
pub mod search_results;
pub mod settings;
pub mod side_panel;
pub mod support;
pub mod thread;
@@ -24,6 +25,8 @@ pub use note::{PostReplyView, PostView};
pub use preview::{Preview, PreviewApp, PreviewConfig};
pub use profile::ProfileView;
pub use relay::RelayView;
pub use settings::SettingsView;
pub use settings::ShowSourceClientOption;
pub use side_panel::{DesktopSidePanel, SidePanelAction};
pub use thread::ThreadView;
pub use timeline::TimelineView;
+39 -28
View File
@@ -2,7 +2,7 @@ use crate::draft::{Draft, Drafts, MentionHint};
#[cfg(not(target_os = "android"))]
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
use crate::post::{downcast_post_buffer, MentionType, NewPost};
use crate::ui::search_results::SearchResultsView;
use crate::ui::mentions_picker::MentionPickerView;
use crate::ui::{self, Preview, PreviewConfig};
use crate::Result;
@@ -14,13 +14,12 @@ use egui::{
};
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
use nostrdb::{Ndb, Transaction};
use notedeck::media::gif::ensure_latest_texture;
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
use notedeck_ui::{
app_images,
blur::PixelDimensions,
context_menu::{input_context, PasteBehavior},
gif::{handle_repaint, retrieve_latest_texture},
images::{get_render_state, RenderState},
jobs::JobsCache,
note::render_note_preview,
NoteOptions, ProfilePic,
};
@@ -35,7 +34,6 @@ pub struct PostView<'a, 'd> {
draft: &'a mut Draft,
post_type: PostType,
poster: FilledKeypair<'a>,
id_source: Option<egui::Id>,
inner_rect: egui::Rect,
note_options: NoteOptions,
jobs: &'a mut JobsCache,
@@ -112,12 +110,10 @@ impl<'a, 'd> PostView<'a, 'd> {
note_options: NoteOptions,
jobs: &'a mut JobsCache,
) -> Self {
let id_source: Option<egui::Id> = None;
PostView {
note_context,
draft,
poster,
id_source,
post_type,
inner_rect,
note_options,
@@ -125,9 +121,12 @@ impl<'a, 'd> PostView<'a, 'd> {
}
}
pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
self.id_source = Some(egui::Id::new(id_source));
self
fn id() -> egui::Id {
egui::Id::new("post")
}
pub fn scroll_id() -> egui::Id {
PostView::id().with("scroll")
}
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
@@ -213,11 +212,13 @@ impl<'a, 'd> PostView<'a, 'd> {
let focused = out.response.has_focus();
ui.ctx().data_mut(|d| d.insert_temp(self.id(), focused));
ui.ctx()
.data_mut(|d| d.insert_temp(PostView::id(), focused));
out.response
}
// Displays the mention picker and handles when one is selected.
fn show_mention_hints(
&mut self,
txn: &nostrdb::Transaction,
@@ -273,7 +274,7 @@ impl<'a, 'd> PostView<'a, 'd> {
return;
};
let resp = SearchResultsView::new(
let resp = MentionPickerView::new(
self.note_context.img_cache,
self.note_context.ndb,
txn,
@@ -281,35 +282,40 @@ impl<'a, 'd> PostView<'a, 'd> {
)
.show_in_rect(hint_rect, ui);
let mut selection_made = None;
match resp {
ui::search_results::SearchResultsResponse::SelectResult(selection) => {
ui::mentions_picker::MentionPickerResponse::SelectResult(selection) => {
if let Some(hint_index) = selection {
if let Some(pk) = res.get(hint_index) {
let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk);
if let Some(made_selection) =
self.draft.buffer.select_mention_and_replace_name(
mention.index,
get_display_name(record.ok().as_ref()).name(),
Pubkey::new(**pk),
);
)
{
selection_made = Some(made_selection);
}
self.draft.cur_mention_hint = None;
}
}
}
ui::search_results::SearchResultsResponse::DeleteMention => {
ui::mentions_picker::MentionPickerResponse::DeleteMention => {
self.draft.buffer.delete_mention(mention.index)
}
}
if let Some(selection) = selection_made {
selection.process(ui.ctx(), textedit_output);
}
}
fn focused(&self, ui: &egui::Ui) -> bool {
ui.ctx()
.data(|d| d.get_temp::<bool>(self.id()).unwrap_or(false))
}
fn id(&self) -> egui::Id {
self.id_source.unwrap_or_else(|| egui::Id::new("post"))
.data(|d| d.get_temp::<bool>(PostView::id()).unwrap_or(false))
}
pub fn outer_margin() -> i8 {
@@ -321,6 +327,13 @@ impl<'a, 'd> PostView<'a, 'd> {
}
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
ScrollArea::vertical()
.id_salt(PostView::scroll_id())
.show(ui, |ui| self.ui_no_scroll(txn, ui))
.inner
}
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
let focused = self.focused(ui);
let stroke = if focused {
ui.visuals().selection.stroke
@@ -467,7 +480,7 @@ impl<'a, 'd> PostView<'a, 'd> {
self.note_context.img_cache,
cache_type,
url,
notedeck_ui::images::ImageType::Content,
notedeck::ImageType::Content(Some((width, height))),
);
render_post_view_media(
@@ -591,12 +604,10 @@ fn render_post_view_media(
.to_points(ui.pixels_per_point())
.to_vec();
let texture_handle = handle_repaint(
ui,
retrieve_latest_texture(url, render_state.gifs, renderable_media),
);
let texture_handle =
ensure_latest_texture(ui, url, render_state.gifs, renderable_media);
let img_resp = ui.add(
egui::Image::new(texture_handle)
egui::Image::new(&texture_handle)
.max_size(size)
.corner_radius(12.0),
);
@@ -802,13 +813,13 @@ mod preview {
let mut note_context = NoteContext {
ndb: app.ndb,
accounts: app.accounts,
global_wallet: app.global_wallet,
img_cache: app.img_cache,
note_cache: app.note_cache,
zaps: app.zaps,
pool: app.pool,
job_pool: app.job_pool,
unknown_ids: app.unknown_ids,
current_account_has_wallet: false,
clipboard: app.clipboard,
i18n: app.i18n,
};
@@ -4,16 +4,17 @@ use crate::{
ui::{self},
};
use egui::ScrollArea;
use enostr::{FilledKeypair, NoteId};
use notedeck::NoteContext;
use notedeck_ui::{jobs::JobsCache, NoteOptions};
use notedeck::{JobsCache, NoteContext};
use notedeck_ui::NoteOptions;
pub struct QuoteRepostView<'a, 'd> {
note_context: &'a mut NoteContext<'d>,
poster: FilledKeypair<'a>,
draft: &'a mut Draft,
quoting_note: &'a nostrdb::Note<'a>,
id_source: Option<egui::Id>,
scroll_id: egui::Id,
inner_rect: egui::Rect,
note_options: NoteOptions,
jobs: &'a mut JobsCache,
@@ -29,22 +30,36 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
inner_rect: egui::Rect,
note_options: NoteOptions,
jobs: &'a mut JobsCache,
col: usize,
) -> Self {
let id_source: Option<egui::Id> = None;
QuoteRepostView {
note_context,
poster,
draft,
quoting_note,
id_source,
scroll_id: QuoteRepostView::scroll_id(col, quoting_note.id()),
inner_rect,
note_options,
jobs,
}
}
fn id(col: usize, note_id: &[u8; 32]) -> egui::Id {
egui::Id::new(("quote_repost", col, note_id))
}
pub fn scroll_id(col: usize, note_id: &[u8; 32]) -> egui::Id {
QuoteRepostView::id(col, note_id).with("scroll")
}
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
let id = self.id();
ScrollArea::vertical()
.id_salt(self.scroll_id)
.show(ui, |ui| self.show_internal(ui))
.inner
}
fn show_internal(&mut self, ui: &mut egui::Ui) -> PostResponse {
let quoting_note_id = self.quoting_note.id();
let post_resp = ui::PostView::new(
@@ -56,18 +71,7 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
self.note_options,
self.jobs,
)
.id_source(id)
.ui(self.quoting_note.txn().unwrap(), ui);
.ui_no_scroll(self.quoting_note.txn().unwrap(), ui);
post_resp
}
pub fn id_source(mut self, id: egui::Id) -> Self {
self.id_source = Some(id);
self
}
pub fn id(&self) -> egui::Id {
self.id_source
.unwrap_or_else(|| egui::Id::new("quote-repost-view"))
}
}
+18 -15
View File
@@ -4,10 +4,9 @@ use crate::ui::{
note::{PostAction, PostResponse, PostType},
};
use egui::{Rect, Response, Ui};
use egui::{Rect, Response, ScrollArea, Ui};
use enostr::{FilledKeypair, NoteId};
use notedeck::NoteContext;
use notedeck_ui::jobs::JobsCache;
use notedeck::{JobsCache, NoteContext};
use notedeck_ui::{NoteOptions, NoteView, ProfilePic};
pub struct PostReplyView<'a, 'd> {
@@ -15,7 +14,7 @@ pub struct PostReplyView<'a, 'd> {
poster: FilledKeypair<'a>,
draft: &'a mut Draft,
note: &'a nostrdb::Note<'a>,
id_source: Option<egui::Id>,
scroll_id: egui::Id,
inner_rect: egui::Rect,
note_options: NoteOptions,
jobs: &'a mut JobsCache,
@@ -31,31 +30,37 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
inner_rect: egui::Rect,
note_options: NoteOptions,
jobs: &'a mut JobsCache,
col: usize,
) -> Self {
let id_source: Option<egui::Id> = None;
PostReplyView {
note_context,
poster,
draft,
note,
id_source,
scroll_id: PostReplyView::scroll_id(col, note.id()),
inner_rect,
note_options,
jobs,
}
}
pub fn id_source(mut self, id: egui::Id) -> Self {
self.id_source = Some(id);
self
fn id(col: usize, note_id: &[u8; 32]) -> egui::Id {
egui::Id::new(("reply_view", col, note_id))
}
pub fn id(&self) -> egui::Id {
self.id_source
.unwrap_or_else(|| egui::Id::new("post-reply-view"))
pub fn scroll_id(col: usize, note_id: &[u8; 32]) -> egui::Id {
PostReplyView::id(col, note_id).with("scroll")
}
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
ScrollArea::vertical()
.id_salt(self.scroll_id)
.show(ui, |ui| self.show_internal(ui))
.inner
}
// no scroll
fn show_internal(&mut self, ui: &mut egui::Ui) -> PostResponse {
ui.vertical(|ui| {
let avail_rect = ui.available_rect_before_wrap();
@@ -81,7 +86,6 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
})
.inner;
let id = self.id();
let replying_to = self.note.id();
let rect_before_post = ui.min_rect();
@@ -95,8 +99,7 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
self.note_options,
self.jobs,
)
.id_source(id)
.ui(self.note.txn().unwrap(), ui)
.ui_no_scroll(self.note.txn().unwrap(), ui)
};
post_response.action = post_response
@@ -24,9 +24,14 @@ impl<'a> EditProfileView<'a> {
}
}
pub fn scroll_id() -> egui::Id {
egui::Id::new("edit_profile")
}
// return true to save
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
ScrollArea::vertical()
.id_salt(EditProfileView::scroll_id())
.show(ui, |ui| {
banner(ui, self.state.banner(), 188.0);
+10 -6
View File
@@ -6,6 +6,7 @@ use enostr::Pubkey;
use nostrdb::{ProfileRecord, Transaction};
use notedeck::{tr, Localization};
use notedeck_ui::profile::follow_button;
use robius_open::Uri;
use tracing::error;
use crate::{
@@ -13,12 +14,11 @@ use crate::{
ui::timeline::{tabs_ui, TimelineTabView},
};
use notedeck::{
name::get_display_name, profile::get_profile_url, IsFollowing, NoteAction, NoteContext,
NotedeckTextStyle,
name::get_display_name, profile::get_profile_url, IsFollowing, JobsCache, NoteAction,
NoteContext, NotedeckTextStyle,
};
use notedeck_ui::{
app_images,
jobs::JobsCache,
profile::{about_section_widget, banner, display_name_widget},
NoteOptions, ProfilePic,
};
@@ -59,8 +59,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
}
}
pub fn scroll_id(col_id: usize, profile_pubkey: &Pubkey) -> egui::Id {
egui::Id::new(("profile_scroll", col_id, profile_pubkey))
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> {
let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey));
let scroll_id = ProfileView::scroll_id(self.col_id, self.pubkey);
let offset_id = scroll_id.with("scroll_offset");
let mut scroll_area = ScrollArea::vertical().id_salt(scroll_id);
@@ -282,8 +286,8 @@ fn handle_link(ui: &mut egui::Ui, website_url: &str) {
.interact(Sense::click())
.clicked()
{
if let Err(e) = open::that(website_url) {
error!("Failed to open URL {} because: {}", website_url, e);
if let Err(e) = Uri::new(website_url).open() {
error!("Failed to open URL {} because: {:?}", website_url, e);
};
}
}
+5
View File
@@ -36,6 +36,7 @@ impl RelayView<'_> {
ui.add_space(8.0);
egui::ScrollArea::vertical()
.id_salt(RelayView::scroll_id())
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
@@ -51,6 +52,10 @@ impl RelayView<'_> {
action
}
pub fn scroll_id() -> egui::Id {
egui::Id::new("relay_scroll")
}
}
impl<'a> RelayView<'a> {
+12 -7
View File
@@ -5,11 +5,11 @@ use state::TypingType;
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
use egui_winit::clipboard::Clipboard;
use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{tr, tr_plural, Localization, NoteAction, NoteContext, NoteRef};
use notedeck::{tr, tr_plural, JobsCache, Localization, NoteAction, NoteContext, NoteRef};
use notedeck_ui::{
context_menu::{input_context, PasteBehavior},
icons::search_icon,
jobs::JobsCache,
padding, NoteOptions,
};
use std::time::{Duration, Instant};
@@ -19,7 +19,7 @@ mod state;
pub use state::{FocusState, SearchQueryState, SearchState};
use super::search_results::{SearchResultsResponse, SearchResultsView};
use super::mentions_picker::{MentionPickerResponse, MentionPickerView};
pub struct SearchView<'a, 'd> {
query: &'a mut SearchQueryState,
@@ -76,7 +76,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
break 's;
};
let search_res = SearchResultsView::new(
let search_res = MentionPickerView::new(
self.note_context.img_cache,
self.note_context.ndb,
self.txn,
@@ -85,7 +85,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
.show_in_rect(ui.available_rect_before_wrap(), ui);
search_action = match search_res {
SearchResultsResponse::SelectResult(Some(index)) => {
MentionPickerResponse::SelectResult(Some(index)) => {
let Some(pk_bytes) = results.get(index) else {
break 's;
};
@@ -103,8 +103,8 @@ impl<'a, 'd> SearchView<'a, 'd> {
new_search_text: format!("@{username}"),
})
}
SearchResultsResponse::DeleteMention => Some(SearchAction::CloseMention),
SearchResultsResponse::SelectResult(None) => break 's,
MentionPickerResponse::DeleteMention => Some(SearchAction::CloseMention),
MentionPickerResponse::SelectResult(None) => break 's,
};
}
SearchState::PerformSearch(search_type) => {
@@ -151,6 +151,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
fn show_search_results(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
egui::ScrollArea::vertical()
.id_salt(SearchView::scroll_id())
.show(ui, |ui| {
let reversed = false;
TimelineTabView::new(
@@ -165,6 +166,10 @@ impl<'a, 'd> SearchView<'a, 'd> {
})
.inner
}
pub fn scroll_id() -> egui::Id {
egui::Id::new("search_results")
}
}
fn execute_search(
+657
View File
@@ -0,0 +1,657 @@
use egui::{
vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference,
};
use enostr::NoteId;
use nostrdb::Transaction;
use notedeck::{
tr,
ui::{is_narrow, richtext_small},
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
};
use notedeck_ui::{NoteOptions, NoteView};
use strum::Display;
use crate::{nav::RouterAction, Damus, Route};
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
const THEME_LIGHT: &str = "Light";
const THEME_DARK: &str = "Dark";
const MIN_ZOOM: f32 = 0.5;
const MAX_ZOOM: f32 = 3.0;
const ZOOM_STEP: f32 = 0.1;
const RESET_ZOOM: f32 = 1.0;
#[derive(Clone, Copy, PartialEq, Eq, Display)]
pub enum ShowSourceClientOption {
Hide,
Top,
Bottom,
}
impl From<ShowSourceClientOption> for String {
fn from(show_option: ShowSourceClientOption) -> Self {
match show_option {
ShowSourceClientOption::Hide => "hide".to_string(),
ShowSourceClientOption::Top => "top".to_string(),
ShowSourceClientOption::Bottom => "bottom".to_string(),
}
}
}
impl From<NoteOptions> for ShowSourceClientOption {
fn from(note_options: NoteOptions) -> Self {
if note_options.contains(NoteOptions::ShowNoteClientTop) {
ShowSourceClientOption::Top
} else if note_options.contains(NoteOptions::ShowNoteClientBottom) {
ShowSourceClientOption::Bottom
} else {
ShowSourceClientOption::Hide
}
}
}
impl From<String> for ShowSourceClientOption {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"hide" => Self::Hide,
"top" => Self::Top,
"bottom" => Self::Bottom,
_ => Self::Hide, // default fallback
}
}
}
impl ShowSourceClientOption {
pub fn set_note_options(self, note_options: &mut NoteOptions) {
match self {
Self::Hide => {
note_options.set(NoteOptions::ShowNoteClientTop, false);
note_options.set(NoteOptions::ShowNoteClientBottom, false);
}
Self::Bottom => {
note_options.set(NoteOptions::ShowNoteClientTop, false);
note_options.set(NoteOptions::ShowNoteClientBottom, true);
}
Self::Top => {
note_options.set(NoteOptions::ShowNoteClientTop, true);
note_options.set(NoteOptions::ShowNoteClientBottom, false);
}
}
}
fn label(&self, i18n: &mut Localization) -> String {
match self {
Self::Hide => tr!(
i18n,
"Hide",
"Option in settings section to hide the source client label in note display"
),
Self::Top => tr!(
i18n,
"Top",
"Option in settings section to show the source client label at the top of the note"
),
Self::Bottom => tr!(
i18n,
"Bottom",
"Option in settings section to show the source client label at the bottom of the note"
),
}
}
}
pub enum SettingsAction {
SetZoomFactor(f32),
SetTheme(ThemePreference),
SetShowSourceClient(ShowSourceClientOption),
SetLocale(LanguageIdentifier),
SetRepliestNewestFirst(bool),
SetNoteBodyFontSize(f32),
OpenRelays,
OpenCacheFolder,
ClearCacheFolder,
}
impl SettingsAction {
pub fn process_settings_action<'a>(
self,
app: &mut Damus,
settings: &'a mut SettingsHandler,
i18n: &'a mut Localization,
img_cache: &mut Images,
ctx: &egui::Context,
) -> Option<RouterAction> {
let mut route_action: Option<RouterAction> = None;
match self {
Self::OpenRelays => {
route_action = Some(RouterAction::route_to(Route::Relays));
}
Self::SetZoomFactor(zoom_factor) => {
ctx.set_zoom_factor(zoom_factor);
settings.set_zoom_factor(zoom_factor);
}
Self::SetShowSourceClient(option) => {
option.set_note_options(&mut app.note_options);
settings.set_show_source_client(option);
}
Self::SetTheme(theme) => {
ctx.set_theme(theme);
settings.set_theme(theme);
}
Self::SetLocale(language) => {
if i18n.set_locale(language.clone()).is_ok() {
settings.set_locale(language.to_string());
}
}
Self::SetRepliestNewestFirst(value) => {
app.note_options.set(NoteOptions::RepliesNewestFirst, value);
settings.set_show_replies_newest_first(value);
}
Self::OpenCacheFolder => {
use opener;
let _ = opener::open(img_cache.base_path.clone());
}
Self::ClearCacheFolder => {
let _ = img_cache.clear_folder_contents();
}
Self::SetNoteBodyFontSize(size) => {
let mut style = (*ctx.style()).clone();
style.text_styles.insert(
NotedeckTextStyle::NoteBody.text_style(),
FontId::proportional(size),
);
ctx.set_style(style);
settings.set_note_body_font_size(size);
}
}
route_action
}
}
pub struct SettingsView<'a> {
settings: &'a mut Settings,
note_context: &'a mut NoteContext<'a>,
note_options: &'a mut NoteOptions,
jobs: &'a mut JobsCache,
}
fn settings_group<S>(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui))
where
S: Into<String>,
{
Frame::group(ui.style())
.fill(ui.style().visuals.widgets.open.bg_fill)
.inner_margin(10.0)
.show(ui, |ui| {
ui.label(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()));
ui.separator();
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
contents(ui)
});
});
}
impl<'a> SettingsView<'a> {
pub fn new(
settings: &'a mut Settings,
note_context: &'a mut NoteContext<'a>,
note_options: &'a mut NoteOptions,
jobs: &'a mut JobsCache,
) -> Self {
Self {
settings,
note_context,
note_options,
jobs,
}
}
/// Get the localized name for a language identifier
fn get_selected_language_name(&mut self) -> String {
if let Ok(lang_id) = self.settings.locale.parse::<LanguageIdentifier>() {
self.note_context
.i18n
.get_locale_native_name(&lang_id)
.map(|s| s.to_owned())
.unwrap_or_else(|| lang_id.to_string())
} else {
self.settings.locale.clone()
}
}
pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action = None;
let title = tr!(
self.note_context.i18n,
"Appearance",
"Label for appearance settings section",
);
settings_group(ui, title, |ui| {
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Font size:",
"Label for font size, Appearance settings section",
)));
if ui
.add(
egui::Slider::new(&mut self.settings.note_body_font_size, 8.0..=32.0)
.text(""),
)
.changed()
{
action = Some(SettingsAction::SetNoteBodyFontSize(
self.settings.note_body_font_size,
));
};
if ui
.button(richtext_small(tr!(
self.note_context.i18n,
"Reset",
"Label for reset note body font size, Appearance settings section",
)))
.clicked()
{
action = Some(SettingsAction::SetNoteBodyFontSize(
DEFAULT_NOTE_BODY_FONT_SIZE,
));
}
});
let txn = Transaction::new(self.note_context.ndb).unwrap();
if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) {
if let Ok(preview_note) =
self.note_context.ndb.get_note_by_id(&txn, note_id.bytes())
{
notedeck_ui::padding(8.0, ui, |ui| {
if is_narrow(ui.ctx()) {
ui.set_max_width(ui.available_width());
}
NoteView::new(
self.note_context,
&preview_note,
*self.note_options,
self.jobs,
)
.actionbar(false)
.options_button(false)
.show(ui);
});
ui.separator();
}
}
let current_zoom = ui.ctx().zoom_factor();
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Zoom Level:",
"Label for zoom level, Appearance settings section",
)));
let min_reached = current_zoom <= MIN_ZOOM;
let max_reached = current_zoom >= MAX_ZOOM;
if ui
.add_enabled(
!min_reached,
Button::new(
RichText::new("-").text_style(NotedeckTextStyle::Small.text_style()),
),
)
.clicked()
{
let new_zoom = (current_zoom - ZOOM_STEP).max(MIN_ZOOM);
action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
ui.label(
RichText::new(format!("{:.0}%", current_zoom * 100.0))
.text_style(NotedeckTextStyle::Small.text_style()),
);
if ui
.add_enabled(
!max_reached,
Button::new(
RichText::new("+").text_style(NotedeckTextStyle::Small.text_style()),
),
)
.clicked()
{
let new_zoom = (current_zoom + ZOOM_STEP).min(MAX_ZOOM);
action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
if ui
.button(richtext_small(tr!(
self.note_context.i18n,
"Reset",
"Label for reset zoom level, Appearance settings section",
)))
.clicked()
{
action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM));
}
});
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Language:",
"Label for language, Appearance settings section",
)));
//
ComboBox::from_label("")
.selected_text(self.get_selected_language_name())
.show_ui(ui, |ui| {
for lang in self.note_context.i18n.get_available_locales() {
let name = self
.note_context
.i18n
.get_locale_native_name(lang)
.map(|s| s.to_owned())
.unwrap_or_else(|| lang.to_string());
if ui
.selectable_value(&mut self.settings.locale, lang.to_string(), name)
.clicked()
{
action = Some(SettingsAction::SetLocale(lang.to_owned()))
}
}
});
});
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Theme:",
"Label for theme, Appearance settings section",
)));
if ui
.selectable_value(
&mut self.settings.theme,
ThemePreference::Light,
richtext_small(tr!(
self.note_context.i18n,
THEME_LIGHT,
"Label for Theme Light, Appearance settings section",
)),
)
.clicked()
{
action = Some(SettingsAction::SetTheme(ThemePreference::Light));
}
if ui
.selectable_value(
&mut self.settings.theme,
ThemePreference::Dark,
richtext_small(tr!(
self.note_context.i18n,
THEME_DARK,
"Label for Theme Dark, Appearance settings section",
)),
)
.clicked()
{
action = Some(SettingsAction::SetTheme(ThemePreference::Dark));
}
});
});
action
}
pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let id = ui.id();
let mut action: Option<SettingsAction> = None;
let title = tr!(
self.note_context.i18n,
"Storage",
"Label for storage settings section"
);
settings_group(ui, title, |ui| {
ui.horizontal_wrapped(|ui| {
let static_imgs_size = self
.note_context
.img_cache
.static_imgs
.cache_size
.lock()
.unwrap();
let gifs_size = self.note_context.img_cache.gifs.cache_size.lock().unwrap();
ui.label(
RichText::new(format!(
"{} {}",
tr!(
self.note_context.i18n,
"Image cache size:",
"Label for Image cache size, Storage settings section"
),
format_size(
[static_imgs_size, gifs_size]
.iter()
.fold(0_u64, |acc, cur| acc + cur.unwrap_or_default())
)
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
ui.end_row();
if !notedeck::ui::is_compiled_as_mobile()
&& ui
.button(richtext_small(tr!(
self.note_context.i18n,
"View folder",
"Label for view folder button, Storage settings section",
)))
.clicked()
{
action = Some(SettingsAction::OpenCacheFolder);
}
let clearcache_resp = ui.button(
richtext_small(tr!(
self.note_context.i18n,
"Clear cache",
"Label for clear cache button, Storage settings section",
))
.color(Color32::LIGHT_RED),
);
let id_clearcache = id.with("clear_cache");
if clearcache_resp.clicked() {
ui.data_mut(|d| d.insert_temp(id_clearcache, true));
}
if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) {
let mut confirm_pressed = false;
clearcache_resp.show_tooltip_ui(|ui| {
let confirm_resp = ui.button(tr!(
self.note_context.i18n,
"Confirm",
"Label for confirm clear cache, Storage settings section"
));
if confirm_resp.clicked() {
confirm_pressed = true;
}
if confirm_resp.clicked()
|| ui
.button(tr!(
self.note_context.i18n,
"Cancel",
"Label for cancel clear cache, Storage settings section"
))
.clicked()
{
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
}
});
if confirm_pressed {
action = Some(SettingsAction::ClearCacheFolder);
} else if !confirm_pressed && clearcache_resp.clicked_elsewhere() {
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
}
};
});
});
action
}
fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action = None;
let title = tr!(
self.note_context.i18n,
"Others",
"Label for others settings section"
);
settings_group(ui, title, |ui| {
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Sort replies newest first",
"Label for Sort replies newest first, others settings section",
)));
if ui
.toggle_value(
&mut self.settings.show_replies_newest_first,
RichText::new(tr!(self.note_context.i18n, "ON", "ON"))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.changed()
{
action = Some(SettingsAction::SetRepliestNewestFirst(
self.settings.show_replies_newest_first,
));
}
});
ui.horizontal_wrapped(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Source client",
"Label for Source client, others settings section",
)));
for option in [
ShowSourceClientOption::Hide,
ShowSourceClientOption::Top,
ShowSourceClientOption::Bottom,
] {
let mut current: ShowSourceClientOption =
self.settings.show_source_client.clone().into();
if ui
.selectable_value(
&mut current,
option,
RichText::new(option.label(self.note_context.i18n))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.changed()
{
action = Some(SettingsAction::SetShowSourceClient(option));
}
}
});
});
action
}
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action = None;
if ui
.add_sized(
[ui.available_width(), 30.0],
Button::new(richtext_small(tr!(
self.note_context.i18n,
"Configure relays",
"Label for configure relays, settings section",
))),
)
.clicked()
{
action = Some(SettingsAction::OpenRelays);
}
action
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action: Option<SettingsAction> = None;
Frame::default()
.inner_margin(Margin::symmetric(10, 10))
.show(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| {
if let Some(new_action) = self.appearance_section(ui) {
action = Some(new_action);
}
ui.add_space(5.0);
if let Some(new_action) = self.storage_section(ui) {
action = Some(new_action);
}
ui.add_space(5.0);
if let Some(new_action) = self.other_options_section(ui) {
action = Some(new_action);
}
ui.add_space(10.0);
if let Some(new_action) = self.manage_relays_section(ui) {
action = Some(new_action);
}
});
});
action
}
}
pub fn format_size(size_bytes: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let size = size_bytes as f64;
if size < KB {
format!("{size:.0} Bytes")
} else if size < MB {
format!("{:.1} KB", size / KB)
} else if size < GB {
format!("{:.1} MB", size / MB)
} else {
format!("{:.2} GB", size / GB)
}
}
+2 -2
View File
@@ -187,7 +187,7 @@ impl<'a> DesktopSidePanel<'a> {
action: SidePanelAction,
i18n: &mut Localization,
) -> Option<SwitchingAction> {
let router = get_active_columns_mut(i18n, accounts, decks_cache).get_first_router();
let router = get_active_columns_mut(i18n, accounts, decks_cache).get_selected_router();
let mut switching_response = None;
match action {
/*
@@ -280,7 +280,7 @@ impl<'a> DesktopSidePanel<'a> {
{
edit_deck
.columns_mut()
.get_first_router()
.get_selected_router()
.route_to(Route::EditDeck(index));
} else {
error!("Cannot push EditDeck route to index {}", index);
+10 -4
View File
@@ -1,10 +1,10 @@
use crate::support::{Support, SUPPORT_EMAIL};
use egui::{vec2, Button, Label, Layout, RichText};
use notedeck::{tr, Localization, NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{colors::PINK, padding};
use robius_open::Uri;
use tracing::error;
use crate::support::Support;
pub struct SupportView<'a> {
support: &'a mut Support,
i18n: &'a mut Localization,
@@ -44,15 +44,21 @@ impl<'a> SupportView<'a> {
"Open your default email client to get help from the Damus team",
"Instruction to open email client"
));
ui.horizontal_wrapped(|ui| {
ui.label(tr!(self.i18n, "Support email:", "Support email address",));
ui.label(RichText::new(SUPPORT_EMAIL).color(PINK))
});
let size = vec2(120.0, 40.0);
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
let font_size =
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
let button_resp = ui.add(open_email_button(self.i18n, font_size, size));
if button_resp.clicked() {
if let Err(e) = open::that(self.support.get_mailto_url()) {
if let Err(e) = Uri::new(self.support.get_mailto_url()).open() {
error!(
"Failed to open URL {} because: {}",
"Failed to open URL {} because: {:?}",
self.support.get_mailto_url(),
e
);

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