1 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
64 changed files with 920 additions and 2843 deletions
-3
View File
@@ -18,7 +18,4 @@ export OLLAMA_HOST=http://ollama.jb55.com
# simple todo reminders
export TODO_FILE=TODO
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
2>/dev/null todo.sh ls || :
Generated
+17 -47
View File
@@ -1554,7 +1554,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.2.0"
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
source = "git+https://github.com/damus-io/egui-nav?rev=3c67eb6298edbff36d46546897cfac33df4f04db#3c67eb6298edbff36d46546897cfac33df4f04db"
dependencies = [
"egui",
"egui_extras",
@@ -2336,12 +2336,6 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -3070,22 +3064,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lnsocket"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a373bcde8b65d6db11a0cd0f70dd4a24af854dd7a112b0a51258593c65f48ff"
dependencies = [
"bitcoin",
"hashbrown 0.13.2",
"hex",
"lightning-types",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "lock_api"
version = "0.4.13"
@@ -3500,14 +3478,13 @@ dependencies = [
[[package]]
name = "notedeck"
version = "0.6.0"
version = "0.5.9"
dependencies = [
"base32",
"bech32",
"bincode",
"bitflags 2.9.1",
"blurhash",
"chrono",
"dirs",
"eframe",
"egui",
@@ -3540,6 +3517,7 @@ dependencies = [
"sha2",
"strum",
"strum_macros",
"sys-locale",
"tempfile",
"thiserror 2.0.12",
"tokenator",
@@ -3552,9 +3530,8 @@ dependencies = [
[[package]]
name = "notedeck_chrome"
version = "0.6.0"
version = "0.5.9"
dependencies = [
"bitflags 2.9.1",
"eframe",
"egui",
"egui-winit",
@@ -3562,7 +3539,6 @@ dependencies = [
"egui_tabs",
"nostrdb",
"notedeck",
"notedeck_clndash",
"notedeck_columns",
"notedeck_dave",
"notedeck_notebook",
@@ -3582,23 +3558,9 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "notedeck_clndash"
version = "0.6.0"
dependencies = [
"eframe",
"egui",
"lnsocket",
"notedeck",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "notedeck_columns"
version = "0.6.0"
version = "0.5.9"
dependencies = [
"base64 0.22.1",
"bech32",
@@ -3652,7 +3614,7 @@ dependencies = [
[[package]]
name = "notedeck_dave"
version = "0.6.0"
version = "0.5.9"
dependencies = [
"async-openai",
"bytemuck",
@@ -3660,7 +3622,6 @@ dependencies = [
"eframe",
"egui",
"egui-wgpu",
"egui_extras",
"enostr",
"futures",
"hex",
@@ -3677,7 +3638,7 @@ dependencies = [
[[package]]
name = "notedeck_notebook"
version = "0.6.0"
version = "0.5.9"
dependencies = [
"egui",
"jsoncanvas",
@@ -3686,7 +3647,7 @@ dependencies = [
[[package]]
name = "notedeck_ui"
version = "0.6.0"
version = "0.5.9"
dependencies = [
"bitflags 2.9.1",
"eframe",
@@ -5761,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"
+4 -6
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
package.version = "0.6.0"
package.version = "0.5.9"
members = [
"crates/notedeck",
"crates/notedeck_chrome",
@@ -8,14 +8,12 @@ members = [
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/notedeck_clndash",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
]
[workspace.dependencies]
opener = "0.8.2"
chrono = "0.4.40"
base32 = "0.4.0"
base64 = "0.22.1"
rmpv = "1.3.0"
@@ -27,7 +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 = "de6e2d51892478fdd516df166f866e64dedbae07" }
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" }
@@ -49,7 +47,6 @@ nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_clndash = { path = "crates/notedeck_clndash" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
@@ -72,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"] }
-57
View File
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256mm"
height="256mm"
viewBox="0 0 256 256"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="clnlogo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.078823"
inkscape:cx="396.72867"
inkscape:cy="561.25984"
inkscape:window-width="2020"
inkscape:window-height="1420"
inkscape:window-x="270"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
style="display:inline">
<g
id="g4"
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
<path
class="st1"
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
id="path3"
style="fill:#f0d003" />
<path
fill="#fffae6"
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
id="path4" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

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