From dfdcafb071cab08b6b88c73d263143acd4c5e4b8 Mon Sep 17 00:00:00 2001 From: me Date: Mon, 8 Jun 2026 15:30:43 -0700 Subject: [PATCH 01/16] fix(l10n): correct ja repeater terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace machine-translated repeater terms with consistent Japanese: - 繰り返し送信装置 → リピーター (map, listFilter) - 繰り返し設定 → リピーターを自動追加 (contactsSettings) - ホップの繰り返し → リピーターホップ (channelPath) - 繰り返し送信する、近隣 → 近隣のリピーター (neighbors) - 近くの電波中継局 → 近くのリピーター (snrIndicator) - 送信装置名 → リピーター名 (repeater settings) - オフグリッド...の繰り返し → オフグリッドリピーター (settings) - 中継装置およびルームサーバーの設置場所 → リピーター/ルームサーバーの位置情報 - 繰り返し送信に関する情報 → リピーターに関する情報 (repeater_guest) --- lib/l10n/app_ja.arb | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b525ce41..fe8f6893 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -216,7 +216,7 @@ "settings_txPower": "TX 信号電力 (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "無効な送信電力 (0-22 dBm)", - "settings_clientRepeat": "オフグリッド(電力網から孤立した状態)の繰り返し", + "settings_clientRepeat": "オフグリッドリピーター", "settings_clientRepeatSubtitle": "このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。", "settings_clientRepeatFreqWarning": "オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。", "settings_error": "エラー:{message}", @@ -834,7 +834,7 @@ } }, "map_chat": "チャット", - "map_repeater": "繰り返し送信装置", + "map_repeater": "リピーター", "map_room": "部屋", "map_sensor": "センサー", "map_pinDm": "ピン(DM)", @@ -866,7 +866,7 @@ "map_filterNodes": "フィルタノード", "map_nodeTypes": "ノードの種類", "map_chatNodes": "チャットノード", - "map_repeaters": "繰り返し送信装置", + "map_repeaters": "リピーター", "map_otherNodes": "その他のノード", "map_keyPrefix": "主要なプレフィックス", "map_filterByKeyPrefix": "主要なプレフィックスでフィルタリングする", @@ -1187,7 +1187,7 @@ }, "repeater_settingsTitle": "リピーター設定", "repeater_basicSettings": "基本設定", - "repeater_repeaterName": "送信装置名", + "repeater_repeaterName": "リピーター名", "repeater_repeaterNameHelper": "このリピーターの名前", "repeater_adminPassword": "管理者パスワード", "repeater_adminPasswordHelper": "完全アクセス権のパスワード", @@ -1567,7 +1567,7 @@ } } }, - "neighbors_repeatersNeighbors": "繰り返し送信する、近隣", + "neighbors_repeatersNeighbors": "近隣のリピーター", "neighbors_noData": "近隣のデータは利用できません。", "neighbors_unknownContact": "不明な {pubkey}", "@neighbors_unknownContact": { @@ -1588,12 +1588,12 @@ "channelPath_title": "パケットパス", "channelPath_viewMap": "地図を表示する", "channelPath_otherObservedPaths": "観察されたその他の経路", - "channelPath_repeaterHops": "ホップの繰り返し", + "channelPath_repeaterHops": "リピーターホップ", "channelPath_noHopDetails": "このパッケージに関する詳細な情報は提供されていません。", "channelPath_messageDetails": "メッセージの詳細", "channelPath_senderLabel": "送信者", "channelPath_timeLabel": "時間", - "channelPath_repeatsLabel": "繰り返し", + "channelPath_repeatsLabel": "リピーター", "channelPath_pathLabel": "{index} 番目の経路", "channelPath_observedLabel": "観察", "channelPath_observedPathTitle": "観察された経路 {index} • {hops}", @@ -1653,7 +1653,7 @@ } }, "channelPath_mapTitle": "経路図", - "channelPath_noRepeaterLocations": "この経路には、中継装置の設置場所がありません。", + "channelPath_noRepeaterLocations": "この経路にリピーターの位置情報はありません。", "channelPath_primaryPath": "{index}番目の経路(主要経路)", "@channelPath_primaryPath": { "placeholders": { @@ -1831,7 +1831,7 @@ "listFilter_addToFavorites": "お気に入りに追加", "listFilter_removeFromFavorites": "お気に入りから削除", "listFilter_users": "利用者", - "listFilter_repeaters": "繰り返し送信装置", + "listFilter_repeaters": "リピーター", "listFilter_roomServers": "ルーム用サーバー", "listFilter_unreadOnly": "未読のみ", "listFilter_newGroup": "新しいグループ", @@ -1977,7 +1977,7 @@ "contacts_pathTrace": "経路追跡", "contacts_ping": "パング", "contacts_repeaterPathTrace": "リピーターまでの経路を追跡する", - "contacts_repeaterPing": "PING 繰り返し", + "contacts_repeaterPing": "リピーターにPING", "contacts_roomPathTrace": "部屋のサーバーへの経路を追跡する", "contacts_roomPing": "ピンルーム用サーバー", "contacts_chatTraceRoute": "経路の追跡ルート", @@ -2047,19 +2047,19 @@ "settings_gpxExportNoContacts": "エクスポートする連絡先は存在しません。", "settings_gpxExportNotAvailable": "このデバイス/OSではサポートされていません", "settings_gpxExportError": "エクスポート時にエラーが発生しました。", - "settings_gpxExportRepeatersRoom": "中継装置およびルームサーバーの設置場所", + "settings_gpxExportRepeatersRoom": "リピーター/ルームサーバーの位置情報", "settings_gpxExportChat": "関連施設", "settings_gpxExportAllContacts": "すべての連絡先場所", "settings_gpxExportShareText": "meshcore-openからエクスポートされた地図データ", "settings_gpxExportShareSubject": "meshcore-open GPX形式の地図データのエクスポート", - "snrIndicator_nearByRepeaters": "近くの電波中継局", + "snrIndicator_nearByRepeaters": "近くのリピーター", "snrIndicator_lastSeen": "最後に確認された場所", "contactsSettings_title": "連絡先設定", "contactsSettings_autoAddTitle": "自動検出", "contactsSettings_otherTitle": "その他の連絡に関する設定", "contactsSettings_autoAddUsersTitle": "自動でユーザーを追加する", "contactsSettings_autoAddUsersSubtitle": "利用者が自動的に発見したユーザーを追加できるようにする。", - "contactsSettings_autoAddRepeatersTitle": "自動で繰り返し設定", + "contactsSettings_autoAddRepeatersTitle": "リピーターを自動追加", "contactsSettings_autoAddRepeatersSubtitle": "発見した中継局を、自動的に追加できるようにする。", "contactsSettings_autoAddRoomServersTitle": "自動でルームサーバーを追加", "contactsSettings_autoAddRoomServersSubtitle": "利用者が、発見した部屋のサーバーを自動的に追加できるようにする。", @@ -2232,7 +2232,7 @@ "repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。", "room_guest": "ルームサーバーに関する情報", "chat_sendMessage": "メッセージを送信する", - "repeater_guest": "繰り返し送信に関する情報", + "repeater_guest": "リピーターに関する情報", "repeater_guestTools": "ゲスト向けツール", "repeater_getCategory": "価値を取得する", "repeater_powerMgmt": "電力管理", From 13d3a107da9089b9a55c8143d74a699cd9cfe3d7 Mon Sep 17 00:00:00 2001 From: me Date: Mon, 8 Jun 2026 16:18:18 -0700 Subject: [PATCH 02/16] fix(l10n): replace flood/repeater machine translations in Japanese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 洪水 → フラッド (flood routing terminology) - リピーター → リピータ (consistent katakana without long vowel) - 中継局 → リピータ - 繰り返し送信装置 → リピータ - オフグリッド...の繰り返し → オフグリッドリピータ - 最大浸水範囲の回数 → フラッドパケットの最大ホップ数 - インバウンドフラッパケット → インバウンドフラッドパケット (typo fix) - ルーティングループに見えるような、洪水パケットを送信する → ルーティングループを検知する - カスタムパスには、メッセージを中継できる中間地点が必要です → リピータが必要です - ワンホップでの広告放送(中継なし)→(リピータなし) --- lib/l10n/app_ja.arb | 148 ++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index fe8f6893..e000a0d9 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -216,7 +216,7 @@ "settings_txPower": "TX 信号電力 (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "無効な送信電力 (0-22 dBm)", - "settings_clientRepeat": "オフグリッドリピーター", + "settings_clientRepeat": "オフグリッドリピータ", "settings_clientRepeatSubtitle": "このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。", "settings_clientRepeatFreqWarning": "オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。", "settings_error": "エラー:{message}", @@ -270,7 +270,7 @@ "appSettings_pathsWillBeCleared": "5回失敗した後、経路が再開されます。", "appSettings_pathsWillNotBeCleared": "パスは自動で削除されません。", "appSettings_autoRouteRotation": "自動ルートの切り替え", - "appSettings_autoRouteRotationSubtitle": "最適なルートと、洪水モードを切り替える", + "appSettings_autoRouteRotationSubtitle": "最適なルートと、フラッドモードを切り替える", "appSettings_autoRouteRotationEnabled": "自動ルートの切り替え機能が有効になっています", "appSettings_autoRouteRotationDisabled": "自動ルートの変更機能が無効になっています。", "appSettings_maxRouteWeight": "最大ルート重量", @@ -310,7 +310,7 @@ "appSettings_batteryLipo": "LiPo (3.0-4.2V)", "appSettings_mapDisplay": "地図の表示", "appSettings_showRepeaters": "繰り返し再生機能", - "appSettings_showRepeatersSubtitle": "地図上にリピーターノードを表示する", + "appSettings_showRepeatersSubtitle": "地図上にリピータノードを表示する", "appSettings_showChatNodes": "チャットノードの表示", "appSettings_showChatNodesSubtitle": "地図上にチャットノードを表示する", "appSettings_showOtherNodes": "他のノードを表示する", @@ -424,7 +424,7 @@ } } }, - "contacts_manageRepeater": "リピーターの管理", + "contacts_manageRepeater": "リピータの管理", "contacts_manageRoom": "ルームサーバーの管理", "contacts_roomLogin": "ルームサーバーへのログイン", "contacts_openChat": "自由な会話", @@ -735,7 +735,7 @@ "chat_ShowAllPaths": "すべての経路を表示", "chat_routingMode": "ルーティングモード", "chat_autoUseSavedPath": "自動 (保存されたパスを使用)", - "chat_forceFloodMode": "強制的に洪水モードを起動", + "chat_forceFloodMode": "強制的にフラッドモードを起動", "chat_recentAckPaths": "最近使用したACKパス(タップして使用):", "chat_pathHistoryFull": "パスの履歴は完全です。エントリを削除して、新しいものを追加できます。", "chat_hopSingular": "ジャンプ", @@ -758,7 +758,7 @@ "chat_clearPathSubtitle": "次回送信時に、以前の情報を再取得する", "chat_pathCleared": "経路が確保されました。次のメッセージでルートを再確認します。", "chat_floodModeSubtitle": "アプリのバーにあるルーティング切り替え機能を使用する", - "chat_floodModeEnabled": "洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。", + "chat_floodModeEnabled": "フラッドモードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。", "chat_fullPath": "フルパス", "chat_pathDetailsNotAvailable": "経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。", "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", @@ -779,7 +779,7 @@ "chat_path": "道", "chat_publicKey": "公開鍵", "chat_compressOutgoingMessages": "送信されるメッセージを圧縮する", - "chat_floodForced": "洪水(強制的な)", + "chat_floodForced": "フラッド(強制的な)", "chat_directForced": "直接的な(強制的な)", "chat_hopsForced": "{count} 本のホップ(強制的に採取)", "@chat_hopsForced": { @@ -789,7 +789,7 @@ } } }, - "chat_floodAuto": "洪水 (自動)", + "chat_floodAuto": "フラッド (自動)", "chat_direct": "直接", "chat_poiShared": "共有されたPOI", "chat_unread": "未読: {count}", @@ -834,7 +834,7 @@ } }, "map_chat": "チャット", - "map_repeater": "リピーター", + "map_repeater": "リピータ", "map_room": "部屋", "map_sensor": "センサー", "map_pinDm": "ピン(DM)", @@ -866,7 +866,7 @@ "map_filterNodes": "フィルタノード", "map_nodeTypes": "ノードの種類", "map_chatNodes": "チャットノード", - "map_repeaters": "リピーター", + "map_repeaters": "リピータ", "map_otherNodes": "その他のノード", "map_keyPrefix": "主要なプレフィックス", "map_filterByKeyPrefix": "主要なプレフィックスでフィルタリングする", @@ -879,7 +879,7 @@ "map_lastSeenTime": "最後に確認された時間", "map_sharedPin": "共有パスワード", "map_joinRoom": "部屋に参加する", - "map_manageRepeater": "リピーターの管理", + "map_manageRepeater": "リピータの管理", "map_tapToAdd": "ノードをクリックして、パスに追加します。", "map_runTrace": "パスの追跡を実行", "map_removeLast": "最後のものを削除", @@ -1012,12 +1012,12 @@ "login_enterPassword": "パスワードを入力してください", "login_savePassword": "パスワードを保存する", "login_savePasswordSubtitle": "パスワードは、このデバイスに安全に保存されます。", - "login_repeaterDescription": "設定やステータスにアクセスするために、リピーターのパスワードを入力してください。", + "login_repeaterDescription": "設定やステータスにアクセスするために、リピータのパスワードを入力してください。", "login_roomDescription": "設定やステータスへのアクセスには、部屋のパスワードを入力してください。", "login_routing": "経路設定", "login_routingMode": "ルーティングモード", "login_autoUseSavedPath": "自動 (保存されたパスを使用)", - "login_forceFloodMode": "強制的に洪水モードを起動", + "login_forceFloodMode": "強制的にフラッドモードを起動", "login_managePaths": "パスの管理", "login_login": "ログイン", "login_attempt": "試行回数:{current}/{max}", @@ -1066,7 +1066,7 @@ "path_helperMaxHops": "最大64個のホップ。各プレフィックスは2つの16進数文字(1バイト)で構成されています。", "path_selectFromContacts": "または、連絡先リストから選択してください:", "path_noRepeatersFound": "繰り返し機能やルームサーバーは見つかりませんでした。", - "path_customPathsRequire": "カスタムパスには、メッセージを中継できる中間地点が必要です。", + "path_customPathsRequire": "カスタムパスには、メッセージを中継できるリピータが必要です。", "path_invalidHexPrefixes": "無効な16進数プレフィックス: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { @@ -1077,23 +1077,23 @@ }, "path_tooLong": "経路が長すぎる。最大64回のジャンプのみ許可。", "path_setPath": "パスを設定", - "repeater_management": "リピーター管理", + "repeater_management": "リピータ管理", "room_management": "ルームサーバーの管理", "repeater_managementTools": "管理ツール", "repeater_status": "ステータス", - "repeater_statusSubtitle": "リピーターの状態、統計情報、および隣接するネットワークの情報を表示する", + "repeater_statusSubtitle": "リピータの状態、統計情報、および隣接するネットワークの情報を表示する", "repeater_telemetry": "テレメトリー", "repeater_telemetrySubtitle": "センサーおよびシステムの状態に関するテレメトリの表示", "repeater_cli": "CLI(コマンドラインインターフェース)", - "repeater_cliSubtitle": "リピーターへのコマンドを送信する", + "repeater_cliSubtitle": "リピータへのコマンドを送信する", "repeater_neighbors": "近隣住民", "repeater_neighborsSubtitle": "ゼロホップの隣接ノードを表示する。", "repeater_settings": "設定", - "repeater_settingsSubtitle": "リピーターのパラメータを設定する", + "repeater_settingsSubtitle": "リピータのパラメータを設定する", "repeater_statusTitle": "再送ステータス", "repeater_routingMode": "ルーティングモード", "repeater_autoUseSavedPath": "自動 (保存されたパスを使用)", - "repeater_forceFloodMode": "強制的に洪水モードを起動", + "repeater_forceFloodMode": "強制的にフラッドモードを起動", "repeater_pathManagement": "経路管理", "repeater_refresh": "リフレッシュ", "repeater_statusRequestTimeout": "ステータス情報の取得に失敗しました。", @@ -1138,7 +1138,7 @@ } } }, - "repeater_packetTxTotal": "合計: {total}, 洪水: {flood}, 直接: {direct}", + "repeater_packetTxTotal": "合計: {total}, フラッド: {flood}, 直接: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -1152,7 +1152,7 @@ } } }, - "repeater_packetRxTotal": "合計: {total}, 洪水: {flood}, 直接: {direct}", + "repeater_packetRxTotal": "合計: {total}, フラッド: {flood}, 直接: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -1185,10 +1185,10 @@ } } }, - "repeater_settingsTitle": "リピーター設定", + "repeater_settingsTitle": "リピータ設定", "repeater_basicSettings": "基本設定", - "repeater_repeaterName": "リピーター名", - "repeater_repeaterNameHelper": "このリピーターの名前", + "repeater_repeaterName": "リピータ名", + "repeater_repeaterNameHelper": "このリピータの名前", "repeater_adminPassword": "管理者パスワード", "repeater_adminPasswordHelper": "完全アクセス権のパスワード", "repeater_guestPassword": "ゲスト用のパスワード", @@ -1208,7 +1208,7 @@ "repeater_longitudeHelper": "度分表記(例:-122.4194)", "repeater_features": "特徴", "repeater_packetForwarding": "パケット転送", - "repeater_packetForwardingSubtitle": "リピーターがパケットを転送できるように設定する", + "repeater_packetForwardingSubtitle": "リピータがパケットを転送できるように設定する", "repeater_guestAccess": "ゲストへのアクセス", "repeater_guestAccessSubtitle": "ゲストへの読み取り専用アクセスを許可する", "repeater_privacyMode": "プライバシーモード", @@ -1223,7 +1223,7 @@ } } }, - "repeater_floodAdvertInterval": "洪水に関する広告の表示間隔", + "repeater_floodAdvertInterval": "フラッドに関する広告の表示間隔", "repeater_floodAdvertIntervalHours": "{hours} 時間", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1234,15 +1234,15 @@ }, "repeater_encryptedAdvertInterval": "暗号化された広告表示間", "repeater_dangerZone": "危険区域", - "repeater_rebootRepeater": "リピーターを再起動する", - "repeater_rebootRepeaterSubtitle": "リピーターデバイスを再起動する", - "repeater_rebootRepeaterConfirm": "本当にこのリピーターを再起動したいですか?", + "repeater_rebootRepeater": "リピータを再起動する", + "repeater_rebootRepeaterSubtitle": "リピータデバイスを再起動する", + "repeater_rebootRepeaterConfirm": "本当にこのリピータを再起動したいですか?", "repeater_regenerateIdentityKey": "IDキーの再生成", "repeater_regenerateIdentityKeySubtitle": "新しい公開鍵/秘密鍵のペアを生成する", - "repeater_regenerateIdentityKeyConfirm": "これにより、リピーターには新しい識別情報が割り当てられます。続行しますか?", + "repeater_regenerateIdentityKeyConfirm": "これにより、リピータには新しい識別情報が割り当てられます。続行しますか?", "repeater_eraseFileSystem": "ファイルシステムを削除する", - "repeater_eraseFileSystemSubtitle": "リピーターファイルシステムをフォーマットする", - "repeater_eraseFileSystemConfirm": "警告:この操作により、リピーター内のすべてのデータが消去されます。この操作は元に戻すことができません!", + "repeater_eraseFileSystemSubtitle": "リピータファイルシステムをフォーマットする", + "repeater_eraseFileSystemConfirm": "警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!", "repeater_eraseSerialOnly": "Erase機能は、シリアルコンソール経由でのみ利用可能です。", "repeater_commandSent": "送信されたコマンド: {command}", "@repeater_commandSent": { @@ -1270,7 +1270,7 @@ "repeater_refreshMultiAcks": "複数のACKをリフレッシュする", "repeater_networkHealth": "ネットワークの状態", "repeater_loopDetect": "ループ検出", - "repeater_loopDetectHelper": "ルーティングループに見えるような、洪水パケットを送信する", + "repeater_loopDetectHelper": "ルーティングループを検知する", "repeater_loopDetectOff": "オフ", "repeater_loopDetectMinimal": "最小限の", "repeater_loopDetectModerate": "適度な", @@ -1286,16 +1286,16 @@ } }, "repeater_ownerInfo": "事業者の情報", - "repeater_ownerInfoHelper": "このリピーターに関する公開メタデータ", + "repeater_ownerInfoHelper": "このリピータに関する公開メタデータ", "repeater_refreshOwnerInfo": "オペレーター情報の更新", "repeater_floodMax": "最大ホップ数", - "repeater_floodMaxHelper": "洪水パケットが移動できる最大ホップ数 (0-64)", + "repeater_floodMaxHelper": "フラッドパケットが移動できる最大ホップ数 (0-64)", "repeater_advancedSettings": "高度な", "repeater_advancedSettingsSubtitle": "経験豊富なオペレーター向けの調整ノブ", "repeater_pathHashMode": "パスハッシュモード", - "repeater_pathHashModeHelper": "このリピーターのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。", + "repeater_pathHashModeHelper": "このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。", "repeater_txDelay": "フロイド・TXでの遅延", - "repeater_txDelayHelper": "洪水時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。", + "repeater_txDelayHelper": "フラッド時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。", "repeater_directTxDelay": "直接的なTX遅延", "repeater_directTxDelayHelper": "直接(フラッドではない)トラフィックに対する再送信間隔を、パケットの空中時間(0~2、デフォルト0.3)の倍数として設定する。", "repeater_intThresh": "干渉閾値", @@ -1303,10 +1303,10 @@ "repeater_agcResetInterval": "AGCのリセット間隔", "repeater_agcResetIntervalHelper": "ラジオの自動ゲイン制御をリセットする頻度について:ゲインが固定状態になった場合に、回復するために、何度リセットするかを設定します。4の倍数でリセットする場合、0を設定すると、定期的なリセットは停止します。", "repeater_actionsTitle": "行動", - "repeater_sendAdvert": "洪水に関する広告を送信", - "repeater_sendAdvertSubtitle": "ネットワークを通じて、洪水に関する広告を放送する", + "repeater_sendAdvert": "フラッドに関する広告を送信", + "repeater_sendAdvertSubtitle": "ネットワークを通じて、フラッドに関する広告を放送する", "repeater_sendAdvertZeroHop": "ゼロホップ形式の広告を送信する", - "repeater_sendAdvertZeroHopSubtitle": "ワンホップでの広告放送(中継なし)", + "repeater_sendAdvertZeroHopSubtitle": "ワンホップでの広告放送(リピータなし)", "repeater_clockSync": "現在、時刻を同期する", "repeater_clockSyncSubtitle": "スマートフォンの時刻をルーターに設定する", "repeater_actionSucceeded": "{action} が成功しました", @@ -1328,7 +1328,7 @@ } } }, - "repeater_settingsSavedRebootNeeded": "設定を保存しました — リピーターを再起動して適用してください", + "repeater_settingsSavedRebootNeeded": "設定を保存しました — リピータを再起動して適用してください", "repeater_settingsPartialFailure": "設定の一部でエラーが発生しました:{failures}", "@repeater_settingsPartialFailure": { "placeholders": { @@ -1367,7 +1367,7 @@ } } }, - "repeater_cliTitle": "リピーターのコマンドラインインターフェース", + "repeater_cliTitle": "リピータのコマンドラインインターフェース", "repeater_debugNextCommand": "次のコマンドのデバッグ", "repeater_commandHelp": "コマンドヘルプ", "repeater_clearHistory": "明確な歴史", @@ -1401,14 +1401,14 @@ "repeater_cliHelpClearStats": "さまざまな統計カウンターをゼロにリセットする。", "repeater_cliHelpSetAf": "空き時間係数を設定します。", "repeater_cliHelpSetTx": "LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)", - "repeater_cliHelpSetRepeat": "このノードに対するリピーターの役割を有効化または無効化します。", + "repeater_cliHelpSetRepeat": "このノードに対するリピータの役割を有効化または無効化します。", "repeater_cliHelpSetAllowReadOnly": "(ルームサーバー設定)「オン」に設定した場合、空白のパスワードでのログインは可能ですが、ルームへの投稿はできません。(閲覧のみ)", - "repeater_cliHelpSetFloodMax": "インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。", + "repeater_cliHelpSetFloodMax": "インバウンドフラッドパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。", "repeater_cliHelpSetIntThresh": "干渉閾値を設定します(dB単位)。デフォルト値は14です。0に設定すると、チャンネル間の干渉を検出する機能を無効にします。", "repeater_cliHelpSetAgcResetInterval": "オートゲインコントローラーのリセット間隔を設定します。 0 に設定すると無効化されます。", "repeater_cliHelpSetMultiAcks": "「ダブルACK」機能の有効化または無効化を可能にします。", "repeater_cliHelpSetAdvertInterval": "ローカル(ホップなし)の広告パケットを送信する間隔を分単位で設定します。 0 に設定すると、機能を無効にします。", - "repeater_cliHelpSetFloodAdvertInterval": "洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。", + "repeater_cliHelpSetFloodAdvertInterval": "フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。", "repeater_cliHelpSetGuestPassword": "ゲストのパスワードを設定/更新します。(繰り返し利用の場合、ゲストのログインは「統計情報を取得」のリクエストを送信できます)", "repeater_cliHelpSetName": "広告の名前を設定します。", "repeater_cliHelpSetLat": "広告表示の地図の緯度を設定します。(度分秒表記)", @@ -1429,14 +1429,14 @@ "repeater_cliHelpLogStart": "パケットのログ記録を開始し、ファイルシステムに保存する。", "repeater_cliHelpLogStop": "ファイルシステムへのパケットログの記録を停止する。", "repeater_cliHelpLogErase": "ファイルシステムからパケットログを削除する。", - "repeater_cliHelpNeighbors": "ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。", + "repeater_cliHelpNeighbors": "ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。", "repeater_cliHelpNeighborRemove": "隣接リストから、最初に一致するエントリ(pubkeyプレフィックス(16進数)で特定)を削除します。", - "repeater_cliHelpRegion": "(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。", + "repeater_cliHelpRegion": "(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。", "repeater_cliHelpRegionLoad": "注:これは特殊な複数コマンドの呼び出しです。その後の各コマンドは、地域名であり(スペースを使用して親階層を示し、少なくとも1つのスペースが必要です)、空行/コマンドで終了します。", "repeater_cliHelpRegionGet": "指定された名前のプレフィックスを持つ地域を検索します(または、グローバルな範囲の場合は「*」)。結果として、「region-name (parent-name) 'F'」と返答します。", "repeater_cliHelpRegionPut": "指定された名前で、領域の定義を追加または更新します。", "repeater_cliHelpRegionRemove": "指定された名前を持つ領域の定義を削除します。(正確に一致している必要があり、子領域は存在してはなりません)", - "repeater_cliHelpRegionAllowf": "指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)", + "repeater_cliHelpRegionAllowf": "指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)", "repeater_cliHelpRegionDenyf": "指定された領域における「FLOOD」権限を削除します。(注:現時点では、グローバル/従来の範囲での使用は推奨されません!)", "repeater_cliHelpRegionHome": "現在の「ホーム」地域に返信します。(まだ適用されていない、将来利用を予定)", "repeater_cliHelpRegionHomeSet": "「ホーム」地域を設定します。", @@ -1453,7 +1453,7 @@ "repeater_settingsCategory": "設定", "repeater_bridge": "橋", "repeater_logging": "ログ記録", - "repeater_neighborsRepeaterOnly": "近隣住民(リピーターのみ)", + "repeater_neighborsRepeaterOnly": "近隣住民(リピータのみ)", "repeater_regionManagementRepeaterOnly": "地域管理(ブロードキャスト用のみ)", "repeater_regionNote": "地域レベルでの管理のため、地域定義と権限の管理を行うための機能が導入されました。", "repeater_gpsManagement": "GPS管理", @@ -1567,7 +1567,7 @@ } } }, - "neighbors_repeatersNeighbors": "近隣のリピーター", + "neighbors_repeatersNeighbors": "近隣のリピータ", "neighbors_noData": "近隣のデータは利用できません。", "neighbors_unknownContact": "不明な {pubkey}", "@neighbors_unknownContact": { @@ -1588,12 +1588,12 @@ "channelPath_title": "パケットパス", "channelPath_viewMap": "地図を表示する", "channelPath_otherObservedPaths": "観察されたその他の経路", - "channelPath_repeaterHops": "リピーターホップ", + "channelPath_repeaterHops": "リピータホップ", "channelPath_noHopDetails": "このパッケージに関する詳細な情報は提供されていません。", "channelPath_messageDetails": "メッセージの詳細", "channelPath_senderLabel": "送信者", "channelPath_timeLabel": "時間", - "channelPath_repeatsLabel": "リピーター", + "channelPath_repeatsLabel": "リピータ", "channelPath_pathLabel": "{index} 番目の経路", "channelPath_observedLabel": "観察", "channelPath_observedPathTitle": "観察された経路 {index} • {hops}", @@ -1631,7 +1631,7 @@ } }, "channelPath_unknownPath": "不明", - "channelPath_floodPath": "洪水", + "channelPath_floodPath": "フラッド", "channelPath_directPath": "直接", "channelPath_observedZeroOf": "{total}個のホップ", "@channelPath_observedZeroOf": { @@ -1653,7 +1653,7 @@ } }, "channelPath_mapTitle": "経路図", - "channelPath_noRepeaterLocations": "この経路にリピーターの位置情報はありません。", + "channelPath_noRepeaterLocations": "この経路にリピータの位置情報はありません。", "channelPath_primaryPath": "{index}番目の経路(主要経路)", "@channelPath_primaryPath": { "placeholders": { @@ -1831,7 +1831,7 @@ "listFilter_addToFavorites": "お気に入りに追加", "listFilter_removeFromFavorites": "お気に入りから削除", "listFilter_users": "利用者", - "listFilter_repeaters": "リピーター", + "listFilter_repeaters": "リピータ", "listFilter_roomServers": "ルーム用サーバー", "listFilter_unreadOnly": "未読のみ", "listFilter_newGroup": "新しいグループ", @@ -1976,8 +1976,8 @@ }, "contacts_pathTrace": "経路追跡", "contacts_ping": "パング", - "contacts_repeaterPathTrace": "リピーターまでの経路を追跡する", - "contacts_repeaterPing": "リピーターにPING", + "contacts_repeaterPathTrace": "リピータまでの経路を追跡する", + "contacts_repeaterPing": "リピータにPING", "contacts_roomPathTrace": "部屋のサーバーへの経路を追跡する", "contacts_roomPing": "ピンルーム用サーバー", "contacts_chatTraceRoute": "経路の追跡ルート", @@ -1994,7 +1994,7 @@ "contacts_contactImported": "連絡先が登録されました。", "contacts_contactImportFailed": "連絡先のインポートに失敗しました。", "contacts_zeroHopAdvert": "ゼロホップ広告", - "contacts_floodAdvert": "洪水に関する広告", + "contacts_floodAdvert": "フラッドに関する広告", "contacts_copyAdvertToClipboard": "広告をクリップボードにコピー", "contacts_addContactFromClipboard": "クリップボードから連絡先を追加する", "contacts_ShareContact": "連絡先をクリップボードにコピー", @@ -2037,7 +2037,7 @@ } }, "notification_receivedNewMessage": "新しいメッセージを受信", - "settings_gpxExportRepeaters": "GPX へのエクスポート用リピーター/ルームサーバー", + "settings_gpxExportRepeaters": "GPX へのエクスポート用リピータ/ルームサーバー", "settings_gpxExportRepeatersSubtitle": "GPXファイルに場所情報を付加した、レピーター/ルームサーバーのエクスポート", "settings_gpxExportContacts": "GPX 形式へのエクスポート", "settings_gpxExportContactsSubtitle": "GPXファイルに位置情報を保存して、他の人と共有する。", @@ -2047,20 +2047,20 @@ "settings_gpxExportNoContacts": "エクスポートする連絡先は存在しません。", "settings_gpxExportNotAvailable": "このデバイス/OSではサポートされていません", "settings_gpxExportError": "エクスポート時にエラーが発生しました。", - "settings_gpxExportRepeatersRoom": "リピーター/ルームサーバーの位置情報", + "settings_gpxExportRepeatersRoom": "リピータ/ルームサーバーの位置情報", "settings_gpxExportChat": "関連施設", "settings_gpxExportAllContacts": "すべての連絡先場所", "settings_gpxExportShareText": "meshcore-openからエクスポートされた地図データ", "settings_gpxExportShareSubject": "meshcore-open GPX形式の地図データのエクスポート", - "snrIndicator_nearByRepeaters": "近くのリピーター", + "snrIndicator_nearByRepeaters": "近くのリピータ", "snrIndicator_lastSeen": "最後に確認された場所", "contactsSettings_title": "連絡先設定", "contactsSettings_autoAddTitle": "自動検出", "contactsSettings_otherTitle": "その他の連絡に関する設定", "contactsSettings_autoAddUsersTitle": "自動でユーザーを追加する", "contactsSettings_autoAddUsersSubtitle": "利用者が自動的に発見したユーザーを追加できるようにする。", - "contactsSettings_autoAddRepeatersTitle": "リピーターを自動追加", - "contactsSettings_autoAddRepeatersSubtitle": "発見した中継局を、自動的に追加できるようにする。", + "contactsSettings_autoAddRepeatersTitle": "リピータを自動追加", + "contactsSettings_autoAddRepeatersSubtitle": "発見したリピータを、自動的に追加できるようにする。", "contactsSettings_autoAddRoomServersTitle": "自動でルームサーバーを追加", "contactsSettings_autoAddRoomServersSubtitle": "利用者が、発見した部屋のサーバーを自動的に追加できるようにする。", "contactsSettings_autoAddSensorsTitle": "自動でセンサーを追加", @@ -2162,7 +2162,7 @@ "contact_teleLocSubtitle": "位置情報共有を許可する", "contact_teleEnv": "テレメトリ環境", "contact_teleEnvSubtitle": "環境センサーのデータを共有することを許可する", - "map_showOverlaps": "リピーターキーの重複", + "map_showOverlaps": "リピータキーの重複", "map_runTraceWithReturnPath": "元の経路に戻る。", "@translation_downloadFailed": { "placeholders": { @@ -2232,7 +2232,7 @@ "repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。", "room_guest": "ルームサーバーに関する情報", "chat_sendMessage": "メッセージを送信する", - "repeater_guest": "リピーターに関する情報", + "repeater_guest": "リピータに関する情報", "repeater_guestTools": "ゲスト向けツール", "repeater_getCategory": "価値を取得する", "repeater_powerMgmt": "電力管理", @@ -2243,7 +2243,7 @@ "repeater_cliHelpStartOta": "サポートされているボードに対して、無線でファームウェアのアップデートを開始します。", "repeater_cliHelpTime": "デバイスのクロックを、指定されたUnixエポックの秒数に設定します。クロックは逆方向に進むことはできません。", "repeater_cliHelpBoard": "製造元の名前/ハードウェア識別子を表示します。", - "repeater_cliHelpDiscoverNeighbors": "近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)", + "repeater_cliHelpDiscoverNeighbors": "近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)", "repeater_cliHelpPowersaving": "省電力モードがオンになっているかどうかを表示します。", "repeater_cliHelpPowersavingOnOff": "省電力モード(対応している場合)を有効または無効にします。", "repeater_cliHelpErase": "(シリアルモードのみ)デバイスのファイルシステムをフォーマットします。すべての設定と連絡先を消去します。", @@ -2256,10 +2256,10 @@ "repeater_cliHelpSetFreq": "(シリアル設定のみ)特定の周波数のみを素早く設定できます。再起動が必要です。「ラジオ設定」を使用すると、ラジオのすべてのパラメータを設定できます。", "repeater_cliHelpSetBridgeChannel": "(ESPNowブリッジのみ)ブリッジで使用するWi-Fiチャンネル(1~14)を設定します。", "repeater_cliHelpGetName": "設定されたノードの名前を表示します。", - "repeater_cliHelpGetRole": "ファームウェアの役割(リピーター、ルームサーバーなど)を表示します。", + "repeater_cliHelpGetRole": "ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。", "repeater_cliHelpGetPublicKey": "デバイスの公開鍵を表示します。", "repeater_cliHelpGetPrvKey": "(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。", - "repeater_cliHelpGetRepeat": "パケット転送(リピーター機能)が有効になっているかどうかを表示します。", + "repeater_cliHelpGetRepeat": "パケット転送(リピータ機能)が有効になっているかどうかを表示します。", "repeater_cliHelpGetTx": "現在のTX(送信)電力のdBm値を表示します。", "repeater_cliHelpGetFreq": "設定された無線周波数をMHzで表示します。", "repeater_cliHelpGetRadio": "以下のすべての無線パラメータを表示: 周波数、帯域幅、スプレッドファクター、符号化レート。", @@ -2271,18 +2271,18 @@ "repeater_cliHelpGetMultiAcks": "ダブルACKモードが有効 (1) か無効 (0) かを示す。", "repeater_cliHelpGetAllowReadOnly": "ゲストによる読み取り専用アクセスが許可されているかどうかを示す。", "repeater_cliHelpGetAdvertInterval": "ローカル広告の時間を分単位で表示します。", - "repeater_cliHelpGetFloodAdvertInterval": "洪水に関する広告の放送時間を時間単位で表示します。", + "repeater_cliHelpGetFloodAdvertInterval": "フラッドに関する広告の放送時間を時間単位で表示します。", "repeater_cliHelpGetGuestPassword": "設定されたゲストパスワードを表示します。", "repeater_cliHelpGetLat": "設定された緯度を表示します。", "repeater_cliHelpGetLon": "設定された経度を表示します。", "repeater_cliHelpGetRxDelay": "rxdelay の基本値を表示します。", - "repeater_cliHelpGetTxDelay": "洪水モードにおける送信遅延の要因を示します。", + "repeater_cliHelpGetTxDelay": "フラッドモードにおける送信遅延の要因を示します。", "repeater_cliHelpGetDirectTxDelay": "ダイレクトモードの遅延要素を示します。", - "repeater_cliHelpGetFloodMax": "最大浸水範囲の回数を表示します。", + "repeater_cliHelpGetFloodMax": "フラッドパケットの最大ホップ数を表示します。", "repeater_cliHelpGetOwnerInfo": "所有者の連絡先情報を表示します。", "repeater_cliHelpGetPathHashMode": "パスハッシュモード(0/1/2)を表示します。", "repeater_cliHelpGetLoopDetect": "ループ検出の感度を示す。", - "repeater_cliHelpGetAcl": "(シリアルのみ)リピーター上のアクセス制御設定を一覧表示します。", + "repeater_cliHelpGetAcl": "(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。", "repeater_cliHelpGetBridgeEnabled": "橋が有効になっているかどうかを表示します。", "repeater_cliHelpGetBridgeDelay": "橋の遅延時間をミリ秒(ms)で表示します。", "repeater_cliHelpGetBridgeSource": "RX または TX パケットを橋渡ししているかどうかを示す。", @@ -2300,8 +2300,8 @@ "repeater_cliHelpSensorList": "カスタムセンサーの設定をすべてリスト表示し、オプションで指定できる開始インデックスからページ分割して表示します。", "repeater_cliHelpRegionDefault": "現在のデフォルトの地域範囲を表示します。", "repeater_cliHelpRegionDefaultSet": "デフォルトの地域範囲を設定します。「」を使用すると、設定をリセットできます。", - "repeater_cliHelpRegionListAllowed": "洪水時の通行が許可されている地域の一覧", - "repeater_cliHelpRegionListDenied": "洪水による交通を遮断している地域の一覧", + "repeater_cliHelpRegionListAllowed": "フラッド時の通行が許可されている地域の一覧", + "repeater_cliHelpRegionListDenied": "フラッドによる交通を遮断している地域の一覧", "repeater_cliHelpStatsPackets": "(シリアルのみ)パケットレベルの統計情報を表示します。", "repeater_cliHelpStatsRadio": "(シリーズのみ)ラジオの統計情報を表示します。", "repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。", From cba1e5950cb6da431aef8859b51a2e972e3e5947 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Thu, 11 Jun 2026 00:07:12 -0700 Subject: [PATCH 03/16] feat: add contact UI helpers and path editor for routing management - Implemented contactTypeIcon and contactTypeColor functions for better UI representation of contact types. - Created colorForName and firstCharacterOrEmoji functions to enhance contact display. - Developed PathEditorSheet widget for managing contact paths with a user-friendly interface. - Introduced RoutingSheet for managing contact routing modes and displaying path history. - Added a script for generating proof of concept (PoC) payloads for clipboard contact import validation. --- .claude/worktrees/agent-a01277dd8cf0dedb6 | 1 + .claude/worktrees/agent-a0c7384491c5fc807 | 1 + .claude/worktrees/agent-a1751d6addbf69e1d | 1 + .claude/worktrees/agent-a2fad933ed81b10e2 | 1 + .claude/worktrees/agent-a4d771c23db919c7f | 1 + .claude/worktrees/agent-a6675128f3ee1d6a6 | 1 + .claude/worktrees/agent-a894f10813e684f84 | 1 + .claude/worktrees/agent-a8ba0537ad42768d7 | 1 + .claude/worktrees/agent-a926d49d72757ce68 | 1 + .claude/worktrees/agent-ad2e38b6608f23ab4 | 1 + .claude/worktrees/agent-ada5836798aea71a1 | 1 + .claude/worktrees/agent-aee10bd582e9e36cf | 1 + lib/connector/meshcore_connector.dart | 266 +++-- lib/connector/meshcore_protocol.dart | 7 +- lib/connector/meshcore_uuids.dart | 4 + lib/helpers/contact_ui.dart | 59 + lib/helpers/path_helper.dart | 31 +- lib/l10n/app_en.arb | 162 +-- lib/l10n/app_localizations.dart | 534 +++++---- lib/l10n/app_localizations_bg.dart | 314 ++--- lib/l10n/app_localizations_de.dart | 313 ++--- lib/l10n/app_localizations_en.dart | 309 ++--- lib/l10n/app_localizations_es.dart | 313 ++--- lib/l10n/app_localizations_fr.dart | 315 ++--- lib/l10n/app_localizations_hu.dart | 316 ++--- lib/l10n/app_localizations_it.dart | 313 ++--- lib/l10n/app_localizations_ja.dart | 446 +++---- lib/l10n/app_localizations_ko.dart | 309 ++--- lib/l10n/app_localizations_nl.dart | 313 ++--- lib/l10n/app_localizations_pl.dart | 316 ++--- lib/l10n/app_localizations_pt.dart | 313 ++--- lib/l10n/app_localizations_ru.dart | 317 ++--- lib/l10n/app_localizations_sk.dart | 314 ++--- lib/l10n/app_localizations_sl.dart | 310 ++--- lib/l10n/app_localizations_sv.dart | 310 ++--- lib/l10n/app_localizations_uk.dart | 317 ++--- lib/l10n/app_localizations_zh.dart | 290 +++-- lib/main.dart | 29 +- lib/screens/app_debug_log_screen.dart | 21 +- lib/screens/app_settings_screen.dart | 153 +-- lib/screens/channel_chat_screen.dart | 447 ++++--- lib/screens/channel_message_path_screen.dart | 140 ++- lib/screens/channels_screen.dart | 183 +-- lib/screens/chat_screen.dart | 1031 ++++------------- lib/screens/chrome_required_screen.dart | 33 +- lib/screens/companion_radio_stats_screen.dart | 6 +- lib/screens/contacts_screen.dart | 414 ++++--- lib/screens/discovery_screen.dart | 72 +- lib/screens/line_of_sight_map_screen.dart | 40 +- lib/screens/map_cache_screen.dart | 8 +- lib/screens/map_screen.dart | 371 +++--- lib/screens/neighbors_screen.dart | 90 +- lib/screens/path_trace_map.dart | 214 ++-- lib/screens/repeater_cli_screen.dart | 121 +- lib/screens/repeater_hub_screen.dart | 28 +- lib/screens/repeater_settings_screen.dart | 125 +- lib/screens/repeater_status_screen.dart | 92 +- lib/screens/scanner_screen.dart | 178 +-- lib/screens/settings_screen.dart | 125 +- lib/screens/tcp_screen.dart | 86 +- lib/screens/telemetry_screen.dart | 97 +- lib/screens/usb_screen.dart | 145 +-- lib/services/message_retry_service.dart | 5 + lib/services/notification_service.dart | 5 +- lib/services/translation_service.dart | 31 +- lib/theme/mesh_theme.dart | 103 +- lib/widgets/byte_count_input.dart | 8 +- lib/widgets/device_tile.dart | 22 +- lib/widgets/empty_state.dart | 10 +- lib/widgets/gif_picker.dart | 10 +- lib/widgets/message_status_icon.dart | 151 ++- lib/widgets/path_editor_sheet.dart | 377 ++++++ lib/widgets/path_management_dialog.dart | 510 -------- lib/widgets/path_selection_dialog.dart | 346 ------ lib/widgets/quick_switch_bar.dart | 27 +- lib/widgets/radio_stats_entry.dart | 14 +- lib/widgets/repeater_login_dialog.dart | 19 +- lib/widgets/room_login_dialog.dart | 16 +- lib/widgets/routing_sheet.dart | 709 ++++++++++++ lib/widgets/snr_indicator.dart | 2 +- lib/widgets/telemetry_location_map.dart | 6 +- pubspec.yaml | 2 +- .../security/contact_import_clipboard_pocs.py | 71 ++ test/screens/tcp_flow_test.dart | 6 +- test/screens/usb_flow_test.dart | 18 +- untranslated.json | 988 +++++++++++++++- 86 files changed, 8149 insertions(+), 6379 deletions(-) create mode 160000 .claude/worktrees/agent-a01277dd8cf0dedb6 create mode 160000 .claude/worktrees/agent-a0c7384491c5fc807 create mode 160000 .claude/worktrees/agent-a1751d6addbf69e1d create mode 160000 .claude/worktrees/agent-a2fad933ed81b10e2 create mode 160000 .claude/worktrees/agent-a4d771c23db919c7f create mode 160000 .claude/worktrees/agent-a6675128f3ee1d6a6 create mode 160000 .claude/worktrees/agent-a894f10813e684f84 create mode 160000 .claude/worktrees/agent-a8ba0537ad42768d7 create mode 160000 .claude/worktrees/agent-a926d49d72757ce68 create mode 160000 .claude/worktrees/agent-ad2e38b6608f23ab4 create mode 160000 .claude/worktrees/agent-ada5836798aea71a1 create mode 160000 .claude/worktrees/agent-aee10bd582e9e36cf create mode 100644 lib/helpers/contact_ui.dart create mode 100644 lib/widgets/path_editor_sheet.dart delete mode 100644 lib/widgets/path_management_dialog.dart delete mode 100644 lib/widgets/path_selection_dialog.dart create mode 100644 lib/widgets/routing_sheet.dart create mode 100644 scripts/security/contact_import_clipboard_pocs.py diff --git a/.claude/worktrees/agent-a01277dd8cf0dedb6 b/.claude/worktrees/agent-a01277dd8cf0dedb6 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a01277dd8cf0dedb6 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a0c7384491c5fc807 b/.claude/worktrees/agent-a0c7384491c5fc807 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a0c7384491c5fc807 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a1751d6addbf69e1d b/.claude/worktrees/agent-a1751d6addbf69e1d new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a1751d6addbf69e1d @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a2fad933ed81b10e2 b/.claude/worktrees/agent-a2fad933ed81b10e2 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a2fad933ed81b10e2 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a4d771c23db919c7f b/.claude/worktrees/agent-a4d771c23db919c7f new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a4d771c23db919c7f @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a6675128f3ee1d6a6 b/.claude/worktrees/agent-a6675128f3ee1d6a6 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a6675128f3ee1d6a6 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a894f10813e684f84 b/.claude/worktrees/agent-a894f10813e684f84 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a894f10813e684f84 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a8ba0537ad42768d7 b/.claude/worktrees/agent-a8ba0537ad42768d7 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a8ba0537ad42768d7 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a926d49d72757ce68 b/.claude/worktrees/agent-a926d49d72757ce68 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a926d49d72757ce68 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-ad2e38b6608f23ab4 b/.claude/worktrees/agent-ad2e38b6608f23ab4 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-ad2e38b6608f23ab4 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-ada5836798aea71a1 b/.claude/worktrees/agent-ada5836798aea71a1 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-ada5836798aea71a1 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-aee10bd582e9e36cf b/.claude/worktrees/agent-aee10bd582e9e36cf new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-aee10bd582e9e36cf @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7912962b..dee34fb0 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -160,6 +160,7 @@ class MeshCoreConnector extends ChangeNotifier { {}; // contactPubKeyHex -> Set of "targetHash_emoji" StreamSubscription>? _scanSubscription; + StreamSubscription? _isScanningSubscription; StreamSubscription? _connectionSubscription; StreamSubscription>? _notifySubscription; Timer? _notifyListenersTimer; @@ -507,10 +508,22 @@ class MeshCoreConnector extends ChangeNotifier { if (messages == null) return; final removed = messages.remove(message); if (!removed) return; + _retryService?.untrack(message.messageId); await _messageStore.saveMessages(contactKeyHex, messages); notifyListeners(); } + Future resendMessage(Contact contact, Message message) async { + await deleteMessage(message); + await sendMessage( + contact, + message.text, + originalText: message.originalText, + translatedLanguageCode: message.translatedLanguageCode, + translationModelId: message.translationModelId, + ); + } + Future _loadMessagesForContact(String contactKeyHex) async { if (_loadedConversationKeys.contains(contactKeyHex)) return; _loadedConversationKeys.add(contactKeyHex); @@ -1356,6 +1369,21 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.scanning) return; + // A BLE scan must never disturb an active (or in-progress) non-BLE + // connection. The connection state enum is shared across transports, so + // entering the `scanning` state while connected over TCP/USB would clobber + // the live `connected` state and later reset it to `disconnected`. + if (_state != MeshCoreConnectionState.disconnected || + _tcpConnector.isConnected || + _usbManager.isConnected) { + _appDebugLogService?.warn( + 'startScan ignored: not idle (state=$_state, ' + 'tcp=${_tcpConnector.isConnected}, usb=${_usbManager.isConnected})', + tag: 'BLE Scan', + ); + return; + } + _scanResults.clear(); _linuxSystemScanResults.clear(); _setState(MeshCoreConnectionState.scanning); @@ -1409,20 +1437,40 @@ class MeshCoreConnector extends ChangeNotifier { }); try { + // Filter by the Nordic UART Service UUID rather than by advertised + // name. All MeshCore-compatible firmware (ESP32 + nRF52) advertises this + // service UUID, so this matches every device regardless of the name it + // chooses to advertise (e.g. community forks like the M5 Cardputer that + // do not use a "MeshCore-" name prefix). This mirrors how the official + // app discovers devices. Note: on Android `withKeywords` cannot be + // combined with any other filter, which is why name keywords are not + // used here. await FlutterBluePlus.startScan( - withKeywords: MeshCoreUuids.deviceNamePrefixes, + withServices: [Guid(MeshCoreUuids.service)], webOptionalServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, ); } catch (error) { _appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan'); - _setState(MeshCoreConnectionState.disconnected); + await stopScan(); rethrow; } - await Future.delayed(timeout); - await stopScan(); + // Reset our shared state when the native scan ends — whether it was stopped + // by the user (stopScan), by the platform timeout, or by Bluetooth turning + // off. This replaces a blocking `Future.delayed(timeout)` tail that kept + // startScan() pending for the whole timeout and made Stop appear ineffective. + // `isScanning` is a re-emit stream that replays its latest value on listen, + // so skip(1) to ignore that and only react to a genuine transition to false. + await _isScanningSubscription?.cancel(); + _isScanningSubscription = FlutterBluePlus.isScanning.skip(1).listen(( + scanning, + ) { + if (!scanning && _state == MeshCoreConnectionState.scanning) { + unawaited(stopScan()); + } + }); } Future _loadLinuxSystemDevicesForScan() async { @@ -1430,15 +1478,13 @@ class MeshCoreConnector extends ChangeNotifier { final systemDevices = await FlutterBluePlus.systemDevices([ Guid(MeshCoreUuids.service), ]); + // systemDevices is already filtered by the NUS service UUID above, so no + // additional name-prefix filtering is applied here. This keeps Linux + // discovery name-agnostic and consistent with the main scan path. _linuxSystemScanResults ..clear() ..addAll( systemDevices - .where( - (device) => MeshCoreUuids.deviceNamePrefixes.any( - device.platformName.startsWith, - ), - ) .map( (device) => ScanResult( device: device, @@ -1499,9 +1545,17 @@ class MeshCoreConnector extends ChangeNotifier { } await _scanSubscription?.cancel(); _scanSubscription = null; + await _isScanningSubscription?.cancel(); + _isScanningSubscription = null; if (_state == MeshCoreConnectionState.scanning) { - _setState(MeshCoreConnectionState.disconnected); + // Restore to `connected` if a non-BLE transport is still live, so a stray + // scan can never tear down the reported connection state. Normally there + // is no live transport here and we fall through to `disconnected`. + final restored = (_tcpConnector.isConnected || _usbManager.isConnected) + ? MeshCoreConnectionState.connected + : MeshCoreConnectionState.disconnected; + _setState(restored); } } @@ -1743,6 +1797,17 @@ class MeshCoreConnector extends ChangeNotifier { activeTransport == MeshCoreTransportType.tcp; } + /// Fast (non-timeout) connect failures are usually a stale link left over + /// from a previous session and recover on an immediate retry. Timeouts mean + /// the device is likely off or out of range, so retrying would only delay + /// genuine failure feedback. + @visibleForTesting + static bool shouldRetryBleConnectAfterError(String errorText) { + final lowerErrorText = errorText.toLowerCase(); + return !lowerErrorText.contains('timed out') && + !lowerErrorText.contains('timeout'); + } + Future connect( BluetoothDevice device, { String? displayName, @@ -1911,18 +1976,71 @@ class MeshCoreConnector extends ChangeNotifier { } } } else { - try { - await device.connect( + Future attemptConnect() { + return device.connect( timeout: connectTimeout, mtu: null, license: License.free, ); + } + + // A previous app session (e.g. killed from the iOS app switcher) can + // leave the OS holding a stale link to the peripheral. Clear it before + // connecting so the fresh attempt doesn't race the stale handle. + if (!PlatformInfo.isWeb && device.isConnected) { + _appDebugLogService?.warn( + 'Device reports an existing connection before connect; clearing stale link', + tag: 'BLE Connect', + ); + try { + await device.disconnect(queue: false); + } catch (cleanupError) { + _appDebugLogService?.warn( + 'Stale-link cleanup disconnect failed (continuing): $cleanupError', + tag: 'BLE Connect', + ); + } + } + + try { + await attemptConnect(); } catch (error) { _appDebugLogService?.error( 'device.connect() failure: $error', tag: 'BLE Connect', ); - rethrow; + if (PlatformInfo.isWeb || + !shouldRetryBleConnectAfterError(error.toString())) { + rethrow; + } + // Fast (non-timeout) failures are usually a stale connection left by + // a previous session; clean up and retry once before surfacing. + _appDebugLogService?.warn( + 'Retrying connect once after clearing possible stale connection', + tag: 'BLE Connect', + ); + try { + await device.disconnect(queue: false); + } catch (cleanupError) { + _appDebugLogService?.warn( + 'Pre-retry cleanup disconnect failed (continuing): $cleanupError', + tag: 'BLE Connect', + ); + } + await Future.delayed(const Duration(milliseconds: 500)); + try { + await attemptConnect(); + _appDebugLogService?.info( + 'Retry connect succeeded after stale-connection cleanup', + tag: 'BLE Connect', + ); + } catch (retryError) { + _appDebugLogService?.error( + 'device.connect() retry failure: $retryError', + tag: 'BLE Connect', + ); + rethrow; + } } } @@ -3237,6 +3355,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildRemoveContactFrame(contact.publicKey)); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); _knownContactKeys.remove(contact.publicKeyHex); + unawaited(updateKnownDiscovered()); unawaited(_persistContacts()); _conversations.remove(contact.publicKeyHex); _loadedConversationKeys.remove(contact.publicKeyHex); @@ -3450,12 +3569,10 @@ class MeshCoreConnector extends ChangeNotifier { Future sendCliCommand(String command) async { if (!isConnected) return; - - // CLI commands are sent as UTF-8 text with a special prefix - final commandBytes = utf8.encode(command); - final bytes = Uint8List.fromList([0x01, ...commandBytes, 0x00]); + final selfKey = _selfPublicKey; + if (selfKey == null) return; _lastSentWasCliCommand = true; - await sendFrame(bytes); + await sendFrame(buildSendCliCommandFrame(selfKey, command)); } Future setNodeName(String name) async { @@ -3929,8 +4046,8 @@ class MeshCoreConnector extends ChangeNotifier { _advertLocPolicy = reader.readByte(); final telemetryFlag = reader.readByte(); _telemetryModeBase = telemetryFlag & 0x03; - _telemetryModeEnv = telemetryFlag >> 2 & 0x03; - _telemetryModeLoc = telemetryFlag >> 4 & 0x03; + _telemetryModeLoc = telemetryFlag >> 2 & 0x03; + _telemetryModeEnv = telemetryFlag >> 4 & 0x03; _manualAddContacts = reader.readByte() & 0x01 == 0x00; @@ -4275,7 +4392,14 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'Connector', ); notifyListeners(); - removeContact(contactTmp); + unawaited( + removeContact(contactTmp).catchError( + (e) => appLogger.warn( + 'Failed to remove self contact: $e', + tag: 'Connector', + ), + ), + ); return; } final contact = getFromDiscovered(contactTmp); @@ -5521,6 +5645,9 @@ class MeshCoreConnector extends ChangeNotifier { } messages.add(message); + if (messages.length > _messageWindowSize) { + messages.removeRange(0, messages.length - _messageWindowSize); + } _messageStore.saveMessages(pubKeyHex, messages); notifyListeners(); } @@ -5907,20 +6034,24 @@ class MeshCoreConnector extends ChangeNotifier { bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) { if (existing.text != incoming.text) return false; + // Self-echo: an outgoing message coming back via a repeater. The send is + // delayed by _waitForRadioQuiet (often 10s+) and propagation can add more, + // so the timestamp gap can easily exceed the cross-peer window. + final selfName = _selfName ?? 'Me'; + final isSelfEcho = + existing.isOutgoing && + !incoming.isOutgoing && + (incoming.senderName == selfName || existing.senderName == selfName); + + final windowMs = isSelfEcho ? 10 * 60 * 1000 : 30000; final diffMs = (existing.timestamp.millisecondsSinceEpoch - incoming.timestamp.millisecondsSinceEpoch) .abs(); - if (diffMs > 30000) return false; + if (diffMs > windowMs) return false; if (existing.senderName == incoming.senderName) return true; - - if (existing.isOutgoing && !incoming.isOutgoing) { - final selfName = _selfName ?? 'Me'; - if (incoming.senderName == selfName || existing.senderName == selfName) { - return true; - } - } + if (isSelfEcho) return true; return false; } @@ -6152,6 +6283,7 @@ class MeshCoreConnector extends ChangeNotifier { @override void dispose() { _scanSubscription?.cancel(); + _isScanningSubscription?.cancel(); _connectionSubscription?.cancel(); _usbFrameSubscription?.cancel(); _notifySubscription?.cancel(); @@ -6210,82 +6342,6 @@ class MeshCoreConnector extends ChangeNotifier { } } - void importContact(Uint8List frame) { - final packet = BufferReader(frame); - int payloadType = 0; - Uint8List pathBytes = Uint8List(0); - try { - packet.skipBytes(1); // Skip frame type byte - packet.skipBytes(1); // Skip SNR byte - packet.skipBytes(1); // Skip RSSI byte - final header = packet.readByte(); - final routeType = header & 0x03; - payloadType = (header >> 2) & 0x0F; - if (routeType == _routeTransportFlood || - routeType == _routeTransportDirect) { - packet.skipBytes(4); // Skip transport-specific bytes - } - //final payloadVer = (header >> 6) & 0x03; - final pathLenRaw = packet.readByte(); - final pathByteLen = _decodePathByteLen(pathLenRaw); - pathBytes = packet.readBytes(pathByteLen); - } catch (e) { - appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); - return; - } - double? latitude; - double? longitude; - String name = ''; - Uint8List publicKey = Uint8List(0); - int type = 0; - int timestamp = 0; - bool hasLocation = false; - bool hasName = false; - if (payloadType != payloadTypeADVERT) { - appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector'); - return; - } - try { - publicKey = packet.readBytes(32); - timestamp = packet.readInt32LE(); - //TODO add signature verification - packet.skipBytes(64); // Skip signature for now - final flags = packet.readByte(); - type = flags & 0x0F; - hasLocation = (flags & 0x10) != 0; - // For future use: - //final hasFeature1 = (flags & 0x20) != 0; - //final hasFeature2 = (flags & 0x40) != 0; - hasName = (flags & 0x80) != 0; - if (hasLocation && packet.remaining >= 8) { - latitude = packet.readInt32LE() / 1e6; - longitude = packet.readInt32LE() / 1e6; - } - if (hasName && packet.remaining > 0) { - name = packet.readCString(); - } - } catch (e) { - appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); - return; - } - - importDiscoveredContact( - Contact( - rawPacket: frame, - publicKey: publicKey, - name: name, - type: type, - pathLength: pathBytes.isEmpty ? -1 : pathBytes.length, - path: Uint8List.fromList( - pathBytes.reversed.toList(), - ), // Store path in reverse for easier use in outgoing messages - latitude: latitude, - longitude: longitude, - lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - ), - ); - } - bool hasValidLocation(double? latitude, double? longitude) { const double epsilon = 1e-6; final lat = latitude ?? 0.0; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index a056f4ac..7213b753 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -457,8 +457,13 @@ String pubKeyToHex(Uint8List pubKey) { // Helper to convert hex string to public key Uint8List hexToPubKey(String hex) { + if (hex.length != pubKeySize * 2) { + throw FormatException( + 'Public key hex must be ${pubKeySize * 2} chars, got ${hex.length}', + ); + } final result = Uint8List(pubKeySize); - for (int i = 0; i < pubKeySize && i * 2 + 1 < hex.length; i++) { + for (int i = 0; i < pubKeySize; i++) { result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16); } return result; diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index 084cc424..0a32b2a6 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -3,6 +3,10 @@ class MeshCoreUuids { static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + /// Known advertised-name prefixes used by stock MeshCore firmware builds. + /// Discovery no longer filters on these (it filters on the [service] UUID so + /// that community forks with custom names are still found); kept for + /// reference and possible future display heuristics. static const List deviceNamePrefixes = [ "MeshCore-", "Whisper-", diff --git a/lib/helpers/contact_ui.dart b/lib/helpers/contact_ui.dart new file mode 100644 index 00000000..9e3b31f7 --- /dev/null +++ b/lib/helpers/contact_ui.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../connector/meshcore_protocol.dart'; +import '../utils/emoji_utils.dart'; + +IconData contactTypeIcon(int type) { + switch (type) { + case advTypeChat: + return Icons.chat; + case advTypeRepeater: + return Icons.cell_tower; + case advTypeRoom: + return Icons.group; + case advTypeSensor: + return Icons.sensors; + default: + return Icons.device_unknown; + } +} + +Color contactTypeColor(int type) { + switch (type) { + case advTypeChat: + return Colors.blue; + case advTypeRepeater: + return Colors.orange; + case advTypeRoom: + return Colors.purple; + case advTypeSensor: + return Colors.green; + default: + return Colors.grey; + } +} + +Color colorForName(String name) { + const colors = [ + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.pink, + Colors.teal, + Colors.indigo, + Colors.cyan, + Colors.amber, + Colors.deepOrange, + ]; + return colors[name.hashCode.abs() % colors.length]; +} + +String firstCharacterOrEmoji(String name) { + if (name.isEmpty) return '?'; + final emoji = firstEmoji(name); + if (emoji != null) return emoji; + final runes = name.runes.toList(); + if (runes.isEmpty) return '?'; + return String.fromCharCode(runes[0]).toUpperCase(); +} diff --git a/lib/helpers/path_helper.dart b/lib/helpers/path_helper.dart index fe51d636..bd599c00 100644 --- a/lib/helpers/path_helper.dart +++ b/lib/helpers/path_helper.dart @@ -8,24 +8,29 @@ class PathHelper { .join(','); } + static String hopHex(int byte) { + return byte.toRadixString(16).padLeft(2, '0').toUpperCase(); + } + + static String? hopName(int byte, List allContacts) { + final matches = allContacts + .where( + (c) => + c.publicKey.first == byte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + if (matches.isEmpty) return null; + if (matches.length == 1) return matches.first.name; + return matches.map((c) => c.name).join(' | '); + } + static String resolvePathNames( List pathBytes, List allContacts, ) { return pathBytes - .map((b) { - final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase(); - final matches = allContacts - .where( - (c) => - c.publicKey.first == b && - (c.type == advTypeRepeater || c.type == advTypeRoom), - ) - .toList(); - if (matches.isEmpty) return hex; - if (matches.length == 1) return matches.first.name; - return matches.map((c) => c.name).join(' | '); - }) + .map((b) => hopName(b, allContacts) ?? hopHex(b)) .join(' \u2192 '); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1f05124a..58613d02 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -28,6 +28,12 @@ "common_remove": "Remove", "common_enable": "Enable", "common_disable": "Disable", + "common_undo": "Undo", + "messageStatus_sent": "Sent", + "messageStatus_delivered": "Delivered", + "messageStatus_pending": "Sending", + "messageStatus_failed": "Failed to send", + "messageStatus_repeated": "Heard repeated", "common_reboot": "Reboot", "common_loading": "Loading...", "common_notAvailable": "—", @@ -297,17 +303,6 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery", "appSettings_maxMessageRetries": "Max Message Retries", "appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed", - "path_routeWeight": "{weight}/{max}", - "@path_routeWeight": { - "placeholders": { - "weight": { - "type": "String" - }, - "max": { - "type": "String" - } - } - }, "appSettings_battery": "Battery", "appSettings_batteryChemistry": "Battery Chemistry", "appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})", @@ -453,6 +448,9 @@ } }, "contacts_newGroup": "New Group", + "contacts_moreOptions": "More options", + "contacts_searchOpen": "Search contacts", + "contacts_searchClose": "Close search", "contacts_groupName": "Group name", "contacts_groupNameRequired": "Group name is required", "contacts_groupNameReserved": "This group name is reserved", @@ -777,15 +775,6 @@ } }, "debugFrame_hexDump": "Hex Dump:", - "chat_pathManagement": "Path Management", - "chat_ShowAllPaths": "Show all paths", - "chat_routingMode": "Routing mode", - "chat_autoUseSavedPath": "Auto (use saved path)", - "chat_forceFloodMode": "Force Flood Mode", - "chat_recentAckPaths": "Recent ACK Paths (tap to use):", - "chat_pathHistoryFull": "Path history is full. Remove entries to add new ones.", - "chat_hopSingular": "hop", - "chat_hopPlural": "hops", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { @@ -794,31 +783,80 @@ } } }, - "chat_successes": "successes", - "chat_score": "Score", "chat_removePath": "Remove path", "chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.", - "chat_pathActions": "Path Actions:", - "chat_setCustomPath": "Set Custom Path", - "chat_setCustomPathSubtitle": "Manually specify routing path", - "chat_clearPath": "Clear Path", - "chat_clearPathSubtitle": "Force rediscovery on next send", "chat_pathCleared": "Path cleared. Next message will rediscover route.", - "chat_floodModeSubtitle": "Use routing toggle in app bar", - "chat_floodModeEnabled": "Flood mode enabled. Toggle back via routing icon in app bar.", "chat_fullPath": "Full Path", - "chat_pathDetailsNotAvailable": "Path details not available yet. Try sending a message to refresh.", - "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", - "@chat_pathSetHops": { + "routing_title": "Routing", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Flood", + "routing_modeManual": "Manual", + "routing_modeAutoHint": "Picks the best known path automatically, flooding when none is known.", + "routing_modeFloodHint": "Broadcasts through every repeater. Most reliable, but uses more airtime.", + "routing_modeManualHint": "Always sends along the exact path you set.", + "routing_currentRoute": "Current route", + "routing_directNoHops": "Direct — no repeater hops", + "routing_noPathYet": "No path yet. The next message floods until a route is discovered.", + "routing_floodBroadcast": "Broadcast through every repeater", + "routing_editPath": "Edit path", + "routing_forgetPath": "Forget path", + "routing_knownPaths": "Known paths", + "routing_knownPathsHint": "Tap a path to switch to it.", + "routing_inUse": "In use", + "routing_qualityStrong": "Strong first hop", + "routing_qualityGood": "Good first hop", + "routing_qualityFair": "Fair first hop", + "routing_qualityWorked": "Has delivered", + "routing_qualityFlood": "Heard via flood", + "routing_qualityUntested": "Untested", + "routing_lastWorked": "worked {when}", + "@routing_lastWorked": { "placeholders": { - "hopCount": { - "type": "int" - }, - "status": { + "when": { "type": "String" } } }, + "routing_neverWorked": "never confirmed", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "routing_floodDelivery": "Flood delivery", + "pathEditor_title": "Build Path", + "pathEditor_hopCounter": "{count} of 64 hops", + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "pathEditor_noHops": "No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.", + "pathEditor_addHops": "Add hops in order", + "pathEditor_searchRepeaters": "Search repeaters", + "pathEditor_advancedHex": "Advanced: raw hex path", + "pathEditor_hexLabel": "Hex prefixes", + "pathEditor_hexHelper": "Two hex characters per hop, separated by commas", + "pathEditor_invalidTokens": "Invalid: {tokens}", + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "pathEditor_tooManyHops": "Maximum 64 hops", + "pathEditor_usePath": "Use this path", + "pathEditor_removeHop": "Remove hop", + "pathEditor_unknownHop": "Unknown repeater", "chat_pathSavedLocally": "Saved locally. Connect to sync.", "chat_pathDeviceConfirmed": "Device confirmed.", "chat_pathDeviceNotConfirmed": "Device not confirmed yet.", @@ -1100,41 +1138,8 @@ "login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.", "common_reload": "Reload", "common_clear": "Clear", - "path_currentPath": "Current path: {path}", - "@path_currentPath": { - "placeholders": { - "path": { - "type": "String" - } - } - }, - "path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path", - "@path_usingHopsPath": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "path_enterCustomPath": "Enter Custom Path", "path_currentPathLabel": "Current path", - "path_hexPrefixInstructions": "Enter 2-character hex prefixes for each hop, separated by commas.", - "path_hexPrefixExample": "Example: A1,F2,3C (each node uses first byte of its public key)", - "path_labelHexPrefixes": "Path (hex prefixes)", - "path_helperMaxHops": "Max 64 hops. Each prefix is 2 hex characters (1 byte)", - "path_selectFromContacts": "Or select from contacts:", "path_noRepeatersFound": "No repeaters or room servers found.", - "path_customPathsRequire": "Custom paths require intermediate hops that can relay messages.", - "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", - "@path_invalidHexPrefixes": { - "placeholders": { - "prefixes": { - "type": "String" - } - } - }, - "path_tooLong": "Path too long. Maximum 64 hops allowed.", - "path_setPath": "Set Path", "repeater_management": "Repeater Management", "room_management": "Room Server Management", "repeater_guest": "Repeater Information", @@ -1161,9 +1166,6 @@ }, "repeater_statusTitle": "Repeater Status", "repeater_routingMode": "Routing mode", - "repeater_autoUseSavedPath": "Auto (use saved path)", - "repeater_forceFloodMode": "Force Flood Mode", - "repeater_pathManagement": "Path management", "repeater_refresh": "Refresh", "repeater_statusRequestTimeout": "Status request timed out.", "repeater_errorLoadingStatus": "Error loading status: {error}", @@ -2485,5 +2487,19 @@ "contact_typeRepeater": "Repeater", "contact_typeRoom": "Room", "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown" + "contact_typeUnknown": "Unknown", + "map_zoomIn": "Zoom in", + "map_zoomOut": "Zoom out", + "map_centerMap": "Center map", + "chrome_bluetoothRequiresChromium": "Web Bluetooth requires a Chromium browser", + "channels_communityShortId": "ID: {id}...", + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "pathTrace_legendGpsConfirmed": "GPS confirmed", + "pathTrace_legendInferred": "Inferred position" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b45facfe..f55fc02c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -298,6 +298,42 @@ abstract class AppLocalizations { /// **'Disable'** String get common_disable; + /// No description provided for @common_undo. + /// + /// In en, this message translates to: + /// **'Undo'** + String get common_undo; + + /// No description provided for @messageStatus_sent. + /// + /// In en, this message translates to: + /// **'Sent'** + String get messageStatus_sent; + + /// No description provided for @messageStatus_delivered. + /// + /// In en, this message translates to: + /// **'Delivered'** + String get messageStatus_delivered; + + /// No description provided for @messageStatus_pending. + /// + /// In en, this message translates to: + /// **'Sending'** + String get messageStatus_pending; + + /// No description provided for @messageStatus_failed. + /// + /// In en, this message translates to: + /// **'Failed to send'** + String get messageStatus_failed; + + /// No description provided for @messageStatus_repeated. + /// + /// In en, this message translates to: + /// **'Heard repeated'** + String get messageStatus_repeated; + /// No description provided for @common_reboot. /// /// In en, this message translates to: @@ -1534,12 +1570,6 @@ abstract class AppLocalizations { /// **'Number of retry attempts before marking a message as failed'** String get appSettings_maxMessageRetriesSubtitle; - /// No description provided for @path_routeWeight. - /// - /// In en, this message translates to: - /// **'{weight}/{max}'** - String path_routeWeight(String weight, String max); - /// No description provided for @appSettings_battery. /// /// In en, this message translates to: @@ -1882,6 +1912,24 @@ abstract class AppLocalizations { /// **'New Group'** String get contacts_newGroup; + /// No description provided for @contacts_moreOptions. + /// + /// In en, this message translates to: + /// **'More options'** + String get contacts_moreOptions; + + /// No description provided for @contacts_searchOpen. + /// + /// In en, this message translates to: + /// **'Search contacts'** + String get contacts_searchOpen; + + /// No description provided for @contacts_searchClose. + /// + /// In en, this message translates to: + /// **'Close search'** + String get contacts_searchClose; + /// No description provided for @contacts_groupName. /// /// In en, this message translates to: @@ -2710,78 +2758,12 @@ abstract class AppLocalizations { /// **'Hex Dump:'** String get debugFrame_hexDump; - /// No description provided for @chat_pathManagement. - /// - /// In en, this message translates to: - /// **'Path Management'** - String get chat_pathManagement; - - /// No description provided for @chat_ShowAllPaths. - /// - /// In en, this message translates to: - /// **'Show all paths'** - String get chat_ShowAllPaths; - - /// No description provided for @chat_routingMode. - /// - /// In en, this message translates to: - /// **'Routing mode'** - String get chat_routingMode; - - /// No description provided for @chat_autoUseSavedPath. - /// - /// In en, this message translates to: - /// **'Auto (use saved path)'** - String get chat_autoUseSavedPath; - - /// No description provided for @chat_forceFloodMode. - /// - /// In en, this message translates to: - /// **'Force Flood Mode'** - String get chat_forceFloodMode; - - /// No description provided for @chat_recentAckPaths. - /// - /// In en, this message translates to: - /// **'Recent ACK Paths (tap to use):'** - String get chat_recentAckPaths; - - /// No description provided for @chat_pathHistoryFull. - /// - /// In en, this message translates to: - /// **'Path history is full. Remove entries to add new ones.'** - String get chat_pathHistoryFull; - - /// No description provided for @chat_hopSingular. - /// - /// In en, this message translates to: - /// **'hop'** - String get chat_hopSingular; - - /// No description provided for @chat_hopPlural. - /// - /// In en, this message translates to: - /// **'hops'** - String get chat_hopPlural; - /// No description provided for @chat_hopsCount. /// /// In en, this message translates to: /// **'{count} {count, plural, =1{hop} other{hops}}'** String chat_hopsCount(int count); - /// No description provided for @chat_successes. - /// - /// In en, this message translates to: - /// **'successes'** - String get chat_successes; - - /// No description provided for @chat_score. - /// - /// In en, this message translates to: - /// **'Score'** - String get chat_score; - /// No description provided for @chat_removePath. /// /// In en, this message translates to: @@ -2794,71 +2776,251 @@ abstract class AppLocalizations { /// **'No path history yet.\nSend a message to discover paths.'** String get chat_noPathHistoryYet; - /// No description provided for @chat_pathActions. - /// - /// In en, this message translates to: - /// **'Path Actions:'** - String get chat_pathActions; - - /// No description provided for @chat_setCustomPath. - /// - /// In en, this message translates to: - /// **'Set Custom Path'** - String get chat_setCustomPath; - - /// No description provided for @chat_setCustomPathSubtitle. - /// - /// In en, this message translates to: - /// **'Manually specify routing path'** - String get chat_setCustomPathSubtitle; - - /// No description provided for @chat_clearPath. - /// - /// In en, this message translates to: - /// **'Clear Path'** - String get chat_clearPath; - - /// No description provided for @chat_clearPathSubtitle. - /// - /// In en, this message translates to: - /// **'Force rediscovery on next send'** - String get chat_clearPathSubtitle; - /// No description provided for @chat_pathCleared. /// /// In en, this message translates to: /// **'Path cleared. Next message will rediscover route.'** String get chat_pathCleared; - /// No description provided for @chat_floodModeSubtitle. - /// - /// In en, this message translates to: - /// **'Use routing toggle in app bar'** - String get chat_floodModeSubtitle; - - /// No description provided for @chat_floodModeEnabled. - /// - /// In en, this message translates to: - /// **'Flood mode enabled. Toggle back via routing icon in app bar.'** - String get chat_floodModeEnabled; - /// No description provided for @chat_fullPath. /// /// In en, this message translates to: /// **'Full Path'** String get chat_fullPath; - /// No description provided for @chat_pathDetailsNotAvailable. + /// No description provided for @routing_title. /// /// In en, this message translates to: - /// **'Path details not available yet. Try sending a message to refresh.'** - String get chat_pathDetailsNotAvailable; + /// **'Routing'** + String get routing_title; - /// No description provided for @chat_pathSetHops. + /// No description provided for @routing_modeAuto. /// /// In en, this message translates to: - /// **'Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}'** - String chat_pathSetHops(int hopCount, String status); + /// **'Auto'** + String get routing_modeAuto; + + /// No description provided for @routing_modeFlood. + /// + /// In en, this message translates to: + /// **'Flood'** + String get routing_modeFlood; + + /// No description provided for @routing_modeManual. + /// + /// In en, this message translates to: + /// **'Manual'** + String get routing_modeManual; + + /// No description provided for @routing_modeAutoHint. + /// + /// In en, this message translates to: + /// **'Picks the best known path automatically, flooding when none is known.'** + String get routing_modeAutoHint; + + /// No description provided for @routing_modeFloodHint. + /// + /// In en, this message translates to: + /// **'Broadcasts through every repeater. Most reliable, but uses more airtime.'** + String get routing_modeFloodHint; + + /// No description provided for @routing_modeManualHint. + /// + /// In en, this message translates to: + /// **'Always sends along the exact path you set.'** + String get routing_modeManualHint; + + /// No description provided for @routing_currentRoute. + /// + /// In en, this message translates to: + /// **'Current route'** + String get routing_currentRoute; + + /// No description provided for @routing_directNoHops. + /// + /// In en, this message translates to: + /// **'Direct — no repeater hops'** + String get routing_directNoHops; + + /// No description provided for @routing_noPathYet. + /// + /// In en, this message translates to: + /// **'No path yet. The next message floods until a route is discovered.'** + String get routing_noPathYet; + + /// No description provided for @routing_floodBroadcast. + /// + /// In en, this message translates to: + /// **'Broadcast through every repeater'** + String get routing_floodBroadcast; + + /// No description provided for @routing_editPath. + /// + /// In en, this message translates to: + /// **'Edit path'** + String get routing_editPath; + + /// No description provided for @routing_forgetPath. + /// + /// In en, this message translates to: + /// **'Forget path'** + String get routing_forgetPath; + + /// No description provided for @routing_knownPaths. + /// + /// In en, this message translates to: + /// **'Known paths'** + String get routing_knownPaths; + + /// No description provided for @routing_knownPathsHint. + /// + /// In en, this message translates to: + /// **'Tap a path to switch to it.'** + String get routing_knownPathsHint; + + /// No description provided for @routing_inUse. + /// + /// In en, this message translates to: + /// **'In use'** + String get routing_inUse; + + /// No description provided for @routing_qualityStrong. + /// + /// In en, this message translates to: + /// **'Strong first hop'** + String get routing_qualityStrong; + + /// No description provided for @routing_qualityGood. + /// + /// In en, this message translates to: + /// **'Good first hop'** + String get routing_qualityGood; + + /// No description provided for @routing_qualityFair. + /// + /// In en, this message translates to: + /// **'Fair first hop'** + String get routing_qualityFair; + + /// No description provided for @routing_qualityWorked. + /// + /// In en, this message translates to: + /// **'Has delivered'** + String get routing_qualityWorked; + + /// No description provided for @routing_qualityFlood. + /// + /// In en, this message translates to: + /// **'Heard via flood'** + String get routing_qualityFlood; + + /// No description provided for @routing_qualityUntested. + /// + /// In en, this message translates to: + /// **'Untested'** + String get routing_qualityUntested; + + /// No description provided for @routing_lastWorked. + /// + /// In en, this message translates to: + /// **'worked {when}'** + String routing_lastWorked(String when); + + /// No description provided for @routing_neverWorked. + /// + /// In en, this message translates to: + /// **'never confirmed'** + String get routing_neverWorked; + + /// No description provided for @routing_deliveryCounts. + /// + /// In en, this message translates to: + /// **'{successes} delivered, {failures} failed'** + String routing_deliveryCounts(int successes, int failures); + + /// No description provided for @routing_floodDelivery. + /// + /// In en, this message translates to: + /// **'Flood delivery'** + String get routing_floodDelivery; + + /// No description provided for @pathEditor_title. + /// + /// In en, this message translates to: + /// **'Build Path'** + String get pathEditor_title; + + /// No description provided for @pathEditor_hopCounter. + /// + /// In en, this message translates to: + /// **'{count} of 64 hops'** + String pathEditor_hopCounter(int count); + + /// No description provided for @pathEditor_noHops. + /// + /// In en, this message translates to: + /// **'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'** + String get pathEditor_noHops; + + /// No description provided for @pathEditor_addHops. + /// + /// In en, this message translates to: + /// **'Add hops in order'** + String get pathEditor_addHops; + + /// No description provided for @pathEditor_searchRepeaters. + /// + /// In en, this message translates to: + /// **'Search repeaters'** + String get pathEditor_searchRepeaters; + + /// No description provided for @pathEditor_advancedHex. + /// + /// In en, this message translates to: + /// **'Advanced: raw hex path'** + String get pathEditor_advancedHex; + + /// No description provided for @pathEditor_hexLabel. + /// + /// In en, this message translates to: + /// **'Hex prefixes'** + String get pathEditor_hexLabel; + + /// No description provided for @pathEditor_hexHelper. + /// + /// In en, this message translates to: + /// **'Two hex characters per hop, separated by commas'** + String get pathEditor_hexHelper; + + /// No description provided for @pathEditor_invalidTokens. + /// + /// In en, this message translates to: + /// **'Invalid: {tokens}'** + String pathEditor_invalidTokens(String tokens); + + /// No description provided for @pathEditor_tooManyHops. + /// + /// In en, this message translates to: + /// **'Maximum 64 hops'** + String get pathEditor_tooManyHops; + + /// No description provided for @pathEditor_usePath. + /// + /// In en, this message translates to: + /// **'Use this path'** + String get pathEditor_usePath; + + /// No description provided for @pathEditor_removeHop. + /// + /// In en, this message translates to: + /// **'Remove hop'** + String get pathEditor_removeHop; + + /// No description provided for @pathEditor_unknownHop. + /// + /// In en, this message translates to: + /// **'Unknown repeater'** + String get pathEditor_unknownHop; /// No description provided for @chat_pathSavedLocally. /// @@ -3687,90 +3849,18 @@ abstract class AppLocalizations { /// **'Clear'** String get common_clear; - /// No description provided for @path_currentPath. - /// - /// In en, this message translates to: - /// **'Current path: {path}'** - String path_currentPath(String path); - - /// No description provided for @path_usingHopsPath. - /// - /// In en, this message translates to: - /// **'Using {count} {count, plural, =1{hop} other{hops}} path'** - String path_usingHopsPath(int count); - - /// No description provided for @path_enterCustomPath. - /// - /// In en, this message translates to: - /// **'Enter Custom Path'** - String get path_enterCustomPath; - /// No description provided for @path_currentPathLabel. /// /// In en, this message translates to: /// **'Current path'** String get path_currentPathLabel; - /// No description provided for @path_hexPrefixInstructions. - /// - /// In en, this message translates to: - /// **'Enter 2-character hex prefixes for each hop, separated by commas.'** - String get path_hexPrefixInstructions; - - /// No description provided for @path_hexPrefixExample. - /// - /// In en, this message translates to: - /// **'Example: A1,F2,3C (each node uses first byte of its public key)'** - String get path_hexPrefixExample; - - /// No description provided for @path_labelHexPrefixes. - /// - /// In en, this message translates to: - /// **'Path (hex prefixes)'** - String get path_labelHexPrefixes; - - /// No description provided for @path_helperMaxHops. - /// - /// In en, this message translates to: - /// **'Max 64 hops. Each prefix is 2 hex characters (1 byte)'** - String get path_helperMaxHops; - - /// No description provided for @path_selectFromContacts. - /// - /// In en, this message translates to: - /// **'Or select from contacts:'** - String get path_selectFromContacts; - /// No description provided for @path_noRepeatersFound. /// /// In en, this message translates to: /// **'No repeaters or room servers found.'** String get path_noRepeatersFound; - /// No description provided for @path_customPathsRequire. - /// - /// In en, this message translates to: - /// **'Custom paths require intermediate hops that can relay messages.'** - String get path_customPathsRequire; - - /// No description provided for @path_invalidHexPrefixes. - /// - /// In en, this message translates to: - /// **'Invalid hex prefixes: {prefixes}'** - String path_invalidHexPrefixes(String prefixes); - - /// No description provided for @path_tooLong. - /// - /// In en, this message translates to: - /// **'Path too long. Maximum 64 hops allowed.'** - String get path_tooLong; - - /// No description provided for @path_setPath. - /// - /// In en, this message translates to: - /// **'Set Path'** - String get path_setPath; - /// No description provided for @repeater_management. /// /// In en, this message translates to: @@ -3891,24 +3981,6 @@ abstract class AppLocalizations { /// **'Routing mode'** String get repeater_routingMode; - /// No description provided for @repeater_autoUseSavedPath. - /// - /// In en, this message translates to: - /// **'Auto (use saved path)'** - String get repeater_autoUseSavedPath; - - /// No description provided for @repeater_forceFloodMode. - /// - /// In en, this message translates to: - /// **'Force Flood Mode'** - String get repeater_forceFloodMode; - - /// No description provided for @repeater_pathManagement. - /// - /// In en, this message translates to: - /// **'Path management'** - String get repeater_pathManagement; - /// No description provided for @repeater_refresh. /// /// In en, this message translates to: @@ -7587,6 +7659,48 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Unknown'** String get contact_typeUnknown; + + /// No description provided for @map_zoomIn. + /// + /// In en, this message translates to: + /// **'Zoom in'** + String get map_zoomIn; + + /// No description provided for @map_zoomOut. + /// + /// In en, this message translates to: + /// **'Zoom out'** + String get map_zoomOut; + + /// No description provided for @map_centerMap. + /// + /// In en, this message translates to: + /// **'Center map'** + String get map_centerMap; + + /// No description provided for @chrome_bluetoothRequiresChromium. + /// + /// In en, this message translates to: + /// **'Web Bluetooth requires a Chromium browser'** + String get chrome_bluetoothRequiresChromium; + + /// No description provided for @channels_communityShortId. + /// + /// In en, this message translates to: + /// **'ID: {id}...'** + String channels_communityShortId(String id); + + /// No description provided for @pathTrace_legendGpsConfirmed. + /// + /// In en, this message translates to: + /// **'GPS confirmed'** + String get pathTrace_legendGpsConfirmed; + + /// No description provided for @pathTrace_legendInferred. + /// + /// In en, this message translates to: + /// **'Inferred position'** + String get pathTrace_legendInferred; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index af9e693d..68742652 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -92,6 +92,24 @@ class AppLocalizationsBg extends AppLocalizations { @override String get common_disable => 'Деактивирай'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Рестартирай'; @@ -796,11 +814,6 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Батерия'; @@ -1002,6 +1015,15 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_newGroup => 'Нова група'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Група'; @@ -1482,35 +1504,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get debugFrame_hexDump => 'Хексадесетичен Dump:'; - @override - String get chat_pathManagement => 'Управление на пътища'; - - @override - String get chat_ShowAllPaths => 'Покажи всички пътища'; - - @override - String get chat_routingMode => 'Режим на маршрутизиране'; - - @override - String get chat_autoUseSavedPath => 'Автоматично (използвай запазения път)'; - - @override - String get chat_forceFloodMode => 'Принуди режим на наводняване'; - - @override - String get chat_recentAckPaths => - 'Неотдавни ACK пътища (докоснете, за да използвате):'; - - @override - String get chat_pathHistoryFull => - 'Историята на пътя е пълна. Премахнете записи, за да добавите нови.'; - - @override - String get chat_hopSingular => 'скочи'; - - @override - String get chat_hopPlural => 'скоци'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1522,12 +1515,6 @@ class AppLocalizationsBg extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Успехи'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Премахни пътя'; @@ -1535,52 +1522,144 @@ class AppLocalizationsBg extends AppLocalizations { String get chat_noPathHistoryYet => 'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.'; - @override - String get chat_pathActions => 'Действия по пътя:'; - - @override - String get chat_setCustomPath => 'Задайте персонализиран път'; - - @override - String get chat_setCustomPathSubtitle => 'Ръчно укажете маршрутен път'; - - @override - String get chat_clearPath => 'Почисти Път'; - - @override - String get chat_clearPathSubtitle => - 'Принуди преоткриване при следващо изпращане'; - @override String get chat_pathCleared => 'Пътят е почистен. Следващото съобщение ще открие маршрута отново.'; - @override - String get chat_floodModeSubtitle => - 'Използвайте превключвателя за маршрутизиране в лентата на приложението.'; - - @override - String get chat_floodModeEnabled => - 'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.'; - @override String get chat_fullPath => 'Пълен път'; @override - String get chat_pathDetailsNotAvailable => - 'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Пътят е зададен: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Запазено локално. Свържете се за синхронизиране.'; @@ -2050,66 +2129,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get common_clear => 'Изчисти'; - @override - String path_currentPath(String path) { - return 'Текущ път: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Използване на $count $_temp0 път'; - } - - @override - String get path_enterCustomPath => 'Въведете персонализиран път'; - @override String get path_currentPathLabel => 'Текущ път'; - @override - String get path_hexPrefixInstructions => - 'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.'; - - @override - String get path_hexPrefixExample => - 'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)'; - - @override - String get path_labelHexPrefixes => 'Пътеки (шестнадесетични префикси)'; - - @override - String get path_helperMaxHops => - 'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).'; - - @override - String get path_selectFromContacts => 'Изберете от контакти:'; - @override String get path_noRepeatersFound => 'Няма намерени репетитори или сървъри на стаи.'; - @override - String get path_customPathsRequire => - 'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Невалидни шестнадесетични префикси: $prefixes'; - } - - @override - String get path_tooLong => - 'Пътят е твърде дълъг. Максимум 64 скока са разрешени.'; - - @override - String get path_setPath => 'Задайте път'; - @override String get repeater_management => 'Управление на повторители'; @@ -2176,16 +2202,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_routingMode => 'Режим на маршрутизиране'; - @override - String get repeater_autoUseSavedPath => - 'Автоматично (използвай запазения път)'; - - @override - String get repeater_forceFloodMode => 'Принуди режим на наводняване'; - - @override - String get repeater_pathManagement => 'Управление на пътища'; - @override String get repeater_refresh => 'Презареди'; @@ -4437,4 +4453,28 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 3fa9c4a8..62df5bb6 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -92,6 +92,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get common_disable => 'Deaktivieren'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Neustart'; @@ -792,11 +810,6 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Akku'; @@ -998,6 +1011,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_newGroup => 'Neue Gruppe'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Gruppenname'; @@ -1480,36 +1502,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get debugFrame_hexDump => 'Hex-Dump:'; - @override - String get chat_pathManagement => 'Pfadverwaltung'; - - @override - String get chat_ShowAllPaths => 'Alle Pfade anzeigen'; - - @override - String get chat_routingMode => 'Routenmodus'; - - @override - String get chat_autoUseSavedPath => - 'Automatisch (gespeicherten Pfad verwenden)'; - - @override - String get chat_forceFloodMode => 'Flut-Modus erzwingen'; - - @override - String get chat_recentAckPaths => - 'Aktuelle ACK-Pfade (antippen, um zu verwenden):'; - - @override - String get chat_pathHistoryFull => - 'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.'; - - @override - String get chat_hopSingular => 'Sprung'; - - @override - String get chat_hopPlural => 'Sprünge'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1521,12 +1513,6 @@ class AppLocalizationsDe extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Erfolgreich'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Pfad entfernen'; @@ -1534,51 +1520,144 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_noPathHistoryYet => 'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.'; - @override - String get chat_pathActions => 'Pfadaktionen:'; - - @override - String get chat_setCustomPath => 'Lege benutzerdefinierten Pfad fest'; - - @override - String get chat_setCustomPathSubtitle => 'Manuellen Routenpfad festlegen'; - - @override - String get chat_clearPath => 'Pfad zurücksetzen'; - - @override - String get chat_clearPathSubtitle => - 'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.'; - @override String get chat_pathCleared => 'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.'; - @override - String get chat_floodModeSubtitle => - 'Verwende den Routingschalter in der App-Leiste'; - - @override - String get chat_floodModeEnabled => 'Flutmodus aktiviert.'; - @override String get chat_fullPath => 'Vollständiger Pfad'; @override - String get chat_pathDetailsNotAvailable => - 'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Pfad gesetzt: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.'; @@ -2049,65 +2128,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get common_clear => 'Löschen'; - @override - String path_currentPath(String path) { - return 'Aktiver Pfad: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Hops', - one: 'Hop', - ); - return 'Verwenden Sie $count $_temp0 Pfad'; - } - - @override - String get path_enterCustomPath => 'Gebe Pfad ein'; - @override String get path_currentPathLabel => 'Aktueller Pfad'; - @override - String get path_hexPrefixInstructions => - 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; - - @override - String get path_hexPrefixExample => - 'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)'; - - @override - String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)'; - - @override - String get path_helperMaxHops => - 'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)'; - - @override - String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:'; - @override String get path_noRepeatersFound => 'Keine Repeater oder Raumserver gefunden.'; - @override - String get path_customPathsRequire => - 'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Ungültige Hexadezimal-Präfixe: $prefixes'; - } - - @override - String get path_tooLong => 'Pfad zu lang. Maximal 64 Hops erlaubt.'; - - @override - String get path_setPath => 'Pfad festlegen'; - @override String get repeater_management => 'Repeater-Verwaltung'; @@ -2172,16 +2199,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_routingMode => 'Routenmodus'; - @override - String get repeater_autoUseSavedPath => - 'Automatisch (gespeicherten Pfad verwenden)'; - - @override - String get repeater_forceFloodMode => 'Flut-Modus erzwingen'; - - @override - String get repeater_pathManagement => 'Pfadverwaltung'; - @override String get repeater_refresh => 'Aktualisieren'; @@ -4455,4 +4472,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4fbfd5bb..eecfbc41 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -92,6 +92,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get common_disable => 'Disable'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Reboot'; @@ -777,11 +795,6 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Number of retry attempts before marking a message as failed'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Battery'; @@ -981,6 +994,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contacts_newGroup => 'New Group'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Group name'; @@ -1452,34 +1474,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get debugFrame_hexDump => 'Hex Dump:'; - @override - String get chat_pathManagement => 'Path Management'; - - @override - String get chat_ShowAllPaths => 'Show all paths'; - - @override - String get chat_routingMode => 'Routing mode'; - - @override - String get chat_autoUseSavedPath => 'Auto (use saved path)'; - - @override - String get chat_forceFloodMode => 'Force Flood Mode'; - - @override - String get chat_recentAckPaths => 'Recent ACK Paths (tap to use):'; - - @override - String get chat_pathHistoryFull => - 'Path history is full. Remove entries to add new ones.'; - - @override - String get chat_hopSingular => 'hop'; - - @override - String get chat_hopPlural => 'hops'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1491,12 +1485,6 @@ class AppLocalizationsEn extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'successes'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Remove path'; @@ -1504,50 +1492,144 @@ class AppLocalizationsEn extends AppLocalizations { String get chat_noPathHistoryYet => 'No path history yet.\nSend a message to discover paths.'; - @override - String get chat_pathActions => 'Path Actions:'; - - @override - String get chat_setCustomPath => 'Set Custom Path'; - - @override - String get chat_setCustomPathSubtitle => 'Manually specify routing path'; - - @override - String get chat_clearPath => 'Clear Path'; - - @override - String get chat_clearPathSubtitle => 'Force rediscovery on next send'; - @override String get chat_pathCleared => 'Path cleared. Next message will rediscover route.'; - @override - String get chat_floodModeSubtitle => 'Use routing toggle in app bar'; - - @override - String get chat_floodModeEnabled => - 'Flood mode enabled. Toggle back via routing icon in app bar.'; - @override String get chat_fullPath => 'Full Path'; @override - String get chat_pathDetailsNotAvailable => - 'Path details not available yet. Try sending a message to refresh.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Path set: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Saved locally. Connect to sync.'; @@ -2009,64 +2091,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get common_clear => 'Clear'; - @override - String path_currentPath(String path) { - return 'Current path: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Using $count $_temp0 path'; - } - - @override - String get path_enterCustomPath => 'Enter Custom Path'; - @override String get path_currentPathLabel => 'Current path'; - @override - String get path_hexPrefixInstructions => - 'Enter 2-character hex prefixes for each hop, separated by commas.'; - - @override - String get path_hexPrefixExample => - 'Example: A1,F2,3C (each node uses first byte of its public key)'; - - @override - String get path_labelHexPrefixes => 'Path (hex prefixes)'; - - @override - String get path_helperMaxHops => - 'Max 64 hops. Each prefix is 2 hex characters (1 byte)'; - - @override - String get path_selectFromContacts => 'Or select from contacts:'; - @override String get path_noRepeatersFound => 'No repeaters or room servers found.'; - @override - String get path_customPathsRequire => - 'Custom paths require intermediate hops that can relay messages.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Invalid hex prefixes: $prefixes'; - } - - @override - String get path_tooLong => 'Path too long. Maximum 64 hops allowed.'; - - @override - String get path_setPath => 'Set Path'; - @override String get repeater_management => 'Repeater Management'; @@ -2130,15 +2160,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_routingMode => 'Routing mode'; - @override - String get repeater_autoUseSavedPath => 'Auto (use saved path)'; - - @override - String get repeater_forceFloodMode => 'Force Flood Mode'; - - @override - String get repeater_pathManagement => 'Path management'; - @override String get repeater_refresh => 'Refresh'; @@ -4360,4 +4381,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 383dd032..aa17930b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -92,6 +92,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get common_disable => 'Desactivar'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Reiniciar'; @@ -791,11 +809,6 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Número de intentos de reintento antes de marcar un mensaje como fallido.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Batería'; @@ -997,6 +1010,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_newGroup => 'Nuevo Grupo'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Nombre del grupo'; @@ -1479,34 +1501,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get debugFrame_hexDump => 'Mapeo Hexadecimal:'; - @override - String get chat_pathManagement => 'Gestión de Rutas'; - - @override - String get chat_ShowAllPaths => 'Mostrar todos los caminos'; - - @override - String get chat_routingMode => 'Modo de enrutamiento'; - - @override - String get chat_autoUseSavedPath => 'Auto (usar la ruta guardada)'; - - @override - String get chat_forceFloodMode => 'Modo Inundación Forzado'; - - @override - String get chat_recentAckPaths => 'Rutas de ACK Recientes (tocar para usar):'; - - @override - String get chat_pathHistoryFull => - 'El historial de rutas está completo. Eliminar entradas para añadir nuevas.'; - - @override - String get chat_hopSingular => 'salta'; - - @override - String get chat_hopPlural => 'salta'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1518,12 +1512,6 @@ class AppLocalizationsEs extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Éxitos'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Eliminar ruta'; @@ -1531,53 +1519,144 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_noPathHistoryYet => 'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.'; - @override - String get chat_pathActions => 'Acciones de Ruta:'; - - @override - String get chat_setCustomPath => 'Establecer Ruta Personalizada'; - - @override - String get chat_setCustomPathSubtitle => - 'Especificar manualmente la ruta de enrutamiento'; - - @override - String get chat_clearPath => 'Limpiar Ruta'; - - @override - String get chat_clearPathSubtitle => - 'Forzar redescubrimiento en el próximo envío'; - @override String get chat_pathCleared => 'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; - @override - String get chat_floodModeSubtitle => - 'Utilizar el interruptor de enrutamiento en la barra de herramientas'; - - @override - String get chat_floodModeEnabled => - 'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.'; - @override String get chat_fullPath => 'Ruta completa'; @override - String get chat_pathDetailsNotAvailable => - 'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Ruta establecida: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Guardado localmente. Conéctate para sincronizar.'; @@ -2045,66 +2124,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get common_clear => 'Borrar'; - @override - String path_currentPath(String path) { - return 'Ruta actual: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Usando $count $_temp0 ruta'; - } - - @override - String get path_enterCustomPath => 'Introducir Ruta Personalizada'; - @override String get path_currentPathLabel => 'Ruta actual'; - @override - String get path_hexPrefixInstructions => - 'Introduzca los prefijos hexadecimales de 2 caracteres para cada salto, separados por comas.'; - - @override - String get path_hexPrefixExample => - 'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).'; - - @override - String get path_labelHexPrefixes => 'Prefijos hexadecimales'; - - @override - String get path_helperMaxHops => - 'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).'; - - @override - String get path_selectFromContacts => 'O seleccionar de contactos:'; - @override String get path_noRepeatersFound => 'No se encontraron repetidores ni servidores de sala.'; - @override - String get path_customPathsRequire => - 'Las rutas personalizadas requieren saltos intermedios que pueden transmitir mensajes.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Prefijos hexadecimales inválidos: $prefixes'; - } - - @override - String get path_tooLong => - 'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.'; - - @override - String get path_setPath => 'Establecer Ruta'; - @override String get repeater_management => 'Gestión de Repetidores'; @@ -2169,15 +2195,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_routingMode => 'Modo de enrutamiento'; - @override - String get repeater_autoUseSavedPath => 'Auto (usar la ruta guardada)'; - - @override - String get repeater_forceFloodMode => 'Modo Inundación Forzado'; - - @override - String get repeater_pathManagement => 'Gestión de rutas'; - @override String get repeater_refresh => 'Actualizar'; @@ -4442,4 +4459,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 990c49c6..69e2af85 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -92,6 +92,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get common_disable => 'Désactiver'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Redémarrer'; @@ -797,11 +815,6 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Batterie'; @@ -1003,6 +1016,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_newGroup => 'Nouveau Groupe'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Nom du groupe'; @@ -1485,35 +1507,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; - @override - String get chat_pathManagement => 'Gestion des chemins'; - - @override - String get chat_ShowAllPaths => 'Afficher tous les chemins'; - - @override - String get chat_routingMode => 'Mode de routage'; - - @override - String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; - - @override - String get chat_forceFloodMode => 'Mode tout le réseau forcé'; - - @override - String get chat_recentAckPaths => - 'Chemins ACK récents (touchez pour utiliser) :'; - - @override - String get chat_pathHistoryFull => - 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; - - @override - String get chat_hopSingular => 'saut'; - - @override - String get chat_hopPlural => 'sauts'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1525,12 +1518,6 @@ class AppLocalizationsFr extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Succès'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Supprimer le chemin'; @@ -1538,53 +1525,144 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_noPathHistoryYet => 'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; - @override - String get chat_pathActions => 'Actions du chemin :'; - - @override - String get chat_setCustomPath => 'Définir un chemin personnalisé'; - - @override - String get chat_setCustomPathSubtitle => - 'Spécifier manuellement le chemin de routage'; - - @override - String get chat_clearPath => 'Effacer le chemin'; - - @override - String get chat_clearPathSubtitle => - 'Forcer la redécouverte lors de la prochaine envoi'; - @override String get chat_pathCleared => 'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; - @override - String get chat_floodModeSubtitle => - 'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.'; - - @override - String get chat_floodModeEnabled => - 'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.'; - @override String get chat_fullPath => 'Chemin complet'; @override - String get chat_pathDetailsNotAvailable => - 'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Chemin défini : $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Sauvegardé localement. Connectez-vous pour synchroniser.'; @@ -2056,66 +2134,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get common_clear => 'Effacer'; - @override - String path_currentPath(String path) { - return 'Chemin actuel : $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Utiliser $count $_temp0 chemin'; - } - - @override - String get path_enterCustomPath => 'Entrer un chemin personnalisé'; - @override String get path_currentPathLabel => 'Chemin actuel'; - @override - String get path_hexPrefixInstructions => - 'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.'; - - @override - String get path_hexPrefixExample => - 'Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).'; - - @override - String get path_labelHexPrefixes => 'Préfixes hexadécimaux'; - - @override - String get path_helperMaxHops => - 'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)'; - - @override - String get path_selectFromContacts => 'Sélectionner à partir des contacts :'; - @override String get path_noRepeatersFound => 'Aucun répéteur ou room server n\'a été trouvé.'; - @override - String get path_customPathsRequire => - 'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Préfixes hexadécimaux invalides : $prefixes'; - } - - @override - String get path_tooLong => - 'Le chemin est trop long. Maximum 64 sauts autorisés.'; - - @override - String get path_setPath => 'Définir le chemin'; - @override String get repeater_management => 'Gestion des répéteurs'; @@ -2181,16 +2206,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_routingMode => 'Mode de routage'; - @override - String get repeater_autoUseSavedPath => - 'Auto (utiliser le chemin sauvegardé)'; - - @override - String get repeater_forceFloodMode => 'Mode tout le réseau forcé'; - - @override - String get repeater_pathManagement => 'Gestion des chemins'; - @override String get repeater_refresh => 'Rafraîchir'; @@ -4471,4 +4486,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 58f263b3..f58b8afc 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -92,6 +92,24 @@ class AppLocalizationsHu extends AppLocalizations { @override String get common_disable => 'Leteteszt'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Újraindítás'; @@ -795,11 +813,6 @@ class AppLocalizationsHu extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Akku'; @@ -1003,6 +1016,15 @@ class AppLocalizationsHu extends AppLocalizations { @override String get contacts_newGroup => 'Új csoport'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Csoport neve'; @@ -1488,36 +1510,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get debugFrame_hexDump => 'Hex-dump:'; - @override - String get chat_pathManagement => 'Útvonal-kezelés'; - - @override - String get chat_ShowAllPaths => 'Mutasson meg minden útvonalat'; - - @override - String get chat_routingMode => 'Útvonal-kezelési mód'; - - @override - String get chat_autoUseSavedPath => - 'Automatikus (az eddigi útvonal használata)'; - - @override - String get chat_forceFloodMode => 'Erőforrás-alapú áramlás mód'; - - @override - String get chat_recentAckPaths => - 'Legutóbbi használt útvonalak (gombra kattintva):'; - - @override - String get chat_pathHistoryFull => - 'Az előző lépések listája teljes. Törölj ki a bejegyzéseket, hogy újokat hozzáadhatsd.'; - - @override - String get chat_hopSingular => 'ugor'; - - @override - String get chat_hopPlural => 'babér'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1529,12 +1521,6 @@ class AppLocalizationsHu extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'sikerek'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Törölje a elérési útvonalat'; @@ -1542,52 +1528,144 @@ class AppLocalizationsHu extends AppLocalizations { String get chat_noPathHistoryYet => 'Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.'; - @override - String get chat_pathActions => 'Céltúrások:'; - - @override - String get chat_setCustomPath => 'Beállítsd a saját útvonalat'; - - @override - String get chat_setCustomPathSubtitle => 'Kézzel megadott útvonal'; - - @override - String get chat_clearPath => 'Egyértelmű út'; - - @override - String get chat_clearPathSubtitle => - 'A parancs új küldéskor újra kell aktivizálnia.'; - @override String get chat_pathCleared => 'Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.'; - @override - String get chat_floodModeSubtitle => - 'Használja a \"útvonal\" kapcsolót az alkalmazás sávjában.'; - - @override - String get chat_floodModeEnabled => - 'Árvízvédelmi mód bekapcsolva. A visszaállítás a alkalmazásban található útvonal ikon segítségével.'; - @override String get chat_fullPath => 'Teljes elérési út'; @override - String get chat_pathDetailsNotAvailable => - 'Az útvonal részletei még nem elérhetők. Próbálja meg küldeni egy üzenetet, hogy frissítse az információkat.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Path set: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.'; @@ -2058,67 +2136,13 @@ class AppLocalizationsHu extends AppLocalizations { @override String get common_clear => 'Egyértelmű'; - @override - String path_currentPath(String path) { - return 'Jelenlegi útvonal: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'ugrások', - one: 'ugrás', - ); - return '$count $_temp0 útvonal használata'; - } - - @override - String get path_enterCustomPath => 'Adja meg a saját elérési útvonalat'; - @override String get path_currentPathLabel => 'Jelenlegi útvonal'; - @override - String get path_hexPrefixInstructions => - 'Adja meg a 2 karakteres hexadecimális előtagokat minden lépéshez, tagolva kommával.'; - - @override - String get path_hexPrefixExample => - 'Példa: A1, F2, 3C (minden csomó az első részét használja a nyilvános kulcsából)'; - - @override - String get path_labelHexPrefixes => 'Út (hex-prefixek)'; - - @override - String get path_helperMaxHops => - 'A maximális hossz 64 karakter. Minden előző rész 2 hatos számjegyből áll (1 bájt).'; - - @override - String get path_selectFromContacts => - 'Válasszon a kontaktlista elembek közül:'; - @override String get path_noRepeatersFound => 'Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.'; - @override - String get path_customPathsRequire => - 'Az egyedi útvonalaknak szükségük van átjáró pontokra, amelyek képesek üzeneteket továbbítani.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Érvénytelen hexadecimális előtagok: $prefixes'; - } - - @override - String get path_tooLong => - 'Az út túl hosszú. A maximális engedélyezett lépések száma 64.'; - - @override - String get path_setPath => 'Útvonal meghatározása'; - @override String get repeater_management => 'Adatkapcsolás kezelése'; @@ -2184,16 +2208,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_routingMode => 'Útvonal-kezelési mód'; - @override - String get repeater_autoUseSavedPath => - 'Automatikus (az eddigi útvonal használata)'; - - @override - String get repeater_forceFloodMode => 'Erőforrás-alapú áramlás mód'; - - @override - String get repeater_pathManagement => 'Útvonal-kezelés'; - @override String get repeater_refresh => 'Újrafriszol'; @@ -4458,4 +4472,28 @@ class AppLocalizationsHu extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a0149bd0..7a7043bb 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -92,6 +92,24 @@ class AppLocalizationsIt extends AppLocalizations { @override String get common_disable => 'Disattivare'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Riavvia'; @@ -794,11 +812,6 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Numero di tentativi di riprova prima di considerare un messaggio come fallito.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Batteria'; @@ -999,6 +1012,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_newGroup => 'Nuovo Gruppo'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Nome gruppo'; @@ -1481,34 +1503,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get debugFrame_hexDump => 'Dumpa Esadecimale:'; - @override - String get chat_pathManagement => 'Gestione Percorsi'; - - @override - String get chat_ShowAllPaths => 'Mostra tutti i percorsi'; - - @override - String get chat_routingMode => 'Modalità di routing'; - - @override - String get chat_autoUseSavedPath => 'Utilizza il percorso salvato'; - - @override - String get chat_forceFloodMode => 'Modalità Inondamento Forzato'; - - @override - String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):'; - - @override - String get chat_pathHistoryFull => - 'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.'; - - @override - String get chat_hopSingular => 'salta'; - - @override - String get chat_hopPlural => 'salta'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1520,12 +1514,6 @@ class AppLocalizationsIt extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'successi'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Rimuovi percorso'; @@ -1533,53 +1521,144 @@ class AppLocalizationsIt extends AppLocalizations { String get chat_noPathHistoryYet => 'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; - @override - String get chat_pathActions => 'Azioni Percorso:'; - - @override - String get chat_setCustomPath => 'Imposta Percorso Personalizzato'; - - @override - String get chat_setCustomPathSubtitle => - 'Specifica manualmente il percorso di routing'; - - @override - String get chat_clearPath => 'Cancella Percorso'; - - @override - String get chat_clearPathSubtitle => - 'Riprova la scoperta alla prossima invio'; - @override String get chat_pathCleared => 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; - @override - String get chat_floodModeSubtitle => - 'Utilizza l\'interruttore di routing nella barra delle applicazioni'; - - @override - String get chat_floodModeEnabled => - 'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.'; - @override String get chat_fullPath => 'Percorso Completo'; @override - String get chat_pathDetailsNotAvailable => - 'I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Percorso impostato: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Salvatato localmente. Connetti per sincronizzare.'; @@ -2047,66 +2126,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get common_clear => 'Cancella'; - @override - String path_currentPath(String path) { - return 'Percorso corrente: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Utilizzare $count $_temp0 percorso'; - } - - @override - String get path_enterCustomPath => 'Inserisci percorso personalizzato'; - @override String get path_currentPathLabel => 'Percorso corrente'; - @override - String get path_hexPrefixInstructions => - 'Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.'; - - @override - String get path_hexPrefixExample => - 'Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)'; - - @override - String get path_labelHexPrefixes => 'Prefisso esadecimale (percorso)'; - - @override - String get path_helperMaxHops => - 'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)'; - - @override - String get path_selectFromContacts => 'Seleziona da contatti:'; - @override String get path_noRepeatersFound => 'Non sono stati trovati ripetitori o server di stanza.'; - @override - String get path_customPathsRequire => - 'I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Prefissi esadecimali non validi: $prefixes'; - } - - @override - String get path_tooLong => - 'Il percorso è troppo lungo. Massimo 64 salti consentiti.'; - - @override - String get path_setPath => 'Imposta Percorso'; - @override String get repeater_management => 'Gestione Ripetitori'; @@ -2173,15 +2199,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_routingMode => 'Modalità di routing'; - @override - String get repeater_autoUseSavedPath => 'Percorso salvato automatico'; - - @override - String get repeater_forceFloodMode => 'Modalità Inondamento Forzato'; - - @override - String get repeater_pathManagement => 'Gestione dei percorsi'; - @override String get repeater_refresh => 'Aggiorna'; @@ -4447,4 +4464,28 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 12396086..ee79c6ee 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -92,6 +92,24 @@ class AppLocalizationsJa extends AppLocalizations { @override String get common_disable => '無効化する'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => '再起動'; @@ -559,7 +577,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)'; @override - String get settings_clientRepeat => 'オフグリッド(電力網から孤立した状態)の繰り返し'; + String get settings_clientRepeat => 'オフグリッドリピータ'; @override String get settings_clientRepeatSubtitle => @@ -709,7 +727,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_autoRouteRotation => '自動ルートの切り替え'; @override - String get appSettings_autoRouteRotationSubtitle => '最適なルートと、洪水モードを切り替える'; + String get appSettings_autoRouteRotationSubtitle => '最適なルートと、フラッドモードを切り替える'; @override String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています'; @@ -751,11 +769,6 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'メッセージを「失敗」とマークするまでの、再試行回数'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'バッテリー'; @@ -786,7 +799,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_showRepeaters => '繰り返し再生機能'; @override - String get appSettings_showRepeatersSubtitle => '地図上にリピーターノードを表示する'; + String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する'; @override String get appSettings_showChatNodes => 'チャットノードの表示'; @@ -925,7 +938,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get contacts_manageRepeater => 'リピーターの管理'; + String get contacts_manageRepeater => 'リピータの管理'; @override String get contacts_manageRoom => 'ルームサーバーの管理'; @@ -950,6 +963,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String get contacts_newGroup => '新しいグループ'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'グループ名'; @@ -1416,33 +1438,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get debugFrame_hexDump => 'ヘックスダンプ:'; - @override - String get chat_pathManagement => '経路管理'; - - @override - String get chat_ShowAllPaths => 'すべての経路を表示'; - - @override - String get chat_routingMode => 'ルーティングモード'; - - @override - String get chat_autoUseSavedPath => '自動 (保存されたパスを使用)'; - - @override - String get chat_forceFloodMode => '強制的に洪水モードを起動'; - - @override - String get chat_recentAckPaths => '最近使用したACKパス(タップして使用):'; - - @override - String get chat_pathHistoryFull => 'パスの履歴は完全です。エントリを削除して、新しいものを追加できます。'; - - @override - String get chat_hopSingular => 'ジャンプ'; - - @override - String get chat_hopPlural => 'ホップ'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1454,61 +1449,149 @@ class AppLocalizationsJa extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => '成功事例'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'パスを削除する'; @override String get chat_noPathHistoryYet => 'まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。'; - @override - String get chat_pathActions => 'パスの操作:'; - - @override - String get chat_setCustomPath => 'カスタムパスを設定'; - - @override - String get chat_setCustomPathSubtitle => '手動で経路を指定する'; - - @override - String get chat_clearPath => '明確な道'; - - @override - String get chat_clearPathSubtitle => '次回送信時に、以前の情報を再取得する'; - @override String get chat_pathCleared => '経路が確保されました。次のメッセージでルートを再確認します。'; - @override - String get chat_floodModeSubtitle => 'アプリのバーにあるルーティング切り替え機能を使用する'; - - @override - String get chat_floodModeEnabled => - '洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。'; - @override String get chat_fullPath => 'フルパス'; @override - String get chat_pathDetailsNotAvailable => - '経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Path set: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。'; @@ -1531,7 +1614,7 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する'; @override - String get chat_floodForced => '洪水(強制的な)'; + String get chat_floodForced => 'フラッド(強制的な)'; @override String get chat_directForced => '直接的な(強制的な)'; @@ -1542,7 +1625,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get chat_floodAuto => '洪水 (自動)'; + String get chat_floodAuto => 'フラッド (自動)'; @override String get chat_direct => '直接'; @@ -1607,7 +1690,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_chat => 'チャット'; @override - String get map_repeater => '繰り返し送信装置'; + String get map_repeater => 'リピータ'; @override String get map_room => '部屋'; @@ -1702,13 +1785,13 @@ class AppLocalizationsJa extends AppLocalizations { String get map_chatNodes => 'チャットノード'; @override - String get map_repeaters => '繰り返し送信装置'; + String get map_repeaters => 'リピータ'; @override String get map_otherNodes => 'その他のノード'; @override - String get map_showOverlaps => 'リピーターキーの重複'; + String get map_showOverlaps => 'リピータキーの重複'; @override String get map_keyPrefix => '主要なプレフィックス'; @@ -1747,7 +1830,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_joinRoom => '部屋に参加する'; @override - String get map_manageRepeater => 'リピーターの管理'; + String get map_manageRepeater => 'リピータの管理'; @override String get map_tapToAdd => 'ノードをクリックして、パスに追加します。'; @@ -1920,7 +2003,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get login_repeaterDescription => - '設定やステータスにアクセスするために、リピーターのパスワードを入力してください。'; + '設定やステータスにアクセスするために、リピータのパスワードを入力してください。'; @override String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。'; @@ -1935,7 +2018,7 @@ class AppLocalizationsJa extends AppLocalizations { String get login_autoUseSavedPath => '自動 (保存されたパスを使用)'; @override - String get login_forceFloodMode => '強制的に洪水モードを起動'; + String get login_forceFloodMode => '強制的にフラッドモードを起動'; @override String get login_managePaths => 'パスの管理'; @@ -1963,70 +2046,20 @@ class AppLocalizationsJa extends AppLocalizations { @override String get common_clear => '明確'; - @override - String path_currentPath(String path) { - return '現在のパス: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'ホップ', - one: 'ホップ', - ); - return '$count $_temp0のパスを使用'; - } - - @override - String get path_enterCustomPath => 'カスタムパスを入力'; - @override String get path_currentPathLabel => '現在の経路'; - @override - String get path_hexPrefixInstructions => - '各ホップに対して、2文字の16進数プレフィックスをカンマで区切って入力してください。'; - - @override - String get path_hexPrefixExample => '例:A1, F2, 3C (各ノードは、自身の公開鍵の最初のバイトを使用)'; - - @override - String get path_labelHexPrefixes => 'パス (ヘックスプレフィックス)'; - - @override - String get path_helperMaxHops => - '最大64個のホップ。各プレフィックスは2つの16進数文字(1バイト)で構成されています。'; - - @override - String get path_selectFromContacts => 'または、連絡先リストから選択してください:'; - @override String get path_noRepeatersFound => '繰り返し機能やルームサーバーは見つかりませんでした。'; @override - String get path_customPathsRequire => 'カスタムパスには、メッセージを中継できる中間地点が必要です。'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return '無効な16進数プレフィックス: $prefixes'; - } - - @override - String get path_tooLong => '経路が長すぎる。最大64回のジャンプのみ許可。'; - - @override - String get path_setPath => 'パスを設定'; - - @override - String get repeater_management => 'リピーター管理'; + String get repeater_management => 'リピータ管理'; @override String get room_management => 'ルームサーバーの管理'; @override - String get repeater_guest => '繰り返し送信に関する情報'; + String get repeater_guest => 'リピータに関する情報'; @override String get room_guest => 'ルームサーバーに関する情報'; @@ -2041,7 +2074,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_status => 'ステータス'; @override - String get repeater_statusSubtitle => 'リピーターの状態、統計情報、および隣接するネットワークの情報を表示する'; + String get repeater_statusSubtitle => 'リピータの状態、統計情報、および隣接するネットワークの情報を表示する'; @override String get repeater_telemetry => 'テレメトリー'; @@ -2053,7 +2086,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cli => 'CLI(コマンドラインインターフェース)'; @override - String get repeater_cliSubtitle => 'リピーターへのコマンドを送信する'; + String get repeater_cliSubtitle => 'リピータへのコマンドを送信する'; @override String get repeater_neighbors => '近隣住民'; @@ -2065,7 +2098,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_settings => '設定'; @override - String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; + String get repeater_settingsSubtitle => 'リピータのパラメータを設定する'; @override String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; @@ -2080,15 +2113,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_routingMode => 'ルーティングモード'; - @override - String get repeater_autoUseSavedPath => '自動 (保存されたパスを使用)'; - - @override - String get repeater_forceFloodMode => '強制的に洪水モードを起動'; - - @override - String get repeater_pathManagement => '経路管理'; - @override String get repeater_refresh => 'リフレッシュ'; @@ -2163,12 +2187,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return '合計: $total, 洪水: $flood, 直接: $direct'; + return '合計: $total, フラッド: $flood, 直接: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return '合計: $total, 洪水: $flood, 直接: $direct'; + return '合計: $total, フラッド: $flood, 直接: $direct'; } @override @@ -2182,16 +2206,16 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_settingsTitle => 'リピーター設定'; + String get repeater_settingsTitle => 'リピータ設定'; @override String get repeater_basicSettings => '基本設定'; @override - String get repeater_repeaterName => '送信装置名'; + String get repeater_repeaterName => 'リピータ名'; @override - String get repeater_repeaterNameHelper => 'このリピーターの名前'; + String get repeater_repeaterNameHelper => 'このリピータの名前'; @override String get repeater_adminPassword => '管理者パスワード'; @@ -2251,7 +2275,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_packetForwarding => 'パケット転送'; @override - String get repeater_packetForwardingSubtitle => 'リピーターがパケットを転送できるように設定する'; + String get repeater_packetForwardingSubtitle => 'リピータがパケットを転送できるように設定する'; @override String get repeater_guestAccess => 'ゲストへのアクセス'; @@ -2277,7 +2301,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => '洪水に関する広告の表示間隔'; + String get repeater_floodAdvertInterval => 'フラッドに関する広告の表示間隔'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2291,13 +2315,13 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_dangerZone => '危険区域'; @override - String get repeater_rebootRepeater => 'リピーターを再起動する'; + String get repeater_rebootRepeater => 'リピータを再起動する'; @override - String get repeater_rebootRepeaterSubtitle => 'リピーターデバイスを再起動する'; + String get repeater_rebootRepeaterSubtitle => 'リピータデバイスを再起動する'; @override - String get repeater_rebootRepeaterConfirm => '本当にこのリピーターを再起動したいですか?'; + String get repeater_rebootRepeaterConfirm => '本当にこのリピータを再起動したいですか?'; @override String get repeater_regenerateIdentityKey => 'IDキーの再生成'; @@ -2307,17 +2331,17 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'これにより、リピーターには新しい識別情報が割り当てられます。続行しますか?'; + 'これにより、リピータには新しい識別情報が割り当てられます。続行しますか?'; @override String get repeater_eraseFileSystem => 'ファイルシステムを削除する'; @override - String get repeater_eraseFileSystemSubtitle => 'リピーターファイルシステムをフォーマットする'; + String get repeater_eraseFileSystemSubtitle => 'リピータファイルシステムをフォーマットする'; @override String get repeater_eraseFileSystemConfirm => - '警告:この操作により、リピーター内のすべてのデータが消去されます。この操作は元に戻すことができません!'; + '警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!'; @override String get repeater_eraseSerialOnly => 'Erase機能は、シリアルコンソール経由でのみ利用可能です。'; @@ -2363,7 +2387,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_loopDetect => 'ループ検出'; @override - String get repeater_loopDetectHelper => 'ルーティングループに見えるような、洪水パケットを送信する'; + String get repeater_loopDetectHelper => 'ルーティングループを検知する'; @override String get repeater_loopDetectOff => 'オフ'; @@ -2392,7 +2416,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_ownerInfo => '事業者の情報'; @override - String get repeater_ownerInfoHelper => 'このリピーターに関する公開メタデータ'; + String get repeater_ownerInfoHelper => 'このリピータに関する公開メタデータ'; @override String get repeater_refreshOwnerInfo => 'オペレーター情報の更新'; @@ -2401,7 +2425,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_floodMax => '最大ホップ数'; @override - String get repeater_floodMaxHelper => '洪水パケットが移動できる最大ホップ数 (0-64)'; + String get repeater_floodMaxHelper => 'フラッドパケットが移動できる最大ホップ数 (0-64)'; @override String get repeater_advancedSettings => '高度な'; @@ -2414,14 +2438,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_pathHashModeHelper => - 'このリピーターのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; + 'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; @override String get repeater_txDelay => 'フロイド・TXでの遅延'; @override String get repeater_txDelayHelper => - '洪水時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。'; + 'フラッド時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。'; @override String get repeater_directTxDelay => '直接的なTX遅延'; @@ -2448,16 +2472,16 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_actionsTitle => '行動'; @override - String get repeater_sendAdvert => '洪水に関する広告を送信'; + String get repeater_sendAdvert => 'フラッドに関する広告を送信'; @override - String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、洪水に関する広告を放送する'; + String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、フラッドに関する広告を放送する'; @override String get repeater_sendAdvertZeroHop => 'ゼロホップ形式の広告を送信する'; @override - String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(中継なし)'; + String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(リピータなし)'; @override String get repeater_clockSync => '現在、時刻を同期する'; @@ -2477,7 +2501,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_settingsSavedRebootNeeded => - '設定を保存しました — リピーターを再起動して適用してください'; + '設定を保存しました — リピータを再起動して適用してください'; @override String repeater_settingsPartialFailure(String failures) { @@ -2518,7 +2542,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_cliTitle => 'リピーターのコマンドラインインターフェース'; + String get repeater_cliTitle => 'リピータのコマンドラインインターフェース'; @override String get repeater_debugNextCommand => '次のコマンドのデバッグ'; @@ -2609,7 +2633,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpSetTx => 'LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)'; @override - String get repeater_cliHelpSetRepeat => 'このノードに対するリピーターの役割を有効化または無効化します。'; + String get repeater_cliHelpSetRepeat => 'このノードに対するリピータの役割を有効化または無効化します。'; @override String get repeater_cliHelpSetAllowReadOnly => @@ -2617,7 +2641,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpSetFloodMax => - 'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; + 'インバウンドフラッドパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; @override String get repeater_cliHelpSetIntThresh => @@ -2636,7 +2660,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpSetFloodAdvertInterval => - '洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; + 'フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; @override String get repeater_cliHelpSetGuestPassword => @@ -2710,7 +2734,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; + 'ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; @override String get repeater_cliHelpNeighborRemove => @@ -2718,7 +2742,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpRegion => - '(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。'; + '(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。'; @override String get repeater_cliHelpRegionLoad => @@ -2737,7 +2761,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpRegionAllowf => - '指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; + '指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; @override String get repeater_cliHelpRegionDenyf => @@ -2793,7 +2817,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_logging => 'ログ記録'; @override - String get repeater_neighborsRepeaterOnly => '近隣住民(リピーターのみ)'; + String get repeater_neighborsRepeaterOnly => '近隣住民(リピータのみ)'; @override String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)'; @@ -2838,7 +2862,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpDiscoverNeighbors => - '近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)'; + '近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)'; @override String get repeater_cliHelpPowersaving => '省電力モードがオンになっているかどうかを表示します。'; @@ -2887,7 +2911,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetName => '設定されたノードの名前を表示します。'; @override - String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピーター、ルームサーバーなど)を表示します。'; + String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。'; @override String get repeater_cliHelpGetPublicKey => 'デバイスの公開鍵を表示します。'; @@ -2897,7 +2921,7 @@ class AppLocalizationsJa extends AppLocalizations { '(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。'; @override - String get repeater_cliHelpGetRepeat => 'パケット転送(リピーター機能)が有効になっているかどうかを表示します。'; + String get repeater_cliHelpGetRepeat => 'パケット転送(リピータ機能)が有効になっているかどうかを表示します。'; @override String get repeater_cliHelpGetTx => '現在のTX(送信)電力のdBm値を表示します。'; @@ -2936,7 +2960,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpGetFloodAdvertInterval => - '洪水に関する広告の放送時間を時間単位で表示します。'; + 'フラッドに関する広告の放送時間を時間単位で表示します。'; @override String get repeater_cliHelpGetGuestPassword => '設定されたゲストパスワードを表示します。'; @@ -2951,13 +2975,13 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetRxDelay => 'rxdelay の基本値を表示します。'; @override - String get repeater_cliHelpGetTxDelay => '洪水モードにおける送信遅延の要因を示します。'; + String get repeater_cliHelpGetTxDelay => 'フラッドモードにおける送信遅延の要因を示します。'; @override String get repeater_cliHelpGetDirectTxDelay => 'ダイレクトモードの遅延要素を示します。'; @override - String get repeater_cliHelpGetFloodMax => '最大浸水範囲の回数を表示します。'; + String get repeater_cliHelpGetFloodMax => 'フラッドパケットの最大ホップ数を表示します。'; @override String get repeater_cliHelpGetOwnerInfo => '所有者の連絡先情報を表示します。'; @@ -2969,7 +2993,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetLoopDetect => 'ループ検出の感度を示す。'; @override - String get repeater_cliHelpGetAcl => '(シリアルのみ)リピーター上のアクセス制御設定を一覧表示します。'; + String get repeater_cliHelpGetAcl => '(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。'; @override String get repeater_cliHelpGetBridgeEnabled => '橋が有効になっているかどうかを表示します。'; @@ -3032,10 +3056,10 @@ class AppLocalizationsJa extends AppLocalizations { 'デフォルトの地域範囲を設定します。「」を使用すると、設定をリセットできます。'; @override - String get repeater_cliHelpRegionListAllowed => '洪水時の通行が許可されている地域の一覧'; + String get repeater_cliHelpRegionListAllowed => 'フラッド時の通行が許可されている地域の一覧'; @override - String get repeater_cliHelpRegionListDenied => '洪水による交通を遮断している地域の一覧'; + String get repeater_cliHelpRegionListDenied => 'フラッドによる交通を遮断している地域の一覧'; @override String get repeater_cliHelpStatsPackets => '(シリアルのみ)パケットレベルの統計情報を表示します。'; @@ -3245,7 +3269,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get neighbors_repeatersNeighbors => '繰り返し送信する、近隣'; + String get neighbors_repeatersNeighbors => '近隣のリピータ'; @override String get neighbors_noData => '近隣のデータは利用できません。'; @@ -3270,7 +3294,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_otherObservedPaths => '観察されたその他の経路'; @override - String get channelPath_repeaterHops => 'ホップの繰り返し'; + String get channelPath_repeaterHops => 'リピータホップ'; @override String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。'; @@ -3285,7 +3309,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_timeLabel => '時間'; @override - String get channelPath_repeatsLabel => '繰り返し'; + String get channelPath_repeatsLabel => 'リピータ'; @override String channelPath_pathLabel(int index) { @@ -3317,7 +3341,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_unknownPath => '不明'; @override - String get channelPath_floodPath => '洪水'; + String get channelPath_floodPath => 'フラッド'; @override String get channelPath_directPath => '直接'; @@ -3336,7 +3360,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_mapTitle => '経路図'; @override - String get channelPath_noRepeaterLocations => 'この経路には、中継装置の設置場所がありません。'; + String get channelPath_noRepeaterLocations => 'この経路にリピータの位置情報はありません。'; @override String channelPath_primaryPath(int index) { @@ -3556,7 +3580,7 @@ class AppLocalizationsJa extends AppLocalizations { String get listFilter_users => '利用者'; @override - String get listFilter_repeaters => '繰り返し送信装置'; + String get listFilter_repeaters => 'リピータ'; @override String get listFilter_roomServers => 'ルーム用サーバー'; @@ -3756,10 +3780,10 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_ping => 'パング'; @override - String get contacts_repeaterPathTrace => 'リピーターまでの経路を追跡する'; + String get contacts_repeaterPathTrace => 'リピータまでの経路を追跡する'; @override - String get contacts_repeaterPing => 'PING 繰り返し'; + String get contacts_repeaterPing => 'リピータにPING'; @override String get contacts_roomPathTrace => '部屋のサーバーへの経路を追跡する'; @@ -3791,7 +3815,7 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_zeroHopAdvert => 'ゼロホップ広告'; @override - String get contacts_floodAdvert => '洪水に関する広告'; + String get contacts_floodAdvert => 'フラッドに関する広告'; @override String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー'; @@ -3862,7 +3886,7 @@ class AppLocalizationsJa extends AppLocalizations { String get notification_receivedNewMessage => '新しいメッセージを受信'; @override - String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピーター/ルームサーバー'; + String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピータ/ルームサーバー'; @override String get settings_gpxExportRepeatersSubtitle => @@ -3895,7 +3919,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。'; @override - String get settings_gpxExportRepeatersRoom => '中継装置およびルームサーバーの設置場所'; + String get settings_gpxExportRepeatersRoom => 'リピータ/ルームサーバーの位置情報'; @override String get settings_gpxExportChat => '関連施設'; @@ -3911,7 +3935,7 @@ class AppLocalizationsJa extends AppLocalizations { 'meshcore-open GPX形式の地図データのエクスポート'; @override - String get snrIndicator_nearByRepeaters => '近くの電波中継局'; + String get snrIndicator_nearByRepeaters => '近くのリピータ'; @override String get snrIndicator_lastSeen => '最後に確認された場所'; @@ -3933,11 +3957,11 @@ class AppLocalizationsJa extends AppLocalizations { '利用者が自動的に発見したユーザーを追加できるようにする。'; @override - String get contactsSettings_autoAddRepeatersTitle => '自動で繰り返し設定'; + String get contactsSettings_autoAddRepeatersTitle => 'リピータを自動追加'; @override String get contactsSettings_autoAddRepeatersSubtitle => - '発見した中継局を、自動的に追加できるようにする。'; + '発見したリピータを、自動的に追加できるようにする。'; @override String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加'; @@ -4212,4 +4236,28 @@ class AppLocalizationsJa extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 606a33dc..64c5a6a6 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -92,6 +92,24 @@ class AppLocalizationsKo extends AppLocalizations { @override String get common_disable => '비활성화'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => '재부팅'; @@ -749,11 +767,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appSettings_maxMessageRetriesSubtitle => '메시지를 실패로 처리하기 전 시도 횟수'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => '배터리'; @@ -945,6 +958,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get contacts_newGroup => '새로운 그룹'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => '그룹 이름'; @@ -1411,34 +1433,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get debugFrame_hexDump => '헥스 덤프:'; - @override - String get chat_pathManagement => '경로 관리'; - - @override - String get chat_ShowAllPaths => '모든 경로 표시'; - - @override - String get chat_routingMode => '라우팅 방식'; - - @override - String get chat_autoUseSavedPath => '자동 (저장된 경로 사용)'; - - @override - String get chat_forceFloodMode => '강수 모드 활성화'; - - @override - String get chat_recentAckPaths => '최근 사용한 ACK 경로 (사용하려면 탭):'; - - @override - String get chat_pathHistoryFull => - '이력 기록은 이미 가득 차 있습니다. 항목을 삭제하여 새로운 항목을 추가할 수 있습니다.'; - - @override - String get chat_hopSingular => '점프'; - - @override - String get chat_hopPlural => '홉'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1450,61 +1444,149 @@ class AppLocalizationsKo extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => '성공 사례'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => '경로 제거'; @override String get chat_noPathHistoryYet => '아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.'; - @override - String get chat_pathActions => '경로 작업:'; - - @override - String get chat_setCustomPath => '사용자 지정 경로 설정'; - - @override - String get chat_setCustomPathSubtitle => '수동으로 경로를 지정'; - - @override - String get chat_clearPath => '명확한 길'; - - @override - String get chat_clearPathSubtitle => '다음 전송 시, 강제 재전송 설정'; - @override String get chat_pathCleared => '경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.'; - @override - String get chat_floodModeSubtitle => '앱 바에서 라우팅 스위치를 사용'; - - @override - String get chat_floodModeEnabled => - '홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.'; - @override String get chat_fullPath => '전체 경로'; @override - String get chat_pathDetailsNotAvailable => - '경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Path set: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결'; @@ -1958,64 +2040,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get common_clear => '명확하게'; - @override - String path_currentPath(String path) { - return '현재 경로: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Using $count $_temp0 path'; - } - - @override - String get path_enterCustomPath => '사용자 지정 경로 입력'; - @override String get path_currentPathLabel => '현재 경로'; - @override - String get path_hexPrefixInstructions => - '각 단계에 대한 2자리 헥사데진 접두사를 쉼표로 구분하여 입력하세요.'; - - @override - String get path_hexPrefixExample => - '예시: A1, F2, 3C (각 노드는 자신의 공개 키의 첫 번째 바이트를 사용)'; - - @override - String get path_labelHexPrefixes => '경로 (헥스 접두사)'; - - @override - String get path_helperMaxHops => - '최대 64개의 홉. 각 접두사는 2개의 16진수 문자(1바이트)로 구성됩니다.'; - - @override - String get path_selectFromContacts => '또 연락처 목록에서 선택:'; - @override String get path_noRepeatersFound => '반복 장치 또는 서버는 찾을 수 없습니다.'; - @override - String get path_customPathsRequire => - '사용자 정의 경로에는 메시지를 전달할 수 있는 중간 경로가 필요합니다.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return '유효하지 않은 16진수 접두사: $prefixes'; - } - - @override - String get path_tooLong => '경로가 너무 길어. 최대 64개의 연결만 허용됩니다.'; - - @override - String get path_setPath => '경로 설정'; - @override String get repeater_management => '리피터 관리'; @@ -2077,15 +2107,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_routingMode => '라우팅 방식'; - @override - String get repeater_autoUseSavedPath => '자동 (저장된 경로 사용)'; - - @override - String get repeater_forceFloodMode => '강수 모드 활성화'; - - @override - String get repeater_pathManagement => '경로 관리'; - @override String get repeater_refresh => '새롭게'; @@ -4213,4 +4234,28 @@ class AppLocalizationsKo extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 90a446df..6f4d9fbb 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -92,6 +92,24 @@ class AppLocalizationsNl extends AppLocalizations { @override String get common_disable => 'Uitschakelen'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Herstarten'; @@ -786,11 +804,6 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Batterij'; @@ -991,6 +1004,15 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_newGroup => 'Nieuwe Groep'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Groepnaam'; @@ -1468,34 +1490,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get debugFrame_hexDump => 'Hex-dump:'; - @override - String get chat_pathManagement => 'Beheer van Paden'; - - @override - String get chat_ShowAllPaths => 'Toon alle paden'; - - @override - String get chat_routingMode => 'Routeerwijze'; - - @override - String get chat_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)'; - - @override - String get chat_forceFloodMode => 'Dwing Floodsmodus'; - - @override - String get chat_recentAckPaths => 'Recente ACK Paden (tik om te gebruiken):'; - - @override - String get chat_pathHistoryFull => - 'De voorgeschiedenis is vol. Verwijder vermeldingen om er nieuwe aan toe te voegen.'; - - @override - String get chat_hopSingular => 'Hop'; - - @override - String get chat_hopPlural => 'hoppen'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1507,12 +1501,6 @@ class AppLocalizationsNl extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Succesvol'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Pad verwijderen'; @@ -1520,52 +1508,144 @@ class AppLocalizationsNl extends AppLocalizations { String get chat_noPathHistoryYet => 'Geen geschiedenis van paden nog beschikbaar.\nVerzend een bericht om paden te ontdekken.'; - @override - String get chat_pathActions => 'Padacties:'; - - @override - String get chat_setCustomPath => 'Stel aangepaste pad in'; - - @override - String get chat_setCustomPathSubtitle => 'Handmatig routepad specificeren'; - - @override - String get chat_clearPath => 'Duidelijke Pad'; - - @override - String get chat_clearPathSubtitle => - 'Dwing herontdekking bij volgende verzending'; - @override String get chat_pathCleared => 'Pad is vrijgegeven. Volgende bericht herontdekt route.'; - @override - String get chat_floodModeSubtitle => - 'Gebruik de route-schakelaar in de app-balk'; - - @override - String get chat_floodModeEnabled => - 'Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.'; - @override String get chat_fullPath => 'Volledige Pad'; @override - String get chat_pathDetailsNotAvailable => - 'De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Pad ingesteld: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Opgeslagen lokaal. Verbinden om te synchroniseren.'; @@ -2034,65 +2114,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get common_clear => 'Schoonmaken'; - @override - String path_currentPath(String path) { - return 'Huidige pad: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Gebruik $count $_temp0 pad'; - } - - @override - String get path_enterCustomPath => 'Voer aangepaste pad in'; - @override String get path_currentPathLabel => 'Huidige pad'; - @override - String get path_hexPrefixInstructions => - 'Voer 2-letter hex-voorgiffen voor elke hop in, gescheiden door komma\'s.'; - - @override - String get path_hexPrefixExample => - 'Voorbeeld: A1,F2,3C (elke node gebruikt het eerste byte van zijn openbare sleutel)'; - - @override - String get path_labelHexPrefixes => 'Pad (hex-voorkeursletters)'; - - @override - String get path_helperMaxHops => - 'Maximaal 64 sprongen. Elke prefix is 2 hexadecimale tekens (1 byte)'; - - @override - String get path_selectFromContacts => 'Of select contacten:'; - @override String get path_noRepeatersFound => 'Geen repeaters of roomservers gevonden.'; - @override - String get path_customPathsRequire => - 'Aangepaste paden vereisen tussentse overstappen die berichten kunnen doorgeven.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Ongeldige hex-voorkeursletters: $prefixes'; - } - - @override - String get path_tooLong => - 'Pad is te lang. Maximaal 64 sprongen zijn toegestaan.'; - - @override - String get path_setPath => 'Stel Pad in'; - @override String get repeater_management => 'Beheer Repeaters'; @@ -2157,16 +2184,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_routingMode => 'Routeerwijze'; - @override - String get repeater_autoUseSavedPath => - 'Automatisch (gebruik opgeslagen pad)'; - - @override - String get repeater_forceFloodMode => 'Dwing Floodmodus Af'; - - @override - String get repeater_pathManagement => 'Beheer van paden'; - @override String get repeater_refresh => 'Vernieuwen'; @@ -4422,4 +4439,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index f30cbb7f..f582d2df 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -92,6 +92,24 @@ class AppLocalizationsPl extends AppLocalizations { @override String get common_disable => 'Wyłącz'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Uruchom ponownie'; @@ -796,11 +814,6 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Bateria'; @@ -1009,6 +1022,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_newGroup => 'Nowa Grupa'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Nazwa grupy'; @@ -1491,35 +1513,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get debugFrame_hexDump => 'Zrzut hex:'; - @override - String get chat_pathManagement => 'Zarządzanie ścieżkami'; - - @override - String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki'; - - @override - String get chat_routingMode => 'Tryb routingu'; - - @override - String get chat_autoUseSavedPath => 'Automatyczne (użyj zapisanej ścieżki)'; - - @override - String get chat_forceFloodMode => 'Wymuś tryb zalewowy'; - - @override - String get chat_recentAckPaths => - 'Ostatnie ścieżki ACK (naciśnij, aby użyć):'; - - @override - String get chat_pathHistoryFull => - 'Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.'; - - @override - String get chat_hopSingular => 'skok'; - - @override - String get chat_hopPlural => 'skoki'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1533,12 +1526,6 @@ class AppLocalizationsPl extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Sukcesy'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Usuń ścieżkę'; @@ -1546,52 +1533,144 @@ class AppLocalizationsPl extends AppLocalizations { String get chat_noPathHistoryYet => 'Brak historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.'; - @override - String get chat_pathActions => 'Działania ścieżki:'; - - @override - String get chat_setCustomPath => 'Ustaw ścieżkę niestandardową'; - - @override - String get chat_setCustomPathSubtitle => 'Ręcznie określ trasę.'; - - @override - String get chat_clearPath => 'Wyczyść Ścieżkę'; - - @override - String get chat_clearPathSubtitle => - 'Wymuś ponowne wyznaczenie trasy przy następnym wysłaniu'; - @override String get chat_pathCleared => 'Ścieżka wyczyszczona. Następna wiadomość odnajdzie trasę.'; - @override - String get chat_floodModeSubtitle => - 'Użyj przełącznika routingu w pasku narzędzi.'; - - @override - String get chat_floodModeEnabled => - 'Tryb zalewowy włączony. Przełącz z powrotem ikoną routingu w pasku aplikacji.'; - @override String get chat_fullPath => 'Pełna ścieżka'; @override - String get chat_pathDetailsNotAvailable => - 'Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Ścieżka ustawiona: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Zapisano lokalnie. Połącz się, aby zsynchronizować.'; @@ -2061,68 +2140,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get common_clear => 'Wyczyść'; - @override - String path_currentPath(String path) { - return 'Aktualna ścieżka: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'skoków', - many: 'skoków', - few: 'skoki', - one: 'skok', - ); - return 'Użyj ścieżki $count $_temp0.'; - } - - @override - String get path_enterCustomPath => 'Wprowadź własną ścieżkę'; - @override String get path_currentPathLabel => 'Aktualna ścieżka'; - @override - String get path_hexPrefixInstructions => - 'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.'; - - @override - String get path_hexPrefixExample => - 'A1,F2,3C (każdy węzeł używa pierwszego bajtu swojego klucza publicznego)'; - - @override - String get path_labelHexPrefixes => 'Ścieżka (prefiksy hex)'; - - @override - String get path_helperMaxHops => - 'Maksymalnie 64 skoki. Każdy prefiks ma 2 znaki szesnastkowe (1 bajt).'; - - @override - String get path_selectFromContacts => 'Albo wybierz z kontaktów:'; - @override String get path_noRepeatersFound => 'Nie znaleziono przekaźników ani serwerów pokoi.'; - @override - String get path_customPathsRequire => - 'Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Nieprawidłowe prefiksy szesnastkowe: $prefixes'; - } - - @override - String get path_tooLong => - 'Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.'; - - @override - String get path_setPath => 'Ustaw Ścieżkę'; - @override String get repeater_management => 'Zarządzanie przekaźnikami'; @@ -2187,16 +2211,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_routingMode => 'Tryb routingu'; - @override - String get repeater_autoUseSavedPath => - 'Automatycznie (użyj zapisanej ścieżki)'; - - @override - String get repeater_forceFloodMode => 'Wymuś tryb zalewowy'; - - @override - String get repeater_pathManagement => 'Zarządzanie ścieżkami'; - @override String get repeater_refresh => 'Odśwież'; @@ -4459,4 +4473,28 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5fe15028..23495d55 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -92,6 +92,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get common_disable => 'Desativar'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Reiniciar'; @@ -793,11 +811,6 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Número de tentativas de reenvio antes de classificar uma mensagem como falha.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Bateria'; @@ -999,6 +1012,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_newGroup => 'Novo Grupo'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Nome do grupo'; @@ -1478,34 +1500,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get debugFrame_hexDump => 'Espaço Hexadecimal:'; - @override - String get chat_pathManagement => 'Gerenciamento de Caminhos'; - - @override - String get chat_ShowAllPaths => 'Mostrar todos os caminhos'; - - @override - String get chat_routingMode => 'Modo de roteamento'; - - @override - String get chat_autoUseSavedPath => 'Auto (usar caminho salvo)'; - - @override - String get chat_forceFloodMode => 'Modo de Inundação Forçado'; - - @override - String get chat_recentAckPaths => 'Rotas de ACK Recentes (toque para usar):'; - - @override - String get chat_pathHistoryFull => - 'O histórico está cheio. Remova entradas para adicionar novas.'; - - @override - String get chat_hopSingular => 'pule'; - - @override - String get chat_hopPlural => 'salta'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1517,12 +1511,6 @@ class AppLocalizationsPt extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Sucessos'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Remover caminho'; @@ -1530,53 +1518,144 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_noPathHistoryYet => 'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.'; - @override - String get chat_pathActions => 'Ações do Caminho:'; - - @override - String get chat_setCustomPath => 'Definir Caminho Personalizado'; - - @override - String get chat_setCustomPathSubtitle => - 'Especifique manualmente o caminho de roteamento'; - - @override - String get chat_clearPath => 'Limpar Caminho'; - - @override - String get chat_clearPathSubtitle => - 'Forçar a descoberta na próxima transmissão'; - @override String get chat_pathCleared => 'Caminho limpo. A próxima mensagem redescobrirá a rota.'; - @override - String get chat_floodModeSubtitle => - 'Use a chave de roteamento na barra de ferramentas'; - - @override - String get chat_floodModeEnabled => - 'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.'; - @override String get chat_fullPath => 'Caminho Completo'; @override - String get chat_pathDetailsNotAvailable => - 'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Caminho definido: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Salvo localmente. Conectar para sincronizar.'; @@ -2044,66 +2123,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get common_clear => 'Limpar'; - @override - String path_currentPath(String path) { - return 'Caminho atual: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Usando $count $_temp0 caminho'; - } - - @override - String get path_enterCustomPath => 'Insira Caminho Personalizado'; - @override String get path_currentPathLabel => 'Caminho atual'; - @override - String get path_hexPrefixInstructions => - 'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.'; - - @override - String get path_hexPrefixExample => - 'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)'; - - @override - String get path_labelHexPrefixes => 'Prefixo Hexadecimal'; - - @override - String get path_helperMaxHops => - 'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)'; - - @override - String get path_selectFromContacts => 'Ou selecione de contatos:'; - @override String get path_noRepeatersFound => 'Não foram encontrados repetidores ou servidores de sala.'; - @override - String get path_customPathsRequire => - 'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Prefixos hexadecimais inválidos: $prefixes'; - } - - @override - String get path_tooLong => - 'Caminho muito longo. Máximo de 64 saltos permitidos.'; - - @override - String get path_setPath => 'Definir Caminho'; - @override String get repeater_management => 'Gerenciamento de Repetidor'; @@ -2168,15 +2194,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_routingMode => 'Modo de roteamento'; - @override - String get repeater_autoUseSavedPath => 'Auto (usar caminho salvo)'; - - @override - String get repeater_forceFloodMode => 'Modo de Inundação Forçado'; - - @override - String get repeater_pathManagement => 'Gerenciamento de caminhos'; - @override String get repeater_refresh => 'Atualizar'; @@ -4435,4 +4452,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 84929424..fe535172 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -92,6 +92,24 @@ class AppLocalizationsRu extends AppLocalizations { @override String get common_disable => 'Выключить'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Перезагрузить'; @@ -795,11 +813,6 @@ class AppLocalizationsRu extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Батарея'; @@ -1000,6 +1013,15 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_newGroup => 'Новая группа'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Имя группы'; @@ -1479,35 +1501,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; - @override - String get chat_pathManagement => 'Управление маршрутами'; - - @override - String get chat_ShowAllPaths => 'Показать все пути'; - - @override - String get chat_routingMode => 'Режим маршрутизации'; - - @override - String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - - @override - String get chat_forceFloodMode => 'Принудительный режим рассылки'; - - @override - String get chat_recentAckPaths => - 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; - - @override - String get chat_pathHistoryFull => - 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; - - @override - String get chat_hopSingular => 'хоп'; - - @override - String get chat_hopPlural => 'хопов'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1521,12 +1514,6 @@ class AppLocalizationsRu extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'успешно'; - - @override - String get chat_score => 'Оценка'; - @override String get chat_removePath => 'Удалить маршрут'; @@ -1534,54 +1521,144 @@ class AppLocalizationsRu extends AppLocalizations { String get chat_noPathHistoryYet => 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; - @override - String get chat_pathActions => 'Действия с маршрутом:'; - - @override - String get chat_setCustomPath => 'Указать маршрут вручную'; - - @override - String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; - - @override - String get chat_clearPath => 'Очистить маршрут'; - - @override - String get chat_clearPathSubtitle => - 'Принудительно обновить маршрут при следующей отправке'; - @override String get chat_pathCleared => 'Маршрут очищен. Следующее сообщение обновит маршрут.'; - @override - String get chat_floodModeSubtitle => - 'Используйте переключатель маршрутизации в панели приложения'; - - @override - String get chat_floodModeEnabled => - 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; - @override String get chat_fullPath => 'Полный маршрут'; @override - String get chat_pathDetailsNotAvailable => - 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', - ); - return 'Маршрут установлен: $hopCount $_temp0 — $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Сохранено локально. Подключитесь для синхронизации.'; @@ -2049,66 +2126,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get common_clear => 'Очистить'; - @override - String path_currentPath(String path) { - return 'Текущий маршрут: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', - ); - return 'Используется маршрут из $count $_temp0'; - } - - @override - String get path_enterCustomPath => 'Введите маршрут вручную'; - @override String get path_currentPathLabel => 'Текущий маршрут'; - @override - String get path_hexPrefixInstructions => - 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; - - @override - String get path_hexPrefixExample => - 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; - - @override - String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; - - @override - String get path_helperMaxHops => - 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; - - @override - String get path_selectFromContacts => 'Или выберите из контактов:'; - @override String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; - @override - String get path_customPathsRequire => - 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; - } - - @override - String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; - - @override - String get path_setPath => 'Установить маршрут'; - @override String get repeater_management => 'Управление репитером'; @@ -2173,16 +2196,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_routingMode => 'Режим маршрутизации'; - @override - String get repeater_autoUseSavedPath => - 'Авто (использовать сохранённый маршрут)'; - - @override - String get repeater_forceFloodMode => 'Принудительный режим рассылки'; - - @override - String get repeater_pathManagement => 'Управление маршрутами'; - @override String get repeater_refresh => 'Обновить'; @@ -4453,4 +4466,28 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contact_typeUnknown => 'Неизвестно'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 2072b634..f15bcfc1 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -92,6 +92,24 @@ class AppLocalizationsSk extends AppLocalizations { @override String get common_disable => 'Zakázať'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Restartovať'; @@ -782,11 +800,6 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Počet pokusov o odošleť pred označením správy ako neúspešnej'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Batéria'; @@ -988,6 +1001,15 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_newGroup => 'Nová skupina'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Názov skupiny'; @@ -1469,35 +1491,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get debugFrame_hexDump => 'Hexová analýza:'; - @override - String get chat_pathManagement => 'Správa ciest'; - - @override - String get chat_ShowAllPaths => 'Zobraziť všetky cesty'; - - @override - String get chat_routingMode => 'Režim trasy'; - - @override - String get chat_autoUseSavedPath => 'Použiť uloženú cestu'; - - @override - String get chat_forceFloodMode => - 'Zavrieť režim núdzového povodňového režimu'; - - @override - String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):'; - - @override - String get chat_pathHistoryFull => - 'História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.'; - - @override - String get chat_hopSingular => 'Skok'; - - @override - String get chat_hopPlural => 'Skákať'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1509,12 +1502,6 @@ class AppLocalizationsSk extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Úspechy'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Odstrániť cestu'; @@ -1522,52 +1509,144 @@ class AppLocalizationsSk extends AppLocalizations { String get chat_noPathHistoryYet => 'Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.'; - @override - String get chat_pathActions => 'Cesty:'; - - @override - String get chat_setCustomPath => 'Nastaviť vlastnú cestu'; - - @override - String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.'; - - @override - String get chat_clearPath => 'Vyčistiš cestu'; - - @override - String get chat_clearPathSubtitle => - 'Znovu nájsť vynútene pri nasledujúcej pošlite'; - @override String get chat_pathCleared => 'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.'; - @override - String get chat_floodModeSubtitle => - 'Použite prepínanie trasy v navigačnom paneli.'; - - @override - String get chat_floodModeEnabled => - 'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.'; - @override String get chat_fullPath => 'Celá cesta'; @override - String get chat_pathDetailsNotAvailable => - 'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Cesta nastavená: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Uložené lokálne. Spojte sa na synchronizáciu.'; @@ -2035,66 +2114,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get common_clear => 'Zmazať'; - @override - String path_currentPath(String path) { - return 'Aktívna cesta: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Používa $count $_temp0 cestu'; - } - - @override - String get path_enterCustomPath => 'Zadajte vlastný priebeh'; - @override String get path_currentPathLabel => 'Aktuálny priebeh'; - @override - String get path_hexPrefixInstructions => - 'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.'; - - @override - String get path_hexPrefixExample => - 'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)'; - - @override - String get path_labelHexPrefixes => 'Cesty (hexové predpony)'; - - @override - String get path_helperMaxHops => - 'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).'; - - @override - String get path_selectFromContacts => 'Vyberte sa z kontaktov:'; - @override String get path_noRepeatersFound => 'Nenašli sa žiadne opakovače ani serverové miestnosti.'; - @override - String get path_customPathsRequire => - 'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Neplatné hexové predpony: $prefixes'; - } - - @override - String get path_tooLong => - 'Cesta je príliš dlhá. Umožnené je maximum 64 skokov.'; - - @override - String get path_setPath => 'Nastaviť cestu'; - @override String get repeater_management => 'Správa opakérov'; @@ -2159,16 +2185,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_routingMode => 'Režim trasy'; - @override - String get repeater_autoUseSavedPath => 'Použiť uloženú cestu'; - - @override - String get repeater_forceFloodMode => - 'Zavrieť režim núdzového povodňového režimu'; - - @override - String get repeater_pathManagement => 'Správa trás'; - @override String get repeater_refresh => 'Obnoviť'; @@ -4418,4 +4434,28 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e91086e9..c2dc192b 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -92,6 +92,24 @@ class AppLocalizationsSl extends AppLocalizations { @override String get common_disable => 'Izklopiti'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Ponoviti'; @@ -783,11 +801,6 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Baterija'; @@ -987,6 +1000,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_newGroup => 'Nova skupina'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Ime skupine'; @@ -1466,34 +1488,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get debugFrame_hexDump => 'Izpis heksadecimalnih vrednosti:'; - @override - String get chat_pathManagement => 'Upravljanje poti'; - - @override - String get chat_ShowAllPaths => 'Prikaži vse poti'; - - @override - String get chat_routingMode => 'Navodilo za usmerjevalni način'; - - @override - String get chat_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; - - @override - String get chat_forceFloodMode => 'Nasilje obvezati v način'; - - @override - String get chat_recentAckPaths => 'Nedavni poti ACK (tap za uporabo):'; - - @override - String get chat_pathHistoryFull => - 'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.'; - - @override - String get chat_hopSingular => 'skok'; - - @override - String get chat_hopPlural => 'skokov'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1505,12 +1499,6 @@ class AppLocalizationsSl extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Uspešni'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Izbriši pot'; @@ -1518,51 +1506,144 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_noPathHistoryYet => 'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.'; - @override - String get chat_pathActions => 'Potni ukazi:'; - - @override - String get chat_setCustomPath => 'Nastavi Prilozeno Pot'; - - @override - String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.'; - - @override - String get chat_clearPath => 'Počisti pot'; - - @override - String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.'; - @override String get chat_pathCleared => 'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.'; - @override - String get chat_floodModeSubtitle => - 'Uporabi tipko usmerjevanja v meniju aplikacije.'; - - @override - String get chat_floodModeEnabled => - 'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.'; - @override String get chat_fullPath => 'Polna pot'; @override - String get chat_pathDetailsNotAvailable => - 'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Pot nastavljen: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Shrano lokalno. Povežite se za sinhronizacijo.'; @@ -2032,65 +2113,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get common_clear => 'Ponoviti'; - @override - String path_currentPath(String path) { - return 'Trenutna pot: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Uporablja $count $_temp0 pot'; - } - - @override - String get path_enterCustomPath => 'Vnesite prilagojeno pot'; - @override String get path_currentPathLabel => 'Trenutna pot'; - @override - String get path_hexPrefixInstructions => - 'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.'; - - @override - String get path_hexPrefixExample => - 'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)'; - - @override - String get path_labelHexPrefixes => 'Pot (heksafixne skrajšave)'; - - @override - String get path_helperMaxHops => - 'Maksimalno 64 skokov. Vsak prefiks je 2 heksadecimalna znamenja (1 bajt).'; - - @override - String get path_selectFromContacts => 'Izberi iz kontaktov:'; - @override String get path_noRepeatersFound => 'Ne najdenih ponoviteljev ali strežnikov sob.'; - @override - String get path_customPathsRequire => - 'Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Neveljačni šesteročlenski prefiksi: $prefixes'; - } - - @override - String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.'; - - @override - String get path_setPath => 'Nastavi Pot'; - @override String get repeater_management => 'Upravljanje ponovitve'; @@ -2156,15 +2185,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_routingMode => 'Navodilo za usmerjevalni način'; - @override - String get repeater_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; - - @override - String get repeater_forceFloodMode => 'Nasilje obvezati v način'; - - @override - String get repeater_pathManagement => 'Upravljanje poti'; - @override String get repeater_refresh => 'Ponovno obnavljati'; @@ -4416,4 +4436,28 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 95af0f64..ef0df94c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -92,6 +92,24 @@ class AppLocalizationsSv extends AppLocalizations { @override String get common_disable => 'Inaktivera'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Start om'; @@ -777,11 +795,6 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Batteri'; @@ -982,6 +995,15 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_newGroup => 'Ny grupp'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Gruppnamn'; @@ -1460,35 +1482,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get debugFrame_hexDump => 'Hexdump:'; - @override - String get chat_pathManagement => 'Stigarhantering'; - - @override - String get chat_ShowAllPaths => 'Visa alla vägar'; - - @override - String get chat_routingMode => 'Ruttläge'; - - @override - String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; - - @override - String get chat_forceFloodMode => 'Tvinga Översvämningsläge'; - - @override - String get chat_recentAckPaths => - 'Nyligen Ack-vägar (tryck för att använda):'; - - @override - String get chat_pathHistoryFull => - 'Historisk sökväg är full. Ta bort poster för att lägga till nya.'; - - @override - String get chat_hopSingular => 'hoppa'; - - @override - String get chat_hopPlural => 'hoppar'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1500,12 +1493,6 @@ class AppLocalizationsSv extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'framgångar'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Ta bort sökväg'; @@ -1513,50 +1500,144 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_noPathHistoryYet => 'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.'; - @override - String get chat_pathActions => 'Stigar:'; - - @override - String get chat_setCustomPath => 'Ange anpassad sökväg'; - - @override - String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt'; - - @override - String get chat_clearPath => 'Rensa Vägen'; - - @override - String get chat_clearPathSubtitle => 'Tvinga fram omstart vid nästa sändning'; - @override String get chat_pathCleared => 'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.'; - @override - String get chat_floodModeSubtitle => 'Använd routningsomkopplaren i appraden'; - - @override - String get chat_floodModeEnabled => - 'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.'; - @override String get chat_fullPath => 'Fullständig sökväg'; @override - String get chat_pathDetailsNotAvailable => - 'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'hoppar', - one: 'hopp', - ); - return 'Sökväg inställd: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Sparat lokalt. Anslut för att synkronisera.'; @@ -2021,65 +2102,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get common_clear => 'Rensa'; - @override - String path_currentPath(String path) { - return 'Nuvarande sökväg: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'hops', - one: 'hop', - ); - return 'Använda $count $_temp0 sökväg'; - } - - @override - String get path_enterCustomPath => 'Ange anpassad sökväg'; - @override String get path_currentPathLabel => 'Nuvarande sökväg'; - @override - String get path_hexPrefixInstructions => - 'Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.'; - - @override - String get path_hexPrefixExample => - 'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)'; - - @override - String get path_labelHexPrefixes => 'Hexprefixer'; - - @override - String get path_helperMaxHops => - 'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)'; - - @override - String get path_selectFromContacts => 'Välj istället från kontakter:'; - @override String get path_noRepeatersFound => 'Inga återuppspelare eller rumsservrar hittades.'; - @override - String get path_customPathsRequire => - 'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Ogiltiga hex-prefikser: $prefixes'; - } - - @override - String get path_tooLong => 'Sökvägen är för lång. Max 64 hopp tillåtna.'; - - @override - String get path_setPath => 'Ange Sökväg'; - @override String get repeater_management => 'Återuppspelarens Hantering'; @@ -2144,15 +2173,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_routingMode => 'Ruttläge'; - @override - String get repeater_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; - - @override - String get repeater_forceFloodMode => 'Tvinga Översvämningsläge'; - - @override - String get repeater_pathManagement => 'Stigarhantering'; - @override String get repeater_refresh => 'Uppdatera'; @@ -4391,4 +4411,28 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index ae82d64e..beb14024 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -92,6 +92,24 @@ class AppLocalizationsUk extends AppLocalizations { @override String get common_disable => 'Вимкнути'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => 'Перезавантажити'; @@ -789,11 +807,6 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'Батарея'; @@ -995,6 +1008,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_newGroup => 'Нова група'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => 'Назва групи'; @@ -1474,35 +1496,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get debugFrame_hexDump => 'Дамп Hex:'; - @override - String get chat_pathManagement => 'Керування шляхами'; - - @override - String get chat_ShowAllPaths => 'Показати всі шляхи'; - - @override - String get chat_routingMode => 'Режим маршрутизації'; - - @override - String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; - - @override - String get chat_forceFloodMode => 'Примусово через всю мережу'; - - @override - String get chat_recentAckPaths => - 'Підтверджені шляхи (натисніть, щоб використати):'; - - @override - String get chat_pathHistoryFull => - 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; - - @override - String get chat_hopSingular => 'Перехід'; - - @override - String get chat_hopPlural => 'переходів'; - @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1516,12 +1509,6 @@ class AppLocalizationsUk extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Успішно'; - - @override - String get chat_score => 'Оцінка'; - @override String get chat_removePath => 'Видалити шлях'; @@ -1529,54 +1516,144 @@ class AppLocalizationsUk extends AppLocalizations { String get chat_noPathHistoryYet => 'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; - @override - String get chat_pathActions => 'Дії зі шляхом:'; - - @override - String get chat_setCustomPath => 'Встановити власний шлях'; - - @override - String get chat_setCustomPathSubtitle => 'Вказати шлях маршрутизації вручну'; - - @override - String get chat_clearPath => 'Очистити шлях'; - - @override - String get chat_clearPathSubtitle => - 'Примусово повторити пошук при наступному надсиланні'; - @override String get chat_pathCleared => 'Шлях очищено. Наступне повідомлення оновить маршрут.'; - @override - String get chat_floodModeSubtitle => - 'Використовувати перемикач маршрутизації в панелі застосунку'; - - @override - String get chat_floodModeEnabled => - 'Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; - @override String get chat_fullPath => 'Повний шлях'; @override - String get chat_pathDetailsNotAvailable => - 'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( - hopCount, - locale: localeName, - other: 'переходів', - many: 'переходів', - few: 'переходи', - one: 'перехід', - ); - return 'Шлях встановлено: $hopCount $_temp0 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => 'Збережено локально. Підключіться для синхронізації.'; @@ -2043,67 +2120,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get common_clear => 'Очистити'; - @override - String path_currentPath(String path) { - return 'Поточний шлях: $path'; - } - - @override - String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'переходами', - many: 'переходами', - few: 'переходами', - one: 'переходом', - ); - return 'Використання шляху з $count $_temp0'; - } - - @override - String get path_enterCustomPath => 'Ввести власний шлях'; - @override String get path_currentPathLabel => 'Поточний шлях'; - @override - String get path_hexPrefixInstructions => - 'Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.'; - - @override - String get path_hexPrefixExample => - 'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).'; - - @override - String get path_labelHexPrefixes => 'Hex-префікси'; - - @override - String get path_helperMaxHops => - 'Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)'; - - @override - String get path_selectFromContacts => 'Вибрати з контактів:'; - @override String get path_noRepeatersFound => 'Ретрансляторів або серверів кімнат не знайдено.'; - @override - String get path_customPathsRequire => - 'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return 'Некоректні hex-префікси: $prefixes'; - } - - @override - String get path_tooLong => 'Шлях занадто довгий. Максимум 64 переходи.'; - - @override - String get path_setPath => 'Встановити шлях'; - @override String get repeater_management => 'Керування ретранслятором'; @@ -2168,16 +2191,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_routingMode => 'Режим маршрутизації'; - @override - String get repeater_autoUseSavedPath => - 'Авто (використовувати збережений шлях)'; - - @override - String get repeater_forceFloodMode => 'Примусово через всю мережу'; - - @override - String get repeater_pathManagement => 'Керування шляхами'; - @override String get repeater_refresh => 'Оновити'; @@ -4454,4 +4467,28 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contact_typeUnknown => 'Невідомо'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 79e4e22e..41374057 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -92,6 +92,24 @@ class AppLocalizationsZh extends AppLocalizations { @override String get common_disable => '禁用'; + @override + String get common_undo => 'Undo'; + + @override + String get messageStatus_sent => 'Sent'; + + @override + String get messageStatus_delivered => 'Delivered'; + + @override + String get messageStatus_pending => 'Sending'; + + @override + String get messageStatus_failed => 'Failed to send'; + + @override + String get messageStatus_repeated => 'Heard repeated'; + @override String get common_reboot => '重启'; @@ -735,11 +753,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => '电池'; @@ -931,6 +944,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_newGroup => '新建群聊'; + @override + String get contacts_moreOptions => 'More options'; + + @override + String get contacts_searchOpen => 'Search contacts'; + + @override + String get contacts_searchClose => 'Close search'; + @override String get contacts_groupName => '群聊名称'; @@ -1397,85 +1419,154 @@ class AppLocalizationsZh extends AppLocalizations { @override String get debugFrame_hexDump => '十六进制数据:'; - @override - String get chat_pathManagement => '路径管理'; - - @override - String get chat_ShowAllPaths => '显示所有路径'; - - @override - String get chat_routingMode => '路由模式'; - - @override - String get chat_autoUseSavedPath => '自动(使用保存的路径)'; - - @override - String get chat_forceFloodMode => '强制泛洪模式'; - - @override - String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):'; - - @override - String get chat_pathHistoryFull => '路径历史已满,请删除后再添加。'; - - @override - String get chat_hopSingular => '跳'; - - @override - String get chat_hopPlural => '跳'; - @override String chat_hopsCount(int count) { return '$count 跳'; } - @override - String get chat_successes => '成功'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => '移除路径'; @override String get chat_noPathHistoryYet => '暂无路径历史。\n发送消息以探索路径。'; - @override - String get chat_pathActions => '路径操作:'; - - @override - String get chat_setCustomPath => '设置自定义路径'; - - @override - String get chat_setCustomPathSubtitle => '手动指定路由路径'; - - @override - String get chat_clearPath => '清除路径'; - - @override - String get chat_clearPathSubtitle => '清除当前路径,下次发送将重新尝试。'; - @override String get chat_pathCleared => '路径已清除。下一条消息将重新路由。'; - @override - String get chat_floodModeSubtitle => '在应用栏中切换路由模式。'; - - @override - String get chat_floodModeEnabled => '泛洪模式已启用。可通过应用栏的路由图标切换。'; - @override String get chat_fullPath => '完整路径'; @override - String get chat_pathDetailsNotAvailable => '路径信息暂不可用,请尝试发送消息刷新。'; + String get routing_title => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - return '路径设置:$hopCount 跳 - $status'; + String get routing_modeAuto => 'Auto'; + + @override + String get routing_modeFlood => 'Flood'; + + @override + String get routing_modeManual => 'Manual'; + + @override + String get routing_modeAutoHint => + 'Picks the best known path automatically, flooding when none is known.'; + + @override + String get routing_modeFloodHint => + 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + + @override + String get routing_modeManualHint => + 'Always sends along the exact path you set.'; + + @override + String get routing_currentRoute => 'Current route'; + + @override + String get routing_directNoHops => 'Direct — no repeater hops'; + + @override + String get routing_noPathYet => + 'No path yet. The next message floods until a route is discovered.'; + + @override + String get routing_floodBroadcast => 'Broadcast through every repeater'; + + @override + String get routing_editPath => 'Edit path'; + + @override + String get routing_forgetPath => 'Forget path'; + + @override + String get routing_knownPaths => 'Known paths'; + + @override + String get routing_knownPathsHint => 'Tap a path to switch to it.'; + + @override + String get routing_inUse => 'In use'; + + @override + String get routing_qualityStrong => 'Strong first hop'; + + @override + String get routing_qualityGood => 'Good first hop'; + + @override + String get routing_qualityFair => 'Fair first hop'; + + @override + String get routing_qualityWorked => 'Has delivered'; + + @override + String get routing_qualityFlood => 'Heard via flood'; + + @override + String get routing_qualityUntested => 'Untested'; + + @override + String routing_lastWorked(String when) { + return 'worked $when'; } + @override + String get routing_neverWorked => 'never confirmed'; + + @override + String routing_deliveryCounts(int successes, int failures) { + return '$successes delivered, $failures failed'; + } + + @override + String get routing_floodDelivery => 'Flood delivery'; + + @override + String get pathEditor_title => 'Build Path'; + + @override + String pathEditor_hopCounter(int count) { + return '$count of 64 hops'; + } + + @override + String get pathEditor_noHops => + 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + + @override + String get pathEditor_addHops => 'Add hops in order'; + + @override + String get pathEditor_searchRepeaters => 'Search repeaters'; + + @override + String get pathEditor_advancedHex => 'Advanced: raw hex path'; + + @override + String get pathEditor_hexLabel => 'Hex prefixes'; + + @override + String get pathEditor_hexHelper => + 'Two hex characters per hop, separated by commas'; + + @override + String pathEditor_invalidTokens(String tokens) { + return 'Invalid: $tokens'; + } + + @override + String get pathEditor_tooManyHops => 'Maximum 64 hops'; + + @override + String get pathEditor_usePath => 'Use this path'; + + @override + String get pathEditor_removeHop => 'Remove hop'; + + @override + String get pathEditor_unknownHop => 'Unknown repeater'; + @override String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。'; @@ -1928,54 +2019,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get common_clear => '清除'; - @override - String path_currentPath(String path) { - return '当前路径:$path'; - } - - @override - String path_usingHopsPath(int count) { - return '使用 $count 跳路径'; - } - - @override - String get path_enterCustomPath => '输入自定义路径'; - @override String get path_currentPathLabel => '当前路径'; - @override - String get path_hexPrefixInstructions => '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。'; - - @override - String get path_hexPrefixExample => '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)'; - - @override - String get path_labelHexPrefixes => '路径(十六进制前缀)'; - - @override - String get path_helperMaxHops => '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。'; - - @override - String get path_selectFromContacts => '或从联系人列表中选择:'; - @override String get path_noRepeatersFound => '未找到任何转发节点或房间服务器。'; - @override - String get path_customPathsRequire => '自定义路径需要中间节点转发消息。'; - - @override - String path_invalidHexPrefixes(String prefixes) { - return '无效的十六进制前缀:$prefixes'; - } - - @override - String get path_tooLong => '路径过长,最多允许 64 跳。'; - - @override - String get path_setPath => '设置路径'; - @override String get repeater_management => '转发节点管理'; @@ -2036,15 +2085,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_routingMode => '路由模式'; - @override - String get repeater_autoUseSavedPath => '自动(使用保存的路径)'; - - @override - String get repeater_forceFloodMode => '强制泛洪模式'; - - @override - String get repeater_pathManagement => '路径管理'; - @override String get repeater_refresh => '刷新'; @@ -4086,4 +4126,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get map_zoomIn => 'Zoom in'; + + @override + String get map_zoomOut => 'Zoom out'; + + @override + String get map_centerMap => 'Center map'; + + @override + String get chrome_bluetoothRequiresChromium => + 'Web Bluetooth requires a Chromium browser'; + + @override + String channels_communityShortId(String id) { + return 'ID: $id...'; + } + + @override + String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + + @override + String get pathTrace_legendInferred => 'Inferred position'; } diff --git a/lib/main.dart b/lib/main.dart index 37aa29ff..1445bdde 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,11 +24,21 @@ import 'services/translation_service.dart'; import 'services/ui_view_state_service.dart'; import 'services/timeout_prediction_service.dart'; import 'storage/prefs_manager.dart'; +import 'theme/mesh_theme.dart'; import 'utils/app_logger.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // On desktop, debugPrint is not suppressed in release builds and every + // call is a synchronous stdout write. The connector logs heavily on hot + // paths (frame handling, queue/channel sync), which shows up as syscall + // overhead on low-end Linux machines (issue #202). The in-app debug log + // screens are unaffected — they store entries themselves. + if (kReleaseMode) { + debugPrint = (String? message, {int? wrapWidth}) {}; + } + // Initialize SharedPreferences cache await PrefsManager.initialize(); @@ -191,23 +201,8 @@ class MeshCoreApp extends StatelessWidget { locale: _localeFromSetting( settingsService.settings.languageOverride, ), - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), - useMaterial3: true, - snackBarTheme: const SnackBarThemeData( - behavior: SnackBarBehavior.floating, - ), - ), - darkTheme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, - brightness: Brightness.dark, - ), - useMaterial3: true, - snackBarTheme: const SnackBarThemeData( - behavior: SnackBarBehavior.floating, - ), - ), + theme: MeshTheme.light(), + darkTheme: MeshTheme.dark(), themeMode: _themeModeFromSetting( settingsService.settings.themeMode, ), diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index ca6a6bf1..719e5735 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -63,7 +63,7 @@ class AppDebugLogScreen extends StatelessWidget { final entry = entries[index]; return ListTile( dense: true, - leading: _buildLevelIcon(entry.level), + leading: _buildLevelIcon(context, entry.level), title: Text( '[${entry.tag}] ${entry.message}', style: const TextStyle( @@ -75,7 +75,7 @@ class AppDebugLogScreen extends StatelessWidget { entry.formattedTime, style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ); @@ -88,14 +88,14 @@ class AppDebugLogScreen extends StatelessWidget { Icon( Icons.bug_report_outlined, size: 64, - color: Colors.grey[400], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(height: 16), Text( context.l10n.debugLog_noEntries, style: TextStyle( fontSize: 16, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -103,7 +103,7 @@ class AppDebugLogScreen extends StatelessWidget { context.l10n.debugLog_enableInSettings, style: TextStyle( fontSize: 12, - color: Colors.grey[500], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -115,18 +115,19 @@ class AppDebugLogScreen extends StatelessWidget { ); } - Widget _buildLevelIcon(AppDebugLogLevel level) { + Widget _buildLevelIcon(BuildContext context, AppDebugLogLevel level) { + final colorScheme = Theme.of(context).colorScheme; switch (level) { case AppDebugLogLevel.info: - return const Icon(Icons.info_outline, size: 18, color: Colors.blue); + return Icon(Icons.info_outline, size: 18, color: colorScheme.primary); case AppDebugLogLevel.warning: - return const Icon( + return Icon( Icons.warning_amber_outlined, size: 18, - color: Colors.orange, + color: colorScheme.tertiary, ); case AppDebugLogLevel.error: - return const Icon(Icons.error_outline, size: 18, color: Colors.red); + return Icon(Icons.error_outline, size: 18, color: colorScheme.error); } } } diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 9b0fb378..76ce1f07 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -92,11 +92,29 @@ class AppSettingsScreen extends StatelessWidget { ListTile( leading: const Icon(Icons.brightness_6_outlined), title: Text(context.l10n.appSettings_theme), - subtitle: Text( - _themeModeLabel(context, settingsService.settings.themeMode), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8), + child: SegmentedButton( + segments: [ + ButtonSegment( + value: 'system', + label: Text(context.l10n.appSettings_themeSystem), + ), + ButtonSegment( + value: 'light', + label: Text(context.l10n.appSettings_themeLight), + ), + ButtonSegment( + value: 'dark', + label: Text(context.l10n.appSettings_themeDark), + ), + ], + selected: {settingsService.settings.themeMode}, + onSelectionChanged: (selection) { + settingsService.setThemeMode(selection.first); + }, + ), ), - trailing: const Icon(Icons.chevron_right), - onTap: () => _showThemeModeDialog(context, settingsService), ), const Divider(height: 1), ListTile( @@ -111,18 +129,6 @@ class AppSettingsScreen extends StatelessWidget { trailing: const Icon(Icons.chevron_right), onTap: () => _showLanguageDialog(context, settingsService), ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.location_searching), - title: Text(context.l10n.appSettings_enableMessageTracing), - subtitle: Text( - context.l10n.appSettings_enableMessageTracingSubtitle, - ), - value: settingsService.settings.enableMessageTracing, - onChanged: (value) { - settingsService.setEnableMessageTracing(value); - }, - ), ], ), ); @@ -189,14 +195,14 @@ class AppSettingsScreen extends StatelessWidget { Icons.message_outlined, color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), title: Text( context.l10n.appSettings_messageNotifications, style: TextStyle( color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), ), subtitle: Text( @@ -204,7 +210,7 @@ class AppSettingsScreen extends StatelessWidget { style: TextStyle( color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), ), value: settingsService.settings.notifyOnNewMessage, @@ -220,14 +226,14 @@ class AppSettingsScreen extends StatelessWidget { Icons.forum_outlined, color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), title: Text( context.l10n.appSettings_channelMessageNotifications, style: TextStyle( color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), ), subtitle: Text( @@ -235,7 +241,7 @@ class AppSettingsScreen extends StatelessWidget { style: TextStyle( color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), ), value: settingsService.settings.notifyOnNewChannelMessage, @@ -251,14 +257,14 @@ class AppSettingsScreen extends StatelessWidget { Icons.cell_tower, color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), title: Text( context.l10n.appSettings_advertisementNotifications, style: TextStyle( color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), ), subtitle: Text( @@ -266,7 +272,7 @@ class AppSettingsScreen extends StatelessWidget { style: TextStyle( color: settingsService.settings.notificationsEnabled ? null - : Colors.grey, + : Theme.of(context).disabledColor, ), ), value: settingsService.settings.notifyOnNewAdvert, @@ -343,10 +349,16 @@ class AppSettingsScreen extends StatelessWidget { ); }, ), - if (settingsService.settings.autoRouteRotationEnabled) ...[ - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxRouteWeight), + if (settingsService.settings.autoRouteRotationEnabled) + Container( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + padding: const EdgeInsets.only(left: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxRouteWeight), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -454,7 +466,21 @@ class AppSettingsScreen extends StatelessWidget { ], ), ), - ], + ], + ), + ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), ], ), ); @@ -584,15 +610,15 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.auto_awesome_outlined, - color: translationEnabled ? null : Colors.grey, + color: translationEnabled ? null : Theme.of(context).disabledColor, ), title: Text( context.l10n.translation_autoIncomingTitle, - style: TextStyle(color: translationEnabled ? null : Colors.grey), + style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), ), subtitle: Text( context.l10n.translation_autoIncomingSubtitle, - style: TextStyle(color: translationEnabled ? null : Colors.grey), + style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), ), value: settings.autoTranslateIncomingMessages, onChanged: translationEnabled @@ -603,15 +629,15 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.outgoing_mail, - color: translationEnabled ? null : Colors.grey, + color: translationEnabled ? null : Theme.of(context).disabledColor, ), title: Text( context.l10n.translation_composerTitle, - style: TextStyle(color: translationEnabled ? null : Colors.grey), + style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), ), subtitle: Text( context.l10n.translation_composerSubtitle, - style: TextStyle(color: translationEnabled ? null : Colors.grey), + style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), ), value: settings.composerTranslationEnabled, onChanged: translationEnabled @@ -871,61 +897,6 @@ class AppSettingsScreen extends StatelessWidget { ); } - void _showThemeModeDialog( - BuildContext context, - AppSettingsService settingsService, - ) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.appSettings_theme), - content: RadioGroup( - groupValue: settingsService.settings.themeMode, - onChanged: (value) { - if (value != null) { - settingsService.setThemeMode(value); - Navigator.pop(context); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: Text(context.l10n.appSettings_themeSystem), - value: 'system', - ), - RadioListTile( - title: Text(context.l10n.appSettings_themeLight), - value: 'light', - ), - RadioListTile( - title: Text(context.l10n.appSettings_themeDark), - value: 'dark', - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), - ), - ], - ), - ); - } - - String _themeModeLabel(BuildContext context, String value) { - switch (value) { - case 'light': - return context.l10n.appSettings_themeLight; - case 'dark': - return context.l10n.appSettings_themeDark; - default: - return context.l10n.appSettings_themeSystem; - } - } - String _languageLabel(BuildContext context, String? languageCode) { switch (languageCode) { case 'en': diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index b5e151bd..c43c6c67 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -5,7 +5,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; +import 'package:intl/intl.dart' hide TextDirection; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -25,8 +25,9 @@ import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; -import '../utils/emoji_utils.dart'; +import '../helpers/contact_ui.dart'; import '../widgets/byte_count_input.dart'; +import '../widgets/empty_state.dart'; import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; @@ -283,6 +284,8 @@ class _ChannelChatScreenState extends State { ) : widget.channel.name, style: const TextStyle(fontSize: 16), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), Consumer( builder: (context, connector, _) { @@ -311,9 +314,7 @@ class _ChannelChatScreenState extends State { icon: const Icon(Icons.more_vert), onSelected: (value) { if (value == 'clearChat') { - context.read().clearMessagesForChannel( - widget.channel.index, - ); + _confirmClearChat(); } }, itemBuilder: (context) => [ @@ -321,11 +322,17 @@ class _ChannelChatScreenState extends State { value: 'clearChat', child: Row( children: [ - const Icon(Icons.delete, size: 20, color: Colors.red), + Icon( + Icons.delete, + size: 20, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 12), Text( context.l10n.contact_clearChat, - style: const TextStyle(color: Colors.red), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ], ), @@ -344,34 +351,17 @@ class _ChannelChatScreenState extends State { final messages = connector.getChannelMessages(widget.channel); if (messages.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - widget.channel.isPublicChannel - ? Icons.public - : Icons.tag, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - context.l10n.chat_noMessages, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - const SizedBox(height: 8), - Text( - context.l10n.chat_sendMessageToStart, - style: TextStyle( - fontSize: 14, - color: Colors.grey[500], - ), - ), - ], + return EmptyState( + icon: widget.channel.isPublicChannel + ? Icons.public + : Icons.tag, + title: context.l10n.chat_noMessages, + subtitle: context.l10n.chat_sendMessageTo( + widget.channel.name.isEmpty + ? context.l10n.channels_channelIndex( + widget.channel.index, + ) + : widget.channel.name, ), ); } @@ -381,6 +371,25 @@ class _ChannelChatScreenState extends State { final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); + // Prune stale keys (deleted/cleared messages) to avoid + // unbounded growth. + final liveIds = reversedMessages + .map((m) => m.messageId) + .toSet(); + _messageKeys.removeWhere((id, _) => !liveIds.contains(id)); + + // Two messages can collide on messageId (same ms + name/text + // hash). Only the first occurrence owns the shared GlobalKey + // used for scroll-to-message; duplicates get a local key so + // no two widgets share one GlobalKey. + final seenIds = {}; + final keyedIndices = {}; + for (var i = 0; i < reversedMessages.length; i++) { + if (seenIds.add(reversedMessages[i].messageId)) { + keyedIndices.add(i); + } + } + // Auto-scroll to bottom if user is already at bottom WidgetsBinding.instance.addPostFrameCallback((_) { if (_channelSkipNextBottomSnap) { @@ -416,14 +425,20 @@ class _ChannelChatScreenState extends State { } final messageIndex = index; final message = reversedMessages[messageIndex]; - if (!_messageKeys.containsKey(message.messageId)) { - _messageKeys[message.messageId] = GlobalKey(); + final GlobalKey messageKey; + if (keyedIndices.contains(messageIndex)) { + messageKey = _messageKeys.putIfAbsent( + message.messageId, + GlobalKey.new, + ); + } else { + messageKey = GlobalKey(); } final isUnreadAnchor = _unreadDividerMessageId != null && message.messageId == _unreadDividerMessageId; return Container( - key: _messageKeys[message.messageId]!, + key: messageKey, child: Builder( builder: (context) { final textScale = context @@ -495,7 +510,8 @@ class _ChannelChatScreenState extends State { const maxSwipeOffset = 64.0; const replySwipeThreshold = 64.0; const bodyFontSize = 14.0; - final messageBody = Column( + final messageBody = LayoutBuilder( + builder: (context, constraints) => Column( crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start, @@ -512,9 +528,6 @@ class _ChannelChatScreenState extends State { ], Flexible( child: GestureDetector( - onTap: PlatformInfo.isDesktop - ? null - : () => _showMessagePathInfo(message), onLongPress: () => _showMessageActions(message), onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => _showMessageActions(message) @@ -524,7 +537,7 @@ class _ChannelChatScreenState extends State { ? const EdgeInsets.all(4) : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, + maxWidth: constraints.maxWidth * 0.65, ), decoration: BoxDecoration( color: isOutgoing @@ -566,20 +579,6 @@ class _ChannelChatScreenState extends State { isOutgoing, textScale, message.senderName, - trailing: (!enableTracing && isOutgoing) - ? Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, - ), - ) - : null, ) else if (gifId != null) Stack( @@ -599,36 +598,6 @@ class _ChannelChatScreenState extends State { .withValues(alpha: 0.6), ), ), - if (!enableTracing && isOutgoing) - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of( - context, - ).colorScheme.primaryContainer - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(10), - topRight: Radius.circular(8), - ), - ), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, - ), - ), - ), ], ) else @@ -651,97 +620,89 @@ class _ChannelChatScreenState extends State { ), ), ), - if (!enableTracing && isOutgoing) ...[ - const SizedBox(width: 4), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, + ], + ), + if (enableTracing && displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.channels_via( + _formatPathPrefixes(displayPath), + ), + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatTime(context, message.timestamp), + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + if (enableTracing && message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent, + isRepeated: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isPending: + message.status == + ChannelMessageStatus.pending, + isFailed: + message.status == + ChannelMessageStatus.failed, + ), + ], ], ), - if (enableTracing) ...[ - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.channels_via( - _formatPathPrefixes(displayPath), - ), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(context, message.timestamp), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), - Text( - '${message.repeatCount}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], - ), - ), - ], + ), ], ), ), @@ -757,6 +718,7 @@ class _ChannelChatScreenState extends State { ), ], ], + ), ); if (!isOutgoing && !PlatformInfo.isDesktop) { @@ -958,9 +920,9 @@ class _ChannelChatScreenState extends State { IconButton( icon: Icon(Icons.location_on_outlined, color: channelColor), padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), onPressed: () { - final selfName = context.read().selfName ?? 'Me'; + final selfName = context.read().selfName ?? context.l10n.chat_me; final fromName = isOutgoing ? selfName : senderName; final key = buildSharedMarkerKey( sourceId: 'channel:${widget.channel.index}', @@ -1020,8 +982,8 @@ class _ChannelChatScreenState extends State { } Widget _buildAvatar(String senderName) { - final initial = _getFirstCharacterOrEmoji(senderName); - final color = _getColorForName(senderName); + final initial = firstCharacterOrEmoji(senderName); + final color = colorForName(senderName); return CircleAvatar( radius: 18, @@ -1037,36 +999,6 @@ class _ChannelChatScreenState extends State { ); } - String _getFirstCharacterOrEmoji(String name) { - if (name.isEmpty) return '?'; - - final emoji = firstEmoji(name); - if (emoji != null) return emoji; - - final runes = name.runes.toList(); - if (runes.isEmpty) return '?'; - return String.fromCharCode(runes[0]).toUpperCase(); - } - - Color _getColorForName(String name) { - // Generate a consistent color based on the name hash - final hash = name.hashCode; - final colors = [ - Colors.blue, - Colors.green, - Colors.orange, - Colors.purple, - Colors.pink, - Colors.teal, - Colors.indigo, - Colors.cyan, - Colors.amber, - Colors.deepOrange, - ]; - - return colors[hash.abs() % colors.length]; - } - Widget _buildReplyBanner(double textScale) { final message = _replyingToMessage!; return Container( @@ -1116,8 +1048,7 @@ class _ChannelChatScreenState extends State { icon: const Icon(Icons.close, size: 18), onPressed: _cancelReply, color: Theme.of(context).colorScheme.onSecondaryContainer, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + constraints: const BoxConstraints(minWidth: 44, minHeight: 44), ), ], ), @@ -1412,15 +1343,14 @@ class _ChannelChatScreenState extends State { _setReplyingTo(message); }, ), - if (PlatformInfo.isDesktop) - ListTile( - leading: const Icon(Icons.route), - title: Text(context.l10n.chat_path), - onTap: () { - Navigator.pop(sheetContext); - _showMessagePathInfo(message); - }, - ), + ListTile( + leading: const Icon(Icons.route), + title: Text(context.l10n.chat_path), + onTap: () { + Navigator.pop(sheetContext); + _showMessagePathInfo(message); + }, + ), // Can't react to your own messages if (!message.isOutgoing) ListTile( @@ -1463,19 +1393,21 @@ class _ChannelChatScreenState extends State { _markAsUnread(message); }, ), + const Divider(), ListTile( - leading: const Icon(Icons.delete_outline), - title: Text(context.l10n.common_delete), + leading: Icon( + Icons.delete_outline, + color: Theme.of(context).colorScheme.error, + ), + title: Text( + context.l10n.common_delete, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), onTap: () async { Navigator.pop(sheetContext); await _deleteMessage(message); }, ), - ListTile( - leading: const Icon(Icons.close), - title: Text(context.l10n.common_cancel), - onTap: () => Navigator.pop(sheetContext), - ), ], ), ), @@ -1516,6 +1448,34 @@ class _ChannelChatScreenState extends State { ); } + Future _confirmClearChat() async { + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.contact_clearChat), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: Text(context.l10n.common_delete), + ), + ], + ), + ); + if (confirmed == true) { + if (!mounted) return; + context.read().clearMessagesForChannel( + widget.channel.index, + ); + } + } + Future _deleteMessage(ChannelMessage message) async { await context.read().deleteChannelMessage(message); if (!mounted) return; @@ -1557,6 +1517,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { double _maxSwipeDistance = 0; int? _swipePointerId; bool _swipeLockedToHorizontal = false; + bool _isRtl = false; void _handleSwipeStart(Offset position) { _swipeStartPosition = position; @@ -1577,11 +1538,13 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { return; } - final dx = event.position.dx - _swipeStartPosition!.dx; + final rawDx = event.position.dx - _swipeStartPosition!.dx; + // In LTR swipe left (rawDx < 0) triggers reply; in RTL swipe right (rawDx > 0). + final signedDx = _isRtl ? rawDx : -rawDx; const axisLockThreshold = 12.0; if (!_swipeLockedToHorizontal) { - if (-dx < axisLockThreshold) { + if (signedDx < axisLockThreshold) { return; } _swipeLockedToHorizontal = true; @@ -1593,28 +1556,32 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { void _handleSwipeUpdate(Offset position) { if (_swipeStartPosition == null) return; - final dx = position.dx - _swipeStartPosition!.dx; - if (dx >= 0) return; + final rawDx = position.dx - _swipeStartPosition!.dx; + final signedDx = _isRtl ? rawDx : -rawDx; + if (signedDx <= 0) return; - if (-dx < 6) return; + if (signedDx < 6) return; - if (-dx > _maxSwipeDistance) { - _maxSwipeDistance = -dx; + if (signedDx > _maxSwipeDistance) { + _maxSwipeDistance = signedDx; } - final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble(); + final double clamped = signedDx.clamp(0.0, widget.maxSwipeOffset); final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset); - if (adjusted != _swipeOffset) { - setState(() => _swipeOffset = adjusted); + // Translate in the gesture direction: negative for LTR (left), positive for RTL (right). + final translationOffset = _isRtl ? adjusted : -adjusted; + if (translationOffset != _swipeOffset) { + setState(() => _swipeOffset = translationOffset); } } void _handleSwipePointerUp(Offset position) { if (_swipeLockedToHorizontal && _swipeStartPosition != null) { - final dx = position.dx - _swipeStartPosition!.dx; + final rawDx = position.dx - _swipeStartPosition!.dx; + final signedDx = _isRtl ? rawDx : -rawDx; final peak = math.max( _maxSwipeDistance, - (-dx).clamp(0.0, double.infinity), + signedDx.clamp(0.0, double.infinity), ); if (peak >= widget.replySwipeThreshold) { widget.onReplyTriggered(); @@ -1654,6 +1621,10 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { @override Widget build(BuildContext context) { + _isRtl = Directionality.of(context) == TextDirection.rtl; + // In LTR, the bubble slides left and the hint appears on the right (isStart: false). + // In RTL, the bubble slides right and the hint appears on the left (isStart: true). + final hintIsStart = _isRtl; return Listener( onPointerDown: _handleSwipePointerDown, onPointerMove: _handleSwipePointerMove, @@ -1667,7 +1638,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { Positioned.fill( child: Opacity( opacity: _swipeOffset.abs() / widget.maxSwipeOffset, - child: widget.hintBuilder(isStart: false), + child: widget.hintBuilder(isStart: hintIsStart), ), ), AnimatedContainer( diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index afb2b906..6e9f3882 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -106,7 +106,7 @@ class ChannelMessagePathScreen extends StatelessWidget { if (!hasHopDetails) Text( l10n.channelPath_noHopDetails, - style: const TextStyle(color: Colors.grey), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), ) else ..._buildHopTiles(context, hops), @@ -131,22 +131,25 @@ class ChannelMessagePathScreen extends StatelessWidget { style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), - _buildDetailRow(l10n.channelPath_senderLabel, message.senderName), + _buildDetailRow(context, l10n.channelPath_senderLabel, message.senderName), _buildDetailRow( + context, l10n.channelPath_timeLabel, _formatTime(message.timestamp, l10n), ), if (message.repeatCount > 0) _buildDetailRow( + context, l10n.channelPath_repeatsLabel, message.repeatCount.toString(), ), _buildDetailRow( + context, l10n.channelPath_pathLabelTitle, _formatPathLabel(message.pathLength, l10n), ), if (observedLabel != null) - _buildDetailRow(l10n.channelPath_observedLabel, observedLabel), + _buildDetailRow(context, l10n.channelPath_observedLabel, observedLabel), ], ), ), @@ -250,7 +253,7 @@ class ChannelMessagePathScreen extends StatelessWidget { return l10n.channelPath_observedSomeOf(observedCount, pathLength); } - Widget _buildDetailRow(String label, String value) { + Widget _buildDetailRow(BuildContext context, String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( @@ -258,7 +261,7 @@ class ChannelMessagePathScreen extends StatelessWidget { children: [ SizedBox( width: 70, - child: Text(label, style: TextStyle(color: Colors.grey[600])), + child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)), ), Expanded(child: Text(value)), ], @@ -412,17 +415,17 @@ class _ChannelMessagePathMapScreenState children: [ IconButton( icon: const Icon(Icons.add), - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( icon: const Icon(Icons.remove), - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( icon: const Icon(Icons.my_location), - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: () => _resetMapView( initialCenter: initialCenter, initialZoom: initialZoom, @@ -593,7 +596,7 @@ class _ChannelMessagePathMapScreenState if (points.isEmpty) Center( child: Card( - color: Colors.white.withValues(alpha: 0.9), + color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9), child: Padding( padding: EdgeInsets.all(12), child: Text( @@ -664,7 +667,7 @@ class _ChannelMessagePathMapScreenState label, _formatPathPrefixes(selectedPath.pathBytes), ), - style: TextStyle(color: Colors.grey[700], fontSize: 12), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12), ), ], ), @@ -685,28 +688,32 @@ class _ChannelMessagePathMapScreenState markers.add( Marker( point: point, - width: 35, - height: 35, - child: Container( - decoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - hop.index.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -729,28 +736,32 @@ class _ChannelMessagePathMapScreenState markers.add( Marker( point: selfPoint, - width: 35, - height: 35, - child: Container( - decoration: BoxDecoration( - color: Colors.teal, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -804,6 +815,12 @@ class _ChannelMessagePathMapScreenState ); } + Widget _colorDot(Color color) => Container( + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ); + Widget _buildLegendCard( BuildContext context, List<_PathHop> hops, @@ -826,9 +843,22 @@ class _ChannelMessagePathMapScreenState children: [ Padding( padding: const EdgeInsets.all(12), - child: Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', - style: const TextStyle(fontWeight: FontWeight.w600), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 6), + Row( + children: [ + _colorDot(Colors.green), + const SizedBox(width: 4), + Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)), + ], + ), + ], ), ), const Divider(height: 1), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 40726021..b725cc61 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -111,24 +111,26 @@ class _ChannelsScreenState extends State PopupMenuItem( child: Row( children: [ - const Icon(Icons.logout, color: Colors.red), + Icon( + Icons.logout, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 8), Text(context.l10n.common_disconnect), ], ), onTap: () => _disconnect(context), ), - if (_communities.isNotEmpty) - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.groups), - const SizedBox(width: 8), - Text(context.l10n.community_manageCommunities), - ], - ), - onTap: () => _showManageCommunitiesDialog(context), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.groups), + const SizedBox(width: 8), + Text(context.l10n.community_manageCommunities), + ], ), + onTap: () => _showManageCommunitiesDialog(context), + ), PopupMenuItem( child: Row( children: [ @@ -241,32 +243,22 @@ class _ChannelsScreenState extends State ), Expanded( child: filteredChannels.isEmpty - ? ListView( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height - 300, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.search_off, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( + ? LayoutBuilder( + builder: (context, constraints) => ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: EmptyState( + icon: Icons.search_off, + title: context.l10n.channels_noChannelsFound, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - ], ), ), - ), - ], + ], + ), ) : (viewState.channelsSortOption == ChannelSortOption.manual && @@ -357,6 +349,9 @@ class _ChannelsScreenState extends State int? dragIndex, }) { final unreadCount = connector.getUnreadCountForChannel(channel); + final isMuted = context.watch().isChannelMuted( + channel.name, + ); // Determine icon and colors based on channel type IconData icon; @@ -449,37 +444,45 @@ class _ChannelsScreenState extends State trailing: Row( mainAxisSize: MainAxisSize.min, children: [ + if (isMuted) ...[ + Icon( + Icons.notifications_off, + size: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + ], if (unreadCount > 0) ...[ UnreadBadge(count: unreadCount), const SizedBox(width: 4), ], if (showDragHandle && dragIndex != null) - ReorderableDelayedDragStartListener( + ReorderableDragStartListener( index: dragIndex, - child: Icon( - Icons.drag_handle, - color: Theme.of(context).colorScheme.onSurfaceVariant, + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon( + Icons.drag_handle, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ], ), - onTap: () async { + onTap: () { final unread = connector.getUnreadCountForChannelIndex( channel.index, ); connector.markChannelRead(channel.index); - await Future.delayed(const Duration(milliseconds: 50)); - if (context.mounted) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChannelChatScreen( - channel: channel, - initialUnreadCount: unread, - ), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelChatScreen( + channel: channel, + initialUnreadCount: unread, ), - ); - } + ), + ); }, onLongPress: () => _showChannelActions( context, @@ -540,10 +543,15 @@ class _ChannelsScreenState extends State }, ), ListTile( - leading: const Icon(Icons.delete_outline, color: Colors.red), + leading: Icon( + Icons.delete_outline, + color: Theme.of(sheetContext).colorScheme.error, + ), title: Text( context.l10n.channels_deleteChannel, - style: const TextStyle(color: Colors.red), + style: TextStyle( + color: Theme.of(sheetContext).colorScheme.error, + ), ), onTap: () async { Navigator.pop(sheetContext); @@ -725,23 +733,39 @@ class _ChannelsScreenState extends State ? (isSelected ? Theme.of(dialogContext).colorScheme.primaryContainer : null) - : Colors.grey.withValues(alpha: 0.2), + : Theme.of( + dialogContext, + ).colorScheme.onSurface.withValues(alpha: 0.12), child: Icon( icon, color: enabled ? (isSelected ? Theme.of(dialogContext).colorScheme.primary : null) - : Colors.grey, + : Theme.of( + dialogContext, + ).colorScheme.onSurface.withValues(alpha: 0.38), ), ), title: Text( title, - style: TextStyle(color: enabled ? null : Colors.grey), + style: TextStyle( + color: enabled + ? null + : Theme.of( + dialogContext, + ).colorScheme.onSurface.withValues(alpha: 0.38), + ), ), subtitle: Text( subtitle, - style: TextStyle(color: enabled ? null : Colors.grey), + style: TextStyle( + color: enabled + ? null + : Theme.of( + dialogContext, + ).colorScheme.onSurface.withValues(alpha: 0.38), + ), ), trailing: enabled ? const Icon(Icons.chevron_right) : null, selected: isSelected, @@ -929,7 +953,7 @@ class _ChannelsScreenState extends State Channel.publicChannelPsk, ); Navigator.pop(dialogContext); - connector.setChannel(nextIndex, 'Public', psk); + connector.setChannel(nextIndex, context.l10n.channels_public, psk); if (context.mounted) { showDismissibleSnackBar( context, @@ -1043,7 +1067,9 @@ class _ChannelsScreenState extends State dialogContext.l10n.community_hashtagPrivacyHint, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of( + dialogContext, + ).colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), ), @@ -1214,6 +1240,7 @@ class _ChannelsScreenState extends State child: FilledButton( onPressed: () async { final name = nameController.text.trim(); + final publicLabel = context.l10n.channels_public; if (name.isEmpty) { showDismissibleSnackBar( context, @@ -1238,7 +1265,7 @@ class _ChannelsScreenState extends State final psk = community .deriveCommunityPublicPsk(); final channelName = - '${community.name} Public'; + '${community.name} $publicLabel'; connector.setChannel( nextIndex, channelName, @@ -1594,7 +1621,7 @@ class _ChannelsScreenState extends State }, child: Text( dialogContext.l10n.common_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], @@ -1604,7 +1631,7 @@ class _ChannelsScreenState extends State void _addPublicChannel(BuildContext context, MeshCoreConnector connector) { final psk = Channel.parsePskHex(Channel.publicChannelPsk); - connector.setChannel(0, 'Public', psk); + connector.setChannel(0, context.l10n.channels_public, psk); showDismissibleSnackBar( context, content: Text(context.l10n.channels_publicChannelAdded), @@ -1653,14 +1680,19 @@ class _ChannelsScreenState extends State Icon( Icons.groups_outlined, size: 64, - color: Colors.grey[400], + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withValues(alpha: 0.6), ), const SizedBox(height: 16), Text( context.l10n.community_noCommunities, style: TextStyle( fontSize: 16, - color: Colors.grey[600], + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -1668,7 +1700,10 @@ class _ChannelsScreenState extends State context.l10n.community_scanOrCreate, style: TextStyle( fontSize: 14, - color: Colors.grey[500], + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withValues(alpha: 0.8), ), textAlign: TextAlign.center, ), @@ -1692,10 +1727,12 @@ class _ChannelsScreenState extends State ), title: Text(community.name), subtitle: Text( - 'ID: ${community.shortCommunityId}...', + context.l10n.channels_communityShortId(community.shortCommunityId), style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), trailing: PopupMenuButton( @@ -1722,14 +1759,20 @@ class _ChannelsScreenState extends State value: 'leave', child: Row( children: [ - const Icon( + Icon( Icons.exit_to_app, - color: Colors.red, + color: Theme.of( + context, + ).colorScheme.error, ), const SizedBox(width: 12), Text( context.l10n.community_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.error, + ), ), ], ), @@ -1830,7 +1873,7 @@ class _ChannelsScreenState extends State }, child: Text( dialogContext.l10n.community_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 5e9f960e..b96095bf 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../utils/platform_info.dart'; @@ -15,35 +14,32 @@ import '../connector/meshcore_protocol.dart'; import '../helpers/cyr2lat.dart'; import '../helpers/reaction_helper.dart'; import '../widgets/message_status_icon.dart'; +import '../widgets/empty_state.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/gif_helper.dart'; -import '../helpers/path_helper.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; import '../l10n/contact_localization.dart'; import '../models/message.dart'; -import '../models/path_history.dart'; import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; import '../services/translation_service.dart'; import '../widgets/chat_zoom_wrapper.dart'; -import '../widgets/elements_ui.dart'; import '../widgets/byte_count_input.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; -import '../utils/emoji_utils.dart'; +import '../helpers/contact_ui.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import '../widgets/message_translation_button.dart'; -import '../widgets/path_selection_dialog.dart'; +import '../widgets/routing_sheet.dart'; import '../widgets/radio_stats_entry.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/translated_message_content.dart'; -import '../utils/app_logger.dart'; import '../l10n/l10n.dart'; import '../helpers/snack_bar_builder.dart'; import '../widgets/unread_divider.dart'; @@ -187,28 +183,26 @@ class _ChatScreenState extends State { final unreadLabel = context.l10n.chat_unread(unreadCount); final pathLabel = _currentPathLabel(contact); - // Show path details if we have non-empty path data (from device or override) - final effectivePath = contact.pathOverrideBytes ?? contact.path; - final hasPathData = effectivePath.isNotEmpty; - return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(contact.name), + Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), GestureDetector( behavior: HitTestBehavior.opaque, - onTap: hasPathData - ? () => _showFullPathDialog(context, effectivePath) - : null, - child: Text( - '$pathLabel • $unreadLabel', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.normal, - decoration: hasPathData ? TextDecoration.underline : null, - decorationStyle: TextDecorationStyle.dotted, + onTap: () => + ContactRoutingSheet.show(context, contact: contact), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Text( + '$pathLabel • $unreadLabel', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.normal, + decoration: TextDecoration.underline, + decorationStyle: TextDecorationStyle.dotted, + ), ), ), ), @@ -219,139 +213,44 @@ class _ChatScreenState extends State { centerTitle: false, bottom: const SyncProgressAppBarBottom(), actions: [ - Consumer( - builder: (context, connector, _) { - final contact = _resolveContact(connector); - final isFloodMode = contact.pathOverride == -1; - - final isDirectMode = contact.pathOverride == 0; - final activeMode = isFloodMode - ? 'flood' - : isDirectMode - ? 'direct' - : 'auto'; - - return PopupMenuButton( - icon: Icon(isFloodMode ? Icons.waves : Icons.route), - tooltip: context.l10n.chat_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(contact, pathLen: -1); - } else if (mode == 'direct') { - await connector.setPathOverride( - contact, - pathLen: 0, - pathBytes: Uint8List(0), - ); - } else { - await connector.setPathOverride(contact, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: activeMode == 'auto' - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - context.l10n.chat_autoUseSavedPath, - style: TextStyle( - fontWeight: activeMode == 'auto' - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'direct', - child: Row( - children: [ - Icon( - Icons.near_me, - size: 20, - color: activeMode == 'direct' - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - context.l10n.chat_direct, - style: TextStyle( - fontWeight: activeMode == 'direct' - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: activeMode == 'flood' - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - context.l10n.chat_forceFloodMode, - style: TextStyle( - fontWeight: activeMode == 'flood' - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ); - }, - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: context.l10n.chat_pathManagement, - onPressed: () => _showPathHistory(context), - ), const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { + final contact = _resolveContact(connector); + return PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (value) { - if (value == 'info') { - _showContactInfo(context); - } - if (value == 'settings') { - _showContactSettings(context); - } - if (value == 'telemetry') { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TelemetryScreen(contact: widget.contact), - ), - ); - } - if (value == 'clearChat') { - connector.clearMessagesForContact(widget.contact); + switch (value) { + case 'routing': + ContactRoutingSheet.show(context, contact: contact); + case 'info': + _showContactInfo(context); + case 'settings': + _showContactSettings(context); + case 'telemetry': + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TelemetryScreen(contact: widget.contact), + ), + ); + case 'clearChat': + _confirmClearChat(context, connector); } }, itemBuilder: (context) => [ + PopupMenuItem( + value: 'routing', + child: Row( + children: [ + const Icon(Icons.route, size: 20), + const SizedBox(width: 12), + Text(context.l10n.routing_title), + ], + ), + ), PopupMenuItem( value: 'info', child: Row( @@ -386,11 +285,17 @@ class _ChatScreenState extends State { value: 'clearChat', child: Row( children: [ - const Icon(Icons.delete, size: 20, color: Colors.red), + Icon( + Icons.delete, + size: 20, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 12), Text( context.l10n.contact_clearChat, - style: const TextStyle(color: Colors.red), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ], ), @@ -425,24 +330,11 @@ class _ChatScreenState extends State { } Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.chat_bubble_outline, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - context.l10n.chat_noMessages, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - const SizedBox(height: 8), - Text( - context.l10n.chat_sendMessageTo( - _resolveContact(context.read()).name, - ), - style: TextStyle(fontSize: 14, color: Colors.grey[500]), - ), - ], + return EmptyState( + icon: Icons.chat_bubble_outline, + title: context.l10n.chat_noMessages, + subtitle: context.l10n.chat_sendMessageTo( + _resolveContact(context.read()).name, ), ); } @@ -514,7 +406,6 @@ class _ChatScreenState extends State { : contact.name, sourceId: widget.contact.publicKeyHex, textScale: textScale, - onTap: () => _openMessagePath(message, contact), onLongPress: () => _showMessageActions(message, contact), onRetryReaction: (msg, emoji) => _sendReaction(msg, contact, emoji), @@ -775,389 +666,32 @@ class _ChatScreenState extends State { ); } - void _showPathHistory(BuildContext context) { - final connector = Provider.of(context, listen: false); - bool showAllPaths = false; - showDialog( + Future _confirmClearChat( + BuildContext context, + MeshCoreConnector connector, + ) async { + final confirmed = await showDialog( context: context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => Consumer( - builder: (context, pathService, _) { - final paths = pathService.getRecentPaths( - widget.contact.publicKeyHex, - ); - - final repeatersList = List.of(connector.directRepeaters) - ..sort((a, b) => b.ranking.compareTo(a.ranking)); - - if (repeatersList.isEmpty) { - showAllPaths = true; - } - - final directRepeater = repeatersList.isEmpty - ? null - : repeatersList.first; - final secondDirectRepeater = repeatersList.length < 2 - ? null - : repeatersList.elementAt(1); - final thirdDirectRepeater = repeatersList.length < 3 - ? null - : repeatersList.elementAt(2); - - List>> - pathsWithRepeaters = paths.map((path) { - final isDirectRepeater = - directRepeater != null && - path.pathBytes.isNotEmpty && - directRepeater.pubkeyFirstByte == path.pathBytes.first; - final isSecondDirectRepeater = - secondDirectRepeater != null && - path.pathBytes.isNotEmpty && - secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - final isThirdDirectRepeater = - thirdDirectRepeater != null && - path.pathBytes.isNotEmpty && - thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - - int ranking = -1; - Color color = Colors.grey; - if (isDirectRepeater) { - color = Colors.green; - ranking = 3; - } else if (isSecondDirectRepeater) { - color = Colors.yellow; - ranking = 2; - } else if (isThirdDirectRepeater) { - color = Colors.red; - ranking = 1; - } else if (path.wasFloodDiscovery) { - color = Colors.blue; - ranking = 0; - } - - return MapEntry(ranking, MapEntry(color, path)); - }).toList(); - - pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key)); - - return AlertDialog( - title: Row( - children: [ - const Icon(Icons.timeline), - const SizedBox(width: 8), - Text(context.l10n.chat_pathManagement), - ], - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (pathsWithRepeaters.isNotEmpty) ...[ - if (repeatersList.isNotEmpty) - FeatureToggleRow( - title: context.l10n.chat_ShowAllPaths, - subtitle: "", - value: showAllPaths, - onChanged: (val) { - setDialogState(() { - showAllPaths = val; - }); - }, - ), - Text( - context.l10n.chat_recentAckPaths, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - if (pathsWithRepeaters.length >= 100) ...[ - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.amber[100], - borderRadius: BorderRadius.circular(8), - ), - child: Text( - context.l10n.chat_pathHistoryFull, - style: const TextStyle(fontSize: 12), - ), - ), - ], - const SizedBox(height: 8), - ...pathsWithRepeaters.map((entry) { - final path = entry.value.value; - final color = entry.value.key; - if (!showAllPaths && entry.key < 1) { - return const SizedBox.shrink(); - } else { - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: color, - child: Text( - '${path.hopCount}', - style: const TextStyle(fontSize: 12), - ), - ), - title: Text( - '${path.hopCount} ${path.hopCount == 1 ? context.l10n.chat_hopSingular : context.l10n.chat_hopPlural}', - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(path.timestamp)} • ${path.successCount} ${context.l10n.chat_successes}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: context.l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - widget.contact.publicKeyHex, - path.pathBytes, - ); - }, - ), - path.wasFloodDiscovery - ? const Icon( - Icons.waves, - size: 16, - color: Colors.grey, - ) - : const Icon( - Icons.route, - size: 16, - color: Colors.grey, - ), - ], - ), - onLongPress: () => - _showFullPathDialog(context, path.pathBytes), - onTap: () async { - if (path.pathBytes.isEmpty) { - showDismissibleSnackBar( - context, - content: Text( - context.l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), - ); - return; - } - - final pathBytes = Uint8List.fromList( - path.pathBytes, - ); - final pathLength = path.pathBytes.length; - - // Set the path override to persist user's choice - await connector.setPathOverride( - _resolveContact(connector), - pathLen: pathLength, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - await _notifyPathSet( - connector, - _resolveContact(connector), - pathBytes, - path.hopCount, - ); - }, - ), - ); - } - }), - const Divider(), - ] else ...[ - Text(context.l10n.chat_noPathHistoryYet), - const Divider(), - ], - const SizedBox(height: 8), - Text( - context.l10n.chat_pathActions, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - const SizedBox(height: 8), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.purple, - child: Icon(Icons.edit_road, size: 16), - ), - title: Text( - context.l10n.chat_setCustomPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_setCustomPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () { - Navigator.pop(context); - _showCustomPathDialog(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.orange, - child: Icon(Icons.clear_all, size: 16), - ), - title: Text( - context.l10n.chat_clearPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_clearPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.clearContactPath( - _resolveContact(connector), - ); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ); - Navigator.pop(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.blue, - child: Icon(Icons.waves, size: 16), - ), - title: Text( - context.l10n.chat_forceFloodMode, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_floodModeSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.setPathOverride( - _resolveContact(connector), - pathLen: -1, - ); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ); - Navigator.pop(context); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), - ), - ], - ); - }, - ), - ), - ); - } - - String _formatRelativeTime(DateTime? time) { - if (time == null) return '—'; - final diff = DateTime.now().difference(time); - if (diff.inSeconds < 60) return context.l10n.time_justNow; - if (diff.inMinutes < 60) { - return context.l10n.time_minutesAgo(diff.inMinutes); - } - if (diff.inHours < 24) return context.l10n.time_hoursAgo(diff.inHours); - return context.l10n.time_daysAgo(diff.inDays); - } - - void _showFullPathDialog(BuildContext context, List pathBytes) { - if (pathBytes.isEmpty) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ); - return; - } - - final connector = context.read(); - final allContacts = connector.allContacts; - - final formattedPath = PathHelper.formatPathHex(pathBytes); - final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.chat_fullPath), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText(formattedPath), - const SizedBox(height: 8), - SelectableText( - resolvedNames, - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.contact_clearChat), actions: [ TextButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PathTraceMapScreen( - title: context.l10n.contacts_repeaterPathTrace, - path: Uint8List.fromList(pathBytes), - flipPathAround: true, - targetContact: widget.contact, - pathHashByteWidth: connector.pathHashByteWidth, - ), - ), - ), - child: Text(context.l10n.contacts_pathTrace), + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(context.l10n.common_cancel), ), TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), + onPressed: () => Navigator.pop(dialogContext, true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: Text(context.l10n.common_delete), ), ], ), ); + if (confirmed == true) { + connector.clearMessagesForContact(widget.contact); + } } int _resolveContactIndex = -1; @@ -1202,29 +736,6 @@ class _ChatScreenState extends State { return context.l10n.chat_hopsCount(contact.pathLength); } - Future _notifyPathSet( - MeshCoreConnector connector, - Contact contact, - Uint8List pathBytes, - int hopCount, - ) async { - final verified = connector.isConnected - ? await connector.verifyContactPathOnDevice(contact, pathBytes) - : false; - if (!mounted) return; - - final status = !connector.isConnected - ? context.l10n.chat_pathSavedLocally - : (verified - ? context.l10n.chat_pathDeviceConfirmed - : context.l10n.chat_pathDeviceNotConfirmed); - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_pathSetHops(hopCount, status)), - duration: const Duration(seconds: 3), - ); - } - void _showContactInfo(BuildContext context) { final connector = Provider.of(context, listen: false); final contact = _resolveContact(connector); @@ -1439,7 +950,12 @@ class _ChatScreenState extends State { children: [ SizedBox( width: 80, - child: Text(label, style: TextStyle(color: Colors.grey[600])), + child: Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), Expanded(child: SelectableText(value)), ], @@ -1480,72 +996,6 @@ class _ChatScreenState extends State { ); } - Future _showCustomPathDialog(BuildContext context) async { - final connector = Provider.of(context, listen: false); - - final currentContact = _resolveContact(connector); - if (currentContact.pathLength > 0 && - currentContact.path.isEmpty && - connector.isConnected) { - connector.getContacts(); - } - - final pathForInput = currentContact.pathFormattedIdList( - connector.pathHashByteWidth, - ); - final currentPathLabel = _currentPathLabel(currentContact); - - // Filter out the current contact from available contacts - final availableContacts = connector.allContacts - .where((c) => c != widget.contact) - .toList(); - - final result = await PathSelectionDialog.show( - context, - availableContacts: availableContacts, - initialPath: pathForInput.isEmpty ? null : pathForInput, - title: context.l10n.chat_setCustomPath, - currentPathLabel: currentPathLabel, - onRefresh: connector.isConnected ? connector.getContacts : null, - ); - - appLogger.info( - 'PathSelectionDialog returned: ${result?.length ?? 0} bytes, mounted: $mounted', - tag: 'ChatScreen', - ); - - if (result == null) { - return; // Cancelled — keep existing path - } - - if (!mounted) { - appLogger.warn( - 'Widget not mounted after dialog, cannot set path', - tag: 'ChatScreen', - ); - return; - } - - appLogger.info( - 'Calling setPathOverride for ${widget.contact.name}', - tag: 'ChatScreen', - ); - await connector.setPathOverride( - _resolveContact(connector), - pathLen: result.length, - pathBytes: result, - ); - appLogger.info('setPathOverride completed', tag: 'ChatScreen'); - - if (!mounted) return; - await _notifyPathSet( - connector, - _resolveContact(connector), - result, - result.length, - ); - } - void _openMessagePath(Message message, Contact contact) { final connector = context.read(); final fourByteHex = message.fourByteRoomContactKey @@ -1605,15 +1055,14 @@ class _ChatScreenState extends State { _showEmojiPicker(message, contact); }, ), - if (PlatformInfo.isDesktop) - ListTile( - leading: const Icon(Icons.route), - title: Text(context.l10n.chat_path), - onTap: () { - Navigator.pop(sheetContext); - _openMessagePath(message, contact); - }, - ), + ListTile( + leading: const Icon(Icons.route), + title: Text(context.l10n.chat_path), + onTap: () { + Navigator.pop(sheetContext); + _openMessagePath(message, contact); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.common_copy), @@ -1646,14 +1095,6 @@ class _ChatScreenState extends State { _markAsUnread(message); }, ), - ListTile( - leading: const Icon(Icons.delete_outline), - title: Text(context.l10n.common_delete), - onTap: () async { - Navigator.pop(sheetContext); - await _deleteMessage(message); - }, - ), if (message.isOutgoing && message.status == MessageStatus.failed) ListTile( leading: const Icon(Icons.refresh), @@ -1673,10 +1114,20 @@ class _ChatScreenState extends State { _openChat(context, contact); }, ), + const Divider(), ListTile( - leading: const Icon(Icons.close), - title: Text(context.l10n.common_cancel), - onTap: () => Navigator.pop(sheetContext), + leading: Icon( + Icons.delete_outline, + color: Theme.of(context).colorScheme.error, + ), + title: Text( + context.l10n.common_delete, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + onTap: () async { + Navigator.pop(sheetContext); + await _deleteMessage(message); + }, ), ], ), @@ -1703,8 +1154,7 @@ class _ChatScreenState extends State { void _retryMessage(Message message) { final connector = Provider.of(context, listen: false); - // Retry using the contact's current path override setting - connector.sendMessage(_resolveContact(connector), message.text); + connector.resendMessage(_resolveContact(connector), message); showDismissibleSnackBar( context, content: Text(context.l10n.chat_retryingMessage), @@ -1748,7 +1198,6 @@ class _ChatScreenState extends State { class _MessageBubble extends StatelessWidget { final Message message; final String senderName; - final VoidCallback? onTap; final VoidCallback? onLongPress; final void Function(Message message, String emoji)? onRetryReaction; final double textScale; @@ -1759,7 +1208,6 @@ class _MessageBubble extends StatelessWidget { required this.senderName, required this.sourceId, required this.textScale, - this.onTap, this.onLongPress, this.onRetryReaction, }); @@ -1801,8 +1249,8 @@ class _MessageBubble extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: PlatformInfo.isDesktop ? null : onTap, + LayoutBuilder( + builder: (context, constraints) => GestureDetector( onLongPress: onLongPress, onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress?.call() @@ -1826,11 +1274,14 @@ class _MessageBubble extends StatelessWidget { vertical: 8, ), constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, + maxWidth: constraints.maxWidth * 0.65, ), decoration: BoxDecoration( color: bubbleColor, borderRadius: BorderRadius.circular(16), + border: isFailed + ? Border.all(color: colorScheme.error) + : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1863,20 +1314,6 @@ class _MessageBubble extends StatelessWidget { metaColor, textScale, senderName, - trailing: (!enableTracing && isOutgoing) - ? Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty, - isFailed: - message.status == - MessageStatus.failed, - ), - ) - : null, ) else if (gifId != null) Stack( @@ -1892,30 +1329,6 @@ class _MessageBubble extends StatelessWidget { ), ), ), - if (!enableTracing && isOutgoing) - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: bubbleColor, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(10), - topRight: Radius.circular(12), - ), - ), - child: MessageStatusIcon( - isAcked: - message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty, - isFailed: - message.status == - MessageStatus.failed, - ), - ), - ), ], ) else @@ -1937,94 +1350,95 @@ class _MessageBubble extends StatelessWidget { ), ), ), - if (!enableTracing && isOutgoing) ...[ + ], + ), + if (enableTracing && + isOutgoing && + message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + context + .read() + .settings + .maxMessageRetries, + ), + style: TextStyle( + fontSize: 10 * textScale, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10 * textScale, + color: metaColor, + ), + ), + if (isOutgoing) ...[ const SizedBox(width: 4), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty, - isFailed: - message.status == MessageStatus.failed, + MessageStatusIcon( + size: 12 * textScale, + onColor: metaColor, + isAcked: + message.status == + MessageStatus.delivered, + isPending: + message.status == MessageStatus.pending, + isFailed: + message.status == MessageStatus.failed, + ), + ], + if (enableTracing && + message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10 * textScale, + color: isOutgoing + ? metaColor + : Theme.of( + context, + ).colorScheme.tertiary, + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9 * textScale, + color: isOutgoing + ? metaColor + : Theme.of( + context, + ).colorScheme.tertiary, ), ), ], ], ), - if (enableTracing) ...[ - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - context - .read() - .settings - .maxMessageRetries, - ), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, - ), - ), - if (isOutgoing) ...[ - const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( - fontSize: 9, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - ), - ], - ], - ), - ), - ], + ), ], ), ), @@ -2032,6 +1446,7 @@ class _MessageBubble extends StatelessWidget { ], ), ), + ), if (message.reactions.isNotEmpty) ...[ const SizedBox(height: 4), Padding( @@ -2059,9 +1474,9 @@ class _MessageBubble extends StatelessWidget { IconButton( icon: Icon(Icons.location_on_outlined, color: textColor), padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), onPressed: () async { - final selfName = context.read().selfName ?? 'Me'; + final selfName = context.read().selfName ?? context.l10n.chat_me; final fromName = message.isOutgoing ? selfName : senderName; final key = buildSharedMarkerKey( sourceId: sourceId, @@ -2184,8 +1599,8 @@ class _MessageBubble extends StatelessWidget { } Widget _buildAvatar(String senderName, ColorScheme colorScheme) { - final initial = _getFirstCharacterOrEmoji(senderName); - final color = _getColorForName(senderName); + final initial = firstCharacterOrEmoji(senderName); + final color = colorForName(senderName); return CircleAvatar( radius: 18, @@ -2201,56 +1616,6 @@ class _MessageBubble extends StatelessWidget { ); } - String _getFirstCharacterOrEmoji(String name) { - if (name.isEmpty) return '?'; - - final emoji = firstEmoji(name); - if (emoji != null) return emoji; - - final runes = name.runes.toList(); - if (runes.isEmpty) return '?'; - return String.fromCharCode(runes[0]).toUpperCase(); - } - - Color _getColorForName(String name) { - // Generate a consistent color based on the name hash - final hash = name.hashCode; - final colors = [ - Colors.blue, - Colors.green, - Colors.orange, - Colors.purple, - Colors.pink, - Colors.teal, - Colors.indigo, - Colors.cyan, - Colors.amber, - Colors.deepOrange, - ]; - - return colors[hash.abs() % colors.length]; - } - - Widget _buildStatusIcon(Color color) { - IconData icon; - switch (message.status) { - case MessageStatus.pending: - icon = Icons.access_time; - break; - case MessageStatus.sent: - icon = Icons.schedule; - break; - case MessageStatus.delivered: - icon = Icons.check; - break; - case MessageStatus.failed: - icon = Icons.error_outline; - break; - } - - return Icon(icon, size: 12, color: color); - } - String _formatTime(DateTime time) { final hour = time.hour.toString().padLeft(2, '0'); final minute = time.minute.toString().padLeft(2, '0'); diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart index 1827aeb9..53271d8f 100644 --- a/lib/screens/chrome_required_screen.dart +++ b/lib/screens/chrome_required_screen.dart @@ -8,34 +8,25 @@ class ChromeRequiredScreen extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - + final colorScheme = theme.colorScheme; return Scaffold( body: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 32), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDark - ? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)] - : [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)], - ), - ), + color: colorScheme.surface, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: Colors.orange.withValues(alpha: 0.1), + color: colorScheme.tertiaryContainer.withValues(alpha: 0.4), shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.browser_not_supported_rounded, size: 80, - color: Colors.orange, + color: colorScheme.tertiary, ), ), const SizedBox(height: 32), @@ -44,7 +35,7 @@ class ChromeRequiredScreen extends StatelessWidget { textAlign: TextAlign.center, style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, - color: isDark ? Colors.white : Colors.black87, + color: colorScheme.onSurface, ), ), const SizedBox(height: 16), @@ -52,7 +43,7 @@ class ChromeRequiredScreen extends StatelessWidget { l10n.scanner_chromeRequiredMessage, textAlign: TextAlign.center, style: theme.textTheme.bodyLarge?.copyWith( - color: isDark ? Colors.white70 : Colors.black54, + color: colorScheme.onSurfaceVariant, height: 1.5, ), ), @@ -62,19 +53,19 @@ class ChromeRequiredScreen extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.1), + color: colorScheme.secondaryContainer.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(30), - border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), + border: Border.all(color: colorScheme.outline.withValues(alpha: 0.4)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.info_outline, size: 20, color: Colors.blue), + Icon(Icons.info_outline, size: 20, color: colorScheme.secondary), const SizedBox(width: 12), Text( - "Web Bluetooth requires a Chromium browser", + l10n.chrome_bluetoothRequiresChromium, style: theme.textTheme.bodyMedium?.copyWith( - color: Colors.blue, + color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w500, ), ), diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 9c376769..f666254c 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -210,10 +210,10 @@ class _NoiseChartPainter extends CustomPainter { } final span = maxV - minV; - for (var i = 0; i <= 2; i++) { - final v = maxV - span * i / 2; + for (var i = 0; i <= 4; i++) { + final v = maxV - span * i / 4; final tp = _yAxisLabel(v); - final y = chart.top + (chart.height * i / 2) - tp.height / 2; + final y = chart.top + (chart.height * i / 4) - tp.height / 2; tp.paint(canvas, Offset(4, y)); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index bdadc2b6..d64b8dd1 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -29,6 +29,7 @@ import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/unread_badge.dart'; +import '../helpers/contact_ui.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; @@ -59,7 +60,7 @@ class _ContactsScreenState extends State String _loadedGroupScopeKeyHex = ''; Timer? _searchDebounce; - final Set _pendingOperations = {}; + final List _pendingOperations = []; StreamSubscription? _frameSubscription; @@ -185,59 +186,52 @@ class _ContactsScreenState extends State Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } + // Generic OK/ERR acks carry no command correlation, so consume only + // the oldest pending operation per ack instead of clearing all. if (code == respCodeOk) { - // Show a snackbar indicating success if (!mounted) return; - - if (_pendingOperations.contains(ContactOperationType.import)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactImported), - ); + if (_pendingOperations.isEmpty) return; + final op = _pendingOperations.removeAt(0); + switch (op) { + case ContactOperationType.import: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImported), + ); + case ContactOperationType.zeroHopShare: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), + ); + case ContactOperationType.export: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopied), + ); } - - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_zeroHopContactAdvertSent), - ); - } - - if (_pendingOperations.contains(ContactOperationType.export)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactAdvertCopied), - ); - } - - _pendingOperations.clear(); } if (code == respCodeErr) { - // Show a snackbar indicating failure if (!mounted) return; - - if (_pendingOperations.contains(ContactOperationType.import)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactImportFailed), - ); + if (_pendingOperations.isEmpty) return; + final op = _pendingOperations.removeAt(0); + switch (op) { + case ContactOperationType.import: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), + ); + case ContactOperationType.zeroHopShare: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ); + case ContactOperationType.export: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ); } - - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), - ); - } - if (_pendingOperations.contains(ContactOperationType.export)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactAdvertCopyFailed), - ); - } - - _pendingOperations.clear(); } } catch (e) { appLogger.error( @@ -252,17 +246,37 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); _pendingOperations.add(ContactOperationType.export); - await connector.sendFrame(exportContactFrame, expectsGenericAck: true); + try { + await connector.sendFrame(exportContactFrame, expectsGenericAck: true); + } catch (e) { + _pendingOperations.remove(ContactOperationType.export); + if (mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ); + } + } } Future _contactZeroHop(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactZeroHopFrame = buildZeroHopContact(pubKey); _pendingOperations.add(ContactOperationType.zeroHopShare); - await connector.sendFrame( - exportContactZeroHopFrame, - expectsGenericAck: true, - ); + try { + await connector.sendFrame( + exportContactZeroHopFrame, + expectsGenericAck: true, + ); + } catch (e) { + _pendingOperations.remove(ContactOperationType.zeroHopShare); + if (mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ); + } + } } Future _contactImport() async { @@ -288,11 +302,10 @@ class _ContactsScreenState extends State return; } final hexString = text.substring('meshcore://'.length); + final Uint8List importContactFrame; try { final bytes = hex2Uint8List(hexString); - final importContactFrame = buildImportContactFrame(bytes); - _pendingOperations.add(ContactOperationType.import); - connector.importContact(importContactFrame); + importContactFrame = buildImportContactFrame(bytes); } catch (e) { if (mounted) { showDismissibleSnackBar( @@ -300,6 +313,19 @@ class _ContactsScreenState extends State content: Text(context.l10n.contacts_invalidAdvertFormat), ); } + return; + } + _pendingOperations.add(ContactOperationType.import); + try { + await connector.sendFrame(importContactFrame, expectsGenericAck: true); + } catch (e) { + _pendingOperations.remove(ContactOperationType.import); + if (mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), + ); + } } } @@ -322,7 +348,34 @@ class _ContactsScreenState extends State bottom: const SyncProgressAppBarBottom(), actions: [ PopupMenuButton( - itemBuilder: (context) => [ + tooltip: context.l10n.contacts_moreOptions, + itemBuilder: (context) => >[ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.person_add_rounded), + const SizedBox(width: 8), + Text(context.l10n.discoveredContacts_Title), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), + ), + ), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text(context.l10n.contacts_addContactFromClipboard), + ], + ), + onTap: () => _contactImport(), + ), + const PopupMenuDivider(), PopupMenuItem( child: Row( children: [ @@ -365,46 +418,20 @@ class _ContactsScreenState extends State ), onTap: () => _contactExport(Uint8List.fromList([])), ), + const PopupMenuDivider(), PopupMenuItem( child: Row( children: [ - const Icon(Icons.paste), - const SizedBox(width: 8), - Text(context.l10n.contacts_addContactFromClipboard), - ], - ), - onTap: () => _contactImport(), - ), - ], - icon: const Icon(Icons.connect_without_contact), - ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), + Icon( + Icons.logout, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 8), Text(context.l10n.common_disconnect), ], ), onTap: () => _disconnect(context, connector), ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.person_add_rounded), - const SizedBox(width: 8), - Text(context.l10n.discoveredContacts_Title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DiscoveryScreen(), - ), - ), - ), PopupMenuItem( child: Row( children: [ @@ -426,6 +453,10 @@ class _ContactsScreenState extends State ], ), body: _buildContactsBody(context, connector), + floatingActionButton: FloatingActionButton( + onPressed: () => _showAddContactSheet(context), + child: const Icon(Icons.person_add), + ), bottomNavigationBar: SafeArea( top: false, child: QuickSwitchBar( @@ -440,6 +471,40 @@ class _ContactsScreenState extends State ); } + void _showAddContactSheet(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (sheetContext) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.paste), + title: Text(context.l10n.contacts_addContactFromClipboard), + onTap: () { + Navigator.pop(sheetContext); + _contactImport(); + }, + ), + ListTile( + leading: const Icon(Icons.person_add_rounded), + title: Text(context.l10n.discoveredContacts_Title), + onTap: () { + Navigator.pop(sheetContext); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), + ), + ); + }, + ), + ], + ), + ), + ); + } + Future _disconnect( BuildContext context, MeshCoreConnector connector, @@ -571,7 +636,11 @@ class _ContactsScreenState extends State const SizedBox(width: 8), IconButton( tooltip: menuContext.l10n.contacts_deleteGroup, - icon: const Icon(Icons.delete, size: 20, color: Colors.red), + icon: Icon( + Icons.delete, + size: 20, + color: Theme.of(context).colorScheme.error, + ), onPressed: canManageGroups ? () => _closeDropdownAndRun( menuContext, @@ -589,16 +658,25 @@ class _ContactsScreenState extends State ], child: SizedBox( height: 48, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: [ - Expanded( - child: Text(selectedGroupName, overflow: TextOverflow.ellipsis), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down), - ], + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).colorScheme.outline), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + selectedGroupName, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down), + ], + ), ), ), ), @@ -624,6 +702,16 @@ class _ContactsScreenState extends State icon: Icons.people_outline, title: context.l10n.contacts_noContacts, subtitle: context.l10n.contacts_contactsWillAppear, + action: FilledButton.icon( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), + ), + ), + icon: const Icon(Icons.person_add_rounded), + label: Text(context.l10n.discoveredContacts_Title), + ), ); } @@ -759,6 +847,9 @@ class _ContactsScreenState extends State width: 48, height: 48, child: IconButton( + tooltip: viewState.contactsSearchExpanded + ? context.l10n.contacts_searchClose + : context.l10n.contacts_searchOpen, onPressed: () { if (viewState.contactsSearchExpanded) { _collapseContactsSearch(viewState); @@ -791,25 +882,29 @@ class _ContactsScreenState extends State ), ), Expanded( - child: filteredAndSorted.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - viewState.contactsShowUnreadOnly - ? context.l10n.contacts_noUnreadContacts - : context.l10n.contacts_noContactsFound, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - ], - ), - ) - : RefreshIndicator( - onRefresh: () => connector.getContacts(), - child: ListView.builder( + child: RefreshIndicator( + onRefresh: () => connector.getContacts(), + child: filteredAndSorted.isEmpty + ? LayoutBuilder( + builder: (context, constraints) => ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: EmptyState( + icon: Icons.search_off, + title: viewState.contactsShowUnreadOnly + ? context.l10n.contacts_noUnreadContacts + : context.l10n.contacts_noContactsFound, + ), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.only(bottom: 88), itemCount: filteredAndSorted.length, itemBuilder: (context, index) { final contact = filteredAndSorted[index]; @@ -827,7 +922,7 @@ class _ContactsScreenState extends State ); }, ), - ), + ), ), ], ); @@ -1048,7 +1143,7 @@ class _ContactsScreenState extends State }, child: Text( context.l10n.common_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], @@ -1359,14 +1454,6 @@ class _ContactsScreenState extends State ); }, ), - ListTile( - leading: const Icon(Icons.chat), - title: Text(context.l10n.contacts_openChat), - onTap: () { - Navigator.pop(sheetContext); - _openChat(context, contact); - }, - ), ], ListTile( leading: Icon( @@ -1403,10 +1490,15 @@ class _ContactsScreenState extends State }, ), ListTile( - leading: const Icon(Icons.delete, color: Colors.red), + leading: Icon( + Icons.delete, + color: Theme.of(context).colorScheme.error, + ), title: Text( context.l10n.contacts_deleteContact, - style: const TextStyle(color: Colors.red), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), onTap: () { Navigator.pop(sheetContext); @@ -1441,7 +1533,7 @@ class _ContactsScreenState extends State }, child: Text( context.l10n.common_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], @@ -1473,25 +1565,14 @@ class _ContactTile extends StatelessWidget { onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null, child: ListTile( leading: CircleAvatar( - backgroundColor: _getTypeColor(contact.type), + backgroundColor: contactTypeColor(contact.type), child: _buildContactAvatar(contact), ), title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - contact.pathLabel(context.l10n), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - contact.shortPubKeyHex, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12), - ), - ], + subtitle: Text( + contact.pathLabel(context.l10n), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. @@ -1502,7 +1583,7 @@ class _ContactTile extends StatelessWidget { ), ), child: SizedBox( - width: 120, + width: 96, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, @@ -1516,7 +1597,10 @@ class _ContactTile extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), Row( mainAxisSize: MainAxisSize.min, @@ -1529,7 +1613,9 @@ class _ContactTile extends StatelessWidget { Icon( Icons.location_on, size: 14, - color: Colors.grey[400], + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ), ], ), @@ -1548,37 +1634,7 @@ class _ContactTile extends StatelessWidget { if (emoji != null) { return Text(emoji, style: const TextStyle(fontSize: 18)); } - return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20); - } - - IconData _getTypeIcon(int type) { - switch (type) { - case advTypeChat: - return Icons.chat; - case advTypeRepeater: - return Icons.cell_tower; - case advTypeRoom: - return Icons.group; - case advTypeSensor: - return Icons.sensors; - default: - return Icons.device_unknown; - } - } - - Color _getTypeColor(int type) { - switch (type) { - case advTypeChat: - return Colors.blue; - case advTypeRepeater: - return Colors.orange; - case advTypeRoom: - return Colors.purple; - case advTypeSensor: - return Colors.green; - default: - return Colors.grey; - } + return Icon(contactTypeIcon(contact.type), color: Colors.white, size: 20); } String _formatLastSeen(BuildContext context, DateTime lastSeen) { diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index f9f0e07e..736e8f36 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -12,6 +12,7 @@ import '../utils/contact_search.dart'; import '../utils/platform_info.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; +import '../helpers/contact_ui.dart'; import '../helpers/snack_bar_builder.dart'; enum DiscoverySortOption { lastSeen, name, type } @@ -71,7 +72,10 @@ class _DiscoveryScreenState extends State { PopupMenuItem( child: Row( children: [ - const Icon(Icons.delete, color: Colors.red), + Icon( + Icons.delete, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 8), Text(context.l10n.discoveredContacts_deleteContactAll), ], @@ -99,9 +103,9 @@ class _DiscoveryScreenState extends State { final contact = filteredAndSorted[index]; final tile = ListTile( leading: CircleAvatar( - backgroundColor: _getTypeColor(contact.type), + backgroundColor: contactTypeColor(contact.type), child: Icon( - _getTypeIcon(contact.type), + contactTypeIcon(contact.type), color: Colors.white, size: 20, ), @@ -142,7 +146,9 @@ class _DiscoveryScreenState extends State { textAlign: TextAlign.right, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), Row( @@ -152,7 +158,10 @@ class _DiscoveryScreenState extends State { Icon( Icons.location_on, size: 14, - color: Colors.grey[400], + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withValues(alpha: 0.6), ), if (contact.rawPacket != null) const SizedBox(width: 2), @@ -160,7 +169,10 @@ class _DiscoveryScreenState extends State { Icon( Icons.cell_tower, size: 14, - color: Colors.grey[400], + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withValues(alpha: 0.6), ), ], ), @@ -170,6 +182,17 @@ class _DiscoveryScreenState extends State { ), onTap: () { connector.importDiscoveredContact(contact); + showDismissibleSnackBar( + context, + content: Text( + context.l10n.discoveredContacts_contactAdded, + ), + action: SnackBarAction( + label: context.l10n.common_undo, + onPressed: () => + connector.removeContact(contact), + ), + ); }, onLongPress: () => _showContactContextMenu(contact, connector), @@ -203,11 +226,6 @@ class _DiscoveryScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - leading: const Icon(Icons.add_reaction_sharp), - title: Text(l10n.discoveredContacts_addContact), - onTap: () => Navigator.of(sheetContext).pop('import_contact'), - ), ListTile( leading: const Icon(Icons.copy), title: Text(l10n.discoveredContacts_copyContact), @@ -227,9 +245,6 @@ class _DiscoveryScreenState extends State { if (!mounted || action == null) return; switch (action) { - case 'import_contact': - connector.importDiscoveredContact(contact); - break; case 'copy_contact': if (contact.rawPacket == null) return; final hexString = pubKeyToHex(contact.rawPacket!); @@ -429,35 +444,6 @@ class _DiscoveryScreenState extends State { } } - IconData _getTypeIcon(int type) { - switch (type) { - case advTypeChat: - return Icons.chat; - case advTypeRepeater: - return Icons.cell_tower; - case advTypeRoom: - return Icons.group; - case advTypeSensor: - return Icons.sensors; - default: - return Icons.device_unknown; - } - } - - Color _getTypeColor(int type) { - switch (type) { - case advTypeChat: - return Colors.blue; - case advTypeRepeater: - return Colors.orange; - case advTypeRoom: - return Colors.purple; - case advTypeSensor: - return Colors.green; - default: - return Colors.grey; - } - } String _formatLastSeen(BuildContext context, DateTime lastSeen) { final now = DateTime.now(); diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index f908f5ea..1d63b910 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -73,7 +73,7 @@ class _LineOfSightMapScreenState extends State { double _startAntennaHeight = 5.0; double _endAntennaHeight = 5.0; bool _showHud = true; - bool _menuExpanded = true; + bool _menuExpanded = false; bool _showDisplayNodes = true; bool _showMarkerLabels = true; bool _didReceivePositionUpdate = false; @@ -159,17 +159,17 @@ class _LineOfSightMapScreenState extends State { children: [ IconButton( icon: const Icon(Icons.add), - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( icon: const Icon(Icons.remove), - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( icon: const Icon(Icons.my_location), - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: () => _resetMapView( initialCenter: initialCenter, initialZoom: initialZoom, @@ -224,6 +224,7 @@ class _LineOfSightMapScreenState extends State { setState(() { _result = result; _selectedObstruction = _defaultObstructionFor(result); + _menuExpanded = true; }); } catch (e) { if (!mounted) return; @@ -506,7 +507,7 @@ class _LineOfSightMapScreenState extends State { bottom: 12, child: DecoratedBox( decoration: BoxDecoration( - color: Colors.black54, + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(8), ), child: Padding( @@ -516,11 +517,18 @@ class _LineOfSightMapScreenState extends State { ), child: Text( context.l10n.losElevationAttribution, - style: const TextStyle(fontSize: 10, color: Colors.white), + style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurface), ), ), ), ), + if (_loading) + const Positioned( + left: 0, + right: 0, + top: 0, + child: LinearProgressIndicator(), + ), ], ), floatingActionButton: FloatingActionButton( @@ -623,7 +631,7 @@ class _LineOfSightMapScreenState extends State { const SizedBox(height: 4), Text( context.l10n.losBlockedSpotsHint, - style: TextStyle(fontSize: 11, color: Colors.grey[700]), + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 6), Wrap( @@ -692,7 +700,7 @@ class _LineOfSightMapScreenState extends State { '${_selectedObstruction!.point.longitude.toStringAsFixed(5)}', style: TextStyle( fontSize: 11, - color: Colors.grey[700], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -711,14 +719,14 @@ class _LineOfSightMapScreenState extends State { context.l10n.losFrequencyLabel, style: TextStyle( fontSize: 11, - color: Colors.grey[700], + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 8), Text( '${displayFrequencyMHz.toStringAsFixed(3)} MHz', - style: TextStyle(fontSize: 11, color: Colors.grey[700]), + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), if (kFactorUsed != null) ...[ const SizedBox(width: 8), @@ -726,7 +734,7 @@ class _LineOfSightMapScreenState extends State { 'k=${kFactorUsed.toStringAsFixed(3)}', style: TextStyle( fontSize: 11, - color: Colors.grey[700], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 4), @@ -734,7 +742,7 @@ class _LineOfSightMapScreenState extends State { padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: const Icon(Icons.info_outline, size: 16), - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, tooltip: context.l10n.losFrequencyInfoTooltip, onPressed: () { _showFrequencyInfoDialog( @@ -750,7 +758,7 @@ class _LineOfSightMapScreenState extends State { ), Text( context.l10n.losElevationAttribution, - style: TextStyle(fontSize: 10, color: Colors.grey[700]), + style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 6), ExpansionTile( @@ -1730,12 +1738,12 @@ class _LosLegend extends StatelessWidget { Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.white70, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.w500, ) ?? - const TextStyle( - color: Colors.white70, + TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.w500, ); diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 4057e0ec..6bc62018 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -83,17 +83,17 @@ class _MapCacheScreenState extends State { children: [ IconButton( icon: const Icon(Icons.add), - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( icon: const Icon(Icons.remove), - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( icon: const Icon(Icons.my_location), - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: _resetMapView, ), ], @@ -458,7 +458,7 @@ class _MapCacheScreenState extends State { padding: const EdgeInsets.only(top: 8), child: Text( l10n.mapCache_failedDownloads(_failedTiles), - style: TextStyle(color: Colors.orange[700]), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 766b1852..dd6462b7 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -183,17 +183,17 @@ class _MapScreenState extends State { children: [ IconButton( icon: const Icon(Icons.add), - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( icon: const Icon(Icons.remove), - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( icon: const Icon(Icons.my_location), - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: () => _mapController.move(center, zoom), ), ], @@ -417,61 +417,76 @@ class _MapScreenState extends State { automaticallyImplyLeading: false, bottom: const SyncProgressAppBarBottom(), actions: [ - if (!_isBuildingPathTrace) - IconButton( - icon: const Icon(Icons.radar), - onPressed: () => _startPath( - LatLng(connector.selfLatitude!, connector.selfLongitude!), - ), - tooltip: context.l10n.contacts_pathTrace, - ), - if (!_isBuildingPathTrace) - IconButton( - icon: const LosIcon(), - onPressed: () { - final candidates = []; - if (connector.selfLatitude != null && - connector.selfLongitude != null) { - candidates.add( - LineOfSightEndpoint( - label: context.l10n.pathTrace_you, - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - color: Colors.teal, - icon: Icons.person_pin_circle, - ), - ); - } - for (final c in contactsWithLocation) { - candidates.add( - LineOfSightEndpoint( - label: c.name, - point: LatLng(c.latitude!, c.longitude!), - color: _getNodeColor(c.type), - icon: _getNodeIcon(c.type), - ), - ); - } - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LineOfSightMapScreen( - title: context.l10n.map_losScreenTitle, - candidates: candidates, - ), - ), - ); - }, - tooltip: context.l10n.map_lineOfSight, - ), PopupMenuButton( itemBuilder: (context) => [ + if (!_isBuildingPathTrace && + connector.selfLatitude != null && + connector.selfLongitude != null) + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.radar), + const SizedBox(width: 8), + Text(context.l10n.contacts_pathTrace), + ], + ), + onTap: () => _startPath( + LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + ), + ), + if (!_isBuildingPathTrace) + PopupMenuItem( + child: Row( + children: [ + const LosIcon(), + const SizedBox(width: 8), + Text(context.l10n.map_lineOfSight), + ], + ), + onTap: () { + final candidates = []; + if (connector.selfLatitude != null && + connector.selfLongitude != null) { + candidates.add( + LineOfSightEndpoint( + label: context.l10n.pathTrace_you, + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + color: Colors.teal, + icon: Icons.person_pin_circle, + ), + ); + } + for (final c in contactsWithLocation) { + candidates.add( + LineOfSightEndpoint( + label: c.name, + point: LatLng(c.latitude!, c.longitude!), + color: _getNodeColor(c.type), + icon: _getNodeIcon(c.type), + ), + ); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LineOfSightMapScreen( + title: context.l10n.map_losScreenTitle, + candidates: candidates, + ), + ), + ); + }, + ), PopupMenuItem( child: Row( children: [ - const Icon(Icons.logout, color: Colors.red), + Icon(Icons.logout, color: Theme.of(context).colorScheme.error), const SizedBox(width: 8), Text(context.l10n.common_disconnect), ], @@ -906,8 +921,8 @@ class _MapScreenState extends State { final color = _getNodeColor(guess.contact.type); final marker = Marker( point: guess.position, - width: 35, - height: 35, + width: 48, + height: 48, child: GestureDetector( onLongPress: () => _isBuildingPathTrace ? _showNodeInfo(context, guess.contact) @@ -919,26 +934,28 @@ class _MapScreenState extends State { guess.contact, guessedPosition: guess.position, ), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: color.withValues( - alpha: guess.highConfidence ? 0.55 : 0.30, - ), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + child: Center( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues( + alpha: guess.highConfidence ? 0.55 : 0.30, ), - ], - ), - child: const Icon( - Icons.not_listed_location, - color: Colors.white, - size: 20, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), ), ), ), @@ -1030,39 +1047,37 @@ class _MapScreenState extends State { for (final contact in filteredContacts) { final marker = Marker( point: LatLng(contact.latitude!, contact.longitude!), - width: 35, - height: 35, + width: 48, + height: 48, child: GestureDetector( onLongPress: () => _isBuildingPathTrace ? _showNodeInfo(context, contact) : null, onTap: () => _isBuildingPathTrace ? _addToPath(context, contact) : _showNodeInfo(context, contact), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: settings.mapShowOverlaps && !_isBuildingPathTrace - ? Colors.red - : _getNodeColor(contact.type), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Icon( - _getNodeIcon(contact.type), - color: Colors.white, - size: 20, - ), + child: Center( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: settings.mapShowOverlaps && !_isBuildingPathTrace + ? Colors.red + : _getNodeColor(contact.type), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - ], + child: Icon( + _getNodeIcon(contact.type), + color: Colors.white, + size: 20, + ), + ), ), ), ); @@ -1208,7 +1223,7 @@ class _MapScreenState extends State { Icon( Icons.location_on, size: 16, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), Text( ": $nodeCount", @@ -1221,10 +1236,10 @@ class _MapScreenState extends State { ), Row( children: [ - const Icon( + Icon( Icons.wrong_location, size: 16, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), Text( ": ${nodeCountAll - nodeCount}", @@ -1237,10 +1252,10 @@ class _MapScreenState extends State { ), Row( children: [ - const Icon( + Icon( Icons.add_outlined, size: 16, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), Text( ": $nodeCountAll", @@ -1536,56 +1551,14 @@ class _MapScreenState extends State { LatLng? guessedPosition, }) { final connector = context.read(); - showDialog( + showModalBottomSheet( context: context, - builder: (dialogContext) => AlertDialog( - title: Row( - children: [ - Icon( - _getNodeIcon(contact.type), - color: _getNodeColor(contact.type), - ), - const SizedBox(width: 8), - Expanded(child: SelectableText(contact.name)), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow( - context.l10n.map_type, - contact.typeLabel(context.l10n), - ), - _buildInfoRow( - context.l10n.map_path, - contact.pathLabel(context.l10n), - ), - if (contact.hasLocation) - _buildInfoRow( - context.l10n.map_location, - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ) - else if (guessedPosition != null) - _buildInfoRow( - context.l10n.map_estLocation, - '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', - ), - _buildInfoRow( - context.l10n.map_lastSeen, - _formatLastSeen(contact.lastSeen), - ), - _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(context.l10n.common_close), - ), - if (contact.type == - advTypeChat) // Only show chat button for chat nodes - TextButton( + showDragHandle: true, + builder: (sheetContext) { + final actions = []; + if (contact.type == advTypeChat) { + actions.add( + FilledButton( onPressed: () { if (!contact.isActive) { connector.importDiscoveredContact(contact); @@ -1593,7 +1566,7 @@ class _MapScreenState extends State { final unread = connector.getUnreadCountForContactKey( contact.publicKeyHex, ); - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); Navigator.push( context, MaterialPageRoute( @@ -1606,30 +1579,88 @@ class _MapScreenState extends State { }, child: Text(context.l10n.contacts_openChat), ), - if (contact.type == advTypeRepeater) - TextButton( + ); + } + if (contact.type == advTypeRepeater) { + actions.add( + FilledButton( onPressed: () { if (!contact.isActive) { connector.importDiscoveredContact(contact); } - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); _showRepeaterLogin(context, contact); }, child: Text(context.l10n.map_manageRepeater), ), - if (contact.type == advTypeRoom) - TextButton( + ); + } + if (contact.type == advTypeRoom) { + actions.add( + FilledButton( onPressed: () { if (!contact.isActive) { connector.importDiscoveredContact(contact); } - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); _showRoomLogin(context, contact); }, child: Text(context.l10n.map_joinRoom), ), - ], - ), + ); + } + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + _getNodeIcon(contact.type), + color: _getNodeColor(contact.type), + ), + const SizedBox(width: 8), + Expanded(child: SelectableText(contact.name)), + ], + ), + const SizedBox(height: 8), + _buildInfoRow( + context.l10n.map_type, + contact.typeLabel(context.l10n), + ), + _buildInfoRow( + context.l10n.map_path, + contact.pathLabel(context.l10n), + ), + if (contact.hasLocation) + _buildInfoRow( + context.l10n.map_location, + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + context.l10n.map_estLocation, + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), + _buildInfoRow( + context.l10n.map_lastSeen, + _formatLastSeen(contact.lastSeen), + ), + _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex), + const SizedBox(height: 16), + ...actions, + TextButton( + onPressed: () => Navigator.pop(sheetContext), + child: Text(context.l10n.common_close), + ), + ], + ), + ), + ); + }, ); } @@ -1714,6 +1745,9 @@ class _MapScreenState extends State { child: Text(context.l10n.common_hide), ), TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), onPressed: () async { setState(() { _hiddenMarkerIds.add(marker.id); @@ -1745,7 +1779,7 @@ class _MapScreenState extends State { label, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), @@ -1810,9 +1844,8 @@ class _MapScreenState extends State { ); await connector.refreshDeviceInfo(); if (!mounted) return; - showDismissibleSnackBar( - messenger.context, - content: Text(successMsg), + messenger.showSnackBar( + SnackBar(content: Text(successMsg)), ); }, ), @@ -2202,7 +2235,7 @@ class _MapScreenState extends State { const SizedBox(height: 8), Text( _getTimeFilterLabel(settings.mapTimeFilterHours), - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), Slider( value: _hoursToSliderValue(settings.mapTimeFilterHours), @@ -2354,7 +2387,7 @@ class _MapScreenState extends State { if (_pathTrace.isNotEmpty) Text( "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", - style: TextStyle(fontSize: 12, color: Colors.grey[700]), + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), ), SelectableText( _pathTrace diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 77559d48..e92a5cc0 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -9,7 +9,8 @@ import '../models/path_selection.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; -import '../widgets/path_management_dialog.dart'; +import '../widgets/empty_state.dart'; +import '../widgets/routing_sheet.dart'; import '../widgets/snr_indicator.dart'; import '../helpers/snack_bar_builder.dart'; @@ -167,7 +168,7 @@ class _NeighborsScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.neighbors_receivedData), - backgroundColor: Colors.green, + backgroundColor: Theme.of(context).colorScheme.tertiary, ); _statusTimeout?.cancel(); if (!mounted) return; @@ -227,7 +228,7 @@ class _NeighborsScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.neighbors_requestTimedOut), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); _recordStatusResult(false); }); @@ -241,7 +242,7 @@ class _NeighborsScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.neighbors_errorLoading(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -279,7 +280,9 @@ class _NeighborsScreenState extends State { children: [ Text( l10n.neighbors_repeatersNeighbors, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), Text( repeater.name, @@ -287,75 +290,18 @@ class _NeighborsScreenState extends State { fontSize: 14, fontWeight: FontWeight.normal, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), centerTitle: false, actions: [ - PopupMenuButton( + IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), tooltip: l10n.repeater_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); - } else { - await connector.setPathOverride(repeater, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: !isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: l10n.repeater_pathManagement, onPressed: () => - PathManagementDialog.show(context, contact: repeater), + ContactRoutingSheet.show(context, contact: repeater), ), IconButton( icon: _isLoading @@ -380,11 +326,9 @@ class _NeighborsScreenState extends State { if (!_isLoaded && !_hasData && (_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) - Center( - child: Text( - l10n.neighbors_noData, - style: TextStyle(fontSize: 16, color: Colors.grey), - ), + EmptyState( + icon: Icons.wifi_find, + title: l10n.neighbors_noData, ), if (_isLoaded || _hasData && @@ -435,7 +379,7 @@ class _NeighborsScreenState extends State { fmtDuration(entry.value['lastHeard'] + 0.0), ), entry.value['snr'], - connector.currentSf!, + connector.currentSf, ), ], ), @@ -447,7 +391,7 @@ class _NeighborsScreenState extends State { String label, String value, double snr, - int spreadingFactor, + int? spreadingFactor, ) { final snrUi = snrUiFromSNR(snr, spreadingFactor); return Padding( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index c810570d..81082b9e 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -103,6 +103,9 @@ class _PathTraceMapScreenState extends State { double _pathDistanceMeters = 0.0; bool _showNodeLabels = true; Contact? _targetContact; + // Live path resolved at trace time; used by the response handler for + // endpoint inference so it matches the path that was actually traced. + Uint8List _tracedPath = Uint8List(0); String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -168,17 +171,17 @@ class _PathTraceMapScreenState extends State { children: [ IconButton( icon: const Icon(Icons.add), - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( icon: const Icon(Icons.remove), - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( icon: const Icon(Icons.my_location), - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: _resetMapView, ), ], @@ -228,6 +231,23 @@ class _PathTraceMapScreenState extends State { return traceBytes; } + /// Resolves the path bytes to trace. When tracing a specific contact's + /// route (flipPathAround), re-read that contact's live forced/auto path from + /// the connector so a path the user just changed (force flood / set path / + /// reset to auto) is honored immediately, instead of the value captured when + /// this screen was first pushed. + Uint8List _resolveLivePath(MeshCoreConnector connector) { + final target = widget.targetContact; + if (!widget.flipPathAround || target == null) { + return widget.path; + } + final live = connector.allContactsUnfiltered.firstWhere( + (c) => c.publicKeyHex == target.publicKeyHex, + orElse: () => target, + ); + return live.pathBytesForDisplay; + } + Future _doPathTrace() async { if (mounted) { setState(() { @@ -236,9 +256,13 @@ class _PathTraceMapScreenState extends State { }); } + final connector = Provider.of(context, listen: false); + final livePath = _resolveLivePath(connector); + _tracedPath = livePath; + final pathTmp = widget.reversePathAround - ? Uint8List.fromList(widget.path.reversed.toList()) - : widget.path; + ? Uint8List.fromList(livePath.reversed.toList()) + : livePath; final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; @@ -248,7 +272,6 @@ class _PathTraceMapScreenState extends State { noNotify: !mounted, ); - final connector = Provider.of(context, listen: false); final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, 0, //flags @@ -414,13 +437,13 @@ class _PathTraceMapScreenState extends State { final tc = _targetContact!; if (tc.hasLocation) { targetPos = LatLng(tc.latitude!, tc.longitude!); - } else if (widget.path.length > 1) { + } else if (_tracedPath.length > 1) { // Infer from the last hop: average GPS contacts sharing that hop. // For a round-trip path (flipPathAround/reversePathAround), the target-side hop // sits in the middle of the symmetric sequence; .last is the local side. final lastHop = widget.reversePathAround - ? widget.path.first - : widget.path.last; + ? _tracedPath.first + : _tracedPath.last; final peers = connector.allContacts .where( @@ -593,7 +616,7 @@ class _PathTraceMapScreenState extends State { !_failed2Loaded) Center( child: Card( - color: Colors.white.withValues(alpha: 0.9), + color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9), child: Padding( padding: EdgeInsets.all(12), child: Text( @@ -640,31 +663,35 @@ class _PathTraceMapScreenState extends State { markers.add( Marker( point: point, - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: hasGps - ? Colors.green - : Colors.orange.withValues(alpha: 0.75), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: hasGps + ? Colors.green + : Colors.orange.withValues(alpha: 0.75), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + hasGps ? label : '~$label', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - hasGps ? label : '~$label', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -689,29 +716,33 @@ class _PathTraceMapScreenState extends State { markers.add( Marker( point: selfPoint, - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -735,26 +766,30 @@ class _PathTraceMapScreenState extends State { markers.add( Marker( point: targetPos, - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: isGuessed - ? Colors.purple.withValues(alpha: 0.55) - : Colors.red, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isGuessed + ? Colors.purple.withValues(alpha: 0.55) + : Colors.red, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: const Icon(Icons.person, color: Colors.white, size: 18), ), - alignment: Alignment.center, - child: const Icon(Icons.person, color: Colors.white, size: 18), ), ), ); @@ -927,6 +962,12 @@ class _PathTraceMapScreenState extends State { ); } + Widget _colorDot(Color color) => Container( + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ); + Widget _buildLegendCard( BuildContext context, PathTraceData pathTraceData, @@ -949,9 +990,26 @@ class _PathTraceMapScreenState extends State { children: [ Padding( padding: const EdgeInsets.all(12), - child: Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', - style: const TextStyle(fontWeight: FontWeight.w600), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 6), + Row( + children: [ + _colorDot(Colors.green), + const SizedBox(width: 4), + Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)), + const SizedBox(width: 12), + _colorDot(Colors.orange), + const SizedBox(width: 4), + Text(l10n.pathTrace_legendInferred, style: const TextStyle(fontSize: 11)), + ], + ), + ], ), ), const Divider(height: 1), diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 10e325ac..93720f28 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../widgets/debug_frame_viewer.dart'; import '../services/repeater_command_service.dart'; -import '../widgets/path_management_dialog.dart'; +import '../widgets/routing_sheet.dart'; import '../helpers/snack_bar_builder.dart'; class RepeaterCliScreen extends StatefulWidget { @@ -252,97 +252,29 @@ class _RepeaterCliScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(l10n.repeater_cliTitle), + Text( + l10n.repeater_cliTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), Text( repeater.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.normal, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), centerTitle: false, actions: [ - PopupMenuButton( + IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), tooltip: l10n.repeater_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); - } else { - await connector.setPathOverride(repeater, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: !isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: l10n.repeater_pathManagement, onPressed: () => - PathManagementDialog.show(context, contact: repeater), - ), - IconButton( - icon: const Icon(Icons.bug_report), - tooltip: l10n.repeater_debugNextCommand, - onPressed: () { - // Set a flag or just send next command with debug - if (_commandController.text.trim().isNotEmpty) { - _sendCommand(showDebug: true); - } else { - showDismissibleSnackBar( - context, - content: Text(l10n.repeater_enterCommandFirst), - ); - } - }, + ContactRoutingSheet.show(context, contact: repeater), ), IconButton( icon: const Icon(Icons.help_outline), @@ -354,6 +286,33 @@ class _RepeaterCliScreenState extends State { tooltip: l10n.repeater_clearHistory, onPressed: _commandHistory.isEmpty ? null : _clearHistory, ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (value) { + if (value == 'debug') { + if (_commandController.text.trim().isNotEmpty) { + _sendCommand(showDebug: true); + } else { + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_enterCommandFirst), + ); + } + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'debug', + child: Row( + children: [ + const Icon(Icons.bug_report), + const SizedBox(width: 8), + Text(l10n.repeater_debugNextCommand), + ], + ), + ), + ], + ), ], ), body: Column( @@ -426,16 +385,16 @@ class _RepeaterCliScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.terminal, size: 64, color: Colors.grey[400]), + Icon(Icons.terminal, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( l10n.repeater_noCommandsSent, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), Text( l10n.repeater_typeCommandOrUseQuick, - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 5b1e30ca..337d148c 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -72,11 +72,11 @@ class RepeaterHubScreen extends StatelessWidget { children: [ CircleAvatar( radius: 40, - backgroundColor: Colors.orange, - child: const Icon( + backgroundColor: Theme.of(context).colorScheme.tertiaryContainer, + child: Icon( Icons.cell_tower, size: 40, - color: Colors.white, + color: Theme.of(context).colorScheme.onTertiaryContainer, ), ), const SizedBox(height: 16), @@ -90,12 +90,12 @@ class RepeaterHubScreen extends StatelessWidget { const SizedBox(height: 8), Text( repeater.shortPubKeyHex, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), Text( repeater.pathLabel(context.l10n), - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), if (repeater.hasLocation) ...[ const SizedBox(height: 4), @@ -105,14 +105,14 @@ class RepeaterHubScreen extends StatelessWidget { Icon( Icons.location_on, size: 14, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( '${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -193,7 +193,7 @@ class RepeaterHubScreen extends StatelessWidget { icon: Icons.analytics, title: l10n.repeater_status, subtitle: l10n.repeater_statusSubtitle, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, onTap: () { Navigator.push( context, @@ -213,7 +213,7 @@ class RepeaterHubScreen extends StatelessWidget { icon: Icons.bar_chart_sharp, title: l10n.repeater_telemetry, subtitle: l10n.repeater_telemetrySubtitle, - color: Colors.teal, + color: Theme.of(context).colorScheme.secondary, onTap: () { Navigator.push( context, @@ -231,7 +231,7 @@ class RepeaterHubScreen extends StatelessWidget { icon: Icons.terminal, title: l10n.repeater_cli, subtitle: l10n.repeater_cliSubtitle, - color: Colors.green, + color: Theme.of(context).colorScheme.tertiary, onTap: () { Navigator.push( context, @@ -251,7 +251,7 @@ class RepeaterHubScreen extends StatelessWidget { icon: Icons.group, title: l10n.repeater_neighbors, subtitle: l10n.repeater_neighborsSubtitle, - color: Colors.orange, + color: Theme.of(context).colorScheme.tertiary, onTap: () { Navigator.push( context, @@ -270,7 +270,7 @@ class RepeaterHubScreen extends StatelessWidget { icon: Icons.settings, title: l10n.repeater_settings, subtitle: l10n.repeater_settingsSubtitle, - color: Colors.deepOrange, + color: Theme.of(context).colorScheme.error, onTap: () { Navigator.push( context, @@ -329,12 +329,12 @@ class RepeaterHubScreen extends StatelessWidget { const SizedBox(height: 4), Text( subtitle, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), ), - Icon(Icons.chevron_right, color: Colors.grey[400]), + Icon(Icons.chevron_right, color: Theme.of(context).colorScheme.onSurfaceVariant), ], ), ), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 9ffd1b37..1d8ae3bc 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; import '../services/storage_service.dart'; -import '../widgets/path_management_dialog.dart'; +import '../widgets/routing_sheet.dart'; import '../helpers/snack_bar_builder.dart'; class RepeaterSettingsScreen extends StatefulWidget { @@ -126,6 +126,8 @@ class _RepeaterSettingsScreenState extends State { // Location settings final TextEditingController _latController = TextEditingController(); final TextEditingController _lonController = TextEditingController(); + bool _latInvalid = false; + bool _lonInvalid = false; // Feature toggles bool _repeatEnabled = true; @@ -457,7 +459,7 @@ class _RepeaterSettingsScreenState extends State { ? l10n.repeater_refreshed(label) : l10n.repeater_errorRefreshing(label), ), - backgroundColor: successCount > 0 ? Colors.green : Colors.red, + backgroundColor: successCount > 0 ? null : Theme.of(context).colorScheme.error, ); setState(() => setRefreshing(false)); } @@ -667,15 +669,15 @@ class _RepeaterSettingsScreenState extends State { : l10n.repeater_actionSucceeded(label), ), backgroundColor: outcome == _SaveOutcome.error - ? Colors.red - : Colors.green, + ? Theme.of(context).colorScheme.error + : null, ); } catch (e) { if (!mounted) return; showDismissibleSnackBar( context, content: Text(l10n.repeater_actionFailed(label, e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } finally { if (mounted) setState(() => _runningAction = false); @@ -768,14 +770,16 @@ class _RepeaterSettingsScreenState extends State { } if (_dirtyFields.contains(_SettingField.lat) && - _latController.text.isNotEmpty) { + _latController.text.isNotEmpty && + _isValidCoordinate(_latController.text, 90)) { pending.add(( field: _SettingField.lat, command: 'set lat ${_latController.text}', )); } if (_dirtyFields.contains(_SettingField.lon) && - _lonController.text.isNotEmpty) { + _lonController.text.isNotEmpty && + _isValidCoordinate(_lonController.text, 180)) { pending.add(( field: _SettingField.lon, command: 'set lon ${_lonController.text}', @@ -944,13 +948,12 @@ class _RepeaterSettingsScreenState extends State { showDismissibleSnackBar( context, content: Text(l10n.repeater_settingsSavedRebootNeeded), - backgroundColor: Colors.orange, + backgroundColor: Theme.of(context).colorScheme.tertiary, ); } else if (failures.isEmpty) { showDismissibleSnackBar( context, content: Text(l10n.repeater_settingsSaved), - backgroundColor: Colors.green, ); } else { showDismissibleSnackBar( @@ -958,7 +961,7 @@ class _RepeaterSettingsScreenState extends State { content: Text( l10n.repeater_settingsPartialFailure(failures.join('; ')), ), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -973,7 +976,7 @@ class _RepeaterSettingsScreenState extends State { content: Text( context.l10n.repeater_errorSavingSettings(e.toString()), ), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -984,6 +987,12 @@ class _RepeaterSettingsScreenState extends State { _flagHasChanges(); } + static bool _isValidCoordinate(String text, double max) { + if (text.trim().isEmpty) return true; + final value = double.tryParse(text.trim()); + return value != null && value >= -max && value <= max; + } + void _flagHasChanges() { if (!_hasChanges) { setState(() { @@ -1072,73 +1081,11 @@ class _RepeaterSettingsScreenState extends State { ), centerTitle: false, actions: [ - PopupMenuButton( + IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), tooltip: l10n.repeater_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); - } else { - await connector.setPathOverride(repeater, pathLen: null); - } - if (mounted) { - setState(() {}); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: !isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: l10n.repeater_pathManagement, onPressed: () => - PathManagementDialog.show(context, contact: repeater), + ContactRoutingSheet.show(context, contact: repeater), ), if (_hasChanges) TextButton.icon( @@ -1173,6 +1120,8 @@ class _RepeaterSettingsScreenState extends State { const SizedBox(height: 16), _buildAdvancedCard(), const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), _buildDangerZoneCard(), ], ), @@ -1388,13 +1337,22 @@ class _RepeaterSettingsScreenState extends State { decoration: InputDecoration( labelText: l10n.repeater_latitude, helperText: l10n.repeater_latitudeHelper, + errorText: _latInvalid + ? l10n.settings_locationInvalid + : null, border: const OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions( decimal: true, signed: true, ), - onChanged: (_) => _markChanged(_SettingField.lat), + onChanged: (value) { + _markChanged(_SettingField.lat); + final invalid = !_isValidCoordinate(value, 90); + if (invalid != _latInvalid) { + setState(() => _latInvalid = invalid); + } + }, ), ), const SizedBox(width: 8), @@ -1415,13 +1373,22 @@ class _RepeaterSettingsScreenState extends State { decoration: InputDecoration( labelText: l10n.repeater_longitude, helperText: l10n.repeater_longitudeHelper, + errorText: _lonInvalid + ? l10n.settings_locationInvalid + : null, border: const OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions( decimal: true, signed: true, ), - onChanged: (_) => _markChanged(_SettingField.lon), + onChanged: (value) { + _markChanged(_SettingField.lon); + final invalid = !_isValidCoordinate(value, 180); + if (invalid != _lonInvalid) { + setState(() => _lonInvalid = invalid); + } + }, ), ), const SizedBox(width: 8), @@ -2233,7 +2200,7 @@ class _RepeaterSettingsScreenState extends State { showDismissibleSnackBar( context, content: Text(l10n.repeater_errorSendingCommand(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -2262,7 +2229,7 @@ class _RepeaterSettingsScreenState extends State { onConfirm(); }, style: isDestructive - ? FilledButton.styleFrom(backgroundColor: Colors.red) + ? FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error) : null, child: Text(l10n.repeater_confirm), ), diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 9dbd8e31..f121605d 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -11,7 +11,7 @@ import '../connector/meshcore_protocol.dart'; import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../utils/battery_utils.dart'; -import '../widgets/path_management_dialog.dart'; +import '../widgets/routing_sheet.dart'; import '../helpers/snack_bar_builder.dart'; class RepeaterStatusScreen extends StatefulWidget { @@ -318,7 +318,7 @@ class _RepeaterStatusScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.repeater_statusRequestTimeout), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); _recordStatusResult(false); }); @@ -331,7 +331,7 @@ class _RepeaterStatusScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } _recordStatusResult(false); @@ -360,82 +360,29 @@ class _RepeaterStatusScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(l10n.repeater_statusTitle), + Text( + l10n.repeater_statusTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), Text( repeater.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.normal, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), centerTitle: false, actions: [ - PopupMenuButton( + IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), tooltip: l10n.repeater_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); - } else { - await connector.setPathOverride(repeater, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: !isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: l10n.repeater_pathManagement, onPressed: () => - PathManagementDialog.show(context, contact: repeater), + ContactRoutingSheet.show(context, contact: repeater), ), IconButton( icon: _isLoading @@ -588,21 +535,20 @@ class _RepeaterStatusScreenState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 130, + Expanded( child: Text( label, style: TextStyle( - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ), - Expanded( - child: Text( - value, - style: const TextStyle(fontWeight: FontWeight.w400), - ), + const SizedBox(width: 8), + Text( + value, + style: const TextStyle(fontWeight: FontWeight.w400), + textAlign: TextAlign.end, ), ], ), diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index ec148c5f..e665cbc7 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart'; import '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; +import '../widgets/empty_state.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'tcp_screen.dart'; @@ -25,6 +26,7 @@ class ScannerScreen extends StatefulWidget { class _ScannerScreenState extends State { bool _changedNavigation = false; + String? _connectingDeviceId; late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; @@ -101,6 +103,32 @@ class _ScannerScreenState extends State { title: AdaptiveAppBarTitle(context.l10n.scanner_title), centerTitle: true, automaticallyImplyLeading: false, + actions: [ + if (PlatformInfo.supportsUsbSerial) + IconButton( + icon: const Icon(Icons.usb), + tooltip: context.l10n.connectionChoiceUsbLabel, + onPressed: () { + appLogger.info( + 'USB selected, opening UsbScreen', + tag: 'ScannerScreen', + ); + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + ), + if (!PlatformInfo.isWeb) + IconButton( + icon: const Icon(Icons.lan), + tooltip: context.l10n.connectionChoiceTcpLabel, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const TcpScreen()), + ); + }, + ), + ], ), body: SafeArea( top: false, @@ -122,84 +150,24 @@ class _ScannerScreenState extends State { }, ), ), - bottomNavigationBar: Consumer( + floatingActionButton: Consumer( builder: (context, connector, child) { final isScanning = connector.state == MeshCoreConnectionState.scanning; final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; - final usbSupported = PlatformInfo.supportsUsbSerial; - final tcpSupported = !PlatformInfo.isWeb; - return SafeArea( - top: false, - minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (usbSupported) - FloatingActionButton.extended( - onPressed: () { - appLogger.info( - 'USB selected, opening UsbScreen', - tag: 'ScannerScreen', - ); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const UsbScreen()), - ); - }, - heroTag: 'scanner_usb_action', - icon: const Icon(Icons.usb), - label: Text(context.l10n.connectionChoiceUsbLabel), - ), - if (usbSupported) const SizedBox(width: 12), - if (tcpSupported) - FloatingActionButton.extended( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const TcpScreen()), - ); - }, - heroTag: 'scanner_tcp_action', - icon: const Icon(Icons.lan), - label: Text(context.l10n.connectionChoiceTcpLabel), - ), - if (tcpSupported) const SizedBox(width: 12), - FloatingActionButton.extended( - heroTag: 'scanner_ble_action', - onPressed: isBluetoothOff - ? null - : () { - if (isScanning) { - connector.stopScan(); - } else { - unawaited( - connector.startScan().catchError((e) { - appLogger.warn( - 'startScan error: $e', - tag: 'ScannerScreen', - ); - }), - ); - } - }, - icon: isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.bluetooth_searching), - label: Text( - isScanning - ? context.l10n.scanner_stop - : context.l10n.scanner_scan, - ), - ), - ], - ), + return FloatingActionButton.extended( + heroTag: 'scanner_ble_action', + onPressed: isBluetoothOff ? null : () => _toggleScan(connector), + icon: isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.bluetooth_searching), + label: Text( + isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan, ), ); }, @@ -207,6 +175,18 @@ class _ScannerScreenState extends State { ); } + void _toggleScan(MeshCoreConnector connector) { + if (connector.state == MeshCoreConnectionState.scanning) { + connector.stopScan(); + } else { + unawaited( + connector.startScan().catchError((e) { + appLogger.warn('startScan error: $e', tag: 'ScannerScreen'); + }), + ); + } + } + Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { String statusText; Color statusColor; @@ -254,32 +234,43 @@ class _ScannerScreenState extends State { Widget _buildDeviceList(BuildContext context, MeshCoreConnector connector) { if (connector.scanResults.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - connector.state == MeshCoreConnectionState.scanning - ? context.l10n.scanner_searchingDevices - : context.l10n.scanner_tapToScan, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - ], - ), + final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; + final isScanning = connector.state == MeshCoreConnectionState.scanning; + return EmptyState( + icon: isBluetoothOff ? Icons.bluetooth_disabled : Icons.bluetooth, + title: isBluetoothOff + ? context.l10n.scanner_bluetoothOff + : isScanning + ? context.l10n.scanner_searchingDevices + : context.l10n.scanner_tapToScan, + subtitle: isBluetoothOff + ? context.l10n.scanner_bluetoothOffMessage + : null, + action: (isBluetoothOff || isScanning) + ? null + : FilledButton.icon( + onPressed: () => _toggleScan(connector), + icon: const Icon(Icons.bluetooth_searching), + label: Text(context.l10n.scanner_scan), + ), ); } + final isConnecting = + connector.state == MeshCoreConnectionState.connecting; return ListView.separated( padding: const EdgeInsets.all(8), itemCount: connector.scanResults.length, separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) { final result = connector.scanResults[index]; + final deviceId = result.device.remoteId.toString(); return DeviceTile( scanResult: result, - onTap: () => _connectToDevice(context, connector, result), + isConnecting: isConnecting && _connectingDeviceId == deviceId, + onTap: isConnecting + ? null + : () => _connectToDevice(context, connector, result), ); }, ); @@ -293,6 +284,9 @@ class _ScannerScreenState extends State { final name = result.device.platformName.isNotEmpty ? result.device.platformName : result.advertisementData.advName; + setState(() { + _connectingDeviceId = result.device.remoteId.toString(); + }); try { await connector.connect( result.device, @@ -321,9 +315,15 @@ class _ScannerScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.scanner_connectionFailed(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } + } finally { + if (mounted) { + setState(() { + _connectingDeviceId = null; + }); + } } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index ead36eaf..0fe077cb 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -53,6 +53,7 @@ class _SettingsScreenState extends State { Future _loadVersionInfo() async { final packageInfo = await PackageInfo.fromPlatform(); + if (!mounted) return; setState(() { _appVersion = packageInfo.version; }); @@ -213,12 +214,12 @@ class _SettingsScreenState extends State { if (percent == null) { icon = Icons.battery_unknown; - iconColor = Colors.grey; + iconColor = Theme.of(context).colorScheme.onSurfaceVariant; valueColor = null; } else if (percent <= 15) { icon = Icons.battery_alert; - iconColor = Colors.orange; - valueColor = Colors.orange; + iconColor = Theme.of(context).colorScheme.tertiary; + valueColor = Theme.of(context).colorScheme.tertiary; } else { icon = Icons.battery_full; iconColor = null; @@ -307,18 +308,6 @@ class _SettingsScreenState extends State { trailing: const Icon(Icons.chevron_right), onTap: () => _editLocation(context, connector), ), - if (connector.currentCustomVars?.containsKey('gps') ?? false) ...[ - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.gps_fixed), - title: Text(l10n.settings_locationGPSEnable), - subtitle: Text(l10n.settings_locationGPSEnableSubtitle), - value: connector.currentCustomVars?['gps'] == '1', - onChanged: (value) async { - await connector.setCustomVar(value ? 'gps:1' : 'gps:0'); - }, - ), - ], const Divider(height: 1), ListTile( leading: const Icon(Icons.group_add_outlined), @@ -354,13 +343,13 @@ class _SettingsScreenState extends State { ), ), ListTile( - leading: const Icon(Icons.delete_outline, color: Colors.red), + leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), title: Text(l10n.settings_deleteAllPaths), subtitle: Text( l10n.settings_deleteAllPathsSubtitle, - style: TextStyle(color: Colors.red[700]), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), - onTap: () => connector.deleteAllPaths(), + onTap: () => _confirmDeleteAllPaths(context, connector), ), const Divider(height: 1), ListTile( @@ -378,7 +367,7 @@ class _SettingsScreenState extends State { ), const Divider(height: 1), ListTile( - leading: const Icon(Icons.restart_alt, color: Colors.orange), + leading: Icon(Icons.restart_alt, color: Theme.of(context).colorScheme.tertiary), title: Text(l10n.settings_rebootDevice), subtitle: Text(l10n.settings_rebootDeviceSubtitle), onTap: () => _confirmReboot(context, connector), @@ -565,6 +554,8 @@ class _SettingsScreenState extends State { int.tryParse(customVars["gps_interval"] ?? "") ?? 900; intervalController.text = currentInterval.toString(); + String? intervalError; + showDialog( context: context, builder: (dialogContext) => StatefulBuilder( @@ -600,9 +591,15 @@ class _SettingsScreenState extends State { const SizedBox(height: 16), TextField( controller: intervalController, + onChanged: (_) { + if (intervalError != null) { + setDialogState(() => intervalError = null); + } + }, decoration: InputDecoration( labelText: l10n.settings_locationIntervalSec, border: const OutlineInputBorder(), + errorText: intervalError, ), keyboardType: const TextInputType.numberWithOptions( decimal: false, @@ -633,24 +630,25 @@ class _SettingsScreenState extends State { ), TextButton( onPressed: () async { - Navigator.pop(context); - + int? interval; if (hasGPS) { final intervalText = intervalController.text.trim(); - if (intervalText.isEmpty) { - return; + if (intervalText.isNotEmpty) { + interval = int.tryParse(intervalText); + if (interval == null || + interval < 60 || + interval >= 86400) { + setDialogState(() { + intervalError = l10n.settings_locationIntervalInvalid; + }); + return; + } } + } - final interval = int.tryParse(intervalText); - if (interval == null || interval < 60 || interval >= 86400) { - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(l10n.settings_locationIntervalInvalid), - ); - return; - } + Navigator.pop(context); + if (interval != null) { await connector.setCustomVar("gps_interval:$interval"); await connector.refreshDeviceInfo(); if (!context.mounted) return; @@ -716,6 +714,36 @@ class _SettingsScreenState extends State { ); } + void _confirmDeleteAllPaths( + BuildContext context, + MeshCoreConnector connector, + ) { + final l10n = context.l10n; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.settings_deleteAllPaths), + content: Text(l10n.settings_deleteAllPathsSubtitle), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + connector.deleteAllPaths(); + }, + child: Text( + l10n.common_deleteAll, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ); + } + void _confirmReboot(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; showDialog( @@ -735,7 +763,7 @@ class _SettingsScreenState extends State { }, child: Text( l10n.common_reboot, - style: const TextStyle(color: Colors.orange), + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), ), ), ], @@ -1126,6 +1154,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { bool _clientRepeat = false; int? _selectedPresetIndex; _RadioSettingsSnapshot? _lastNonRepeatSnapshot; + String? _frequencyError; + String? _txPowerError; AppDebugLogService get _appLog => Provider.of(context, listen: false); @@ -1392,7 +1422,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { void _handleManualSettingsChanged(String source) { _logRadioSettingsState('Manual settings edit: $source'); - setState(_syncPresetSelection); + setState(() { + _validateFields(); + _syncPresetSelection(); + }); + } + + void _validateFields() { + final l10n = context.l10n; + final freqMHz = double.tryParse(_frequencyController.text); + _frequencyError = (freqMHz == null || freqMHz < 300 || freqMHz > 2500) + ? l10n.settings_frequencyInvalid + : null; + + final maxTxPower = widget.connector.maxTxPower ?? 22; + final txPower = int.tryParse(_txPowerController.text); + _txPowerError = (txPower == null || txPower < 0 || txPower > maxTxPower) + ? '${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)' + : null; } void _handleClientRepeatChanged(bool enabled) { @@ -1504,6 +1551,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { content: Text(l10n.settings_error(e.toString())), ); } + if (!mounted) return; Navigator.pop(context); } @@ -1579,6 +1627,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { labelText: l10n.settings_frequency, border: const OutlineInputBorder(), helperText: l10n.settings_frequencyHelper, + errorText: _frequencyError, ), keyboardType: const TextInputType.numberWithOptions( decimal: true, @@ -1662,6 +1711,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { helperText: widget.connector.maxTxPower != null ? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)' : l10n.settings_txPowerHelper, + errorText: _txPowerError, ), keyboardType: TextInputType.number, ), @@ -1683,7 +1733,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { onPressed: () => Navigator.pop(context), child: Text(l10n.common_cancel), ), - FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)), + FilledButton( + onPressed: (_frequencyError != null || _txPowerError != null) + ? null + : _saveSettings, + child: Text(l10n.common_save), + ), ], ); } diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index a0d71922..755a4298 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -95,12 +95,14 @@ class _TcpScreenState extends State { final isConnecting = connector.state == MeshCoreConnectionState.connecting && connector.activeTransport == MeshCoreTransportType.tcp; - final isButtonDisabled = - isConnecting || - connector.state == MeshCoreConnectionState.scanning; + // A running BLE scan must not block TCP connect: connectTcp() stops + // any active scan before connecting, so the only reason to disable + // the button is a TCP connect already in flight. + final isButtonDisabled = isConnecting; return Column( children: [ _buildStatusBar(context, connector), + _buildTransportLinks(context), Padding( padding: const EdgeInsets.all(16), child: Column( @@ -154,40 +156,32 @@ class _TcpScreenState extends State { }, ), ), - bottomNavigationBar: SafeArea( - top: false, - minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (PlatformInfo.supportsUsbSerial) - FloatingActionButton.extended( - onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const UsbScreen()), - ); - }, - heroTag: 'tcp_usb_action', - extendedPadding: const EdgeInsets.symmetric(horizontal: 12), - icon: const Icon(Icons.usb), - label: Text(context.l10n.connectionChoiceUsbLabel), - ), - if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12), - FloatingActionButton.extended( - onPressed: () { - Navigator.of(context).maybePop(); - }, - heroTag: 'tcp_ble_action', - extendedPadding: const EdgeInsets.symmetric(horizontal: 12), - icon: const Icon(Icons.bluetooth), - label: Text(context.l10n.connectionChoiceBluetoothLabel), - ), - ], + ); + } + + Widget _buildTransportLinks(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Wrap( + spacing: 12, + runSpacing: 8, + children: [ + if (PlatformInfo.supportsUsbSerial) + OutlinedButton.icon( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + icon: const Icon(Icons.usb), + label: Text(context.l10n.connectionChoiceUsbLabel), + ), + OutlinedButton.icon( + onPressed: () => Navigator.of(context).maybePop(), + icon: const Icon(Icons.bluetooth), + label: Text(context.l10n.connectionChoiceBluetoothLabel), ), - ), + ], ), ); } @@ -214,7 +208,7 @@ class _TcpScreenState extends State { statusColor = Colors.orange; } else { statusText = l10n.tcpStatus_notConnected; - statusColor = Colors.grey; + statusColor = Theme.of(context).colorScheme.onSurfaceVariant; } return Container( @@ -226,15 +220,13 @@ class _TcpScreenState extends State { Icon(Icons.circle, size: 12, color: statusColor), const SizedBox(width: 8), Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - statusText, - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.w500, - ), + child: Text( + statusText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.w500, ), ), ), @@ -274,7 +266,7 @@ class _TcpScreenState extends State { showDismissibleSnackBar( context, content: Text(message), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index f277a8e1..6634a6af 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -13,7 +13,7 @@ import '../connector/meshcore_protocol.dart'; import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../utils/app_logger.dart'; -import '../widgets/path_management_dialog.dart'; +import '../widgets/routing_sheet.dart'; import '../helpers/cayenne_lpp.dart'; import '../utils/battery_utils.dart'; import '../helpers/snack_bar_builder.dart'; @@ -118,7 +118,7 @@ class _TelemetryScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.telemetry_requestTimeout), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } if (isAutoRefreshRequest && _isAutoRefreshEnabled) { @@ -178,7 +178,6 @@ class _TelemetryScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.telemetry_receivedData), - backgroundColor: Colors.green, ); } _statusTimeout?.cancel(); @@ -235,7 +234,7 @@ class _TelemetryScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.telemetry_errorLoading(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -323,7 +322,11 @@ class _TelemetryScreenState extends State { final connector = context.watch(); final settings = context.watch().settings; final isImperialUnits = settings.unitSystem == UnitSystem.imperial; - final isFloodMode = widget.contact.pathOverride == -1; + final contact = connector.contacts.firstWhere( + (c) => c.publicKeyHex == widget.contact.publicKeyHex, + orElse: () => widget.contact, + ); + final isFloodMode = contact.pathOverride == -1; return Scaffold( appBar: AppBar( @@ -347,70 +350,11 @@ class _TelemetryScreenState extends State { centerTitle: false, bottom: const SyncProgressAppBarBottom(), actions: [ - PopupMenuButton( + IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), tooltip: l10n.repeater_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(widget.contact, pathLen: -1); - } else { - await connector.setPathOverride(widget.contact, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: !isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: isFloodMode - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: l10n.repeater_pathManagement, onPressed: () => - PathManagementDialog.show(context, contact: widget.contact), + ContactRoutingSheet.show(context, contact: widget.contact), ), IconButton( icon: _isLoading @@ -441,7 +385,7 @@ class _TelemetryScreenState extends State { Center( child: Text( l10n.telemetry_noData, - style: const TextStyle(fontSize: 16, color: Colors.grey), + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ), if ((_isLoaded || _hasData) && @@ -718,14 +662,14 @@ class _TelemetryScreenState extends State { alignment: Alignment.center, children: [ Center(child: Text(l10n.common_disable)), - const Positioned( + Positioned( right: 0, child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onPrimary, ), ), ), @@ -971,21 +915,20 @@ class _TelemetryScreenState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 130, + Expanded( child: Text( label, style: TextStyle( - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ), - Expanded( - child: Text( - value, - style: const TextStyle(fontWeight: FontWeight.w400), - ), + const SizedBox(width: 8), + Text( + value, + style: const TextStyle(fontWeight: FontWeight.w400), + textAlign: TextAlign.end, ), ], ), diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 25992de8..331de5e3 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -12,7 +12,6 @@ import '../utils/usb_port_labels.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; -import 'scanner_screen.dart'; import 'tcp_screen.dart'; class UsbScreen extends StatefulWidget { @@ -100,81 +99,62 @@ class _UsbScreenState extends State { return Column( children: [ _buildStatusBar(context, connector), + _buildTransportLinks(context), Expanded(child: _buildPortList(context, connector)), ], ); }, ), ), - bottomNavigationBar: Consumer( - builder: (context, connector, child) { - final isLoading = _isLoadingPorts; - final showBle = true; - final showTcp = !PlatformInfo.isWeb; - - return SafeArea( - top: false, - minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, + bottomNavigationBar: _supportsHotPlug + ? null + : SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - if (showTcp) - FloatingActionButton.extended( - onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const TcpScreen()), - ); - }, - heroTag: 'usb_tcp_action', - extendedPadding: const EdgeInsets.symmetric( - horizontal: 12, - ), - icon: const Icon(Icons.lan), - label: Text(context.l10n.connectionChoiceTcpLabel), - ), - if (showTcp && showBle) const SizedBox(width: 12), - if (showBle) - FloatingActionButton.extended( - onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => const ScannerScreen(), - ), - ); - }, - heroTag: 'usb_ble_action', - extendedPadding: const EdgeInsets.symmetric( - horizontal: 12, - ), - icon: const Icon(Icons.bluetooth), - label: Text(context.l10n.connectionChoiceBluetoothLabel), - ), - if ((showTcp || showBle) && !_supportsHotPlug) - const SizedBox(width: 12), - if (!_supportsHotPlug) - FloatingActionButton.extended( - onPressed: isLoading ? null : _loadPorts, - heroTag: 'usb_refresh_action', - extendedPadding: const EdgeInsets.symmetric( - horizontal: 12, - ), - icon: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.usb), - label: Text(context.l10n.scanner_scan), - ), + FloatingActionButton.extended( + onPressed: _isLoadingPorts ? null : _loadPorts, + heroTag: 'usb_refresh_action', + icon: _isLoadingPorts + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.usb), + label: Text(context.l10n.scanner_scan), + ), ], ), ), - ); - }, + ); + } + + Widget _buildTransportLinks(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Wrap( + spacing: 12, + runSpacing: 8, + children: [ + if (!PlatformInfo.isWeb) + OutlinedButton.icon( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const TcpScreen()), + ); + }, + icon: const Icon(Icons.lan), + label: Text(context.l10n.connectionChoiceTcpLabel), + ), + OutlinedButton.icon( + onPressed: () => Navigator.of(context).maybePop(), + icon: const Icon(Icons.bluetooth), + label: Text(context.l10n.connectionChoiceBluetoothLabel), + ), + ], ), ); } @@ -186,7 +166,7 @@ class _UsbScreenState extends State { if (_isLoadingPorts) { statusText = l10n.usbStatus_searching; - statusColor = Colors.blue; + statusColor = Theme.of(context).colorScheme.primary; } else if (connector.isUsbTransportConnected) { switch (connector.state) { case MeshCoreConnectionState.connected: @@ -199,7 +179,7 @@ class _UsbScreenState extends State { statusColor = Colors.orange; default: statusText = l10n.usbStatus_notConnected; - statusColor = Colors.grey; + statusColor = Theme.of(context).colorScheme.onSurfaceVariant; } } else if (connector.state == MeshCoreConnectionState.connecting && connector.activeTransport == MeshCoreTransportType.usb) { @@ -207,7 +187,7 @@ class _UsbScreenState extends State { statusColor = Colors.orange; } else { statusText = l10n.usbStatus_notConnected; - statusColor = Colors.grey; + statusColor = Theme.of(context).colorScheme.onSurfaceVariant; } return Container( @@ -219,15 +199,13 @@ class _UsbScreenState extends State { Icon(Icons.circle, size: 12, color: statusColor), const SizedBox(width: 8), Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - statusText, - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.w500, - ), + child: Text( + statusText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.w500, ), ), ), @@ -244,11 +222,11 @@ class _UsbScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.usb, size: 64, color: Colors.grey[400]), + Icon(Icons.usb, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( l10n.usbStatus_searching, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), @@ -260,12 +238,12 @@ class _UsbScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.usb, size: 64, color: Colors.grey[400]), + Icon(Icons.usb, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( l10n.usbScreenEmptyState, textAlign: TextAlign.center, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), @@ -294,10 +272,7 @@ class _UsbScreenState extends State { style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: showRawName ? Text(rawName) : null, - trailing: ElevatedButton( - onPressed: isConnecting ? null : () => _connectPort(port), - child: Text(l10n.common_connect), - ), + trailing: const Icon(Icons.chevron_right), onTap: isConnecting ? null : () => _connectPort(port), ); }, @@ -387,7 +362,7 @@ class _UsbScreenState extends State { showDismissibleSnackBar( context, content: Text(_friendlyErrorMessage(error)), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 733dfc59..a15b9c7d 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -449,6 +449,11 @@ class MessageRetryService extends ChangeNotifier { }); } + void untrack(String messageId) { + _timeoutTimers[messageId]?.cancel(); + _cleanupMessage(messageId); + } + void _cleanupMessage(String messageId) { _moveAckHashesToHistory(messageId); _ackHashToMessageId.removeWhere( diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index b367e0e5..d8e70ce3 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -248,7 +248,7 @@ class NotificationService { await _notifications.show( id: contactId != null ? 'advert:$contactId'.hashCode - : DateTime.now().millisecondsSinceEpoch, + : DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF, title: _l10n.notification_newTypeDiscovered(contactType), body: contactName, notificationDetails: notificationDetails, @@ -304,7 +304,8 @@ class NotificationService { try { await _notifications.show( - id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + id: channelIndex?.hashCode ?? + DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF, title: channelName, body: body, notificationDetails: notificationDetails, diff --git a/lib/services/translation_service.dart b/lib/services/translation_service.dart index 7b1d7f5f..d01155ab 100644 --- a/lib/services/translation_service.dart +++ b/lib/services/translation_service.dart @@ -47,6 +47,7 @@ class TranslationService extends ChangeNotifier { _langDetectInit = initLangDetect(); } + bool _disposed = false; bool _isBusy = false; bool _isDownloading = false; bool _cancelDownloadRequested = false; @@ -215,7 +216,7 @@ class TranslationService extends ChangeNotifier { } _downloadTotalBytes = totalSize; - notifyListeners(); + _notify(); DownloadedModelFile downloaded; if (supportsRange && @@ -268,7 +269,7 @@ class TranslationService extends ChangeNotifier { throw StateError('Model download failed: HTTP ${response.statusCode}'); } _downloadTotalBytes ??= response.contentLength; - notifyListeners(); + _notify(); final trackedStream = _trackDownloadProgress(response.stream); return await _fileStore.writeModelBytes( fileName: fileName, @@ -313,7 +314,7 @@ class TranslationService extends ChangeNotifier { throw const TranslationDownloadCancelled(); } _downloadFileName = 'Merging chunks...'; - notifyListeners(); + _notify(); combineReached = true; return await _fileStore.combineChunks( fileName: fileName, @@ -361,7 +362,7 @@ class TranslationService extends ChangeNotifier { } _cancelDownloadRequested = true; _lastError = 'Download stopped.'; - notifyListeners(); + _notify(); } Future removeModel(TranslationModelRecord model) async { @@ -469,7 +470,7 @@ class TranslationService extends ChangeNotifier { } catch (error) { _lastError = error.toString(); appLogger.warn('Language detection failed: $error'); - notifyListeners(); + _notify(); return null; } } @@ -538,7 +539,7 @@ class TranslationService extends ChangeNotifier { } catch (error) { _lastError = error.toString(); appLogger.warn('Translation request failed: $error'); - notifyListeners(); + _notify(); return null; } } @@ -631,6 +632,10 @@ class TranslationService extends ChangeNotifier { final completer = Completer(); _setBusy(true); _queue = _queue.then((_) async { + if (_disposed) { + completer.completeError(StateError('TranslationService disposed.')); + return; + } try { completer.complete(await action()); } catch (error, stackTrace) { @@ -648,17 +653,24 @@ class TranslationService extends ChangeNotifier { throw const TranslationDownloadCancelled(); } _downloadedBytes += chunk.length; - notifyListeners(); + _notify(); yield chunk; } } + void _notify() { + if (_disposed) { + return; + } + notifyListeners(); + } + void _setBusy(bool value) { if (_isBusy == value) { return; } _isBusy = value; - notifyListeners(); + _notify(); } void _setDownloading(bool value) { @@ -669,11 +681,12 @@ class TranslationService extends ChangeNotifier { _downloadTotalBytes = null; _downloadFileName = null; } - notifyListeners(); + _notify(); } @override void dispose() { + _disposed = true; final engine = _engine; _engine = null; _loadedModelPath = null; diff --git a/lib/theme/mesh_theme.dart b/lib/theme/mesh_theme.dart index 7c35d57c..00b92ec0 100644 --- a/lib/theme/mesh_theme.dart +++ b/lib/theme/mesh_theme.dart @@ -1,34 +1,30 @@ import 'package:flutter/material.dart'; -/// MeshCore redesign palette — warm field-journal dark theme with -/// phosphor-green signal accents. Mirrors values from the redesign spec. +/// MeshCore palette — cool slate dark theme with sky-blue accents. class MeshPalette { MeshPalette._(); - // Surfaces (warm near-black, olive undertone) - static const bg = Color(0xFF0F1412); - static const bg1 = Color(0xFF161C19); - static const bg2 = Color(0xFF1D2521); - static const bg3 = Color(0xFF28322D); - static const bg4 = Color(0xFF34403A); + // Surfaces (cool near-black, slate undertone) + static const bg = Color(0xFF101417); + static const bg1 = Color(0xFF161B1F); + static const bg2 = Color(0xFF1D242A); + static const bg3 = Color(0xFF28313A); + static const bg4 = Color(0xFF344049); // Lines - static const line = Color(0xFF232C28); - static const line2 = Color(0xFF34403A); - static const line3 = Color(0xFF48564F); + static const line = Color(0xFF222B31); + static const line2 = Color(0xFF344049); + static const line3 = Color(0xFF485762); // Ink - static const ink = Color(0xFFEFF3E8); - static const ink2 = Color(0xFFBAC4B5); - static const ink3 = Color(0xFF7C8B82); - static const ink4 = Color(0xFF55635B); + static const ink = Color(0xFFE9EEF3); + static const ink2 = Color(0xFFB5C0C9); + static const ink3 = Color(0xFF7C8A95); + static const ink4 = Color(0xFF556470); - // Signal (phosphor) + // Signal-quality green (used only for SNR coloring, not UI chrome) static const signal = Color(0xFF7BEFA8); static const signalDim = Color(0xFF4DC580); - static const signalBg = Color(0x177BEFA8); // ~9% alpha - static const signalLine = Color(0x427BEFA8); // ~26% - static const signalGlow = Color(0x597BEFA8); // ~35% // Warn (ember) static const warn = Color(0xFFFFA552); @@ -41,8 +37,9 @@ class MeshPalette { static const alertBg = Color(0x1CFF6A5C); static const alertLine = Color(0x52FF6A5C); - // Blue (dusk sky) + // Blue (sky) — primary accent static const blue = Color(0xFF7FCBF5); + static const blueDim = Color(0xFF4A9CC9); static const blueBg = Color(0x1C7FCBF5); static const blueLine = Color(0x477FCBF5); @@ -51,20 +48,20 @@ class MeshPalette { static const magentaBg = Color(0x1CDE7FDB); static const magentaLine = Color(0x47DE7FDB); - // Me bubble (mossy) - static const me = Color(0xFF1E3527); - static const meBorder = Color(0xFF2D5039); - static const meInk = Color(0xFFDEF0DC); + // Me bubble (dusk blue) + static const me = Color(0xFF1B2C3D); + static const meBorder = Color(0xFF2C4A66); + static const meInk = Color(0xFFDCE9F5); // ── Light variant (used when user explicitly picks light theme) - static const lightBg = Color(0xFFF5F3EC); - static const lightBg1 = Color(0xFFECE9DF); - static const lightBg2 = Color(0xFFE2DED2); - static const lightLine = Color(0xFFCAC5B4); - static const lightInk = Color(0xFF0F1410); - static const lightInk2 = Color(0xFF3D463E); - static const lightInk3 = Color(0xFF6A756D); - static const lightSignal = Color(0xFF1A7A44); + static const lightBg = Color(0xFFF4F6F8); + static const lightBg1 = Color(0xFFEAEEF2); + static const lightBg2 = Color(0xFFDFE5EA); + static const lightLine = Color(0xFFC3CCD4); + static const lightInk = Color(0xFF10161B); + static const lightInk2 = Color(0xFF3C4853); + static const lightInk3 = Color(0xFF69767F); + static const lightBlue = Color(0xFF2F6EA8); } /// Named font stacks — Flutter falls back to system fonts when the named @@ -115,14 +112,18 @@ class MeshTheme { static ThemeData dark() { const scheme = ColorScheme.dark( - primary: MeshPalette.signal, - onPrimary: Color(0xFF0A1810), - primaryContainer: MeshPalette.signalBg, - onPrimaryContainer: MeshPalette.signal, - secondary: MeshPalette.blue, - onSecondary: Color(0xFF0A1520), - tertiary: MeshPalette.magenta, - onTertiary: Color(0xFF201020), + primary: MeshPalette.blue, + onPrimary: Color(0xFF0A1A26), + primaryContainer: MeshPalette.blueBg, + onPrimaryContainer: MeshPalette.blue, + secondary: MeshPalette.magenta, + onSecondary: Color(0xFF201020), + secondaryContainer: Color(0xFF331A33), + onSecondaryContainer: MeshPalette.magenta, + tertiary: MeshPalette.warn, + onTertiary: Color(0xFF1F1206), + tertiaryContainer: Color(0xFF3A2710), + onTertiaryContainer: Color(0xFFFFC58A), error: MeshPalette.alert, onError: Color(0xFF1A0A08), errorContainer: MeshPalette.alertBg, @@ -141,33 +142,39 @@ class MeshTheme { scrim: Colors.black54, inverseSurface: MeshPalette.ink, onInverseSurface: MeshPalette.bg, - inversePrimary: MeshPalette.signalDim, + inversePrimary: MeshPalette.blueDim, ); return _build(scheme, Brightness.dark); } static ThemeData light() { const scheme = ColorScheme.light( - primary: MeshPalette.lightSignal, + primary: MeshPalette.lightBlue, onPrimary: Colors.white, - primaryContainer: Color(0xFFD4E8D8), - onPrimaryContainer: MeshPalette.lightSignal, - secondary: Color(0xFF2F6EA8), + primaryContainer: Color(0xFFD3E4F5), + onPrimaryContainer: Color(0xFF12354F), + secondary: Color(0xFF8C4A8A), onSecondary: Colors.white, - tertiary: Color(0xFF8C4A8A), + secondaryContainer: Color(0xFFEFD6EE), + onSecondaryContainer: Color(0xFF3D1A3C), + tertiary: Color(0xFF9A5B16), onTertiary: Colors.white, + tertiaryContainer: Color(0xFFF8E3C9), + onTertiaryContainer: Color(0xFF4A2A05), error: Color(0xFFB53D2F), onError: Colors.white, + errorContainer: Color(0xFFF6D9D4), + onErrorContainer: Color(0xFF5C1A12), surface: MeshPalette.lightBg, onSurface: MeshPalette.lightInk, surfaceContainerLowest: MeshPalette.lightBg, surfaceContainerLow: MeshPalette.lightBg1, surfaceContainer: MeshPalette.lightBg1, surfaceContainerHigh: MeshPalette.lightBg2, - surfaceContainerHighest: Color(0xFFD5D0C0), + surfaceContainerHighest: Color(0xFFD2DAE1), onSurfaceVariant: MeshPalette.lightInk2, outline: MeshPalette.lightLine, - outlineVariant: Color(0xFFDBD6C6), + outlineVariant: Color(0xFFD8DEE5), ); return _build(scheme, Brightness.light); } diff --git a/lib/widgets/byte_count_input.dart b/lib/widgets/byte_count_input.dart index ca432522..91f638b8 100644 --- a/lib/widgets/byte_count_input.dart +++ b/lib/widgets/byte_count_input.dart @@ -86,7 +86,7 @@ class ByteCountedTextField extends StatelessWidget { final counterColor = ratio > errorThreshold ? Theme.of(context).colorScheme.error : ratio > warningThreshold - ? Colors.orange + ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.onSurfaceVariant; return Column( @@ -118,8 +118,9 @@ class ByteCountedTextField extends StatelessWidget { textInputAction: textInputAction, onSubmitted: onSubmitted, ), - if (showCounter) - Padding( + Opacity( + opacity: showCounter ? 1 : 0, + child: Padding( padding: const EdgeInsets.only(top: 4, right: 4), child: Align( alignment: Alignment.centerRight, @@ -129,6 +130,7 @@ class ByteCountedTextField extends StatelessWidget { ), ), ), + ), ], ); }, diff --git a/lib/widgets/device_tile.dart b/lib/widgets/device_tile.dart index 3f30b38e..9012128c 100644 --- a/lib/widgets/device_tile.dart +++ b/lib/widgets/device_tile.dart @@ -6,9 +6,15 @@ import 'signal_ui.dart'; /// A reusable tile widget for displaying a MeshCore device in a list class DeviceTile extends StatelessWidget { final ScanResult scanResult; - final VoidCallback onTap; + final VoidCallback? onTap; + final bool isConnecting; - const DeviceTile({super.key, required this.scanResult, required this.onTap}); + const DeviceTile({ + super.key, + required this.scanResult, + required this.onTap, + this.isConnecting = false, + }); @override Widget build(BuildContext context) { @@ -19,16 +25,20 @@ class DeviceTile extends StatelessWidget { : scanResult.advertisementData.advName; return ListTile( + enabled: onTap != null || isConnecting, leading: _buildSignalIcon(rssi), title: Text( name.isNotEmpty ? name : context.l10n.common_unknownDevice, style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: Text(device.remoteId.toString()), - trailing: ElevatedButton( - onPressed: onTap, - child: Text(context.l10n.common_connect), - ), + trailing: isConnecting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, onTap: onTap, ); } diff --git a/lib/widgets/empty_state.dart b/lib/widgets/empty_state.dart index 172c9a4e..718c1c44 100644 --- a/lib/widgets/empty_state.dart +++ b/lib/widgets/empty_state.dart @@ -17,18 +17,22 @@ class EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { + final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, size: 64, color: Colors.grey[400]), + Icon(icon, size: 64, color: onSurfaceVariant.withValues(alpha: 0.6)), const SizedBox(height: 16), - Text(title, style: TextStyle(fontSize: 16, color: Colors.grey[600])), + Text(title, style: TextStyle(fontSize: 16, color: onSurfaceVariant)), if (subtitle != null) ...[ const SizedBox(height: 8), Text( subtitle!, - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle( + fontSize: 14, + color: onSurfaceVariant.withValues(alpha: 0.8), + ), textAlign: TextAlign.center, ), ], diff --git a/lib/widgets/gif_picker.dart b/lib/widgets/gif_picker.dart index 9c569512..df27ec68 100644 --- a/lib/widgets/gif_picker.dart +++ b/lib/widgets/gif_picker.dart @@ -180,7 +180,7 @@ class _GifPickerState extends State { const SizedBox(height: 8), Text( context.l10n.gifPicker_poweredBy, - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), @@ -197,11 +197,11 @@ class _GifPickerState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), + Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( _error!, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 16), ElevatedButton.icon( @@ -219,11 +219,11 @@ class _GifPickerState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey[400]), + Icon(Icons.search_off, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( context.l10n.gifPicker_noGifsFound, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart index 0689f0b5..cf9cd7d6 100644 --- a/lib/widgets/message_status_icon.dart +++ b/lib/widgets/message_status_icon.dart @@ -1,36 +1,155 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class MessageStatusIcon extends StatelessWidget { +import '../l10n/l10n.dart'; + +class MessageStatusIcon extends StatefulWidget { final bool isAcked; final bool isFailed; + final bool isPending; + final bool isRepeated; final double size; + /// Base tint for the sent/sending state. On a colored (outgoing) bubble a + /// plain grey tick is nearly invisible, so callers can pass the bubble's own + /// meta/text color for contrast. Falls back to [ColorScheme.onSurfaceVariant]. + final Color? onColor; + const MessageStatusIcon({ super.key, required this.isAcked, this.isFailed = false, + this.isPending = false, + this.isRepeated = false, this.size = 14, + this.onColor, + }); + + @override + State createState() => _MessageStatusIconState(); +} + +class _MessageStatusIconState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + if (widget.isPending) _controller.repeat(); + } + + @override + void didUpdateWidget(MessageStatusIcon oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isPending && !_controller.isAnimating) { + _controller.repeat(); + } else if (!widget.isPending && _controller.isAnimating) { + _controller + ..stop() + ..reset(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; + final double size = widget.size; + final Color baseColor = widget.onColor ?? colorScheme.onSurfaceVariant; + + if (widget.isFailed) { + return Semantics( + label: l10n.messageStatus_failed, + child: Icon(Icons.cancel, size: size, color: colorScheme.error), + ); + } + + if (widget.isPending) { + return Semantics( + label: l10n.messageStatus_pending, + child: _SendingDots( + controller: _controller, + color: baseColor, + size: size, + ), + ); + } + + final bool delivered = widget.isAcked || widget.isRepeated; + final String label = widget.isRepeated + ? l10n.messageStatus_repeated + : widget.isAcked + ? l10n.messageStatus_delivered + : l10n.messageStatus_sent; + final Color color = delivered ? colorScheme.tertiary : baseColor; + + return Semantics( + label: label, + child: delivered + ? SvgPicture.asset( + 'assets/icons/done_all.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ) + : Icon(Icons.done, size: size, color: color), + ); + } +} + +/// Three dots that pulse left-to-right while a message is in flight. +class _SendingDots extends StatelessWidget { + final AnimationController controller; + final Color color; + final double size; + + const _SendingDots({ + required this.controller, + required this.color, + required this.size, }); @override Widget build(BuildContext context) { - if (isFailed) { - return Icon(Icons.cancel, size: size, color: Colors.red); - } - - final Color color; - if (isAcked) { - color = Colors.green; - } else { - color = Colors.grey; - } - - return SvgPicture.asset( - 'assets/icons/done_all.svg', - width: size, + final double dot = (size * 0.24).clamp(2.0, 4.0); + return SizedBox( height: size, - colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + child: AnimatedBuilder( + animation: controller, + builder: (context, _) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(3, (i) { + final double phase = (controller.value - i * 0.18) % 1.0; + final double t = phase < 0.5 ? phase * 2 : (1 - phase) * 2; + final double opacity = 0.25 + 0.75 * t.clamp(0.0, 1.0); + return Padding( + padding: EdgeInsets.symmetric(horizontal: dot * 0.28), + child: Container( + width: dot, + height: dot, + decoration: BoxDecoration( + color: color.withValues(alpha: opacity), + shape: BoxShape.circle, + ), + ), + ); + }), + ); + }, + ), ); } } diff --git a/lib/widgets/path_editor_sheet.dart b/lib/widgets/path_editor_sheet.dart new file mode 100644 index 00000000..66c1fa4b --- /dev/null +++ b/lib/widgets/path_editor_sheet.dart @@ -0,0 +1,377 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; + +import '../connector/meshcore_protocol.dart'; +import '../helpers/path_helper.dart'; +import '../l10n/contact_localization.dart'; +import '../l10n/l10n.dart'; +import '../models/contact.dart'; + +class PathEditorSheet extends StatefulWidget { + final List availableContacts; + final List initialPath; + + const PathEditorSheet({ + super.key, + required this.availableContacts, + this.initialPath = const [], + }); + + static Future show( + BuildContext context, { + required List availableContacts, + List initialPath = const [], + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: FractionallySizedBox( + heightFactor: 0.9, + child: PathEditorSheet( + availableContacts: availableContacts, + initialPath: initialPath, + ), + ), + ), + ); + } + + @override + State createState() => _PathEditorSheetState(); +} + +class _Hop { + final int id; + final int byte; + + const _Hop(this.id, this.byte); +} + +class _PathEditorSheetState extends State { + static const int _maxHops = 64; + + final List<_Hop> _hops = []; + final _hexController = TextEditingController(); + String? _hexError; + bool _syncingHex = false; + String _search = ''; + int _nextHopId = 0; + + @override + void initState() { + super.initState(); + for (final byte in widget.initialPath) { + _hops.add(_Hop(_nextHopId++, byte)); + } + _syncHexFromHops(); + } + + @override + void dispose() { + _hexController.dispose(); + super.dispose(); + } + + List get _repeaters { + final query = _search.trim().toLowerCase(); + return widget.availableContacts + .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) + .where((c) => c.publicKey.isNotEmpty) + .where((c) => query.isEmpty || c.name.toLowerCase().contains(query)) + .toList() + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + } + + void _syncHexFromHops() { + _syncingHex = true; + _hexController.text = PathHelper.formatPathHex( + _hops.map((h) => h.byte).toList(), + ); + _syncingHex = false; + _hexError = null; + } + + void _onHexChanged(String text) { + if (_syncingHex) return; + final l10n = context.l10n; + final tokens = text + .split(RegExp(r'[,\s]+')) + .where((t) => t.isNotEmpty) + .toList(); + final invalid = tokens + .where((t) => t.length != 2 || int.tryParse(t, radix: 16) == null) + .toList(); + setState(() { + if (invalid.isNotEmpty) { + _hexError = l10n.pathEditor_invalidTokens(invalid.join(', ')); + return; + } + if (tokens.length > _maxHops) { + _hexError = l10n.pathEditor_tooManyHops; + return; + } + _hexError = null; + _hops + ..clear() + ..addAll(tokens.map((t) => _Hop(_nextHopId++, int.parse(t, radix: 16)))); + }); + } + + void _addHop(Contact contact) { + if (_hops.length >= _maxHops) return; + setState(() { + _hops.add(_Hop(_nextHopId++, contact.publicKey.first)); + _syncHexFromHops(); + }); + } + + void _removeHop(int index) { + setState(() { + _hops.removeAt(index); + _syncHexFromHops(); + }); + } + + void _reorderHop(int oldIndex, int newIndex) { + setState(() { + final hop = _hops.removeAt(oldIndex); + _hops.insert(newIndex, hop); + _syncHexFromHops(); + }); + } + + void _save() { + Navigator.pop( + context, + Uint8List.fromList(_hops.map((h) => h.byte).toList()), + ); + } + + Widget _hopTile(BuildContext context, int index) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + final hop = _hops[index]; + final hex = PathHelper.hopHex(hop.byte); + final name = PathHelper.hopName(hop.byte, widget.availableContacts); + + return ListTile( + key: ValueKey(hop.id), + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + radius: 14, + backgroundColor: scheme.primaryContainer, + child: Text( + '${index + 1}', + style: TextStyle(fontSize: 12, color: scheme.onPrimaryContainer), + ), + ), + title: Text( + name ?? l10n.pathEditor_unknownHop, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(hex), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove_circle_outline), + tooltip: l10n.pathEditor_removeHop, + constraints: const BoxConstraints(minWidth: 44, minHeight: 44), + onPressed: () => _removeHop(index), + ), + ReorderableDragStartListener( + index: index, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 12), + child: Icon(Icons.drag_handle), + ), + ), + ], + ), + ); + } + + Widget _repeaterTile(BuildContext context, Contact contact) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + final isRepeater = contact.type == advTypeRepeater; + final full = _hops.length >= _maxHops; + + return ListTile( + contentPadding: EdgeInsets.zero, + enabled: !full, + leading: CircleAvatar( + radius: 16, + backgroundColor: isRepeater + ? scheme.primaryContainer + : scheme.secondaryContainer, + child: Icon( + isRepeater ? Icons.router : Icons.meeting_room, + size: 16, + color: isRepeater + ? scheme.onPrimaryContainer + : scheme.onSecondaryContainer, + ), + ), + title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + '${contact.typeLabel(l10n)} • ${PathHelper.hopHex(contact.publicKey.first)}', + ), + trailing: const Icon(Icons.add_circle_outline), + onTap: full ? null : () => _addHop(contact), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + final scheme = theme.colorScheme; + final repeaters = _repeaters; + + return Column( + children: [ + const SizedBox(height: 8), + Container( + width: 32, + height: 4, + decoration: BoxDecoration( + color: scheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.pathEditor_title, style: theme.textTheme.titleLarge), + Text( + l10n.pathEditor_hopCounter(_hops.length), + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + children: [ + if (_hops.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + l10n.pathEditor_noHops, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ) + else + ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + buildDefaultDragHandles: false, + itemCount: _hops.length, + onReorderItem: _reorderHop, + itemBuilder: _hopTile, + ), + const Divider(), + const SizedBox(height: 8), + Text( + l10n.pathEditor_addHops, + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: 8), + TextField( + onChanged: (value) => setState(() => _search = value), + decoration: InputDecoration( + labelText: l10n.pathEditor_searchRepeaters, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + isDense: true, + ), + ), + const SizedBox(height: 4), + if (repeaters.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + l10n.path_noRepeatersFound, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ) + else + ...repeaters.map((c) => _repeaterTile(context, c)), + ExpansionTile( + tilePadding: EdgeInsets.zero, + title: Text( + l10n.pathEditor_advancedHex, + style: theme.textTheme.titleSmall, + ), + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: _hexController, + onChanged: _onHexChanged, + textCapitalization: TextCapitalization.characters, + decoration: InputDecoration( + labelText: l10n.pathEditor_hexLabel, + helperText: _hexError == null + ? l10n.pathEditor_hexHelper + : null, + errorText: _hexError, + border: const OutlineInputBorder(), + ), + ), + ), + ], + ), + ], + ), + ), + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + onPressed: _hexError != null ? null : _save, + child: Text(l10n.pathEditor_usePath), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart deleted file mode 100644 index a2122f46..00000000 --- a/lib/widgets/path_management_dialog.dart +++ /dev/null @@ -1,510 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:meshcore_open/models/path_history.dart'; -import 'package:meshcore_open/screens/path_trace_map.dart'; -import 'package:meshcore_open/widgets/elements_ui.dart'; -import 'package:provider/provider.dart'; - -import '../connector/meshcore_connector.dart'; -import '../l10n/l10n.dart'; -import '../models/contact.dart'; -import '../l10n/contact_localization.dart'; -import '../helpers/path_helper.dart'; -import '../services/path_history_service.dart'; -import '../helpers/snack_bar_builder.dart'; -import 'path_selection_dialog.dart'; - -class PathManagementDialog { - static Future show(BuildContext context, {required Contact contact}) { - return showDialog( - context: context, - builder: (context) => _PathManagementDialog(contact: contact), - ); - } -} - -class _PathManagementDialog extends StatefulWidget { - final Contact contact; - - const _PathManagementDialog({required this.contact}); - - @override - State<_PathManagementDialog> createState() => _PathManagementDialogState(); -} - -class _PathManagementDialogState extends State<_PathManagementDialog> { - bool _showAllPaths = false; - - int _resolveContactIndex = -1; - - Contact _resolveContact(MeshCoreConnector connector) { - if (_resolveContactIndex >= 0 && - _resolveContactIndex < connector.contacts.length && - connector.contacts[_resolveContactIndex].publicKeyHex == - widget.contact.publicKeyHex) { - return connector.contacts[_resolveContactIndex]; - } - _resolveContactIndex = connector.contacts.indexWhere( - (c) => c.publicKeyHex == widget.contact.publicKeyHex, - ); - if (_resolveContactIndex == -1) { - return widget.contact; - } - return connector.contacts[_resolveContactIndex]; - } - - String _formatRelativeTime(BuildContext context, DateTime? time) { - if (time == null) return '—'; - final l10n = context.l10n; - final diff = DateTime.now().difference(time); - if (diff.inSeconds < 60) return l10n.time_justNow; - if (diff.inMinutes < 60) return l10n.time_minutesAgo(diff.inMinutes); - if (diff.inHours < 24) return l10n.time_hoursAgo(diff.inHours); - return l10n.time_daysAgo(diff.inDays); - } - - void _showFullPathDialog(BuildContext context, List pathBytes) { - final l10n = context.l10n; - if (pathBytes.isEmpty) { - showDismissibleSnackBar( - context, - content: Text(l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ); - return; - } - - final connector = context.read(); - final allContacts = connector.allContacts; - - final formattedPath = PathHelper.formatPathHex(pathBytes); - final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.chat_fullPath), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText(formattedPath), - const SizedBox(height: 8), - SelectableText( - resolvedNames, - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PathTraceMapScreen( - title: context.l10n.contacts_repeaterPathTrace, - path: Uint8List.fromList(pathBytes), - flipPathAround: true, - targetContact: widget.contact, - pathHashByteWidth: connector.pathHashByteWidth, - ), - ), - ), - child: Text(context.l10n.contacts_pathTrace), - ), - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.common_close), - ), - ], - ), - ); - } - - Future _setCustomPath( - BuildContext context, - MeshCoreConnector connector, - Contact currentContact, - ) async { - final l10n = context.l10n; - if (currentContact.pathLength > 0 && - currentContact.path.isEmpty && - connector.isConnected) { - connector.getContacts(); - } - - final pathForInput = currentContact.pathFormattedIdList( - connector.pathHashByteWidth, - ); - final availableContacts = connector.allContacts - .where((c) => c.publicKeyHex != currentContact.publicKeyHex) - .toList(); - - final result = await PathSelectionDialog.show( - context, - availableContacts: availableContacts, - initialPath: pathForInput.isEmpty ? null : pathForInput, - currentPathLabel: currentContact.pathLabel(l10n), - onRefresh: connector.isConnected ? connector.getContacts : null, - ); - - if (result != null && context.mounted) { - await connector.setPathOverride( - currentContact, - pathLen: result.length, - pathBytes: result, - ); - - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(l10n.chat_hopsCount(result.length)), - duration: const Duration(seconds: 2), - ); - } - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return Consumer2( - builder: (context, connector, pathService, _) { - final currentContact = _resolveContact(connector); - final paths = pathService.getRecentPaths(currentContact.publicKeyHex); - - final repeatersList = List.of(connector.directRepeaters) - ..sort((a, b) => b.ranking.compareTo(a.ranking)); - - if (repeatersList.isEmpty) { - _showAllPaths = true; - } - - final directRepeater = repeatersList.isEmpty - ? null - : repeatersList.first; - final secondDirectRepeater = repeatersList.length < 2 - ? null - : repeatersList.elementAt(1); - final thirdDirectRepeater = repeatersList.length < 3 - ? null - : repeatersList.elementAt(2); - - List>> pathsWithRepeaters = - paths.map((path) { - final isDirectRepeater = - directRepeater != null && - path.pathBytes.isNotEmpty && - directRepeater.pubkeyFirstByte == path.pathBytes.first; - final isSecondDirectRepeater = - secondDirectRepeater != null && - path.pathBytes.isNotEmpty && - secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - final isThirdDirectRepeater = - thirdDirectRepeater != null && - path.pathBytes.isNotEmpty && - thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - - int ranking = -1; - Color color = Colors.grey; - if (isDirectRepeater) { - color = Colors.green; - ranking = 3; - } else if (isSecondDirectRepeater) { - color = Colors.yellow; - ranking = 2; - } else if (isThirdDirectRepeater) { - color = Colors.red; - ranking = 1; - } else if (path.wasFloodDiscovery) { - color = Colors.blue; - ranking = 0; - } - - return MapEntry(ranking, MapEntry(color, path)); - }).toList(); - - pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key)); - - return AlertDialog( - title: Text(l10n.chat_pathManagement), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.path_currentPath(currentContact.pathLabel(l10n)), - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - const SizedBox(height: 12), - if (paths.isNotEmpty) ...[ - if (repeatersList.isNotEmpty) - FeatureToggleRow( - title: l10n.chat_ShowAllPaths, - subtitle: "", - value: _showAllPaths, - onChanged: (val) { - setState(() { - _showAllPaths = val; - }); - }, - ), - Text( - l10n.chat_recentAckPaths, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - if (pathsWithRepeaters.length >= 100) ...[ - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.amberAccent, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - l10n.chat_pathHistoryFull, - style: const TextStyle(fontSize: 12), - ), - ), - ], - const SizedBox(height: 8), - ...pathsWithRepeaters.map((entry) { - final path = entry.value.value; - final color = entry.value.key; - - if (!_showAllPaths && entry.key < 1) { - return const SizedBox.shrink(); - } else { - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: color, - child: Text( - path.routeWeight.toStringAsFixed(1), - style: const TextStyle(fontSize: 10), - ), - ), - title: Text( - l10n.chat_hopsCount(path.hopCount), - style: const TextStyle(fontSize: 14), - ), - isThreeLine: true, - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}\n${path.successCount} ${l10n.chat_successes} • ${l10n.chat_score}: ${path.routeWeight.toStringAsFixed(1)}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - currentContact.publicKeyHex, - path.pathBytes, - ); - }, - ), - path.wasFloodDiscovery - ? const Icon( - Icons.waves, - size: 16, - color: Colors.grey, - ) - : const Icon( - Icons.route, - size: 16, - color: Colors.grey, - ), - ], - ), - onLongPress: () => - _showFullPathDialog(context, path.pathBytes), - onTap: () async { - if (path.pathBytes.isEmpty) { - showDismissibleSnackBar( - context, - content: Text( - l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), - ); - return; - } - - final pathBytes = Uint8List.fromList( - path.pathBytes, - ); - final pathLength = path.pathBytes.length; - - await connector.setPathOverride( - currentContact, - pathLen: pathLength, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - showDismissibleSnackBar( - context, - content: Text( - l10n.path_usingHopsPath(path.hopCount), - ), - duration: const Duration(seconds: 2), - ); - }, - ), - ); - } - }), - const Divider(), - ] else ...[ - Text(l10n.chat_noPathHistoryYet), - const Divider(), - ], - // Flood delivery stats - Builder( - builder: (context) { - final floodStats = pathService.getFloodStats( - currentContact.publicKeyHex, - ); - if (floodStats == null || - (floodStats.successCount == 0 && - floodStats.failureCount == 0)) { - return const SizedBox.shrink(); - } - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.blue, - child: Icon(Icons.waves, size: 16), - ), - title: const Text( - 'Flood Mode', - style: TextStyle(fontSize: 14), - ), - subtitle: Text( - '${floodStats.successCount} ${l10n.chat_successes} / ${floodStats.failureCount} failures' - '${floodStats.lastTripTimeMs > 0 ? ' • ${(floodStats.lastTripTimeMs / 1000).toStringAsFixed(2)}s' : ''}' - '${floodStats.lastUsed != null ? ' • ${_formatRelativeTime(context, floodStats.lastUsed!)}' : ''}', - style: const TextStyle(fontSize: 11), - ), - ), - ); - }, - ), - const SizedBox(height: 8), - Text( - l10n.chat_pathActions, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - const SizedBox(height: 8), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.purple, - child: Icon(Icons.edit_road, size: 16), - ), - title: Text( - l10n.chat_setCustomPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - l10n.chat_setCustomPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await _setCustomPath(context, connector, currentContact); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.orange, - child: Icon(Icons.clear_all, size: 16), - ), - title: Text( - l10n.chat_clearPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - l10n.chat_clearPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.clearContactPath(currentContact); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ); - Navigator.pop(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.blue, - child: Icon(Icons.waves, size: 16), - ), - title: Text( - l10n.chat_forceFloodMode, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - l10n.chat_floodModeSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.setPathOverride( - currentContact, - pathLen: -1, - ); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ); - Navigator.pop(context); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.common_close), - ), - ], - ); - }, - ); - } -} diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart deleted file mode 100644 index 44ae58f9..00000000 --- a/lib/widgets/path_selection_dialog.dart +++ /dev/null @@ -1,346 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:meshcore_open/connector/meshcore_protocol.dart'; -import '../l10n/l10n.dart'; -import '../models/contact.dart'; -import '../l10n/contact_localization.dart'; -import '../helpers/snack_bar_builder.dart'; - -class PathSelectionDialog extends StatefulWidget { - final List availableContacts; - final String title; - final String? initialPath; - final String? currentPathLabel; - final VoidCallback? onRefresh; - - const PathSelectionDialog({ - super.key, - required this.availableContacts, - required this.title, - this.initialPath, - this.currentPathLabel, - this.onRefresh, - }); - - @override - State createState() => _PathSelectionDialogState(); - - static Future show( - BuildContext context, { - required List availableContacts, - String? title, - String? initialPath, - String? currentPathLabel, - VoidCallback? onRefresh, - }) { - return showDialog( - context: context, - builder: (context) => PathSelectionDialog( - availableContacts: availableContacts, - title: title ?? context.l10n.path_enterCustomPath, - initialPath: initialPath, - currentPathLabel: currentPathLabel, - onRefresh: onRefresh, - ), - ); - } -} - -class _PathSelectionDialogState extends State { - late TextEditingController _controller; - final List _selectedContacts = []; - List _validContacts = []; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.initialPath ?? ''); - _filterValidContacts(); - } - - @override - void didUpdateWidget(PathSelectionDialog oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.availableContacts != oldWidget.availableContacts) { - _filterValidContacts(); - } - } - - void _filterValidContacts() { - _validContacts = widget.availableContacts - .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) - .toList(); - } - - void _updateTextFromContacts() { - final pathParts = _selectedContacts - .map((contact) { - if (contact.publicKeyHex.length >= 2) { - return contact.publicKeyHex.substring(0, 2); - } - return ''; - }) - .where((s) => s.isNotEmpty) - .toList(); - - _controller.text = pathParts.join(','); - } - - void _toggleContact(Contact contact) { - setState(() { - if (_selectedContacts.contains(contact)) { - _selectedContacts.remove(contact); - } else { - _selectedContacts.add(contact); - } - _updateTextFromContacts(); - }); - } - - void _clearSelection() { - setState(() { - _selectedContacts.clear(); - _controller.clear(); - }); - } - - Future _validateAndSubmit() async { - final l10n = context.l10n; - final path = _controller.text.trim().toUpperCase(); - if (path.isEmpty) { - if (mounted) Navigator.pop(context); - return; - } - - // Parse comma-separated hex prefixes - final pathIds = path - .split(',') - .map((s) => s.trim()) - .where((s) => s.isNotEmpty) - .toList(); - final pathBytesList = []; - final invalidPrefixes = []; - - for (final id in pathIds) { - if (id.length < 2) { - invalidPrefixes.add(id); - continue; - } - - final prefix = id.substring(0, 2); - try { - final byte = int.parse(prefix, radix: 16); - pathBytesList.add(byte); - } catch (e) { - invalidPrefixes.add(id); - } - } - - if (!mounted) return; - - // Show error for invalid prefixes - if (invalidPrefixes.isNotEmpty) { - showDismissibleSnackBar( - context, - content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ); - return; - } - - // Check max path length (64 hops) - if (pathBytesList.length > 64) { - showDismissibleSnackBar( - context, - content: Text(l10n.path_tooLong), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ); - return; - } - - if (pathBytesList.isNotEmpty && mounted) { - Navigator.pop(context, Uint8List.fromList(pathBytesList)); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return AlertDialog( - title: Text(widget.title), - content: SingleChildScrollView( - child: SizedBox( - width: double.maxFinite, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.currentPathLabel != null) ...[ - Row( - children: [ - Text( - l10n.path_currentPathLabel, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - if (widget.onRefresh != null) - TextButton.icon( - onPressed: widget.onRefresh, - icon: const Icon(Icons.refresh, size: 16), - label: Text(l10n.common_reload), - ), - ], - ), - Text( - widget.currentPathLabel!, - style: const TextStyle(fontSize: 11, color: Colors.grey), - ), - const SizedBox(height: 16), - ], - Text( - l10n.path_hexPrefixInstructions, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - const SizedBox(height: 8), - Text( - l10n.path_hexPrefixExample, - style: const TextStyle(fontSize: 11, color: Colors.grey), - ), - const SizedBox(height: 16), - TextField( - controller: _controller, - decoration: InputDecoration( - labelText: l10n.path_labelHexPrefixes, - hintText: l10n.path_hexPrefixExample, - border: const OutlineInputBorder(), - helperText: l10n.path_helperMaxHops, - ), - textCapitalization: TextCapitalization.characters, - maxLength: 191, // 64 hops * 2 chars + 63 commas - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 8), - Row( - children: [ - Text( - l10n.path_selectFromContacts, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - if (_selectedContacts.isNotEmpty) - TextButton( - onPressed: _clearSelection, - child: Text(l10n.common_clear), - ), - ], - ), - const SizedBox(height: 8), - if (_validContacts.isEmpty) ...[ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const Icon( - Icons.info_outline, - size: 48, - color: Colors.grey, - ), - const SizedBox(height: 16), - Text( - l10n.path_noRepeatersFound, - style: const TextStyle(fontSize: 14), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - l10n.path_customPathsRequire, - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ] else ...[ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: ListView.builder( - shrinkWrap: true, - itemCount: _validContacts.length, - itemBuilder: (context, index) { - final contact = _validContacts[index]; - final isSelected = _selectedContacts.contains(contact); - - return ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: isSelected - ? Colors.green - : (contact.type == 2 - ? Colors.blue - : Colors.purple), - child: Icon( - contact.type == 2 - ? Icons.router - : Icons.meeting_room, - size: 16, - color: Colors.white, - ), - ), - title: Text( - contact.name, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - '${contact.typeLabel(l10n)} • ${contact.publicKeyHex.substring(0, 2)}', - style: const TextStyle(fontSize: 10), - ), - trailing: isSelected - ? const Icon( - Icons.check_circle, - color: Colors.green, - ) - : const Icon(Icons.add_circle_outline), - onTap: () => _toggleContact(contact), - ); - }, - ), - ), - ], - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.common_cancel), - ), - TextButton( - onPressed: _validateAndSubmit, - child: Text(l10n.path_setPath), - ), - ], - ); - } -} diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 40dcb59a..bcb4781f 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -67,10 +67,12 @@ class QuickSwitchBar extends StatelessWidget { destinations: [ NavigationDestination( icon: _buildIconWithBadge( + context, const Icon(Icons.people_outline), contactsUnreadCount, ), selectedIcon: _buildIconWithBadge( + context, const Icon(Icons.people), contactsUnreadCount, ), @@ -78,10 +80,12 @@ class QuickSwitchBar extends StatelessWidget { ), NavigationDestination( icon: _buildIconWithBadge( + context, const Icon(Icons.tag), channelsUnreadCount, ), selectedIcon: _buildIconWithBadge( + context, const Icon(Icons.tag), channelsUnreadCount, ), @@ -101,26 +105,9 @@ class QuickSwitchBar extends StatelessWidget { ); } - Widget _buildIconWithBadge(Icon icon, int count) { + Widget _buildIconWithBadge(BuildContext context, Icon icon, int count) { if (count <= 0) return icon; - - return Stack( - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -2, - top: -2, - child: Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Colors.redAccent, - shape: BoxShape.circle, - ), - ), - ), - ], - ); + final label = count > 99 ? '99+' : '$count'; + return Badge(label: Text(label), child: icon); } } diff --git a/lib/widgets/radio_stats_entry.dart b/lib/widgets/radio_stats_entry.dart index eda0848a..d5fbc670 100644 --- a/lib/widgets/radio_stats_entry.dart +++ b/lib/widgets/radio_stats_entry.dart @@ -59,11 +59,15 @@ class _RadioStatsIconButtonState extends State { active: connector.radioStatsAirActivityPulse, ); if (widget.compact) { - return GestureDetector( - onTap: () => pushCompanionRadioStatsScreen(context), - child: Padding( - padding: const EdgeInsets.only(left: 4), - child: dot, + return Semantics( + label: context.l10n.radioStats_tooltip, + button: true, + child: GestureDetector( + onTap: () => pushCompanionRadioStatsScreen(context), + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: dot, + ), ), ); } diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 0973faec..9ba8efd9 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import '../utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; @@ -11,7 +10,7 @@ import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../utils/app_logger.dart'; -import 'path_management_dialog.dart'; +import 'routing_sheet.dart'; class RepeaterLoginDialog extends StatefulWidget { final Contact repeater; @@ -276,7 +275,7 @@ class _RepeaterLoginDialogState extends State { return AlertDialog( title: Row( children: [ - const Icon(Icons.cell_tower, color: Colors.orange), + Icon(Icons.cell_tower, color: Theme.of(context).colorScheme.tertiary), const SizedBox(width: 8), Expanded( child: Column( @@ -288,7 +287,7 @@ class _RepeaterLoginDialogState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -365,9 +364,7 @@ class _RepeaterLoginDialogState extends State { } }, onSubmitted: (_) => _handleLogin(), - autofocus: - !PlatformInfo.isMobile && - _passwordController.text.isEmpty, + autofocus: _passwordController.text.isEmpty, ), const SizedBox(height: 12), CheckboxListTile( @@ -469,14 +466,14 @@ class _RepeaterLoginDialogState extends State { const SizedBox(height: 4), Text( repeater.pathLabel(context.l10n), - style: const TextStyle(fontSize: 11, color: Colors.grey), + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: () => - PathManagementDialog.show(context, contact: repeater), + ContactRoutingSheet.show(context, contact: repeater), icon: const Icon(Icons.timeline, size: 18), label: Text(l10n.login_managePaths), ), @@ -497,12 +494,12 @@ class _RepeaterLoginDialogState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( + SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onPrimary, ), ), const SizedBox(width: 12), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 2641c023..8a737130 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -12,7 +12,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../utils/app_logger.dart'; import '../helpers/snack_bar_builder.dart'; -import 'path_management_dialog.dart'; +import 'routing_sheet.dart'; class RoomLoginDialog extends StatefulWidget { final Contact room; @@ -181,7 +181,7 @@ class _RoomLoginDialogState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.login_failed(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -232,7 +232,7 @@ class _RoomLoginDialogState extends State { return AlertDialog( title: Row( children: [ - const Icon(Icons.group, color: Colors.purple), + Icon(Icons.group, color: Theme.of(context).colorScheme.secondary), const SizedBox(width: 8), Expanded( child: Column( @@ -244,7 +244,7 @@ class _RoomLoginDialogState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -395,14 +395,14 @@ class _RoomLoginDialogState extends State { const SizedBox(height: 4), Text( repeater.pathLabel(context.l10n), - style: const TextStyle(fontSize: 11, color: Colors.grey), + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: () => - PathManagementDialog.show(context, contact: repeater), + ContactRoutingSheet.show(context, contact: repeater), icon: const Icon(Icons.timeline, size: 18), label: Text(l10n.login_managePaths), ), @@ -423,12 +423,12 @@ class _RoomLoginDialogState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( + SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onPrimary, ), ), const SizedBox(width: 12), diff --git a/lib/widgets/routing_sheet.dart b/lib/widgets/routing_sheet.dart new file mode 100644 index 00000000..ddb0bb04 --- /dev/null +++ b/lib/widgets/routing_sheet.dart @@ -0,0 +1,709 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../helpers/path_helper.dart'; +import '../l10n/l10n.dart'; +import '../models/contact.dart'; +import '../models/path_history.dart'; +import '../screens/path_trace_map.dart'; +import '../services/path_history_service.dart'; +import 'path_editor_sheet.dart'; + +enum _RoutingMode { auto, flood, manual } + +enum _PathQuality { strong, good, fair, proven, flood, untested } + +class ContactRoutingSheet { + static Future show(BuildContext context, {required Contact contact}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => DraggableScrollableSheet( + expand: false, + initialChildSize: 0.75, + minChildSize: 0.4, + maxChildSize: 0.95, + builder: (context, scrollController) => _RoutingSheetBody( + contact: contact, + scrollController: scrollController, + ), + ), + ); + } +} + +class _RoutingSheetBody extends StatefulWidget { + final Contact contact; + final ScrollController scrollController; + + const _RoutingSheetBody({ + required this.contact, + required this.scrollController, + }); + + @override + State<_RoutingSheetBody> createState() => _RoutingSheetBodyState(); +} + +class _RoutingSheetBodyState extends State<_RoutingSheetBody> { + int _resolveContactIndex = -1; + String? _syncStatus; + + Contact _resolveContact(MeshCoreConnector connector) { + if (_resolveContactIndex >= 0 && + _resolveContactIndex < connector.contacts.length && + connector.contacts[_resolveContactIndex].publicKeyHex == + widget.contact.publicKeyHex) { + return connector.contacts[_resolveContactIndex]; + } + _resolveContactIndex = connector.contacts.indexWhere( + (c) => c.publicKeyHex == widget.contact.publicKeyHex, + ); + if (_resolveContactIndex == -1) { + return widget.contact; + } + return connector.contacts[_resolveContactIndex]; + } + + _RoutingMode _modeOf(Contact contact) { + final override = contact.pathOverride; + if (override == null) return _RoutingMode.auto; + return override < 0 ? _RoutingMode.flood : _RoutingMode.manual; + } + + Future _selectMode( + MeshCoreConnector connector, + Contact contact, + _RoutingMode mode, + ) async { + switch (mode) { + case _RoutingMode.auto: + setState(() => _syncStatus = null); + await connector.setPathOverride(contact, pathLen: null); + case _RoutingMode.flood: + setState(() => _syncStatus = null); + await connector.setPathOverride(contact, pathLen: -1); + case _RoutingMode.manual: + await _editManualPath(connector, contact); + } + } + + Future _editManualPath( + MeshCoreConnector connector, + Contact contact, + ) async { + final override = contact.pathOverride; + final initial = override != null && override > 0 + ? (contact.pathOverrideBytes ?? Uint8List(0)) + : (contact.pathLength > 0 ? contact.path : Uint8List(0)); + final available = connector.allContacts + .where((c) => c.publicKeyHex != contact.publicKeyHex) + .toList(); + final result = await PathEditorSheet.show( + context, + availableContacts: available, + initialPath: initial, + ); + if (result == null || !mounted) return; + await connector.setPathOverride( + contact, + pathLen: result.length, + pathBytes: result, + ); + await _verifyPath(connector, contact, result); + } + + Future _applyHistoryPath( + MeshCoreConnector connector, + Contact contact, + PathRecord record, + ) async { + final bytes = Uint8List.fromList(record.pathBytes); + await connector.setPathOverride( + contact, + pathLen: bytes.length, + pathBytes: bytes, + ); + await _verifyPath(connector, contact, bytes); + } + + Future _verifyPath( + MeshCoreConnector connector, + Contact contact, + Uint8List bytes, + ) async { + if (!mounted) return; + if (!connector.isConnected) { + setState(() => _syncStatus = context.l10n.chat_pathSavedLocally); + return; + } + setState(() => _syncStatus = null); + final verified = await connector.verifyContactPathOnDevice(contact, bytes); + if (!mounted) return; + setState( + () => _syncStatus = verified + ? context.l10n.chat_pathDeviceConfirmed + : context.l10n.chat_pathDeviceNotConfirmed, + ); + } + + Future _forgetPath( + MeshCoreConnector connector, + Contact contact, + ) async { + await connector.clearContactPath(contact); + if (!mounted) return; + setState(() => _syncStatus = context.l10n.chat_pathCleared); + } + + _PathQuality _qualityOf(PathRecord record, List ranked) { + if (record.pathBytes.isNotEmpty) { + final first = record.pathBytes.first; + for (var i = 0; i < ranked.length && i < 3; i++) { + if (ranked[i].pubkeyFirstByte == first) { + return switch (i) { + 0 => _PathQuality.strong, + 1 => _PathQuality.good, + _ => _PathQuality.fair, + }; + } + } + } + if (record.successCount > 0) return _PathQuality.proven; + if (record.wasFloodDiscovery) return _PathQuality.flood; + return _PathQuality.untested; + } + + String _qualityLabel(BuildContext context, _PathQuality quality) { + final l10n = context.l10n; + return switch (quality) { + _PathQuality.strong => l10n.routing_qualityStrong, + _PathQuality.good => l10n.routing_qualityGood, + _PathQuality.fair => l10n.routing_qualityFair, + _PathQuality.proven => l10n.routing_qualityWorked, + _PathQuality.flood => l10n.routing_qualityFlood, + _PathQuality.untested => l10n.routing_qualityUntested, + }; + } + + IconData _qualityIcon(_PathQuality quality) { + return switch (quality) { + _PathQuality.strong => Icons.signal_cellular_alt, + _PathQuality.good => Icons.signal_cellular_alt_2_bar, + _PathQuality.fair => Icons.signal_cellular_alt_1_bar, + _PathQuality.proven => Icons.check_circle_outline, + _PathQuality.flood => Icons.waves, + _PathQuality.untested => Icons.route, + }; + } + + String _relativeTime(BuildContext context, DateTime time) { + final l10n = context.l10n; + final diff = DateTime.now().difference(time); + if (diff.inSeconds < 60) return l10n.time_justNow; + if (diff.inMinutes < 60) return l10n.time_minutesAgo(diff.inMinutes); + if (diff.inHours < 24) return l10n.time_hoursAgo(diff.inHours); + return l10n.time_daysAgo(diff.inDays); + } + + String _modeHint(BuildContext context, _RoutingMode mode) { + final l10n = context.l10n; + return switch (mode) { + _RoutingMode.auto => l10n.routing_modeAutoHint, + _RoutingMode.flood => l10n.routing_modeFloodHint, + _RoutingMode.manual => l10n.routing_modeManualHint, + }; + } + + String _routeText( + BuildContext context, + MeshCoreConnector connector, + Contact contact, + _RoutingMode mode, + ) { + final l10n = context.l10n; + switch (mode) { + case _RoutingMode.flood: + return l10n.routing_floodBroadcast; + case _RoutingMode.manual: + final bytes = contact.pathOverrideBytes ?? Uint8List(0); + if (bytes.isEmpty) return l10n.routing_directNoHops; + return PathHelper.resolvePathNames(bytes, connector.allContacts); + case _RoutingMode.auto: + if (contact.pathLength < 0) return l10n.routing_noPathYet; + if (contact.pathLength == 0) return l10n.routing_directNoHops; + if (contact.path.isEmpty) return l10n.chat_hopsCount(contact.pathLength); + return PathHelper.resolvePathNames(contact.path, connector.allContacts); + } + } + + Uint8List _displayBytes(Contact contact, _RoutingMode mode) { + return switch (mode) { + _RoutingMode.flood => Uint8List(0), + _RoutingMode.manual => contact.pathOverrideBytes ?? Uint8List(0), + _RoutingMode.auto => contact.path, + }; + } + + void _openPathTrace( + BuildContext context, + MeshCoreConnector connector, + Contact contact, + List pathBytes, + ) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(pathBytes), + flipPathAround: true, + targetContact: contact, + pathHashByteWidth: connector.pathHashByteWidth, + ), + ), + ); + } + + void _showPathDetail( + BuildContext context, + MeshCoreConnector connector, + Contact contact, + List pathBytes, + ) { + final l10n = context.l10n; + final formattedPath = PathHelper.formatPathHex(pathBytes); + final resolvedNames = PathHelper.resolvePathNames( + pathBytes, + connector.allContacts, + ); + + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.chat_fullPath), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(formattedPath), + const SizedBox(height: 8), + SelectableText( + resolvedNames, + style: TextStyle( + fontSize: 13, + color: Theme.of(dialogContext).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => + _openPathTrace(dialogContext, connector, contact, pathBytes), + child: Text(l10n.contacts_pathTrace), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(l10n.common_close), + ), + ], + ), + ); + } + + Widget _currentRouteCard( + BuildContext context, + MeshCoreConnector connector, + Contact contact, + _RoutingMode mode, + ({ + int successCount, + int failureCount, + int lastTripTimeMs, + DateTime? lastUsed, + })? floodStats, + ) { + final l10n = context.l10n; + final theme = Theme.of(context); + final scheme = theme.colorScheme; + final displayBytes = _displayBytes(contact, mode); + + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + mode == _RoutingMode.flood ? Icons.waves : Icons.route, + size: 18, + color: scheme.primary, + ), + const SizedBox(width: 8), + Text(l10n.routing_currentRoute, style: theme.textTheme.titleSmall), + ], + ), + const SizedBox(height: 8), + Text( + _routeText(context, connector, contact, mode), + style: theme.textTheme.bodyMedium, + ), + if (mode == _RoutingMode.flood && + floodStats != null && + (floodStats.successCount > 0 || floodStats.failureCount > 0)) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + _floodStatsLine(context, floodStats), + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ), + if (_syncStatus != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + _syncStatus!, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.tertiary, + ), + ), + ), + Wrap( + spacing: 8, + children: [ + if (displayBytes.isNotEmpty) + TextButton.icon( + icon: const Icon(Icons.map_outlined, size: 18), + label: Text(l10n.contacts_pathTrace), + onPressed: () => _openPathTrace( + context, + connector, + contact, + displayBytes, + ), + ), + if (mode == _RoutingMode.manual) + TextButton.icon( + icon: const Icon(Icons.edit, size: 18), + label: Text(l10n.routing_editPath), + onPressed: () => _editManualPath(connector, contact), + ), + if (mode == _RoutingMode.auto && contact.pathLength >= 0) + TextButton.icon( + icon: const Icon(Icons.restart_alt, size: 18), + label: Text(l10n.routing_forgetPath), + onPressed: () => _forgetPath(connector, contact), + ), + ], + ), + ], + ), + ), + ); + } + + String _floodStatsLine( + BuildContext context, + ({ + int successCount, + int failureCount, + int lastTripTimeMs, + DateTime? lastUsed, + }) stats, + ) { + final l10n = context.l10n; + final parts = [ + l10n.routing_deliveryCounts(stats.successCount, stats.failureCount), + if (stats.lastTripTimeMs > 0) + '${(stats.lastTripTimeMs / 1000).toStringAsFixed(1)}s', + if (stats.lastUsed != null) + l10n.routing_lastWorked(_relativeTime(context, stats.lastUsed!)), + ]; + return parts.join(' • '); + } + + Widget _floodTile( + BuildContext context, + MeshCoreConnector connector, + Contact contact, + _RoutingMode mode, + ({ + int successCount, + int failureCount, + int lastTripTimeMs, + DateTime? lastUsed, + }) stats, + ) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + leading: CircleAvatar( + radius: 18, + backgroundColor: scheme.surfaceContainerHighest, + child: Icon(Icons.waves, size: 18, color: scheme.onSurfaceVariant), + ), + title: Text(l10n.routing_floodDelivery), + subtitle: Text( + _floodStatsLine(context, stats), + style: const TextStyle(fontSize: 11), + ), + trailing: mode == _RoutingMode.flood + ? Icon( + Icons.check_circle, + color: scheme.primary, + semanticLabel: l10n.routing_inUse, + ) + : null, + onTap: mode == _RoutingMode.flood + ? null + : () => _selectMode(connector, contact, _RoutingMode.flood), + ), + ); + } + + Widget _pathRecordTile( + BuildContext context, + MeshCoreConnector connector, + Contact contact, + _RoutingMode mode, + PathHistoryService pathService, + PathRecord record, + _PathQuality quality, + ) { + final l10n = context.l10n; + final theme = Theme.of(context); + final scheme = theme.colorScheme; + + final (Color bg, Color fg) = switch (quality) { + _PathQuality.strong => (scheme.primaryContainer, scheme.onPrimaryContainer), + _PathQuality.good => ( + scheme.secondaryContainer, + scheme.onSecondaryContainer, + ), + _PathQuality.fair => ( + scheme.tertiaryContainer, + scheme.onTertiaryContainer, + ), + _PathQuality.proven => ( + scheme.primaryContainer, + scheme.onPrimaryContainer, + ), + _ => (scheme.surfaceContainerHighest, scheme.onSurfaceVariant), + }; + + final hasBytes = record.pathBytes.isNotEmpty; + final inUse = hasBytes && + ((mode == _RoutingMode.manual && + listEquals(record.pathBytes, contact.pathOverrideBytes)) || + (mode == _RoutingMode.auto && + listEquals(record.pathBytes, contact.path))); + + final title = hasBytes + ? PathHelper.resolvePathNames(record.pathBytes, connector.allContacts) + : l10n.chat_hopsCount(record.hopCount); + + final line1 = + '${l10n.chat_hopsCount(record.hopCount)} • ${_qualityLabel(context, quality)}'; + final line2Parts = [ + record.timestamp != null + ? l10n.routing_lastWorked(_relativeTime(context, record.timestamp!)) + : l10n.routing_neverWorked, + if (record.tripTimeMs > 0) + '${(record.tripTimeMs / 1000).toStringAsFixed(1)}s', + l10n.routing_deliveryCounts(record.successCount, record.failureCount), + ]; + + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + enabled: hasBytes, + leading: CircleAvatar( + radius: 18, + backgroundColor: bg, + child: Icon( + _qualityIcon(quality), + size: 18, + color: fg, + semanticLabel: _qualityLabel(context, quality), + ), + ), + title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + '$line1\n${line2Parts.join(' • ')}', + style: const TextStyle(fontSize: 11), + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (inUse) + Tooltip( + message: l10n.routing_inUse, + child: Icon( + Icons.check_circle, + color: scheme.primary, + semanticLabel: l10n.routing_inUse, + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 20), + tooltip: l10n.chat_removePath, + constraints: const BoxConstraints(minWidth: 44, minHeight: 44), + onPressed: () => pathService.removePathRecord( + contact.publicKeyHex, + record.pathBytes, + ), + ), + ], + ), + onTap: hasBytes && !inUse + ? () => _applyHistoryPath(connector, contact, record) + : null, + onLongPress: hasBytes + ? () => + _showPathDetail(context, connector, contact, record.pathBytes) + : null, + ), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + final scheme = theme.colorScheme; + + return Consumer2( + builder: (context, connector, pathService, _) { + final contact = _resolveContact(connector); + final mode = _modeOf(contact); + final floodStats = pathService.getFloodStats(contact.publicKeyHex); + final hasFloodStats = + floodStats != null && + (floodStats.successCount > 0 || floodStats.failureCount > 0); + + final rankedRepeaters = List.of(connector.directRepeaters) + ..sort((a, b) => b.ranking.compareTo(a.ranking)); + final entries = pathService + .getRecentPaths(contact.publicKeyHex) + .map((r) => (quality: _qualityOf(r, rankedRepeaters), record: r)) + .toList() + ..sort((a, b) { + final byQuality = a.quality.index.compareTo(b.quality.index); + if (byQuality != 0) return byQuality; + final aTime = + a.record.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0); + final bTime = + b.record.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0); + return bTime.compareTo(aTime); + }); + + return ListView( + controller: widget.scrollController, + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + children: [ + Center( + child: Container( + width: 32, + height: 4, + decoration: BoxDecoration( + color: scheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 12), + Text(l10n.routing_title, style: theme.textTheme.titleLarge), + Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + SegmentedButton<_RoutingMode>( + style: const ButtonStyle( + minimumSize: WidgetStatePropertyAll(Size.fromHeight(44)), + ), + segments: [ + ButtonSegment( + value: _RoutingMode.auto, + icon: const Icon(Icons.auto_mode), + label: Text(l10n.routing_modeAuto), + ), + ButtonSegment( + value: _RoutingMode.flood, + icon: const Icon(Icons.waves), + label: Text(l10n.routing_modeFlood), + ), + ButtonSegment( + value: _RoutingMode.manual, + icon: const Icon(Icons.edit_road), + label: Text(l10n.routing_modeManual), + ), + ], + selected: {mode}, + onSelectionChanged: (selection) => + _selectMode(connector, contact, selection.first), + ), + const SizedBox(height: 8), + Text( + _modeHint(context, mode), + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + _currentRouteCard(context, connector, contact, mode, floodStats), + const SizedBox(height: 16), + Text(l10n.routing_knownPaths, style: theme.textTheme.titleSmall), + Text( + l10n.routing_knownPathsHint, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + if (hasFloodStats) + _floodTile(context, connector, contact, mode, floodStats), + if (entries.isEmpty && !hasFloodStats) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + l10n.chat_noPathHistoryYet, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ), + ...entries.map( + (entry) => _pathRecordTile( + context, + connector, + contact, + mode, + pathService, + entry.record, + entry.quality, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 99f20539..21efffd1 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -166,7 +166,7 @@ class _SNRIndicatorState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/widgets/telemetry_location_map.dart b/lib/widgets/telemetry_location_map.dart index b47d16d1..a73fb640 100644 --- a/lib/widgets/telemetry_location_map.dart +++ b/lib/widgets/telemetry_location_map.dart @@ -147,19 +147,19 @@ class _TelemetryLocationMapState extends State { children: [ _MapButton( icon: Icons.add, - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomBy(1), ), const SizedBox(height: 6), _MapButton( icon: Icons.remove, - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomBy(-1), ), const SizedBox(height: 6), _MapButton( icon: Icons.my_location, - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: () => _mapController.move(_position, _initialZoom), ), diff --git a/pubspec.yaml b/pubspec.yaml index 72cca35f..504d61f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: path_provider: ^2.1.5 share_plus: ^12.0.1 build_pipe: ^0.3.1 - material_symbols_icons: ^4.2906.0 + material_symbols_icons: ^4.2928.1 web: ^1.1.1 flutter_svg: ^2.0.10+1 flutter_blue_plus_platform_interface: ^8.2.1 diff --git a/scripts/security/contact_import_clipboard_pocs.py b/scripts/security/contact_import_clipboard_pocs.py new file mode 100644 index 00000000..dcf7ced9 --- /dev/null +++ b/scripts/security/contact_import_clipboard_pocs.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Generate local PoCs for MeshCore clipboard contact import validation gaps. + +The output is a meshcore:// URI suitable for manual testing in a local/dev app +session. It does not connect to BLE/USB/TCP devices or transmit anything. +""" + +from __future__ import annotations + +import argparse +import sys + + +SCHEME = "meshcore://" + + +def uri_from_bytes(payload: bytes) -> str: + return SCHEME + payload.hex() + + +def oversized_payload(size: int) -> bytes: + if size < 1: + raise ValueError("size must be positive") + return b"A" * size + + +def short_malformed_payload() -> bytes: + return b"\x00" + + +def non_advert_like_payload() -> bytes: + # 98 bytes matches the UI's minimum exported-contact length check, but the + # content is intentionally not a valid signed advert/contact packet. + payload = bytearray(98) + payload[0:4] = b"POC!" + payload[36] = 0xFF + payload[-4:] = b"END!" + return bytes(payload) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate meshcore:// clipboard import PoC payloads." + ) + parser.add_argument( + "case", + choices=("short", "non-advert", "oversized"), + help="PoC case to generate.", + ) + parser.add_argument( + "--size", + type=int, + default=4096, + help="Byte length for the oversized case. Default: 4096.", + ) + args = parser.parse_args() + + if args.case == "short": + payload = short_malformed_payload() + elif args.case == "non-advert": + payload = non_advert_like_payload() + else: + payload = oversized_payload(args.size) + + sys.stdout.write(uri_from_bytes(payload)) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart index 1d8174c8..d48ae33d 100644 --- a/test/screens/tcp_flow_test.dart +++ b/test/screens/tcp_flow_test.dart @@ -123,11 +123,13 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(FloatingActionButton, 'TCP')); + final scannerContext = tester.element(find.byType(ScannerScreen)); + final scannerL10n = AppLocalizations.of(scannerContext); + await tester.tap(find.byTooltip(scannerL10n.connectionChoiceTcpLabel)); await tester.pumpAndSettle(); expect(find.byType(TcpScreen), findsOneWidget); - await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth')); + await tester.tap(find.widgetWithText(OutlinedButton, 'Bluetooth')); await tester.pumpAndSettle(); expect(find.byType(TcpScreen), findsNothing); diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 16e5a951..ecd33ee9 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -157,10 +157,18 @@ void main() { ); await tester.pumpAndSettle(); + final context = tester.element(find.byType(ScannerScreen)); + final l10n = AppLocalizations.of(context); if (PlatformInfo.supportsUsbSerial) { - expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget); + expect( + find.byTooltip(l10n.connectionChoiceUsbLabel), + findsOneWidget, + ); } else { - expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing); + expect( + find.byTooltip(l10n.connectionChoiceUsbLabel), + findsNothing, + ); } // ScannerScreen.dispose() schedules disconnect work that debounces notify. @@ -186,13 +194,13 @@ void main() { final context = tester.element(find.byType(ScannerScreen)); final l10n = AppLocalizations.of(context); - expect(find.text(l10n.scanner_scan), findsOneWidget); + expect(find.text(l10n.scanner_scan), findsWidgets); if (PlatformInfo.supportsUsbSerial) { - expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget); + expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsOneWidget); } if (!PlatformInfo.isWeb) { - expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget); + expect(find.byTooltip(l10n.connectionChoiceTcpLabel), findsOneWidget); } await tester.pumpWidget(const SizedBox.shrink()); diff --git a/untranslated.json b/untranslated.json index 9e26dfee..c3a9394b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,987 @@ -{} \ No newline at end of file +{ + "bg": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "de": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "es": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "fr": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "hu": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "it": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "ja": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "ko": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "nl": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "pl": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "pt": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "ru": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "sk": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "sl": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "sv": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "uk": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "zh": [ + "common_undo", + "messageStatus_sent", + "messageStatus_delivered", + "messageStatus_pending", + "messageStatus_failed", + "messageStatus_repeated", + "contacts_moreOptions", + "contacts_searchOpen", + "contacts_searchClose", + "routing_title", + "routing_modeAuto", + "routing_modeFlood", + "routing_modeManual", + "routing_modeAutoHint", + "routing_modeFloodHint", + "routing_modeManualHint", + "routing_currentRoute", + "routing_directNoHops", + "routing_noPathYet", + "routing_floodBroadcast", + "routing_editPath", + "routing_forgetPath", + "routing_knownPaths", + "routing_knownPathsHint", + "routing_inUse", + "routing_qualityStrong", + "routing_qualityGood", + "routing_qualityFair", + "routing_qualityWorked", + "routing_qualityFlood", + "routing_qualityUntested", + "routing_lastWorked", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ] +} From 3c26ce2d93d33801c42d636485c21cb8767c6551 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Thu, 11 Jun 2026 00:10:49 -0700 Subject: [PATCH 04/16] chore: stop tracking .claude worktrees and ignore .claude/ The previous commit accidentally added the Claude Code worktree gitlinks under .claude/worktrees. Untrack them and ignore .claude/. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fa4d28d6..6e4fb526 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ keystore.properties # Cloudflare Wrangler .wrangler + +# Claude Code local working dir (worktrees, jobs, settings) +.claude/ From b1de1b4bf00ed06ce61c0b1b476b519189e22fce Mon Sep 17 00:00:00 2001 From: zjs81 Date: Thu, 11 Jun 2026 00:11:56 -0700 Subject: [PATCH 05/16] chore: untrack .claude/worktrees gitlinks Remove the accidentally committed Claude Code worktree gitlinks. .claude/ is already gitignored (prev commit). Worktree files remain on disk locally. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/worktrees/agent-a01277dd8cf0dedb6 | 1 - .claude/worktrees/agent-a0c7384491c5fc807 | 1 - .claude/worktrees/agent-a1751d6addbf69e1d | 1 - .claude/worktrees/agent-a2fad933ed81b10e2 | 1 - .claude/worktrees/agent-a4d771c23db919c7f | 1 - .claude/worktrees/agent-a6675128f3ee1d6a6 | 1 - .claude/worktrees/agent-a894f10813e684f84 | 1 - .claude/worktrees/agent-a8ba0537ad42768d7 | 1 - .claude/worktrees/agent-a926d49d72757ce68 | 1 - .claude/worktrees/agent-ad2e38b6608f23ab4 | 1 - .claude/worktrees/agent-ada5836798aea71a1 | 1 - .claude/worktrees/agent-aee10bd582e9e36cf | 1 - 12 files changed, 12 deletions(-) delete mode 160000 .claude/worktrees/agent-a01277dd8cf0dedb6 delete mode 160000 .claude/worktrees/agent-a0c7384491c5fc807 delete mode 160000 .claude/worktrees/agent-a1751d6addbf69e1d delete mode 160000 .claude/worktrees/agent-a2fad933ed81b10e2 delete mode 160000 .claude/worktrees/agent-a4d771c23db919c7f delete mode 160000 .claude/worktrees/agent-a6675128f3ee1d6a6 delete mode 160000 .claude/worktrees/agent-a894f10813e684f84 delete mode 160000 .claude/worktrees/agent-a8ba0537ad42768d7 delete mode 160000 .claude/worktrees/agent-a926d49d72757ce68 delete mode 160000 .claude/worktrees/agent-ad2e38b6608f23ab4 delete mode 160000 .claude/worktrees/agent-ada5836798aea71a1 delete mode 160000 .claude/worktrees/agent-aee10bd582e9e36cf diff --git a/.claude/worktrees/agent-a01277dd8cf0dedb6 b/.claude/worktrees/agent-a01277dd8cf0dedb6 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a01277dd8cf0dedb6 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a0c7384491c5fc807 b/.claude/worktrees/agent-a0c7384491c5fc807 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a0c7384491c5fc807 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a1751d6addbf69e1d b/.claude/worktrees/agent-a1751d6addbf69e1d deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a1751d6addbf69e1d +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a2fad933ed81b10e2 b/.claude/worktrees/agent-a2fad933ed81b10e2 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a2fad933ed81b10e2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a4d771c23db919c7f b/.claude/worktrees/agent-a4d771c23db919c7f deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a4d771c23db919c7f +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a6675128f3ee1d6a6 b/.claude/worktrees/agent-a6675128f3ee1d6a6 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a6675128f3ee1d6a6 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a894f10813e684f84 b/.claude/worktrees/agent-a894f10813e684f84 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a894f10813e684f84 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a8ba0537ad42768d7 b/.claude/worktrees/agent-a8ba0537ad42768d7 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a8ba0537ad42768d7 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a926d49d72757ce68 b/.claude/worktrees/agent-a926d49d72757ce68 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-a926d49d72757ce68 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-ad2e38b6608f23ab4 b/.claude/worktrees/agent-ad2e38b6608f23ab4 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-ad2e38b6608f23ab4 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-ada5836798aea71a1 b/.claude/worktrees/agent-ada5836798aea71a1 deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-ada5836798aea71a1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-aee10bd582e9e36cf b/.claude/worktrees/agent-aee10bd582e9e36cf deleted file mode 160000 index e37616fa..00000000 --- a/.claude/worktrees/agent-aee10bd582e9e36cf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 From 26fdf74d69fbdd10a1995b09754944b59b94ded8 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Thu, 11 Jun 2026 00:28:13 -0700 Subject: [PATCH 06/16] Refactor UI code for better readability and consistency - Improved formatting of ListTile icons and text styles in settings_screen.dart, telemetry_screen.dart, usb_screen.dart, gif_picker.dart, path_editor_sheet.dart, repeater_login_dialog.dart, and room_login_dialog.dart for better readability. - Consolidated TextStyle definitions into single lines where applicable. - Updated notification_service.dart to enhance readability of notification ID assignment. - Simplified function signatures in routing_sheet.dart for clarity. - Cleaned up test assertions in usb_flow_test.dart for conciseness. - Removed unused translations in untranslated.json to streamline localization files. --- lib/connector/meshcore_connector.dart | 31 +- lib/connector/meshcore_protocol.dart | 2 +- lib/l10n/app_bg.arb | 97 +- lib/l10n/app_de.arb | 97 +- lib/l10n/app_es.arb | 97 +- lib/l10n/app_fr.arb | 97 +- lib/l10n/app_hu.arb | 97 +- lib/l10n/app_it.arb | 97 +- lib/l10n/app_ja.arb | 97 +- lib/l10n/app_ko.arb | 97 +- lib/l10n/app_localizations_bg.dart | 108 +- lib/l10n/app_localizations_de.dart | 110 ++- lib/l10n/app_localizations_es.dart | 107 +- lib/l10n/app_localizations_fr.dart | 110 ++- lib/l10n/app_localizations_hu.dart | 110 ++- lib/l10n/app_localizations_it.dart | 106 +- lib/l10n/app_localizations_ja.dart | 111 +-- lib/l10n/app_localizations_ko.dart | 110 +-- lib/l10n/app_localizations_nl.dart | 104 +- lib/l10n/app_localizations_pl.dart | 110 ++- lib/l10n/app_localizations_pt.dart | 106 +- lib/l10n/app_localizations_ru.dart | 112 ++- lib/l10n/app_localizations_sk.dart | 106 +- lib/l10n/app_localizations_sl.dart | 107 +- lib/l10n/app_localizations_sv.dart | 106 +- lib/l10n/app_localizations_uk.dart | 108 +- lib/l10n/app_localizations_zh.dart | 111 +-- lib/l10n/app_nl.arb | 97 +- lib/l10n/app_pl.arb | 97 +- lib/l10n/app_pt.arb | 97 +- lib/l10n/app_ru.arb | 97 +- lib/l10n/app_sk.arb | 97 +- lib/l10n/app_sl.arb | 97 +- lib/l10n/app_sv.arb | 97 +- lib/l10n/app_uk.arb | 97 +- lib/l10n/app_zh.arb | 97 +- lib/screens/app_debug_log_screen.dart | 12 +- lib/screens/app_settings_screen.dart | 252 +++-- lib/screens/channel_chat_screen.dart | 369 +++---- lib/screens/channel_message_path_screen.dart | 45 +- lib/screens/channels_screen.dart | 16 +- lib/screens/chat_screen.dart | 360 +++---- lib/screens/chrome_required_screen.dart | 10 +- lib/screens/contacts_screen.dart | 8 +- lib/screens/discovery_screen.dart | 4 +- lib/screens/line_of_sight_map_screen.dart | 32 +- lib/screens/map_cache_screen.dart | 4 +- lib/screens/map_screen.dart | 124 ++- lib/screens/neighbors_screen.dart | 5 +- lib/screens/path_trace_map.dart | 33 +- lib/screens/repeater_cli_screen.dart | 16 +- lib/screens/repeater_hub_screen.dart | 36 +- lib/screens/repeater_settings_screen.dart | 8 +- lib/screens/scanner_screen.dart | 19 +- lib/screens/settings_screen.dart | 10 +- lib/screens/tcp_screen.dart | 5 +- lib/screens/telemetry_screen.dart | 5 +- lib/screens/usb_screen.dart | 27 +- lib/services/notification_service.dart | 3 +- lib/widgets/gif_picker.dart | 27 +- lib/widgets/path_editor_sheet.dart | 9 +- lib/widgets/repeater_login_dialog.dart | 5 +- lib/widgets/room_login_dialog.dart | 5 +- lib/widgets/routing_sheet.dart | 61 +- test/screens/usb_flow_test.dart | 10 +- untranslated.json | 988 +------------------ 66 files changed, 3436 insertions(+), 2596 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index dee34fb0..c9f11dd8 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1484,23 +1484,22 @@ class MeshCoreConnector extends ChangeNotifier { _linuxSystemScanResults ..clear() ..addAll( - systemDevices - .map( - (device) => ScanResult( - device: device, - advertisementData: AdvertisementData( - advName: device.platformName, - txPowerLevel: null, - appearance: null, - connectable: true, - manufacturerData: const >{}, - serviceData: const >{}, - serviceUuids: [Guid(MeshCoreUuids.service)], - ), - rssi: 0, - timeStamp: DateTime.now(), - ), + systemDevices.map( + (device) => ScanResult( + device: device, + advertisementData: AdvertisementData( + advName: device.platformName, + txPowerLevel: null, + appearance: null, + connectable: true, + manufacturerData: const >{}, + serviceData: const >{}, + serviceUuids: [Guid(MeshCoreUuids.service)], ), + rssi: 0, + timeStamp: DateTime.now(), + ), + ), ); _mergeLinuxSystemScanResults(); notifyListeners(); diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 7213b753..7b594894 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -956,7 +956,7 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) { writer.writeBytes(Uint8List(3)); // reserved bytes writer.writeBytes(pubKey); } else { - writer.writeBytes(Uint8List(4)); // reserved bytes + writer.writeBytes(Uint8List(3)); // reserved bytes } return writer.toBytes(); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 496b3e3d..f288b774 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Брой заявки", "telemetry_error": "Неуспешно получаване на данни", "telemetry_noData": "Няма налични данни за телеметрията.", @@ -2353,5 +2353,98 @@ "settings_companionDebugLogSubtitle": "Команди, отговори и сурови данни за протоколите BLE/TCP/USB", "chat_newMessages": "Нови съобщения", "settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)", - "repeater_chanUtil": "Използване на канала" + "repeater_chanUtil": "Използване на канала", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_pending": "Изпращане", + "common_undo": "Отмяни", + "messageStatus_delivered": "Доставен", + "messageStatus_sent": "Изпратено", + "messageStatus_failed": "Не успях да изпратя", + "messageStatus_repeated": "Слушах го многократно", + "contacts_moreOptions": "Повече възможности", + "contacts_searchOpen": "Търсене на контакти", + "contacts_searchClose": "Затвори търсене", + "routing_title": "Маршрутизиране", + "routing_modeAuto": "Автомобил", + "routing_modeFlood": "Наводнение", + "routing_modeManual": "Ръководство", + "routing_modeAutoHint": "Автоматично избира най-известния път, като при липса на информация, използва стратегия за \"запълване\" на празните пространства.", + "routing_modeFloodHint": "Излъчване през всички ретранслатори. Най-надежният начин, но изисква повече време на въздуха.", + "routing_modeManualHint": "Винаги следва точно пътя, който сте определили.", + "routing_currentRoute": "Текущ маршрут", + "routing_directNoHops": "Директ – без превключватели", + "routing_noPathYet": "Все още няма път. Съобщението продължава да се изпраща, докато не бъде открит маршрут.", + "routing_floodBroadcast": "Предаване през всички ретранслатори", + "routing_editPath": "Редактиране на пътя", + "routing_forgetPath": "Забравете за пътя", + "routing_knownPaths": "Известни маршрути", + "routing_knownPathsHint": "Натиснете бутона, за да превключите към него.", + "routing_inUse": "В експлоатация", + "routing_qualityStrong": "Силен първи скок", + "routing_qualityGood": "Добър първи опит", + "routing_qualityFair": "Първият добър скок", + "routing_qualityWorked": "Беше изпълнено/Доведено до край", + "routing_qualityFlood": "Получено чрез информация, разпространена в резултат на навод.", + "routing_qualityUntested": "Не тестван", + "routing_neverWorked": "никога не е потвърдено", + "routing_floodDelivery": "Доставка при навод", + "pathEditor_title": "Създаване на път", + "pathEditor_hopCounter": "{count} от 64 различни вида малц", + "pathEditor_noHops": "Все още няма добавени хмел. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите рецептата без хмел, за да я изпратите директно.", + "pathEditor_addHops": "Добавете хмела в реда, в който е посочено.", + "pathEditor_searchRepeaters": "Търсене на повтори", + "pathEditor_advancedHex": "Разширено: необработен шестничен път", + "pathEditor_hexLabel": "Префикси на шестнадесетична система", + "pathEditor_hexHelper": "Два шест-символни идентификатора на скок, разделени със запетаи", + "pathEditor_invalidTokens": "Невалидно: {tokens}", + "routing_lastWorked": "worked {when}", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_tooManyHops": "Максимум 64 крачета", + "pathEditor_usePath": "Използвайте този маршрут.", + "pathEditor_removeHop": "Премахнете хмела", + "pathEditor_unknownHop": "Неизвестен репитер", + "map_zoomIn": "Увеличи", + "map_zoomOut": "Приближете се по-малко", + "map_centerMap": "Карта на центъра", + "chrome_bluetoothRequiresChromium": "Web Bluetooth изисква браузър, базиран на Chromium.", + "channels_communityShortId": "Идентификационен номер: {id}...", + "pathTrace_legendGpsConfirmed": "GPS потвърдено", + "pathTrace_legendInferred": "Извлечена позиция" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f7cb8040..47062aa6 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Anzahl der Anfragen", "telemetry_error": "Daten konnten nicht abgerufen werden", "telemetry_noData": "Keine Telemetriedaten verfügbar.", @@ -2381,5 +2381,98 @@ "chat_newMessages": "Neue Nachrichten", "settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware", "settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten", - "repeater_chanUtil": "Nutzung des Kanals" + "repeater_chanUtil": "Nutzung des Kanals", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_sent": "Gesendet", + "messageStatus_delivered": "Geliefert", + "common_undo": "Rückgängig machen", + "messageStatus_pending": "Versenden", + "messageStatus_failed": "Nicht gesendet", + "messageStatus_repeated": "Wiederholt gehört", + "contacts_moreOptions": "Weitere Optionen", + "contacts_searchOpen": "Kontakte suchen", + "contacts_searchClose": "Erweiterte Suche", + "routing_title": "Routenplanung", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Überschwemmung", + "routing_modeManual": "Handbuch", + "routing_modeFloodHint": "Übertragung über alle Repeater. Die zuverlässigste Methode, jedoch mit höherem Datenverbrauch.", + "routing_modeAutoHint": "Wählt automatisch den bekanntesten Pfad aus und verwendet eine Flutungsmethode, wenn kein Pfad bekannt ist.", + "routing_modeManualHint": "Sendet immer genau den von Ihnen festgelegten Weg.", + "routing_currentRoute": "Aktuelle Route", + "routing_directNoHops": "Direkt – ohne Zwischenverstärkung", + "routing_noPathYet": "Noch kein Pfad gefunden. Die Nachricht wird gesendet, bis ein Weg entdeckt wurde.", + "routing_floodBroadcast": "Übertragung über jeden Repeater", + "routing_editPath": "Pfad bearbeiten", + "routing_forgetPath": "Vergiss den Weg", + "routing_knownPaths": "Bekannte Routen", + "routing_knownPathsHint": "Wählen Sie den Pfad, um zu diesem zu wechseln.", + "routing_inUse": "Im Gebrauch", + "routing_qualityStrong": "Ein starker erster Sprung", + "routing_qualityGood": "Ein guter erster Schritt", + "routing_qualityFair": "Erster erfolgreicher Schritt", + "routing_qualityWorked": "Hat erfolgreich geliefert", + "routing_qualityFlood": "Information erhalten durch Nachrichten über die Überschwemmung", + "routing_qualityUntested": "Nicht getestet", + "routing_lastWorked": "war beschäftigt {when}", + "routing_neverWorked": "nie bestätigt", + "routing_floodDelivery": "Lieferung bei Überschwemmung", + "pathEditor_title": "Pfad erstellen", + "pathEditor_hopCounter": "{count} von 64 Hopfengewächsen", + "pathEditor_noHops": "Noch keine Hopfen hinzugefügt. Klicken Sie auf die Schaltflächen unten, um sie nacheinander hinzuzufügen, oder speichern Sie die Rezepter ohne Hopfen, um sie direkt zu versenden.", + "pathEditor_addHops": "Fügen Sie die Hopfen in der richtigen Reihenfolge hinzu.", + "pathEditor_searchRepeaters": "Suche nach wiederholten Nachrichten", + "pathEditor_advancedHex": "Fortgeschritten: Roh-Hex-Pfad", + "pathEditor_hexLabel": "Hex-Präfixe", + "pathEditor_hexHelper": "Zwei Hexadezimalzeichen pro Sprung, getrennt durch Kommas", + "pathEditor_invalidTokens": "Ungültig: {tokens}", + "pathEditor_tooManyHops": "Maximal 64 Hopfengreifer", + "pathEditor_usePath": "Verwenden Sie diesen Pfad.", + "pathEditor_removeHop": "Hop entfernen", + "pathEditor_unknownHop": "Unbekannter Repeater", + "map_zoomIn": "Zoomen", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "Auszoomen", + "map_centerMap": "Zentralkarte", + "chrome_bluetoothRequiresChromium": "Web Bluetooth benötigt einen Chromium-Browser.", + "channels_communityShortId": "ID: {id}…", + "pathTrace_legendGpsConfirmed": "GPS-Bestätigung", + "pathTrace_legendInferred": "Abgeleitete Position" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f24fb52a..31b6dc7d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Número de solicitudes", "telemetry_error": "No se pudieron obtener los datos", "telemetry_noData": "No hay datos de telemetría disponibles.", @@ -2381,5 +2381,98 @@ "chat_newMessages": "Nuevos mensajes", "settings_companionDebugLogSubtitle": "Comandos, respuestas y datos brutos para protocolos BLE/TCP/USB", "chat_markAsUnread": "Marcar como no leído", - "repeater_chanUtil": "Utilización del canal" + "repeater_chanUtil": "Utilización del canal", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_pending": "Enviar", + "common_undo": "Deshacer", + "messageStatus_sent": "Sentido", + "messageStatus_delivered": "Entregado", + "messageStatus_failed": "No se pudo enviar", + "messageStatus_repeated": "Escuché repetidamente", + "contacts_moreOptions": "Más opciones", + "contacts_searchOpen": "Buscar contactos", + "contacts_searchClose": "Búsqueda avanzada", + "routing_title": "Ruteo", + "routing_modeAuto": "Coche", + "routing_modeFlood": "Inundación", + "routing_modeManual": "Manual", + "routing_modeAutoHint": "Selecciona automáticamente la ruta más conocida, y si no hay ninguna ruta conocida, utiliza la ruta más directa.", + "routing_modeFloodHint": "Transmisiones a través de todos los repetidores. Es la opción más fiable, pero utiliza más tiempo de transmisión.", + "routing_modeManualHint": "Siempre sigue exactamente la ruta que usted ha definido.", + "routing_currentRoute": "Ruta actual", + "routing_directNoHops": "Directo — sin saltos de repetidor", + "routing_noPathYet": "Aún no hay un camino definido. El mensaje se envía continuamente hasta que se encuentre una ruta.", + "routing_floodBroadcast": "Transmisión a través de todos los repetidores.", + "routing_editPath": "Editar ruta", + "routing_forgetPath": "Olvídate del camino", + "routing_knownPaths": "Rutas conocidas", + "routing_knownPathsHint": "Seleccione una opción para cambiar a esa.", + "routing_inUse": "En uso", + "routing_qualityStrong": "Primer salto exitoso", + "routing_qualityGood": "Primer paso exitoso", + "routing_qualityWorked": "Ha cumplido", + "routing_qualityFair": "Primer salto de calidad", + "routing_qualityFlood": "Se ha escuchado a través de rumores.", + "routing_qualityUntested": "Sin probar", + "routing_lastWorked": "trabajó {when}", + "routing_neverWorked": "nunca confirmado", + "routing_floodDelivery": "Entrega por inundación", + "pathEditor_title": "Crear ruta", + "pathEditor_hopCounter": "{count} de 64 granos de lúpulo", + "pathEditor_noHops": "Aún no se han añadido lúpulos. Haga clic en los repetidores para añadirlos en el orden deseado, o guarde la receta sin lúpulos para enviarla directamente.", + "pathEditor_addHops": "Añadir los lúpulos en el orden adecuado.", + "pathEditor_searchRepeaters": "Buscar repetidores", + "pathEditor_advancedHex": "Avanzado: ruta hexadecimal sin procesar", + "pathEditor_hexLabel": "Prefijos hexadecimales", + "pathEditor_hexHelper": "Dos caracteres hexadecimales por salto, separados por comas.", + "pathEditor_invalidTokens": "Inválido: {tokens}", + "pathEditor_tooManyHops": "Máximo 64 saltos", + "pathEditor_usePath": "Utilice esta ruta.", + "pathEditor_removeHop": "Eliminar el lúpulo", + "pathEditor_unknownHop": "Repetidor desconocido", + "map_zoomIn": "Acercar", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "Acercar", + "map_centerMap": "Mapa del centro", + "chrome_bluetoothRequiresChromium": "Web Bluetooth requiere un navegador Chromium.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "Confirmado mediante GPS", + "pathTrace_legendInferred": "Posición inferida" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 710ee53a..76d6244b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Nombre de requêtes", "telemetry_error": "Impossible de récupérer les données", "telemetry_noData": "Aucune donnée de télémétrie disponible.", @@ -2360,5 +2360,98 @@ "chat_markAsUnread": "Signaler comme non lu", "chat_newMessages": "Nouveaux messages", "settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB", - "repeater_chanUtil": "Utilisation du canal" + "repeater_chanUtil": "Utilisation du canal", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "common_undo": "Annuler", + "messageStatus_sent": "Envoyer", + "messageStatus_delivered": "Livré", + "messageStatus_pending": "Envoyer", + "messageStatus_failed": "Échec de l'envoi", + "messageStatus_repeated": "Répété plusieurs fois", + "contacts_searchOpen": "Rechercher des contacts", + "contacts_moreOptions": "Plus d'options", + "contacts_searchClose": "Recherche avancée", + "routing_title": "Planification des itinéraires", + "routing_modeAuto": "Voiture", + "routing_modeFlood": "Inondation", + "routing_modeManual": "Manuel", + "routing_modeFloodHint": "Diffusion via tous les répéteurs. La méthode la plus fiable, mais qui utilise plus de temps d'antenne.", + "routing_modeAutoHint": "Sélectionne automatiquement le chemin le plus connu, et utilise la méthode de \"inondation\" si aucun chemin n'est connu.", + "routing_modeManualHint": "Il suit toujours le chemin précis que vous avez défini.", + "routing_currentRoute": "Itinéraire actuel", + "routing_directNoHops": "Direct — sans relais", + "routing_noPathYet": "Aucune voie encore trouvée. Le message suivant est envoyé jusqu'à ce qu'une route soit découverte.", + "routing_floodBroadcast": "Diffusion via tous les répéteurs", + "routing_editPath": "Modifier le chemin", + "routing_forgetPath": "Oubliez le chemin", + "routing_knownPaths": "Chemins connus", + "routing_knownPathsHint": "Créez un raccourci pour y accéder.", + "routing_inUse": "En cours d'utilisation", + "routing_qualityStrong": "Première étape réussie", + "routing_qualityGood": "Première étape réussie", + "routing_qualityFair": "Première étape réussie", + "routing_qualityWorked": "A livré", + "routing_qualityFlood": "Rapporté par des informations provenant de plusieurs sources.", + "routing_qualityUntested": "Non testé", + "routing_lastWorked": "a travaillé {when}", + "routing_neverWorked": "jamais confirmé", + "routing_floodDelivery": "Livraison en cas de inondation", + "pathEditor_hopCounter": "{count} parmi 64 houblons", + "pathEditor_title": "Créer un chemin", + "pathEditor_noHops": "Aucun houblon ajouté pour le moment. Cliquez sur les répétiteurs ci-dessous pour les ajouter dans l'ordre souhaité, ou enregistrez sans houblon pour envoyer directement.", + "pathEditor_addHops": "Ajoutez les houblons dans l'ordre souhaité.", + "pathEditor_searchRepeaters": "Rechercher des répétiteurs", + "pathEditor_advancedHex": "Avancé : chemin hexadécimal brut", + "pathEditor_hexLabel": "Préfixes hexadécimaux", + "pathEditor_hexHelper": "Deux caractères hexadécimaux par saut, séparés par des virgules.", + "pathEditor_invalidTokens": "Incorrect : {tokens}", + "pathEditor_tooManyHops": "Maximum 64 sauts", + "pathEditor_usePath": "Utilisez ce chemin.", + "pathEditor_removeHop": "Éliminer le haricot", + "pathEditor_unknownHop": "Répéteur non identifié", + "map_zoomIn": "Zoomez", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "Zoomez", + "map_centerMap": "Carte du centre", + "chrome_bluetoothRequiresChromium": "Web Bluetooth nécessite un navigateur Chromium.", + "channels_communityShortId": "ID : {id}…", + "pathTrace_legendGpsConfirmed": "Le GPS a confirmé.", + "pathTrace_legendInferred": "Position déduite" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 1807fa68..cd5ed8c8 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -1502,7 +1502,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Kérések száma", "telemetry_error": "Nem sikerült lekérni az adatokat", "telemetry_noData": "Nincsenek elérhető telemetriadatok.", @@ -2391,5 +2391,98 @@ "chat_newMessages": "Új üzenetek", "settings_companionDebugLog": "Párhuzamos hibakeresési napló", "settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok", - "repeater_chanUtil": "Csatorna-használat" + "repeater_chanUtil": "Csatorna-használat", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_delivered": "Szállítva", + "common_undo": "Még egyszer", + "messageStatus_sent": "Elküldve", + "messageStatus_pending": "Elküldés", + "messageStatus_failed": "Nem sikerült elküldeni", + "messageStatus_repeated": "Ismételtem", + "contacts_moreOptions": "További lehetőségek", + "contacts_searchOpen": "Keresssz kapcsolatokat", + "contacts_searchClose": "Teljesítse a keresést", + "routing_title": "Útvonal meghatározás", + "routing_modeAuto": "Autó", + "routing_modeFlood": "Áradás", + "routing_modeManual": "Használati útmutató", + "routing_modeAutoHint": "Automatikusan kiválasztja a legismertebb útvonalat, és ha egyik sem ismert, akkor \"vízzel\" tölti ki.", + "routing_modeFloodHint": "Átvisszaadások minden erősítőn keresztül. A legmegbízhatóbb megoldás, de több időt igényel.", + "routing_modeManualHint": "Mindig pontosan az útvonalat követi, amelyet megad.", + "routing_currentRoute": "Jelenlegi útvonal", + "routing_directNoHops": "Közvetlen – nincs átjáró állomás", + "routing_noPathYet": "Még nincs útvonal. A következő üzenet a keresésig vár.", + "routing_floodBroadcast": "Azonnali továbbítás minden erősítőn keresztül.", + "routing_editPath": "Útvonal szerkesztése", + "routing_forgetPath": "Felejtsd el a útvonalat", + "routing_knownPaths": "Jellegzetes útvonalak", + "routing_knownPathsHint": "Készíts egy útvonalat, hogy átválhass rá.", + "routing_inUse": "Használatban", + "routing_qualityStrong": "Erős első lépés", + "routing_qualityGood": "Jó első lépés", + "routing_qualityFair": "Jó első lépés", + "routing_qualityWorked": "Előállított", + "routing_qualityFlood": "Információt hallottam a katasztrófa miatt.", + "routing_qualityUntested": "Vizsgálatnak nem подвержен", + "routing_neverWorked": "sosem megerősítve", + "routing_floodDelivery": "Vízparti szállítás", + "pathEditor_title": "Út megépítése", + "pathEditor_hopCounter": "{count} db 64-ből", + "pathEditor_noHops": "Még nem adtam hozzá a bazsalikomot. A lent található gombokat használhatod, hogy sorrendben adjd hozzá, vagy mentheted anélkül, hogy bazsalikomot adnál hozzá, hogy közvetlenül elküldd.", + "pathEditor_addHops": "Adja hozzá a bazsaidat a megfelelő sorrendben.", + "pathEditor_searchRepeaters": "Ismétlő eszközök keresése", + "pathEditor_advancedHex": "Haladó szint: alapvető hex-út", + "pathEditor_hexLabel": "Hex előtagok", + "pathEditor_hexHelper": "Két hatjegyű szám minden lépésen, amelyek egymástól elválasztják a kommák.", + "pathEditor_invalidTokens": "Érvénytelen: {tokens}", + "routing_lastWorked": "worked {when}", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_tooManyHops": "A maximális szám 64.", + "pathEditor_usePath": "Használja ezt az útvonalat.", + "pathEditor_removeHop": "Távolítsa el a bazsalikomot", + "pathEditor_unknownHop": "Tudatlan erősítő", + "map_zoomIn": "Nagyítva", + "map_zoomOut": "Kicsökkentett nézet", + "map_centerMap": "Központi tér térkép", + "chrome_bluetoothRequiresChromium": "A Web Bluetooth-hoz egy Chromium-alapú böngésző szükséges.", + "channels_communityShortId": "Az azonosító: {id}...", + "pathTrace_legendGpsConfirmed": "GPS-en megerősítve", + "pathTrace_legendInferred": "Feltehető helyzet" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d378d19d..10fc16e4 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Numero di richieste", "telemetry_error": "Impossibile recuperare i dati", "telemetry_noData": "Nessun dato di telemetria disponibile.", @@ -2353,5 +2353,98 @@ "settings_companionDebugLog": "Registro di debug per il supporto", "chat_newMessages": "Nuovi messaggi", "chat_markAsUnread": "Segna come non letto", - "repeater_chanUtil": "Utilizzo del canale" + "repeater_chanUtil": "Utilizzo del canale", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "common_undo": "Annulla", + "messageStatus_delivered": "Consegnato", + "messageStatus_sent": "Invia", + "messageStatus_pending": "Invio", + "messageStatus_failed": "Impossibile inviare", + "messageStatus_repeated": "Sentito ripetutamente", + "contacts_moreOptions": "Ulteriori opzioni", + "contacts_searchOpen": "Cerca contatti", + "contacts_searchClose": "Ricerca avanzata", + "routing_title": "Instradamento", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Inondazione", + "routing_modeManual": "Manuale", + "routing_modeAutoHint": "Seleziona automaticamente il percorso più noto, e in caso di assenza di informazioni, utilizza un percorso casuale.", + "routing_modeFloodHint": "Trasmissioni tramite ogni ripetitore. Il metodo più affidabile, ma richiede più tempo di trasmissione.", + "routing_modeManualHint": "Invia sempre esattamente il percorso che hai definito.", + "routing_currentRoute": "Percorso attuale", + "routing_directNoHops": "Diretto — senza passaggi tramite ripetitori", + "routing_noPathYet": "Al momento non è stata individuata alcuna via. Il messaggio viene inviato ripetutamente finché non viene trovata una rotta.", + "routing_floodBroadcast": "Trasmissione attraverso ogni ripetitore", + "routing_editPath": "Percorso di modifica", + "routing_forgetPath": "Dimentica il percorso", + "routing_knownPaths": "Percorsi noti", + "routing_knownPathsHint": "Seleziona un percorso per accedere a questa opzione.", + "routing_inUse": "In uso", + "routing_qualityStrong": "Primo salto molto deciso", + "routing_qualityGood": "Primo tentativo di successo", + "routing_qualityFair": "Primo salto di qualità", + "routing_qualityWorked": "È stato consegnato", + "routing_qualityFlood": "Ho sentito tramite un messaggio urgente", + "routing_qualityUntested": "Non testato", + "routing_neverWorked": "mai confermato", + "routing_floodDelivery": "Consegna in caso di alluvione", + "pathEditor_title": "Creare percorso", + "pathEditor_hopCounter": "{count} tra 64 varietà di luppolo", + "pathEditor_noHops": "Al momento non ci sono ingredienti aggiuntivi. Per aggiungerli nell'ordine desiderato, cliccate sui ripetitori sottostanti. In alternativa, potete salvare la ricetta senza ingredienti aggiuntivi per inviarla direttamente.", + "pathEditor_addHops": "Aggiungere i luppoli nell'ordine desiderato.", + "pathEditor_searchRepeaters": "Ricerca ripetitori", + "pathEditor_advancedHex": "Avanzato: percorso esadecimale grezzo", + "pathEditor_hexLabel": "Prefissi esadecimali", + "pathEditor_hexHelper": "Due caratteri esadecimali per ogni salto, separati da virgole.", + "pathEditor_invalidTokens": "Non valido: {tokens}", + "routing_lastWorked": "worked {when}", + "pathEditor_tooManyHops": "Massimo 64 orari", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_usePath": "Utilizza questo percorso", + "pathEditor_removeHop": "Rimuovere il luppolo", + "pathEditor_unknownHop": "Ripetitore sconosciuto", + "map_zoomIn": "Ingrandisci", + "map_zoomOut": "Riduci la visualizzazione", + "map_centerMap": "Mappa del centro", + "channels_communityShortId": "ID: {id}...", + "chrome_bluetoothRequiresChromium": "Web Bluetooth richiede un browser basato su Chromium.", + "pathTrace_legendGpsConfirmed": "Il GPS conferma", + "pathTrace_legendInferred": "Posizione dedotta" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index e000a0d9..4afea945 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1502,7 +1502,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "リクエスト数", "telemetry_error": "データを取得できません", "telemetry_noData": "テレメトリデータは利用できません。", @@ -2391,5 +2391,98 @@ "settings_companionDebugLog": "同伴デバッグログ", "chat_newMessages": "新しいメッセージ", "chat_markAsUnread": "未読としてマークする", - "repeater_chanUtil": "チャンネルの利用状況" + "repeater_chanUtil": "チャンネルの利用状況", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_delivered": "配達", + "common_undo": "元に戻す", + "messageStatus_pending": "送信", + "messageStatus_sent": "送信", + "messageStatus_failed": "送信できませんでした", + "contacts_moreOptions": "さらに多くの選択肢", + "messageStatus_repeated": "何度も聞いた", + "contacts_searchOpen": "連絡先を検索する", + "contacts_searchClose": "検索を終了", + "routing_modeFlood": "洪水", + "routing_title": "経路設定", + "routing_modeAuto": "自動車", + "routing_modeManual": "マニュアル", + "routing_modeAutoHint": "最も一般的な経路を自動的に選択し、経路が不明な場合は、水没状態にします。", + "routing_modeFloodHint": "すべてのリピーターを通じて放送。最も信頼性が高いですが、より多くの時間を使用します。", + "routing_modeManualHint": "常に、あなたが設定した正確な経路を辿って移動します。", + "routing_currentRoute": "現在までのルート", + "routing_directNoHops": "直接接続—中継装置を経由しない", + "routing_noPathYet": "まだ経路は確立されていません。「次のメッセージを送信し、経路が特定されるまで待ちます」。", + "routing_floodBroadcast": "すべてのリピーターを通じて放送", + "routing_editPath": "パスの編集", + "routing_forgetPath": "道にこだわらない", + "routing_knownPaths": "既知の経路", + "routing_knownPathsHint": "そのアプリケーションに切り替えるためのショートカットを作成します。", + "routing_inUse": "使用中", + "routing_qualityStrong": "最初の段階で大きな成果を上げる", + "routing_qualityGood": "最初の成功", + "routing_qualityFair": "最初の試みは成功を収めた", + "routing_qualityWorked": "完了しました", + "routing_qualityFlood": "氾濫によって伝聞", + "routing_qualityUntested": "未検証", + "routing_lastWorked": "{when}に勤務", + "routing_neverWorked": "確認されていない", + "routing_floodDelivery": "洪水による配送", + "pathEditor_title": "経路の作成", + "pathEditor_hopCounter": "64個のホップのうち、{count}個", + "pathEditor_noHops": "まだホップは追加されていません。ホップを順番に追加するには、以下の「タップ」ボタンをクリックしてください。または、ホップを一切追加せずに直接送信するには、「保存」ボタンをクリックしてください。", + "pathEditor_addHops": "ホップを、指定された順番に加える", + "pathEditor_searchRepeaters": "繰り返し検索", + "pathEditor_advancedHex": "高度なレベル:生のヘックスパス", + "pathEditor_hexLabel": "ヘックスプレフィックス", + "pathEditor_hexHelper": "各ホップごとに2つのハッシュ文字を、カンマで区切って記述", + "pathEditor_invalidTokens": "無効: {tokens}", + "pathEditor_tooManyHops": "最大64段階", + "pathEditor_usePath": "この経路を使用してください", + "pathEditor_removeHop": "ホップを取り除く", + "pathEditor_unknownHop": "不明な増幅器", + "map_zoomIn": "ズームイン", + "map_zoomOut": "ズームアウト", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_centerMap": "中心地図", + "chrome_bluetoothRequiresChromium": "Web Bluetooth は、Chromium ブラウザが必要です。", + "channels_communityShortId": "ID: {id}…", + "pathTrace_legendGpsConfirmed": "GPSによる確認", + "pathTrace_legendInferred": "推測される位置" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 8f6781e3..f6f804e4 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1502,7 +1502,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "요청 수", "telemetry_error": "데이터를 가져올 수 없습니다", "telemetry_noData": "텔레메트리 데이터는 제공되지 않습니다.", @@ -2391,5 +2391,98 @@ "chat_newMessages": "새로운 메시지", "settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터", "chat_markAsUnread": "미리 읽지 않음으로 표시", - "repeater_chanUtil": "채널 활용도" + "repeater_chanUtil": "채널 활용도", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_pending": "발송", + "messageStatus_sent": "발송", + "messageStatus_delivered": "배송 완료", + "common_undo": "취소", + "messageStatus_failed": "발송 실패", + "messageStatus_repeated": "반복적으로 들었습니다", + "contacts_searchOpen": "연락처 검색", + "contacts_moreOptions": "더 많은 옵션", + "contacts_searchClose": "검색 닫기", + "routing_title": "라우팅", + "routing_modeAuto": "자동", + "routing_modeFlood": "홍수", + "routing_modeManual": "사용 설명서", + "routing_modeAutoHint": "가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없을 경우에는 무작위로 경로를 선택합니다.", + "routing_modeFloodHint": "모든 증폭기를 통해 방송됩니다. 가장 안정적이지만, 더 많은 송출 시간을 사용합니다.", + "routing_modeManualHint": "항상 설정하신 정확한 경로를 따라 이동합니다.", + "routing_currentRoute": "현재 경로", + "routing_directNoHops": "직접 연결 – 중계 장치 사용 없이", + "routing_noPathYet": "아직 경로가 없습니다. 다음 메시지가 도착할 때까지 계속 탐색합니다.", + "routing_floodBroadcast": "모든 증폭기를 통해 방송", + "routing_editPath": "경로 편집", + "routing_forgetPath": "길을 잊어라", + "routing_knownPaths": "알려진 경로", + "routing_knownPathsHint": "해당 항목으로 전환하기 위한 경로를 선택합니다.", + "routing_inUse": "사용 중", + "routing_qualityStrong": "강력한 첫 번째 단계", + "routing_qualityGood": "좋은 첫 시작", + "routing_qualityFair": "처음 시도", + "routing_qualityWorked": "완료됨", + "routing_qualityFlood": "홍수 피해 상황을 통해 들었습니다.", + "routing_qualityUntested": "검증되지 않음", + "routing_lastWorked": "{when}에 일했습니다", + "routing_neverWorked": "확인되지 않음", + "routing_floodDelivery": "홍수 피해 지역 배송", + "pathEditor_title": "경로 만들기", + "pathEditor_hopCounter": "64개의 홉 중 {count}", + "pathEditor_noHops": "현재 홉은 추가되지 않았습니다. 아래의 탭을 사용하여 순서대로 추가하거나, 홉 없이 바로 전송하려면 \"홉 없음\"으로 저장하십시오.", + "pathEditor_addHops": "홉을 순서대로 첨가해주세요.", + "pathEditor_searchRepeaters": "반복 검색", + "pathEditor_advancedHex": "고급: 원시 헥스 경로", + "pathEditor_hexLabel": "헥스 접두사", + "pathEditor_hexHelper": "각 홉마다 2개의 6자리 숫자, 쉼표로 구분", + "pathEditor_invalidTokens": "유효하지 않음: {tokens}", + "pathEditor_tooManyHops": "최대 64개의 홉", + "pathEditor_usePath": "이 경로를 사용하세요", + "pathEditor_removeHop": "홉 제거", + "pathEditor_unknownHop": "알 수 없는 중계기", + "map_zoomIn": "줌 인", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "줌 아웃", + "map_centerMap": "중심 지도", + "chrome_bluetoothRequiresChromium": "웹 블루투스는 크롬 브라우저가 필요합니다.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS 확인 완료", + "pathTrace_legendInferred": "추론된 위치" } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 68742652..d22848c3 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -93,22 +93,22 @@ class AppLocalizationsBg extends AppLocalizations { String get common_disable => 'Деактивирай'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Отмяни'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Изпратено'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Доставен'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Изпращане'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Не успях да изпратя'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Слушах го многократно'; @override String get common_reboot => 'Рестартирай'; @@ -1016,13 +1016,13 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_newGroup => 'Нова група'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Повече възможности'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Търсене на контакти'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Затвори търсене'; @override String get contacts_groupName => 'Група'; @@ -1530,74 +1530,76 @@ class AppLocalizationsBg extends AppLocalizations { String get chat_fullPath => 'Пълен път'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Маршрутизиране'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Автомобил'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Наводнение'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Ръководство'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Автоматично избира най-известния път, като при липса на информация, използва стратегия за \"запълване\" на празните пространства.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Излъчване през всички ретранслатори. Най-надежният начин, но изисква повече време на въздуха.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Винаги следва точно пътя, който сте определили.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Текущ маршрут'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Директ – без превключватели'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Все още няма път. Съобщението продължава да се изпраща, докато не бъде открит маршрут.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Предаване през всички ретранслатори'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Редактиране на пътя'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Забравете за пътя'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Известни маршрути'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Натиснете бутона, за да превключите към него.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'В експлоатация'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Силен първи скок'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Добър първи опит'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Първият добър скок'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Беше изпълнено/Доведено до край'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Получено чрез информация, разпространена в резултат на навод.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Не тестван'; @override String routing_lastWorked(String when) { @@ -1605,7 +1607,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'никога не е потвърдено'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1613,52 +1615,52 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Доставка при навод'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Създаване на път'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count от 64 различни вида малц'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Все още няма добавени хмел. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите рецептата без хмел, за да я изпратите директно.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Добавете хмела в реда, в който е посочено.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Търсене на повтори'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Разширено: необработен шестничен път'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Префикси на шестнадесетична система'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Два шест-символни идентификатора на скок, разделени със запетаи'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Невалидно: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Максимум 64 крачета'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Използвайте този маршрут.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Премахнете хмела'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Неизвестен репитер'; @override String get chat_pathSavedLocally => @@ -4455,26 +4457,26 @@ class AppLocalizationsBg extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Увеличи'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Приближете се по-малко'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Карта на центъра'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth изисква браузър, базиран на Chromium.'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'Идентификационен номер: $id...'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS потвърдено'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Извлечена позиция'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 62df5bb6..98d25a6d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -93,22 +93,22 @@ class AppLocalizationsDe extends AppLocalizations { String get common_disable => 'Deaktivieren'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Rückgängig machen'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Gesendet'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Geliefert'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Versenden'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Nicht gesendet'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Wiederholt gehört'; @override String get common_reboot => 'Neustart'; @@ -1012,13 +1012,13 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_newGroup => 'Neue Gruppe'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Weitere Optionen'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Kontakte suchen'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Erweiterte Suche'; @override String get contacts_groupName => 'Gruppenname'; @@ -1528,82 +1528,84 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_fullPath => 'Vollständiger Pfad'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Routenplanung'; @override String get routing_modeAuto => 'Auto'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Überschwemmung'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Handbuch'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Wählt automatisch den bekanntesten Pfad aus und verwendet eine Flutungsmethode, wenn kein Pfad bekannt ist.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Übertragung über alle Repeater. Die zuverlässigste Methode, jedoch mit höherem Datenverbrauch.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Sendet immer genau den von Ihnen festgelegten Weg.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Aktuelle Route'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Direkt – ohne Zwischenverstärkung'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Noch kein Pfad gefunden. Die Nachricht wird gesendet, bis ein Weg entdeckt wurde.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Übertragung über jeden Repeater'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Pfad bearbeiten'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Vergiss den Weg'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Bekannte Routen'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Wählen Sie den Pfad, um zu diesem zu wechseln.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'Im Gebrauch'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Ein starker erster Sprung'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Ein guter erster Schritt'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Erster erfolgreicher Schritt'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Hat erfolgreich geliefert'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Information erhalten durch Nachrichten über die Überschwemmung'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Nicht getestet'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'war beschäftigt $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nie bestätigt'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1611,52 +1613,54 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Lieferung bei Überschwemmung'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Pfad erstellen'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count von 64 Hopfengewächsen'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Noch keine Hopfen hinzugefügt. Klicken Sie auf die Schaltflächen unten, um sie nacheinander hinzuzufügen, oder speichern Sie die Rezepter ohne Hopfen, um sie direkt zu versenden.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => + 'Fügen Sie die Hopfen in der richtigen Reihenfolge hinzu.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => + 'Suche nach wiederholten Nachrichten'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Fortgeschritten: Roh-Hex-Pfad'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Hex-Präfixe'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Zwei Hexadezimalzeichen pro Sprung, getrennt durch Kommas'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Ungültig: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Maximal 64 Hopfengreifer'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Verwenden Sie diesen Pfad.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Hop entfernen'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Unbekannter Repeater'; @override String get chat_pathSavedLocally => @@ -4474,26 +4478,26 @@ class AppLocalizationsDe extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Zoomen'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Auszoomen'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Zentralkarte'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth benötigt einen Chromium-Browser.'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'ID: $id…'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS-Bestätigung'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Abgeleitete Position'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index aa17930b..e0268b4f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -93,22 +93,22 @@ class AppLocalizationsEs extends AppLocalizations { String get common_disable => 'Desactivar'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Deshacer'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Sentido'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Entregado'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Enviar'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'No se pudo enviar'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Escuché repetidamente'; @override String get common_reboot => 'Reiniciar'; @@ -1011,13 +1011,13 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_newGroup => 'Nuevo Grupo'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Más opciones'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Buscar contactos'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Búsqueda avanzada'; @override String get contacts_groupName => 'Nombre del grupo'; @@ -1527,82 +1527,84 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_fullPath => 'Ruta completa'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Ruteo'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Coche'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Inundación'; @override String get routing_modeManual => 'Manual'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Selecciona automáticamente la ruta más conocida, y si no hay ninguna ruta conocida, utiliza la ruta más directa.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Transmisiones a través de todos los repetidores. Es la opción más fiable, pero utiliza más tiempo de transmisión.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Siempre sigue exactamente la ruta que usted ha definido.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Ruta actual'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Directo — sin saltos de repetidor'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Aún no hay un camino definido. El mensaje se envía continuamente hasta que se encuentre una ruta.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => + 'Transmisión a través de todos los repetidores.'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Editar ruta'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Olvídate del camino'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Rutas conocidas'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Seleccione una opción para cambiar a esa.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'En uso'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Primer salto exitoso'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Primer paso exitoso'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Primer salto de calidad'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Ha cumplido'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Se ha escuchado a través de rumores.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Sin probar'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'trabajó $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nunca confirmado'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1610,52 +1612,53 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Entrega por inundación'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Crear ruta'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count de 64 granos de lúpulo'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Aún no se han añadido lúpulos. Haga clic en los repetidores para añadirlos en el orden deseado, o guarde la receta sin lúpulos para enviarla directamente.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Añadir los lúpulos en el orden adecuado.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Buscar repetidores'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => + 'Avanzado: ruta hexadecimal sin procesar'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Prefijos hexadecimales'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Dos caracteres hexadecimales por salto, separados por comas.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Inválido: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Máximo 64 saltos'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Utilice esta ruta.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Eliminar el lúpulo'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Repetidor desconocido'; @override String get chat_pathSavedLocally => @@ -4461,17 +4464,17 @@ class AppLocalizationsEs extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Acercar'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Acercar'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Mapa del centro'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth requiere un navegador Chromium.'; @override String channels_communityShortId(String id) { @@ -4479,8 +4482,8 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'Confirmado mediante GPS'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Posición inferida'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 69e2af85..7bb38a0d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -93,22 +93,22 @@ class AppLocalizationsFr extends AppLocalizations { String get common_disable => 'Désactiver'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Annuler'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Envoyer'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Livré'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Envoyer'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Échec de l\'envoi'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Répété plusieurs fois'; @override String get common_reboot => 'Redémarrer'; @@ -1017,13 +1017,13 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_newGroup => 'Nouveau Groupe'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Plus d\'options'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Rechercher des contacts'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Recherche avancée'; @override String get contacts_groupName => 'Nom du groupe'; @@ -1533,82 +1533,83 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_fullPath => 'Chemin complet'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Planification des itinéraires'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Voiture'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Inondation'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Manuel'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Sélectionne automatiquement le chemin le plus connu, et utilise la méthode de \"inondation\" si aucun chemin n\'est connu.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Diffusion via tous les répéteurs. La méthode la plus fiable, mais qui utilise plus de temps d\'antenne.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Il suit toujours le chemin précis que vous avez défini.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Itinéraire actuel'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Direct — sans relais'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Aucune voie encore trouvée. Le message suivant est envoyé jusqu\'à ce qu\'une route soit découverte.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Diffusion via tous les répéteurs'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Modifier le chemin'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Oubliez le chemin'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Chemins connus'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => 'Créez un raccourci pour y accéder.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'En cours d\'utilisation'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Première étape réussie'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Première étape réussie'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Première étape réussie'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'A livré'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Rapporté par des informations provenant de plusieurs sources.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Non testé'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'a travaillé $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'jamais confirmé'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1616,52 +1617,53 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Livraison en cas de inondation'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Créer un chemin'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count parmi 64 houblons'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Aucun houblon ajouté pour le moment. Cliquez sur les répétiteurs ci-dessous pour les ajouter dans l\'ordre souhaité, ou enregistrez sans houblon pour envoyer directement.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => + 'Ajoutez les houblons dans l\'ordre souhaité.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Rechercher des répétiteurs'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Avancé : chemin hexadécimal brut'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Préfixes hexadécimaux'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Deux caractères hexadécimaux par saut, séparés par des virgules.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Incorrect : $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Maximum 64 sauts'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Utilisez ce chemin.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Éliminer le haricot'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Répéteur non identifié'; @override String get chat_pathSavedLocally => @@ -4488,26 +4490,26 @@ class AppLocalizationsFr extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Zoomez'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Zoomez'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Carte du centre'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth nécessite un navigateur Chromium.'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'ID : $id…'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'Le GPS a confirmé.'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Position déduite'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index f58b8afc..eef91d09 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -93,22 +93,22 @@ class AppLocalizationsHu extends AppLocalizations { String get common_disable => 'Leteteszt'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Még egyszer'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Elküldve'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Szállítva'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Elküldés'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Nem sikerült elküldeni'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Ismételtem'; @override String get common_reboot => 'Újraindítás'; @@ -1017,13 +1017,13 @@ class AppLocalizationsHu extends AppLocalizations { String get contacts_newGroup => 'Új csoport'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'További lehetőségek'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Keresssz kapcsolatokat'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Teljesítse a keresést'; @override String get contacts_groupName => 'Csoport neve'; @@ -1536,74 +1536,77 @@ class AppLocalizationsHu extends AppLocalizations { String get chat_fullPath => 'Teljes elérési út'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Útvonal meghatározás'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Autó'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Áradás'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Használati útmutató'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Automatikusan kiválasztja a legismertebb útvonalat, és ha egyik sem ismert, akkor \"vízzel\" tölti ki.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Átvisszaadások minden erősítőn keresztül. A legmegbízhatóbb megoldás, de több időt igényel.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Mindig pontosan az útvonalat követi, amelyet megad.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Jelenlegi útvonal'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Közvetlen – nincs átjáró állomás'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Még nincs útvonal. A következő üzenet a keresésig vár.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => + 'Azonnali továbbítás minden erősítőn keresztül.'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Útvonal szerkesztése'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Felejtsd el a útvonalat'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Jellegzetes útvonalak'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Készíts egy útvonalat, hogy átválhass rá.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'Használatban'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Erős első lépés'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Jó első lépés'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Jó első lépés'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Előállított'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Információt hallottam a katasztrófa miatt.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Vizsgálatnak nem подвержен'; @override String routing_lastWorked(String when) { @@ -1611,7 +1614,7 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'sosem megerősítve'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1619,52 +1622,53 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Vízparti szállítás'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Út megépítése'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count db 64-ből'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Még nem adtam hozzá a bazsalikomot. A lent található gombokat használhatod, hogy sorrendben adjd hozzá, vagy mentheted anélkül, hogy bazsalikomot adnál hozzá, hogy közvetlenül elküldd.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => + 'Adja hozzá a bazsaidat a megfelelő sorrendben.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Ismétlő eszközök keresése'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Haladó szint: alapvető hex-út'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Hex előtagok'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Két hatjegyű szám minden lépésen, amelyek egymástól elválasztják a kommák.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Érvénytelen: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'A maximális szám 64.'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Használja ezt az útvonalat.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Távolítsa el a bazsalikomot'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Tudatlan erősítő'; @override String get chat_pathSavedLocally => @@ -4474,26 +4478,26 @@ class AppLocalizationsHu extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Nagyítva'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Kicsökkentett nézet'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Központi tér térkép'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'A Web Bluetooth-hoz egy Chromium-alapú böngésző szükséges.'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'Az azonosító: $id...'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS-en megerősítve'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Feltehető helyzet'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 7a7043bb..89fb77e7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -93,22 +93,22 @@ class AppLocalizationsIt extends AppLocalizations { String get common_disable => 'Disattivare'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Annulla'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Invia'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Consegnato'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Invio'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Impossibile inviare'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Sentito ripetutamente'; @override String get common_reboot => 'Riavvia'; @@ -1013,13 +1013,13 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_newGroup => 'Nuovo Gruppo'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Ulteriori opzioni'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Cerca contatti'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Ricerca avanzata'; @override String get contacts_groupName => 'Nome gruppo'; @@ -1529,74 +1529,77 @@ class AppLocalizationsIt extends AppLocalizations { String get chat_fullPath => 'Percorso Completo'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Instradamento'; @override String get routing_modeAuto => 'Auto'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Inondazione'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Manuale'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Seleziona automaticamente il percorso più noto, e in caso di assenza di informazioni, utilizza un percorso casuale.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Trasmissioni tramite ogni ripetitore. Il metodo più affidabile, ma richiede più tempo di trasmissione.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Invia sempre esattamente il percorso che hai definito.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Percorso attuale'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => + 'Diretto — senza passaggi tramite ripetitori'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Al momento non è stata individuata alcuna via. Il messaggio viene inviato ripetutamente finché non viene trovata una rotta.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => + 'Trasmissione attraverso ogni ripetitore'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Percorso di modifica'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Dimentica il percorso'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Percorsi noti'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Seleziona un percorso per accedere a questa opzione.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'In uso'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Primo salto molto deciso'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Primo tentativo di successo'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Primo salto di qualità'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'È stato consegnato'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Ho sentito tramite un messaggio urgente'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Non testato'; @override String routing_lastWorked(String when) { @@ -1604,7 +1607,7 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'mai confermato'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1612,52 +1615,53 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Consegna in caso di alluvione'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Creare percorso'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count tra 64 varietà di luppolo'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Al momento non ci sono ingredienti aggiuntivi. Per aggiungerli nell\'ordine desiderato, cliccate sui ripetitori sottostanti. In alternativa, potete salvare la ricetta senza ingredienti aggiuntivi per inviarla direttamente.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => + 'Aggiungere i luppoli nell\'ordine desiderato.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Ricerca ripetitori'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Avanzato: percorso esadecimale grezzo'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Prefissi esadecimali'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Due caratteri esadecimali per ogni salto, separati da virgole.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Non valido: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Massimo 64 orari'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Utilizza questo percorso'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Rimuovere il luppolo'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Ripetitore sconosciuto'; @override String get chat_pathSavedLocally => @@ -4466,17 +4470,17 @@ class AppLocalizationsIt extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Ingrandisci'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Riduci la visualizzazione'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Mappa del centro'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth richiede un browser basato su Chromium.'; @override String channels_communityShortId(String id) { @@ -4484,8 +4488,8 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'Il GPS conferma'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Posizione dedotta'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index ee79c6ee..c60816ee 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -93,22 +93,22 @@ class AppLocalizationsJa extends AppLocalizations { String get common_disable => '無効化する'; @override - String get common_undo => 'Undo'; + String get common_undo => '元に戻す'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => '送信'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => '配達'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => '送信'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => '送信できませんでした'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => '何度も聞いた'; @override String get common_reboot => '再起動'; @@ -964,13 +964,13 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_newGroup => '新しいグループ'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'さらに多くの選択肢'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => '連絡先を検索する'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => '検索を終了'; @override String get contacts_groupName => 'グループ名'; @@ -1462,82 +1462,80 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_fullPath => 'フルパス'; @override - String get routing_title => 'Routing'; + String get routing_title => '経路設定'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => '自動車'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => '洪水'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'マニュアル'; @override - String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + String get routing_modeAutoHint => '最も一般的な経路を自動的に選択し、経路が不明な場合は、水没状態にします。'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'すべてのリピーターを通じて放送。最も信頼性が高いですが、より多くの時間を使用します。'; @override - String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + String get routing_modeManualHint => '常に、あなたが設定した正確な経路を辿って移動します。'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => '現在までのルート'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => '直接接続—中継装置を経由しない'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'まだ経路は確立されていません。「次のメッセージを送信し、経路が特定されるまで待ちます」。'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'すべてのリピーターを通じて放送'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'パスの編集'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => '道にこだわらない'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => '既知の経路'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => 'そのアプリケーションに切り替えるためのショートカットを作成します。'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => '使用中'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => '最初の段階で大きな成果を上げる'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => '最初の成功'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => '最初の試みは成功を収めた'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => '完了しました'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => '氾濫によって伝聞'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => '未検証'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return '$whenに勤務'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => '確認されていない'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1545,52 +1543,51 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => '洪水による配送'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => '経路の作成'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '64個のホップのうち、$count個'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'まだホップは追加されていません。ホップを順番に追加するには、以下の「タップ」ボタンをクリックしてください。または、ホップを一切追加せずに直接送信するには、「保存」ボタンをクリックしてください。'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'ホップを、指定された順番に加える'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => '繰り返し検索'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => '高度なレベル:生のヘックスパス'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'ヘックスプレフィックス'; @override - String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + String get pathEditor_hexHelper => '各ホップごとに2つのハッシュ文字を、カンマで区切って記述'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return '無効: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => '最大64段階'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'この経路を使用してください'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'ホップを取り除く'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => '不明な増幅器'; @override String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。'; @@ -4238,26 +4235,26 @@ class AppLocalizationsJa extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'ズームイン'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'ズームアウト'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => '中心地図'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth は、Chromium ブラウザが必要です。'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'ID: $id…'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPSによる確認'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => '推測される位置'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 64c5a6a6..812c2f94 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -93,22 +93,22 @@ class AppLocalizationsKo extends AppLocalizations { String get common_disable => '비활성화'; @override - String get common_undo => 'Undo'; + String get common_undo => '취소'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => '발송'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => '배송 완료'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => '발송'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => '발송 실패'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => '반복적으로 들었습니다'; @override String get common_reboot => '재부팅'; @@ -959,13 +959,13 @@ class AppLocalizationsKo extends AppLocalizations { String get contacts_newGroup => '새로운 그룹'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => '더 많은 옵션'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => '연락처 검색'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => '검색 닫기'; @override String get contacts_groupName => '그룹 이름'; @@ -1457,82 +1457,80 @@ class AppLocalizationsKo extends AppLocalizations { String get chat_fullPath => '전체 경로'; @override - String get routing_title => 'Routing'; + String get routing_title => '라우팅'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => '자동'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => '홍수'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => '사용 설명서'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + '가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없을 경우에는 무작위로 경로를 선택합니다.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + '모든 증폭기를 통해 방송됩니다. 가장 안정적이지만, 더 많은 송출 시간을 사용합니다.'; @override - String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + String get routing_modeManualHint => '항상 설정하신 정확한 경로를 따라 이동합니다.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => '현재 경로'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => '직접 연결 – 중계 장치 사용 없이'; @override - String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + String get routing_noPathYet => '아직 경로가 없습니다. 다음 메시지가 도착할 때까지 계속 탐색합니다.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => '모든 증폭기를 통해 방송'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => '경로 편집'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => '길을 잊어라'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => '알려진 경로'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => '해당 항목으로 전환하기 위한 경로를 선택합니다.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => '사용 중'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => '강력한 첫 번째 단계'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => '좋은 첫 시작'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => '처음 시도'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => '완료됨'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => '홍수 피해 상황을 통해 들었습니다.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => '검증되지 않음'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return '$when에 일했습니다'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => '확인되지 않음'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1540,52 +1538,51 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => '홍수 피해 지역 배송'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => '경로 만들기'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '64개의 홉 중 $count'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + '현재 홉은 추가되지 않았습니다. 아래의 탭을 사용하여 순서대로 추가하거나, 홉 없이 바로 전송하려면 \"홉 없음\"으로 저장하십시오.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => '홉을 순서대로 첨가해주세요.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => '반복 검색'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => '고급: 원시 헥스 경로'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => '헥스 접두사'; @override - String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + String get pathEditor_hexHelper => '각 홉마다 2개의 6자리 숫자, 쉼표로 구분'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return '유효하지 않음: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => '최대 64개의 홉'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => '이 경로를 사용하세요'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => '홉 제거'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => '알 수 없는 중계기'; @override String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결'; @@ -4236,17 +4233,16 @@ class AppLocalizationsKo extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => '줌 인'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => '줌 아웃'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => '중심 지도'; @override - String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + String get chrome_bluetoothRequiresChromium => '웹 블루투스는 크롬 브라우저가 필요합니다.'; @override String channels_communityShortId(String id) { @@ -4254,8 +4250,8 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS 확인 완료'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => '추론된 위치'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6f4d9fbb..94d5e1e3 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -93,22 +93,22 @@ class AppLocalizationsNl extends AppLocalizations { String get common_disable => 'Uitschakelen'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Achterhalen/Annuleren'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Verzonden'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Leverd'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Verzenden'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Niet verzonden'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Hearsay, herhaald'; @override String get common_reboot => 'Herstarten'; @@ -1005,13 +1005,13 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_newGroup => 'Nieuwe Groep'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Meer opties'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Zoek contactpersonen'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Zoeken'; @override String get contacts_groupName => 'Groepnaam'; @@ -1516,74 +1516,74 @@ class AppLocalizationsNl extends AppLocalizations { String get chat_fullPath => 'Volledige Pad'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Routeplanning'; @override String get routing_modeAuto => 'Auto'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Overstroming'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Handleiding'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Selecteert automatisch het bekendste pad, en gebruikt een flood-algoritme als er geen bekend pad is.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Uitzendingen via elke zender. De meest betrouwbare methode, maar vereist meer uitzendtijd.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Stuurt altijd de exacte route die u heeft aangegeven.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Huidige route'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Direct – zonder tussenliggende schakels'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Er is nog geen route gevonden. De berichten blijven binnenkomen totdat een route is ontdekt.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Uitgestoten via elke zender.'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Pad bewerken'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Vergeet het pad'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Bekende routes'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => 'Maak een route om er naartoe te gaan.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'In gebruik'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Sterke eerste sprong'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Een goede eerste stap'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Een goede eerste hop'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Is geleverd'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Hears via een overstroming'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Niet getest'; @override String routing_lastWorked(String when) { @@ -1591,60 +1591,60 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nooit bevestigd'; @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes zijn behaald, $failures zijn mislukt'; } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Levering bij overstroming'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Pad creëren'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count van 64 hopgranen'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Er zijn nog geen hop toegevoegd. Klik op de onderstaande knoppen om ze in de juiste volgorde toe te voegen, of sla de bestelling op zonder hop om deze direct te versturen.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Voeg hop toe in de juiste volgorde.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Zoek naar herhaaldelijke zenders'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Geavanceerd: ruwe hex-pad'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Hex-voorkanten'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Twee hex-tekens per stap, gescheiden door komma\'s'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Ongeldig: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Maximaal 64 hopken'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Gebruik deze route.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Verwijder de hop'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Onbekend type zender'; @override String get chat_pathSavedLocally => @@ -4441,17 +4441,17 @@ class AppLocalizationsNl extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Inzoomen'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Inzoomen'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Centraal overzicht'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth vereist een Chromium-browser.'; @override String channels_communityShortId(String id) { @@ -4459,8 +4459,8 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS-locatie bevestigd'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Afgeleide positie'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index f582d2df..b95dcbfc 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -93,22 +93,22 @@ class AppLocalizationsPl extends AppLocalizations { String get common_disable => 'Wyłącz'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Wycofaj'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Wysłane'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Dostarczone'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Wysyłanie'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Nie udało się wysłać'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Usłyszałem to wielokrotnie'; @override String get common_reboot => 'Uruchom ponownie'; @@ -1023,13 +1023,13 @@ class AppLocalizationsPl extends AppLocalizations { String get contacts_newGroup => 'Nowa Grupa'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Więcej opcji'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Wyszukaj kontakty'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Zaawansowane wyszukiwanie'; @override String get contacts_groupName => 'Nazwa grupy'; @@ -1541,82 +1541,85 @@ class AppLocalizationsPl extends AppLocalizations { String get chat_fullPath => 'Pełna ścieżka'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Planowanie tras'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Samochód'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Powódź'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Instrukcja obsługi'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Automatycznie wybiera najpopularniejszą ścieżkę, a w przypadku braku znanej, przechodzi do trybu \"przepływu\".'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Transmisje za pośrednictwem każdego repeatera. Najbardziej niezawodna metoda, ale zużywa więcej czasu transmisji.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Zawsze prowadzi dokładnie po trasie, którą określiłeś.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Obecna trasa'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => + 'Bezpośrednio – bez pośrednictwa repeaterów'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Na razie nie ma żadnej ścieżki. Komunikacja trwa do momentu, gdy zostanie odkryta trasa.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => + 'Transmisja za pośrednictwem każdego urządzenia powielającego'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Edytuj ścieżkę'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Zapomnij o ścieżce'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Znane trasy'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Wybierz ścieżkę, aby przełączyć się na nią.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'W użyciu'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Silny pierwszy skok'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Świetny początek'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Świetny pierwszy krzak'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Zostało dostarczone'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Usłyszano dzięki doniesieniom'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Nieużywany'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'pracował $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nigdy nie zostało potwierdzone'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1624,52 +1627,53 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Dostawa w przypadku powodzi'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Stworzenie ścieżki'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count z 64 rodzajów chmielu'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Na razie nie dodano żadnych chmielu. Aby dodać je w odpowiedniej kolejności, kliknij w odpowiednie przyciski poniżej, lub zapisz przepis bez chmielu, aby wysłać go bezpośrednio.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Dodawaj chmiel zgodnie z kolejnością.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Funkcje powtarzania'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => + 'Zaawansowane: ścieżka w formacie szesnastkowym'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Prefiksy heksadecymalne'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Dwa znaki szesnastkowe na każdym kroku, oddzielone przecinkami'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Nieprawidłowe: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Maksymalnie 64 hopów'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Użyj tej ścieżki.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Usuń dziką psiankę'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Nieznany repeater'; @override String get chat_pathSavedLocally => @@ -4475,17 +4479,17 @@ class AppLocalizationsPl extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Przybliż'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Przybliż z powrotem'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Mapa centrum'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth wymaga przeglądarki Chromium.'; @override String channels_communityShortId(String id) { @@ -4493,8 +4497,8 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS potwierdzone'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Wywnioskowana pozycja'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 23495d55..09a4fa79 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -93,22 +93,22 @@ class AppLocalizationsPt extends AppLocalizations { String get common_disable => 'Desativar'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Desfazer'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Enviado'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Entregue'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Enviar'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Falhou ao enviar'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Ouvi repetidamente'; @override String get common_reboot => 'Reiniciar'; @@ -1013,13 +1013,13 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_newGroup => 'Novo Grupo'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Mais opções'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Pesquisar contatos'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Pesquisa avançada'; @override String get contacts_groupName => 'Nome do grupo'; @@ -1526,74 +1526,77 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_fullPath => 'Caminho Completo'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Rotas'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Carro'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Inundação'; @override String get routing_modeManual => 'Manual'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Seleciona automaticamente o caminho mais conhecido, e, se nenhum caminho conhecido for encontrado, utiliza a estratégia de \"inundação\".'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Transmissão através de todos os repetidores. É a opção mais confiável, mas utiliza mais tempo de transmissão.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Sempre segue exatamente o caminho que você define.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Rota atual'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Direto – sem saltos de repetidor'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Ainda não há um caminho definido. A mensagem continua a ser enviada até que uma rota seja encontrada.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => + 'Transmissão através de todos os repetidores'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Editar caminho'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Esqueça o caminho'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Rotas conhecidas'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Toque em um caminho para alternar para ele.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'Em uso'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Primeiro salto notável'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Primeiro salto bem-sucedido'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Primeira etapa bem-sucedida'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Foi entregue'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Informação obtida através de relatos generalizados.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Não testado'; @override String routing_lastWorked(String when) { @@ -1601,7 +1604,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nunca confirmado'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1609,52 +1612,53 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => + 'Entrega em áreas afetadas por inundações'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Criar Caminho'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count de 64 gramas de lúpulo'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Ainda não há lúpulos adicionados. Clique nos repetidores abaixo para adicioná-los na ordem desejada, ou salve sem adicionar lúpulos para enviar diretamente.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Adicione os lúpulos na seguinte ordem.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Encontrar repetidores'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Avançado: caminho hexadecimal bruto'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Prefixos hexadecimais'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Dois caracteres hexadecimais por salto, separados por vírgulas.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Inválido: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Máximo de 64 saltos'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Utilize este caminho.'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Remova o lúpulo'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Repetidor desconhecido'; @override String get chat_pathSavedLocally => @@ -4454,17 +4458,17 @@ class AppLocalizationsPt extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Ampliar'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Ampliar'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Mapa do centro'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'O Web Bluetooth requer um navegador Chromium.'; @override String channels_communityShortId(String id) { @@ -4472,8 +4476,8 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS confirmado'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Posição inferida'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index fe535172..6503c5c1 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -93,22 +93,22 @@ class AppLocalizationsRu extends AppLocalizations { String get common_disable => 'Выключить'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Отменить'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Отправлено'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Доставлено'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Отправка'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Не удалось отправить'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Услышал несколько раз'; @override String get common_reboot => 'Перезагрузить'; @@ -1014,13 +1014,13 @@ class AppLocalizationsRu extends AppLocalizations { String get contacts_newGroup => 'Новая группа'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Больше вариантов'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Найти контакты'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Закрыть поиск'; @override String get contacts_groupName => 'Имя группы'; @@ -1529,74 +1529,77 @@ class AppLocalizationsRu extends AppLocalizations { String get chat_fullPath => 'Полный маршрут'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Маршрутизация'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Авто'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Наводнение'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Инструкция'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Автоматически выбирает наиболее известный путь, и если такой путь неизвестен, использует алгоритм поиска пути.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Передача сигнала через все ретрансляторы. Самый надежный способ, но требует больше времени на передачу.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Всегда следует точно по указанному вами маршруту.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Текущий маршрут'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => + 'Прямое соединение – без использования ретрансляторов'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Пока нет пути. Следующее сообщение будет отправлено до тех пор, пока не будет обнаружен маршрут.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Транслируется через все ретрансляторы'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Изменить путь'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Забудьте о маршруте'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Известные маршруты'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Создайте маршрут для переключения на этот пункт.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'В эксплуатации'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Сильный первый скачок'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Хорошее начало'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Первый хороший урожай'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Осуществлено'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Узнал из новостей, распространяющихся в интернете.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Непроверенный'; @override String routing_lastWorked(String when) { @@ -1604,7 +1607,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'никогда не было подтверждено'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1612,52 +1615,55 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Доставка при затоплении'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Создать маршрут'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count из 64 хмеля'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'На данный момент хмель еще не добавлен. Чтобы добавить его, нажмите на соответствующие кнопки ниже в нужном порядке, или сохраните рецепт без хмеля, чтобы отправить его напрямую.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => + 'Добавляйте хмель в соответствии с указанным порядком.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Поиск повторителей'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => + 'Продвинутый уровень: прямой путь в шестнадцатеричном формате'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Префиксы шестнадцатеричной системы'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Два шестнадцатеричных символа на каждом шаге, разделенные запятыми.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Неверно: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => + 'Максимальное количество ингредиентов – 64'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Используйте этот путь'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Удалить хмель'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Неизвестный ретранслятор'; @override String get chat_pathSavedLocally => @@ -4468,26 +4474,26 @@ class AppLocalizationsRu extends AppLocalizations { String get contact_typeUnknown => 'Неизвестно'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Увеличить масштаб'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Увеличить масштаб'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Карта центра'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Для работы Web Bluetooth требуется браузер на основе Chromium.'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'Идентификатор: $id...'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS подтверждено'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Выведенная позиция'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index f15bcfc1..3bb7ac5f 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -93,22 +93,22 @@ class AppLocalizationsSk extends AppLocalizations { String get common_disable => 'Zakázať'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Zrušiť'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Odoslané'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Doručené'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Odoslanie'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Neúspešné odeslanie'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Slyšal som to opakovane'; @override String get common_reboot => 'Restartovať'; @@ -1002,13 +1002,13 @@ class AppLocalizationsSk extends AppLocalizations { String get contacts_newGroup => 'Nová skupina'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Ďalšie možnosti'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Vyhľadajte kontakty'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Zavrieť vyhľadávanie'; @override String get contacts_groupName => 'Názov skupiny'; @@ -1517,74 +1517,77 @@ class AppLocalizationsSk extends AppLocalizations { String get chat_fullPath => 'Celá cesta'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Navigácia'; @override String get routing_modeAuto => 'Auto'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Povodňová vlna'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Ručná príručka'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Automaticky vyberá najznámejší trasa, a ak žiadna nie je známa, použije náhodnú trasu.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Prenos prostredníctvom všetkých opakovačov. Najspoľahlivejší spôsob, ale vyžaduje viac času vysielania.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Vždy dodáva presne podľa zadaného trasy.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Aktuálna trasa'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Priamo – bez prechodných trás'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Zatiaľ neexistuje žiadna cesta. Nasledujúce správy budú pokračovať, kým sa nenájde trasa.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => + 'Prenos prostredníctvom každého opakovača'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Upraviť trasu'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Zabudnite na trasu'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Známe cesty'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Kliknite na cestu, aby ste sa k nej presunuli.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'V prevádzke'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Silný prvý krok'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Úspešný prvý krok'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Prvá, spravodlivá fáza'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Dosiahnutý úspech'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => + 'Zistil som to z informácií, ktoré som získal v dôsledku povodňovej situácie.'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Neotestované'; @override String routing_lastWorked(String when) { @@ -1592,7 +1595,7 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nikedy nebolo potvrdené'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1600,52 +1603,53 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Doručenie v prípade povodní'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Vytvorenie cesty'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count z 64 chmelových zŕš'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Zatiaľ žiadne chmel. Kliknite na opakované, aby ste ich pridali postupne, alebo uložte bez chmelu, aby ste ho mohli poslať priamo.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Pridávajte chmel podľa zadaného poriadku.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Hľadať opakované'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Pokročilé: pôvodná hexová cesta'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Prefiksy pre hexadecimálne čísla'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Dve hexové čísla na každý krok, oddelené čiarkami'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Neplatné: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Maximálne 64 krokov'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Použite túto cestu'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Odstráňte chmel'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => + 'Neznáme zariadenie na opakované vysielanie'; @override String get chat_pathSavedLocally => @@ -4436,17 +4440,17 @@ class AppLocalizationsSk extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Zväčšiť'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Zmenť zamer zblízka'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Mapa centra'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth vyžaduje prehliadač Chromium.'; @override String channels_communityShortId(String id) { @@ -4454,8 +4458,8 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS potvrdilo'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Odvodená poloha'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index c2dc192b..e1efed05 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -93,22 +93,23 @@ class AppLocalizationsSl extends AppLocalizations { String get common_disable => 'Izklopiti'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Preobrn'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Pošljeno'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Dostavljeno'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Pošiljanje'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => + 'Uspešno ni bilo mogo, da se sporočilo pošlje'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Slišal sem večkrat'; @override String get common_reboot => 'Ponoviti'; @@ -1001,13 +1002,13 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_newGroup => 'Nova skupina'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Več možnosti'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Iskanje kontaktov'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Izklopi iskanje'; @override String get contacts_groupName => 'Ime skupine'; @@ -1514,82 +1515,82 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_fullPath => 'Polna pot'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Navigacija'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Avto'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Poplavo'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Navodilo'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Samodejno izbere najbolj poznano pot, in sicer, ko ni na voljo nobena.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Prenosi preko vseh repetitorjev. Najzanesljivejši način, vendar zahteva več časa.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Vedno sledi natančni poti, ki jo ste določili.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Trenutna pot'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Neposredno – brez prehodov'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Žep trenutno ni mogoče najti. Naslednje sporočilo bo posredovano, dokler ne bo ugotovljeno, kje je pot.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Prenos preko vseh repetitiv'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Uredi pot'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Pozabi na pot'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Poznati poti'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => 'Kliknite na pot, da jo izberete.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'V uporabi'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Močan prvi korak'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Prva uspešna faza'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Prva, uspešna faza'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Izpolnil'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Slišano preko poplave'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Ne preizkušen'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'delal/a $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'nikoli ni bilo potrjeno'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1597,52 +1598,52 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Dostava zaradi poplave'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Izgradnja poti'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count od 64 različnih sort hropa'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Še niso dodani hmelji. Za dodajanje hmelja v vrstnem redu kliknite na povezavo spodaj, ali pa shranite brez dodanega hmelja, da ga lahko posredujete neposredno.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Dodajte suho travo v skladu s postopkom.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Iskanje ponovitev'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Napredno: surovi šestnajstni pot'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Predfiks za heksadecimalno šifro'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Dva šestbitna znaka na vsak skok, ločena z vejico'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Neveljaven: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Največ 64 hopov'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Uporabite to poto'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Odstranite hmelj'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Neznani ponovitelj'; @override String get chat_pathSavedLocally => @@ -4438,17 +4439,17 @@ class AppLocalizationsSl extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Povečaj'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Povečajte pogled'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Krajšarska karta'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth zahteva brskalnik Chromium.'; @override String channels_communityShortId(String id) { @@ -4456,8 +4457,8 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS potrdilo'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Izpeljana lokacija'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index ef0df94c..a95e8903 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -93,22 +93,22 @@ class AppLocalizationsSv extends AppLocalizations { String get common_disable => 'Inaktivera'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Ångra'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Sen'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Levererad'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Skicka'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Misslyckades med att skicka'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Hördes upprepade gånger'; @override String get common_reboot => 'Start om'; @@ -996,13 +996,13 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_newGroup => 'Ny grupp'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Fler alternativ'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Sök efter kontakter'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Avancerad sökning'; @override String get contacts_groupName => 'Gruppnamn'; @@ -1508,82 +1508,82 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_fullPath => 'Fullständig sökväg'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Ruttplanering'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Bil'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Översvämning'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Instruktioner'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Väljer automatiskt den bästa kända vägen, och använder en \"flooding\"-strategi om ingen väg är känd.'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Sändningar via alla repetrar. Det mest pålitliga alternativet, men kräver mer sändtid.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Skickar alltid den exakta väg du har angivit.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Nuvarande rutt'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => 'Direkt – utan mellanliggande routrar'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Ingen väg hittad ännu. Nästa meddelande skickas tills en rutt har upptäckts.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Sändas via alla repetrar'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Redigera sökväg'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Glöm vägen'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Kända vägar'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => 'Välj en väg för att byta till den.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'I användning'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'En stark start'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Bra första steg'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Bra första hopp'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Har levererat'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Fått information via nyhetsflöde'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Ej testat'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'arbetade $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'aldrig bekräftat'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1591,52 +1591,52 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Leverans vid översvämningsområde'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Skapa väg'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count av 64 humlor'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Inga humle än. Använd knapparna nedan för att lägga till dem i rätt ordning, eller spara utan humle för att skicka direkt.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Tillsätt humlen i rätt ordning.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Sök efter återupptagna samtal'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => 'Avancerat: rå hex-sökväg'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => 'Hex-prefikser'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Två hex-tecken per steg, separerade med kommatecken.'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Ogiltigt: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Maximalt 64 humlörter'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Använd denna väg'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Ta bort humlen'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Okänd förstärkare'; @override String get chat_pathSavedLocally => @@ -4413,17 +4413,17 @@ class AppLocalizationsSv extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Zooma in'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Zooma ut'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Kartöversikt'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth kräver en Chromium-baserad webbläsare.'; @override String channels_communityShortId(String id) { @@ -4431,8 +4431,8 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS-verifierat'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Antagen position'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index beb14024..989acdb8 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -93,22 +93,22 @@ class AppLocalizationsUk extends AppLocalizations { String get common_disable => 'Вимкнути'; @override - String get common_undo => 'Undo'; + String get common_undo => 'Скасувати'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => 'Надіслано'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => 'Доставлено'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => 'Надсилання'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => 'Не вдалося надіслати'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => 'Почув неодноразово'; @override String get common_reboot => 'Перезавантажити'; @@ -1009,13 +1009,13 @@ class AppLocalizationsUk extends AppLocalizations { String get contacts_newGroup => 'Нова група'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => 'Більше можливостей'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => 'Пошук контактів'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => 'Закрити пошук'; @override String get contacts_groupName => 'Назва групи'; @@ -1524,74 +1524,76 @@ class AppLocalizationsUk extends AppLocalizations { String get chat_fullPath => 'Повний шлях'; @override - String get routing_title => 'Routing'; + String get routing_title => 'Маршрутизація'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Автомобіль'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => 'Повені'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => 'Інструкція'; @override String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + 'Автоматично обирає найкращий відомий шлях, та у разі відсутності відомого шляху, використовує алгоритм \"занурення\".'; @override String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + 'Передавання через усі ретранслятори. Найбільш надійний спосіб, але потребує більше часу.'; @override String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + 'Завжди доставляє точно за вказаним вами маршрутом.'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => 'Поточний маршрут'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => + 'Пряме з\'єднання – без проміжних ретрансляторів'; @override String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + 'Поки що немає жодного шляху. Повідомлення продовжуються надходити, поки не буде знайдено маршрут.'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => 'Поширення через усі ретранслятори'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => 'Редагувати шлях'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => 'Забудь про шлях'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => 'Відомі маршрути'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => + 'Виберіть опцію, щоб переключитися на неї.'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => 'У робочому стані'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => 'Сильний перший стрибок'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => 'Чудова перша спроба'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => 'Перший, але вдалий, крок'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => 'Доставлено'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => 'Дізнався через новини'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => 'Не протестовано'; @override String routing_lastWorked(String when) { @@ -1599,7 +1601,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => 'ніколи не підтверджено'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1607,52 +1609,54 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => 'Доставка під час повені'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => 'Створити маршрут'; @override String pathEditor_hopCounter(int count) { - return '$count of 64 hops'; + return '$count з 64 штук хмелю'; } @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + 'Ще не додано хміль. Натисніть на відповідні кнопки, щоб додати його в потрібному порядку, або збережіть рецепт без хмілю, щоб відправити його безпосередньо.'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => 'Додавайте хміль у наступній послідовності.'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => 'Пошук повторювачів'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => + 'Просунутий рівень: пряма шлях у форматі шестнадцяткової системи.'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => + 'Префікси для шестнадцяткової системи числення'; @override String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + 'Два шестизначні символи на кожний крок, розділені комами'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return 'Неправильно: $tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => 'Максимум 64 хмелеві колоди'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => 'Використовуйте цей шлях'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => 'Видалити хміль'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => 'Невідомий ретранслятор'; @override String get chat_pathSavedLocally => @@ -4469,17 +4473,17 @@ class AppLocalizationsUk extends AppLocalizations { String get contact_typeUnknown => 'Невідомо'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => 'Увійти в режим збільшення'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => 'Видалити зум'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => 'Карта центру'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth вимагає браузера на основі Chromium'; @override String channels_communityShortId(String id) { @@ -4487,8 +4491,8 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => 'GPS підтверджено'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => 'Висновок щодо положення'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 41374057..bf4131b8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -93,22 +93,22 @@ class AppLocalizationsZh extends AppLocalizations { String get common_disable => '禁用'; @override - String get common_undo => 'Undo'; + String get common_undo => '撤销'; @override - String get messageStatus_sent => 'Sent'; + String get messageStatus_sent => '发送'; @override - String get messageStatus_delivered => 'Delivered'; + String get messageStatus_delivered => '已送达'; @override - String get messageStatus_pending => 'Sending'; + String get messageStatus_pending => '发送'; @override - String get messageStatus_failed => 'Failed to send'; + String get messageStatus_failed => '发送失败'; @override - String get messageStatus_repeated => 'Heard repeated'; + String get messageStatus_repeated => '多次听到'; @override String get common_reboot => '重启'; @@ -945,13 +945,13 @@ class AppLocalizationsZh extends AppLocalizations { String get contacts_newGroup => '新建群聊'; @override - String get contacts_moreOptions => 'More options'; + String get contacts_moreOptions => '更多选择'; @override - String get contacts_searchOpen => 'Search contacts'; + String get contacts_searchOpen => '搜索联系人'; @override - String get contacts_searchClose => 'Close search'; + String get contacts_searchClose => '高级搜索'; @override String get contacts_groupName => '群聊名称'; @@ -1437,82 +1437,78 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_fullPath => '完整路径'; @override - String get routing_title => 'Routing'; + String get routing_title => '路由'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => '汽车'; @override - String get routing_modeFlood => 'Flood'; + String get routing_modeFlood => '洪水'; @override - String get routing_modeManual => 'Manual'; + String get routing_modeManual => '手册'; @override - String get routing_modeAutoHint => - 'Picks the best known path automatically, flooding when none is known.'; + String get routing_modeAutoHint => '自动选择已知最佳路径,当没有已知路径时,则进行“洪水”搜索。'; @override - String get routing_modeFloodHint => - 'Broadcasts through every repeater. Most reliable, but uses more airtime.'; + String get routing_modeFloodHint => '通过所有中继站进行广播。 这种方式最可靠,但占用更多的时间。'; @override - String get routing_modeManualHint => - 'Always sends along the exact path you set.'; + String get routing_modeManualHint => '总是按照您设置的路径进行导航。'; @override - String get routing_currentRoute => 'Current route'; + String get routing_currentRoute => '当前路线'; @override - String get routing_directNoHops => 'Direct — no repeater hops'; + String get routing_directNoHops => '直接连接— 无中继跳'; @override - String get routing_noPathYet => - 'No path yet. The next message floods until a route is discovered.'; + String get routing_noPathYet => '目前还没有找到路径。直到找到路径,才会收到后续消息。'; @override - String get routing_floodBroadcast => 'Broadcast through every repeater'; + String get routing_floodBroadcast => '通过所有中继器进行广播'; @override - String get routing_editPath => 'Edit path'; + String get routing_editPath => '编辑路径'; @override - String get routing_forgetPath => 'Forget path'; + String get routing_forgetPath => '忘记原路'; @override - String get routing_knownPaths => 'Known paths'; + String get routing_knownPaths => '已知的路径'; @override - String get routing_knownPathsHint => 'Tap a path to switch to it.'; + String get routing_knownPathsHint => '点击该路径以切换到它。'; @override - String get routing_inUse => 'In use'; + String get routing_inUse => '使用中'; @override - String get routing_qualityStrong => 'Strong first hop'; + String get routing_qualityStrong => '强劲的初始阶段'; @override - String get routing_qualityGood => 'Good first hop'; + String get routing_qualityGood => '不错的开端'; @override - String get routing_qualityFair => 'Fair first hop'; + String get routing_qualityFair => '第一次尝试,结果良好'; @override - String get routing_qualityWorked => 'Has delivered'; + String get routing_qualityWorked => '已完成'; @override - String get routing_qualityFlood => 'Heard via flood'; + String get routing_qualityFlood => '通过新闻报道'; @override - String get routing_qualityUntested => 'Untested'; + String get routing_qualityUntested => '未经测试'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return '工作于 $when'; } @override - String get routing_neverWorked => 'never confirmed'; + String get routing_neverWorked => '从未得到证实'; @override String routing_deliveryCounts(int successes, int failures) { @@ -1520,10 +1516,10 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get routing_floodDelivery => 'Flood delivery'; + String get routing_floodDelivery => '洪水配送'; @override - String get pathEditor_title => 'Build Path'; + String get pathEditor_title => '构建路径'; @override String pathEditor_hopCounter(int count) { @@ -1532,40 +1528,39 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathEditor_noHops => - 'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'; + '目前还没有添加任何啤酒花。点击下面的“添加”按钮,按顺序添加,或者直接保存,不添加任何啤酒花。'; @override - String get pathEditor_addHops => 'Add hops in order'; + String get pathEditor_addHops => '按照顺序添加啤酒花'; @override - String get pathEditor_searchRepeaters => 'Search repeaters'; + String get pathEditor_searchRepeaters => '重复搜索'; @override - String get pathEditor_advancedHex => 'Advanced: raw hex path'; + String get pathEditor_advancedHex => '高级:原始十六进制路径'; @override - String get pathEditor_hexLabel => 'Hex prefixes'; + String get pathEditor_hexLabel => '十六进制前缀'; @override - String get pathEditor_hexHelper => - 'Two hex characters per hop, separated by commas'; + String get pathEditor_hexHelper => '每次跳跃,使用两个十六进制字符,用逗号分隔。'; @override String pathEditor_invalidTokens(String tokens) { - return 'Invalid: $tokens'; + return '无效:$tokens'; } @override - String get pathEditor_tooManyHops => 'Maximum 64 hops'; + String get pathEditor_tooManyHops => '最多 64 个跳跃'; @override - String get pathEditor_usePath => 'Use this path'; + String get pathEditor_usePath => '请使用此路径'; @override - String get pathEditor_removeHop => 'Remove hop'; + String get pathEditor_removeHop => '去除啤酒花'; @override - String get pathEditor_unknownHop => 'Unknown repeater'; + String get pathEditor_unknownHop => '未知的重复器'; @override String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。'; @@ -4128,26 +4123,26 @@ class AppLocalizationsZh extends AppLocalizations { String get contact_typeUnknown => 'Unknown'; @override - String get map_zoomIn => 'Zoom in'; + String get map_zoomIn => '放大'; @override - String get map_zoomOut => 'Zoom out'; + String get map_zoomOut => '放大'; @override - String get map_centerMap => 'Center map'; + String get map_centerMap => '中心地图'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth requires a Chromium browser'; + 'Web Bluetooth 需要 Chromium 浏览器'; @override String channels_communityShortId(String id) { - return 'ID: $id...'; + return 'ID:$id...'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS confirmed'; + String get pathTrace_legendGpsConfirmed => '通过GPS确认'; @override - String get pathTrace_legendInferred => 'Inferred position'; + String get pathTrace_legendInferred => '推测的位置'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index dfcf03b1..7c72900b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Aantal aanvragen", "telemetry_error": "Kan gegevens niet ophalen", "telemetry_noData": "Geen telemetriedata beschikbaar.", @@ -2353,5 +2353,98 @@ "chat_newMessages": "Nieuwe berichten", "chat_markAsUnread": "Markeer als ongelezen", "settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data", - "repeater_chanUtil": "Gebruik van het kanaal" + "repeater_chanUtil": "Gebruik van het kanaal", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_sent": "Verzonden", + "common_undo": "Achterhalen/Annuleren", + "messageStatus_delivered": "Leverd", + "messageStatus_pending": "Verzenden", + "messageStatus_failed": "Niet verzonden", + "messageStatus_repeated": "Hearsay, herhaald", + "contacts_moreOptions": "Meer opties", + "contacts_searchOpen": "Zoek contactpersonen", + "contacts_searchClose": "Zoeken", + "routing_title": "Routeplanning", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Overstroming", + "routing_modeManual": "Handleiding", + "routing_modeAutoHint": "Selecteert automatisch het bekendste pad, en gebruikt een flood-algoritme als er geen bekend pad is.", + "routing_modeFloodHint": "Uitzendingen via elke zender. De meest betrouwbare methode, maar vereist meer uitzendtijd.", + "routing_modeManualHint": "Stuurt altijd de exacte route die u heeft aangegeven.", + "routing_currentRoute": "Huidige route", + "routing_directNoHops": "Direct – zonder tussenliggende schakels", + "routing_noPathYet": "Er is nog geen route gevonden. De berichten blijven binnenkomen totdat een route is ontdekt.", + "routing_floodBroadcast": "Uitgestoten via elke zender.", + "routing_editPath": "Pad bewerken", + "routing_forgetPath": "Vergeet het pad", + "routing_knownPaths": "Bekende routes", + "routing_knownPathsHint": "Maak een route om er naartoe te gaan.", + "routing_inUse": "In gebruik", + "routing_qualityStrong": "Sterke eerste sprong", + "routing_qualityGood": "Een goede eerste stap", + "routing_qualityFair": "Een goede eerste hop", + "routing_qualityWorked": "Is geleverd", + "routing_qualityFlood": "Hears via een overstroming", + "routing_qualityUntested": "Niet getest", + "routing_neverWorked": "nooit bevestigd", + "routing_deliveryCounts": "{successes} zijn behaald, {failures} zijn mislukt", + "routing_floodDelivery": "Levering bij overstroming", + "pathEditor_title": "Pad creëren", + "pathEditor_hopCounter": "{count} van 64 hopgranen", + "pathEditor_noHops": "Er zijn nog geen hop toegevoegd. Klik op de onderstaande knoppen om ze in de juiste volgorde toe te voegen, of sla de bestelling op zonder hop om deze direct te versturen.", + "pathEditor_addHops": "Voeg hop toe in de juiste volgorde.", + "pathEditor_searchRepeaters": "Zoek naar herhaaldelijke zenders", + "pathEditor_advancedHex": "Geavanceerd: ruwe hex-pad", + "pathEditor_hexLabel": "Hex-voorkanten", + "pathEditor_hexHelper": "Twee hex-tekens per stap, gescheiden door komma's", + "pathEditor_invalidTokens": "Ongeldig: {tokens}", + "pathEditor_tooManyHops": "Maximaal 64 hopken", + "pathEditor_usePath": "Gebruik deze route.", + "pathEditor_removeHop": "Verwijder de hop", + "pathEditor_unknownHop": "Onbekend type zender", + "map_zoomIn": "Inzoomen", + "routing_lastWorked": "worked {when}", + "map_zoomOut": "Inzoomen", + "map_centerMap": "Centraal overzicht", + "chrome_bluetoothRequiresChromium": "Web Bluetooth vereist een Chromium-browser.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS-locatie bevestigd", + "pathTrace_legendInferred": "Afgeleide positie" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2dcdc7d8..92fc6797 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1326,7 +1326,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Liczba żądań", "telemetry_error": "Nie udało się pobrać danych", "telemetry_noData": "Brak dostępnych danych telemetrycznych.", @@ -2391,5 +2391,98 @@ "settings_companionDebugLogSubtitle": "Polecenia, odpowiedzi i surowe dane związane z protokołami BLE/TCP/USB", "chat_markAsUnread": "Oznacz jako nieprzeczytane", "settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)", - "repeater_chanUtil": "Wykorzystanie kanału" + "repeater_chanUtil": "Wykorzystanie kanału", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_sent": "Wysłane", + "messageStatus_delivered": "Dostarczone", + "messageStatus_pending": "Wysyłanie", + "common_undo": "Wycofaj", + "messageStatus_failed": "Nie udało się wysłać", + "messageStatus_repeated": "Usłyszałem to wielokrotnie", + "contacts_moreOptions": "Więcej opcji", + "contacts_searchOpen": "Wyszukaj kontakty", + "contacts_searchClose": "Zaawansowane wyszukiwanie", + "routing_title": "Planowanie tras", + "routing_modeAuto": "Samochód", + "routing_modeFlood": "Powódź", + "routing_modeManual": "Instrukcja obsługi", + "routing_modeAutoHint": "Automatycznie wybiera najpopularniejszą ścieżkę, a w przypadku braku znanej, przechodzi do trybu \"przepływu\".", + "routing_modeFloodHint": "Transmisje za pośrednictwem każdego repeatera. Najbardziej niezawodna metoda, ale zużywa więcej czasu transmisji.", + "routing_modeManualHint": "Zawsze prowadzi dokładnie po trasie, którą określiłeś.", + "routing_currentRoute": "Obecna trasa", + "routing_directNoHops": "Bezpośrednio – bez pośrednictwa repeaterów", + "routing_noPathYet": "Na razie nie ma żadnej ścieżki. Komunikacja trwa do momentu, gdy zostanie odkryta trasa.", + "routing_floodBroadcast": "Transmisja za pośrednictwem każdego urządzenia powielającego", + "routing_editPath": "Edytuj ścieżkę", + "routing_forgetPath": "Zapomnij o ścieżce", + "routing_knownPaths": "Znane trasy", + "routing_knownPathsHint": "Wybierz ścieżkę, aby przełączyć się na nią.", + "routing_inUse": "W użyciu", + "routing_qualityStrong": "Silny pierwszy skok", + "routing_qualityGood": "Świetny początek", + "routing_qualityFair": "Świetny pierwszy krzak", + "routing_qualityWorked": "Zostało dostarczone", + "routing_qualityFlood": "Usłyszano dzięki doniesieniom", + "routing_qualityUntested": "Nieużywany", + "routing_lastWorked": "pracował {when}", + "routing_neverWorked": "nigdy nie zostało potwierdzone", + "routing_floodDelivery": "Dostawa w przypadku powodzi", + "pathEditor_title": "Stworzenie ścieżki", + "pathEditor_hopCounter": "{count} z 64 rodzajów chmielu", + "pathEditor_noHops": "Na razie nie dodano żadnych chmielu. Aby dodać je w odpowiedniej kolejności, kliknij w odpowiednie przyciski poniżej, lub zapisz przepis bez chmielu, aby wysłać go bezpośrednio.", + "pathEditor_addHops": "Dodawaj chmiel zgodnie z kolejnością.", + "pathEditor_searchRepeaters": "Funkcje powtarzania", + "pathEditor_advancedHex": "Zaawansowane: ścieżka w formacie szesnastkowym", + "pathEditor_hexLabel": "Prefiksy heksadecymalne", + "pathEditor_hexHelper": "Dwa znaki szesnastkowe na każdym kroku, oddzielone przecinkami", + "pathEditor_invalidTokens": "Nieprawidłowe: {tokens}", + "pathEditor_tooManyHops": "Maksymalnie 64 hopów", + "pathEditor_usePath": "Użyj tej ścieżki.", + "pathEditor_removeHop": "Usuń dziką psiankę", + "pathEditor_unknownHop": "Nieznany repeater", + "map_zoomIn": "Przybliż", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "Przybliż z powrotem", + "map_centerMap": "Mapa centrum", + "chrome_bluetoothRequiresChromium": "Web Bluetooth wymaga przeglądarki Chromium.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS potwierdzone", + "pathTrace_legendInferred": "Wywnioskowana pozycja" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 9dff4417..e9c72cf4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Número de solicitações", "telemetry_error": "Não foi possível obter os dados", "telemetry_noData": "Não estão disponíveis dados de telemetria.", @@ -2353,5 +2353,98 @@ "settings_companionDebugLogSubtitle": "Comandos, respostas e dados brutos para protocolos BLE/TCP/USB", "chat_markAsUnread": "Marcar como não lido", "chat_newMessages": "Novas mensagens", - "repeater_chanUtil": "Utilização do canal" + "repeater_chanUtil": "Utilização do canal", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "common_undo": "Desfazer", + "messageStatus_sent": "Enviado", + "messageStatus_pending": "Enviar", + "messageStatus_delivered": "Entregue", + "messageStatus_failed": "Falhou ao enviar", + "messageStatus_repeated": "Ouvi repetidamente", + "contacts_moreOptions": "Mais opções", + "contacts_searchOpen": "Pesquisar contatos", + "contacts_searchClose": "Pesquisa avançada", + "routing_title": "Rotas", + "routing_modeAuto": "Carro", + "routing_modeFlood": "Inundação", + "routing_modeManual": "Manual", + "routing_modeAutoHint": "Seleciona automaticamente o caminho mais conhecido, e, se nenhum caminho conhecido for encontrado, utiliza a estratégia de \"inundação\".", + "routing_modeFloodHint": "Transmissão através de todos os repetidores. É a opção mais confiável, mas utiliza mais tempo de transmissão.", + "routing_modeManualHint": "Sempre segue exatamente o caminho que você define.", + "routing_currentRoute": "Rota atual", + "routing_directNoHops": "Direto – sem saltos de repetidor", + "routing_noPathYet": "Ainda não há um caminho definido. A mensagem continua a ser enviada até que uma rota seja encontrada.", + "routing_floodBroadcast": "Transmissão através de todos os repetidores", + "routing_editPath": "Editar caminho", + "routing_forgetPath": "Esqueça o caminho", + "routing_knownPaths": "Rotas conhecidas", + "routing_knownPathsHint": "Toque em um caminho para alternar para ele.", + "routing_inUse": "Em uso", + "routing_qualityStrong": "Primeiro salto notável", + "routing_qualityGood": "Primeiro salto bem-sucedido", + "routing_qualityFair": "Primeira etapa bem-sucedida", + "routing_qualityWorked": "Foi entregue", + "routing_qualityFlood": "Informação obtida através de relatos generalizados.", + "routing_qualityUntested": "Não testado", + "routing_neverWorked": "nunca confirmado", + "routing_floodDelivery": "Entrega em áreas afetadas por inundações", + "pathEditor_title": "Criar Caminho", + "pathEditor_hopCounter": "{count} de 64 gramas de lúpulo", + "pathEditor_noHops": "Ainda não há lúpulos adicionados. Clique nos repetidores abaixo para adicioná-los na ordem desejada, ou salve sem adicionar lúpulos para enviar diretamente.", + "pathEditor_addHops": "Adicione os lúpulos na seguinte ordem.", + "pathEditor_searchRepeaters": "Encontrar repetidores", + "pathEditor_advancedHex": "Avançado: caminho hexadecimal bruto", + "pathEditor_hexLabel": "Prefixos hexadecimais", + "pathEditor_hexHelper": "Dois caracteres hexadecimais por salto, separados por vírgulas.", + "pathEditor_invalidTokens": "Inválido: {tokens}", + "routing_lastWorked": "worked {when}", + "pathEditor_tooManyHops": "Máximo de 64 saltos", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_usePath": "Utilize este caminho.", + "pathEditor_removeHop": "Remova o lúpulo", + "pathEditor_unknownHop": "Repetidor desconhecido", + "map_zoomIn": "Ampliar", + "map_zoomOut": "Ampliar", + "map_centerMap": "Mapa do centro", + "chrome_bluetoothRequiresChromium": "O Web Bluetooth requer um navegador Chromium.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS confirmado", + "pathTrace_legendInferred": "Posição inferida" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0ae3eda4..58ddacc8 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -722,7 +722,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Количество запросов", "telemetry_error": "Не удалось получить данные", "neighbors_receivedData": "Полученные данные о соседях", @@ -1656,5 +1656,98 @@ "repeater_cliHelpStatsCore": "(Только для серийного оборудования) Отображает основные статистические данные прошивки.", "settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.", "repeater_chanUtil": "Использование канала", - "settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)" + "settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_pending": "Отправка", + "common_undo": "Отменить", + "messageStatus_delivered": "Доставлено", + "messageStatus_sent": "Отправлено", + "messageStatus_failed": "Не удалось отправить", + "messageStatus_repeated": "Услышал несколько раз", + "contacts_moreOptions": "Больше вариантов", + "contacts_searchOpen": "Найти контакты", + "contacts_searchClose": "Закрыть поиск", + "routing_title": "Маршрутизация", + "routing_modeAuto": "Авто", + "routing_modeFlood": "Наводнение", + "routing_modeManual": "Инструкция", + "routing_modeAutoHint": "Автоматически выбирает наиболее известный путь, и если такой путь неизвестен, использует алгоритм поиска пути.", + "routing_modeFloodHint": "Передача сигнала через все ретрансляторы. Самый надежный способ, но требует больше времени на передачу.", + "routing_modeManualHint": "Всегда следует точно по указанному вами маршруту.", + "routing_currentRoute": "Текущий маршрут", + "routing_directNoHops": "Прямое соединение – без использования ретрансляторов", + "routing_noPathYet": "Пока нет пути. Следующее сообщение будет отправлено до тех пор, пока не будет обнаружен маршрут.", + "routing_floodBroadcast": "Транслируется через все ретрансляторы", + "routing_editPath": "Изменить путь", + "routing_forgetPath": "Забудьте о маршруте", + "routing_knownPaths": "Известные маршруты", + "routing_knownPathsHint": "Создайте маршрут для переключения на этот пункт.", + "routing_inUse": "В эксплуатации", + "routing_qualityStrong": "Сильный первый скачок", + "routing_qualityGood": "Хорошее начало", + "routing_qualityFair": "Первый хороший урожай", + "routing_qualityWorked": "Осуществлено", + "routing_qualityFlood": "Узнал из новостей, распространяющихся в интернете.", + "routing_qualityUntested": "Непроверенный", + "routing_neverWorked": "никогда не было подтверждено", + "routing_floodDelivery": "Доставка при затоплении", + "pathEditor_title": "Создать маршрут", + "pathEditor_hopCounter": "{count} из 64 хмеля", + "pathEditor_noHops": "На данный момент хмель еще не добавлен. Чтобы добавить его, нажмите на соответствующие кнопки ниже в нужном порядке, или сохраните рецепт без хмеля, чтобы отправить его напрямую.", + "pathEditor_addHops": "Добавляйте хмель в соответствии с указанным порядком.", + "pathEditor_searchRepeaters": "Поиск повторителей", + "pathEditor_advancedHex": "Продвинутый уровень: прямой путь в шестнадцатеричном формате", + "pathEditor_hexLabel": "Префиксы шестнадцатеричной системы", + "pathEditor_hexHelper": "Два шестнадцатеричных символа на каждом шаге, разделенные запятыми.", + "pathEditor_invalidTokens": "Неверно: {tokens}", + "routing_lastWorked": "worked {when}", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_tooManyHops": "Максимальное количество ингредиентов – 64", + "pathEditor_usePath": "Используйте этот путь", + "pathEditor_removeHop": "Удалить хмель", + "pathEditor_unknownHop": "Неизвестный ретранслятор", + "map_zoomIn": "Увеличить масштаб", + "map_zoomOut": "Увеличить масштаб", + "map_centerMap": "Карта центра", + "chrome_bluetoothRequiresChromium": "Для работы Web Bluetooth требуется браузер на основе Chromium.", + "channels_communityShortId": "Идентификатор: {id}...", + "pathTrace_legendGpsConfirmed": "GPS подтверждено", + "pathTrace_legendInferred": "Выведенная позиция" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 3ade2e0d..2a8ac872 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Počet požiadaviek", "telemetry_error": "Nepodarilo sa získať údaje", "telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.", @@ -2353,5 +2353,98 @@ "settings_companionDebugLogSubtitle": "Príkazy, odpovede a surové dáta pre protokoly BLE/TCP/USB", "settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)", "chat_newMessages": "Nové správy", - "repeater_chanUtil": "Využitie kanálu" + "repeater_chanUtil": "Využitie kanálu", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_sent": "Odoslané", + "messageStatus_delivered": "Doručené", + "messageStatus_pending": "Odoslanie", + "common_undo": "Zrušiť", + "messageStatus_failed": "Neúspešné odeslanie", + "messageStatus_repeated": "Slyšal som to opakovane", + "contacts_moreOptions": "Ďalšie možnosti", + "contacts_searchOpen": "Vyhľadajte kontakty", + "contacts_searchClose": "Zavrieť vyhľadávanie", + "routing_title": "Navigácia", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Povodňová vlna", + "routing_modeManual": "Ručná príručka", + "routing_modeAutoHint": "Automaticky vyberá najznámejší trasa, a ak žiadna nie je známa, použije náhodnú trasu.", + "routing_modeFloodHint": "Prenos prostredníctvom všetkých opakovačov. Najspoľahlivejší spôsob, ale vyžaduje viac času vysielania.", + "routing_modeManualHint": "Vždy dodáva presne podľa zadaného trasy.", + "routing_currentRoute": "Aktuálna trasa", + "routing_directNoHops": "Priamo – bez prechodných trás", + "routing_noPathYet": "Zatiaľ neexistuje žiadna cesta. Nasledujúce správy budú pokračovať, kým sa nenájde trasa.", + "routing_floodBroadcast": "Prenos prostredníctvom každého opakovača", + "routing_editPath": "Upraviť trasu", + "routing_forgetPath": "Zabudnite na trasu", + "routing_knownPaths": "Známe cesty", + "routing_knownPathsHint": "Kliknite na cestu, aby ste sa k nej presunuli.", + "routing_inUse": "V prevádzke", + "routing_qualityStrong": "Silný prvý krok", + "routing_qualityGood": "Úspešný prvý krok", + "routing_qualityFair": "Prvá, spravodlivá fáza", + "routing_qualityWorked": "Dosiahnutý úspech", + "routing_qualityFlood": "Zistil som to z informácií, ktoré som získal v dôsledku povodňovej situácie.", + "routing_qualityUntested": "Neotestované", + "routing_neverWorked": "nikedy nebolo potvrdené", + "routing_floodDelivery": "Doručenie v prípade povodní", + "pathEditor_title": "Vytvorenie cesty", + "pathEditor_hopCounter": "{count} z 64 chmelových zŕš", + "pathEditor_noHops": "Zatiaľ žiadne chmel. Kliknite na opakované, aby ste ich pridali postupne, alebo uložte bez chmelu, aby ste ho mohli poslať priamo.", + "pathEditor_addHops": "Pridávajte chmel podľa zadaného poriadku.", + "pathEditor_searchRepeaters": "Hľadať opakované", + "pathEditor_advancedHex": "Pokročilé: pôvodná hexová cesta", + "pathEditor_hexLabel": "Prefiksy pre hexadecimálne čísla", + "pathEditor_hexHelper": "Dve hexové čísla na každý krok, oddelené čiarkami", + "routing_lastWorked": "worked {when}", + "pathEditor_invalidTokens": "Neplatné: {tokens}", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_tooManyHops": "Maximálne 64 krokov", + "pathEditor_usePath": "Použite túto cestu", + "pathEditor_removeHop": "Odstráňte chmel", + "pathEditor_unknownHop": "Neznáme zariadenie na opakované vysielanie", + "map_zoomIn": "Zväčšiť", + "map_zoomOut": "Zmenť zamer zblízka", + "map_centerMap": "Mapa centra", + "chrome_bluetoothRequiresChromium": "Web Bluetooth vyžaduje prehliadač Chromium.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS potvrdilo", + "pathTrace_legendInferred": "Odvodená poloha" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index fa3e2676..0c7ce198 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Število zahtev", "telemetry_error": "Podatkov ni bilo mogoče pridobiti", "telemetry_noData": "Niso na voljo podatki o telemetriji.", @@ -2353,5 +2353,98 @@ "chat_markAsUnread": "Označiti kot neneobdelano", "chat_newMessages": "Nove novice", "settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.", - "repeater_chanUtil": "Uporaba kanala" + "repeater_chanUtil": "Uporaba kanala", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "common_undo": "Preobrn", + "messageStatus_delivered": "Dostavljeno", + "messageStatus_sent": "Pošljeno", + "messageStatus_pending": "Pošiljanje", + "messageStatus_failed": "Uspešno ni bilo mogo, da se sporočilo pošlje", + "messageStatus_repeated": "Slišal sem večkrat", + "contacts_moreOptions": "Več možnosti", + "contacts_searchOpen": "Iskanje kontaktov", + "contacts_searchClose": "Izklopi iskanje", + "routing_title": "Navigacija", + "routing_modeAuto": "Avto", + "routing_modeFlood": "Poplavo", + "routing_modeManual": "Navodilo", + "routing_modeAutoHint": "Samodejno izbere najbolj poznano pot, in sicer, ko ni na voljo nobena.", + "routing_modeFloodHint": "Prenosi preko vseh repetitorjev. Najzanesljivejši način, vendar zahteva več časa.", + "routing_modeManualHint": "Vedno sledi natančni poti, ki jo ste določili.", + "routing_currentRoute": "Trenutna pot", + "routing_directNoHops": "Neposredno – brez prehodov", + "routing_noPathYet": "Žep trenutno ni mogoče najti. Naslednje sporočilo bo posredovano, dokler ne bo ugotovljeno, kje je pot.", + "routing_floodBroadcast": "Prenos preko vseh repetitiv", + "routing_editPath": "Uredi pot", + "routing_forgetPath": "Pozabi na pot", + "routing_knownPaths": "Poznati poti", + "routing_knownPathsHint": "Kliknite na pot, da jo izberete.", + "routing_inUse": "V uporabi", + "routing_qualityStrong": "Močan prvi korak", + "routing_qualityGood": "Prva uspešna faza", + "routing_qualityFair": "Prva, uspešna faza", + "routing_qualityWorked": "Izpolnil", + "routing_qualityFlood": "Slišano preko poplave", + "routing_qualityUntested": "Ne preizkušen", + "routing_lastWorked": "delal/a {when}", + "routing_neverWorked": "nikoli ni bilo potrjeno", + "routing_floodDelivery": "Dostava zaradi poplave", + "pathEditor_title": "Izgradnja poti", + "pathEditor_hopCounter": "{count} od 64 različnih sort hropa", + "pathEditor_noHops": "Še niso dodani hmelji. Za dodajanje hmelja v vrstnem redu kliknite na povezavo spodaj, ali pa shranite brez dodanega hmelja, da ga lahko posredujete neposredno.", + "pathEditor_addHops": "Dodajte suho travo v skladu s postopkom.", + "pathEditor_searchRepeaters": "Iskanje ponovitev", + "pathEditor_advancedHex": "Napredno: surovi šestnajstni pot", + "pathEditor_hexLabel": "Predfiks za heksadecimalno šifro", + "pathEditor_hexHelper": "Dva šestbitna znaka na vsak skok, ločena z vejico", + "pathEditor_invalidTokens": "Neveljaven: {tokens}", + "pathEditor_tooManyHops": "Največ 64 hopov", + "pathEditor_usePath": "Uporabite to poto", + "pathEditor_removeHop": "Odstranite hmelj", + "pathEditor_unknownHop": "Neznani ponovitelj", + "map_zoomIn": "Povečaj", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "Povečajte pogled", + "map_centerMap": "Krajšarska karta", + "chrome_bluetoothRequiresChromium": "Web Bluetooth zahteva brskalnik Chromium.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS potrdilo", + "pathTrace_legendInferred": "Izpeljana lokacija" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 1658f186..e5ae55d8 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1316,7 +1316,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Antal förfrågningar", "telemetry_error": "Det gick inte att hämta data", "telemetry_noData": "Inga telemetridata tillgängliga.", @@ -2353,5 +2353,98 @@ "settings_companionDebugLog": "Följande felsökningslogg", "chat_newMessages": "Nya meddelanden", "settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata", - "repeater_chanUtil": "Användning av kanal" + "repeater_chanUtil": "Användning av kanal", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_sent": "Sen", + "messageStatus_delivered": "Levererad", + "common_undo": "Ångra", + "messageStatus_pending": "Skicka", + "messageStatus_failed": "Misslyckades med att skicka", + "messageStatus_repeated": "Hördes upprepade gånger", + "contacts_moreOptions": "Fler alternativ", + "contacts_searchOpen": "Sök efter kontakter", + "contacts_searchClose": "Avancerad sökning", + "routing_title": "Ruttplanering", + "routing_modeAuto": "Bil", + "routing_modeFlood": "Översvämning", + "routing_modeManual": "Instruktioner", + "routing_modeAutoHint": "Väljer automatiskt den bästa kända vägen, och använder en \"flooding\"-strategi om ingen väg är känd.", + "routing_modeFloodHint": "Sändningar via alla repetrar. Det mest pålitliga alternativet, men kräver mer sändtid.", + "routing_modeManualHint": "Skickar alltid den exakta väg du har angivit.", + "routing_currentRoute": "Nuvarande rutt", + "routing_directNoHops": "Direkt – utan mellanliggande routrar", + "routing_noPathYet": "Ingen väg hittad ännu. Nästa meddelande skickas tills en rutt har upptäckts.", + "routing_floodBroadcast": "Sändas via alla repetrar", + "routing_editPath": "Redigera sökväg", + "routing_forgetPath": "Glöm vägen", + "routing_knownPaths": "Kända vägar", + "routing_knownPathsHint": "Välj en väg för att byta till den.", + "routing_inUse": "I användning", + "routing_qualityStrong": "En stark start", + "routing_qualityGood": "Bra första steg", + "routing_qualityFair": "Bra första hopp", + "routing_qualityWorked": "Har levererat", + "routing_qualityFlood": "Fått information via nyhetsflöde", + "routing_qualityUntested": "Ej testat", + "routing_lastWorked": "arbetade {when}", + "routing_neverWorked": "aldrig bekräftat", + "routing_floodDelivery": "Leverans vid översvämningsområde", + "pathEditor_title": "Skapa väg", + "pathEditor_hopCounter": "{count} av 64 humlor", + "pathEditor_noHops": "Inga humle än. Använd knapparna nedan för att lägga till dem i rätt ordning, eller spara utan humle för att skicka direkt.", + "pathEditor_addHops": "Tillsätt humlen i rätt ordning.", + "pathEditor_searchRepeaters": "Sök efter återupptagna samtal", + "pathEditor_advancedHex": "Avancerat: rå hex-sökväg", + "pathEditor_hexLabel": "Hex-prefikser", + "pathEditor_hexHelper": "Två hex-tecken per steg, separerade med kommatecken.", + "pathEditor_invalidTokens": "Ogiltigt: {tokens}", + "pathEditor_tooManyHops": "Maximalt 64 humlörter", + "pathEditor_usePath": "Använd denna väg", + "pathEditor_removeHop": "Ta bort humlen", + "pathEditor_unknownHop": "Okänd förstärkare", + "map_zoomIn": "Zooma in", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "map_zoomOut": "Zooma ut", + "map_centerMap": "Kartöversikt", + "chrome_bluetoothRequiresChromium": "Web Bluetooth kräver en Chromium-baserad webbläsare.", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS-verifierat", + "pathTrace_legendInferred": "Antagen position" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 990ecb90..4ceecbb2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1327,7 +1327,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "Кількість запитів", "telemetry_error": "Не вдалося отримати дані", "telemetry_noData": "Дані телеметрії недоступні.", @@ -2333,5 +2333,98 @@ "settings_companionDebugLogSubtitle": "Команди, відповіді та необроблена інформація для протоколів BLE/TCP/USB", "chat_newMessages": "Нові повідомлення", "chat_markAsUnread": "Позначити як непрочитане", - "repeater_chanUtil": "Використання каналу" + "repeater_chanUtil": "Використання каналу", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_delivered": "Доставлено", + "messageStatus_sent": "Надіслано", + "common_undo": "Скасувати", + "messageStatus_pending": "Надсилання", + "messageStatus_failed": "Не вдалося надіслати", + "messageStatus_repeated": "Почув неодноразово", + "contacts_moreOptions": "Більше можливостей", + "contacts_searchOpen": "Пошук контактів", + "contacts_searchClose": "Закрити пошук", + "routing_title": "Маршрутизація", + "routing_modeAuto": "Автомобіль", + "routing_modeFlood": "Повені", + "routing_modeManual": "Інструкція", + "routing_modeAutoHint": "Автоматично обирає найкращий відомий шлях, та у разі відсутності відомого шляху, використовує алгоритм \"занурення\".", + "routing_modeFloodHint": "Передавання через усі ретранслятори. Найбільш надійний спосіб, але потребує більше часу.", + "routing_modeManualHint": "Завжди доставляє точно за вказаним вами маршрутом.", + "routing_currentRoute": "Поточний маршрут", + "routing_directNoHops": "Пряме з'єднання – без проміжних ретрансляторів", + "routing_noPathYet": "Поки що немає жодного шляху. Повідомлення продовжуються надходити, поки не буде знайдено маршрут.", + "routing_floodBroadcast": "Поширення через усі ретранслятори", + "routing_editPath": "Редагувати шлях", + "routing_forgetPath": "Забудь про шлях", + "routing_knownPaths": "Відомі маршрути", + "routing_knownPathsHint": "Виберіть опцію, щоб переключитися на неї.", + "routing_inUse": "У робочому стані", + "routing_qualityStrong": "Сильний перший стрибок", + "routing_qualityGood": "Чудова перша спроба", + "routing_qualityFair": "Перший, але вдалий, крок", + "routing_qualityWorked": "Доставлено", + "routing_qualityFlood": "Дізнався через новини", + "routing_qualityUntested": "Не протестовано", + "routing_neverWorked": "ніколи не підтверджено", + "routing_floodDelivery": "Доставка під час повені", + "pathEditor_title": "Створити маршрут", + "pathEditor_hopCounter": "{count} з 64 штук хмелю", + "pathEditor_noHops": "Ще не додано хміль. Натисніть на відповідні кнопки, щоб додати його в потрібному порядку, або збережіть рецепт без хмілю, щоб відправити його безпосередньо.", + "pathEditor_addHops": "Додавайте хміль у наступній послідовності.", + "pathEditor_searchRepeaters": "Пошук повторювачів", + "pathEditor_advancedHex": "Просунутий рівень: пряма шлях у форматі шестнадцяткової системи.", + "pathEditor_hexLabel": "Префікси для шестнадцяткової системи числення", + "pathEditor_hexHelper": "Два шестизначні символи на кожний крок, розділені комами", + "pathEditor_invalidTokens": "Неправильно: {tokens}", + "routing_lastWorked": "worked {when}", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_tooManyHops": "Максимум 64 хмелеві колоди", + "pathEditor_usePath": "Використовуйте цей шлях", + "pathEditor_removeHop": "Видалити хміль", + "pathEditor_unknownHop": "Невідомий ретранслятор", + "map_zoomIn": "Увійти в режим збільшення", + "map_zoomOut": "Видалити зум", + "map_centerMap": "Карта центру", + "chrome_bluetoothRequiresChromium": "Web Bluetooth вимагає браузера на основі Chromium", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS підтверджено", + "pathTrace_legendInferred": "Висновок щодо положення" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 170e7212..d1ee964c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1346,7 +1346,7 @@ "telemetry_directionValue": "{degrees}°", "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", - "telemetry_analogValue": "{value}", + "telemetry_analogValue": "{value}", "telemetry_autoFetchQuantity": "请求次数", "telemetry_error": "无法获取数据", "telemetry_noData": "暂无遥测数据", @@ -2358,5 +2358,98 @@ "settings_companionDebugLog": "调试日志", "chat_newMessages": "新的消息", "settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据", - "repeater_chanUtil": "频道利用率" + "repeater_chanUtil": "频道利用率", + "@routing_lastWorked": { + "placeholders": { + "when": { + "type": "String" + } + } + }, + "@routing_deliveryCounts": { + "placeholders": { + "successes": { + "type": "int" + }, + "failures": { + "type": "int" + } + } + }, + "@pathEditor_hopCounter": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathEditor_invalidTokens": { + "placeholders": { + "tokens": { + "type": "String" + } + } + }, + "@channels_communityShortId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, + "messageStatus_sent": "发送", + "common_undo": "撤销", + "messageStatus_delivered": "已送达", + "messageStatus_pending": "发送", + "messageStatus_failed": "发送失败", + "messageStatus_repeated": "多次听到", + "contacts_moreOptions": "更多选择", + "contacts_searchOpen": "搜索联系人", + "contacts_searchClose": "高级搜索", + "routing_title": "路由", + "routing_modeAuto": "汽车", + "routing_modeFlood": "洪水", + "routing_modeManual": "手册", + "routing_modeAutoHint": "自动选择已知最佳路径,当没有已知路径时,则进行“洪水”搜索。", + "routing_modeFloodHint": "通过所有中继站进行广播。 这种方式最可靠,但占用更多的时间。", + "routing_modeManualHint": "总是按照您设置的路径进行导航。", + "routing_currentRoute": "当前路线", + "routing_directNoHops": "直接连接— 无中继跳", + "routing_noPathYet": "目前还没有找到路径。直到找到路径,才会收到后续消息。", + "routing_floodBroadcast": "通过所有中继器进行广播", + "routing_editPath": "编辑路径", + "routing_forgetPath": "忘记原路", + "routing_knownPaths": "已知的路径", + "routing_knownPathsHint": "点击该路径以切换到它。", + "routing_inUse": "使用中", + "routing_qualityStrong": "强劲的初始阶段", + "routing_qualityGood": "不错的开端", + "routing_qualityFair": "第一次尝试,结果良好", + "routing_qualityWorked": "已完成", + "routing_qualityFlood": "通过新闻报道", + "routing_qualityUntested": "未经测试", + "routing_lastWorked": "工作于 {when}", + "routing_neverWorked": "从未得到证实", + "routing_floodDelivery": "洪水配送", + "pathEditor_title": "构建路径", + "pathEditor_noHops": "目前还没有添加任何啤酒花。点击下面的“添加”按钮,按顺序添加,或者直接保存,不添加任何啤酒花。", + "pathEditor_addHops": "按照顺序添加啤酒花", + "pathEditor_searchRepeaters": "重复搜索", + "pathEditor_advancedHex": "高级:原始十六进制路径", + "pathEditor_hexLabel": "十六进制前缀", + "pathEditor_hexHelper": "每次跳跃,使用两个十六进制字符,用逗号分隔。", + "pathEditor_invalidTokens": "无效:{tokens}", + "pathEditor_tooManyHops": "最多 64 个跳跃", + "pathEditor_usePath": "请使用此路径", + "pathEditor_removeHop": "去除啤酒花", + "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "pathEditor_hopCounter": "{count} of 64 hops", + "pathEditor_unknownHop": "未知的重复器", + "map_zoomIn": "放大", + "map_zoomOut": "放大", + "map_centerMap": "中心地图", + "chrome_bluetoothRequiresChromium": "Web Bluetooth 需要 Chromium 浏览器", + "channels_communityShortId": "ID:{id}...", + "pathTrace_legendGpsConfirmed": "通过GPS确认", + "pathTrace_legendInferred": "推测的位置" } diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index 719e5735..522a2c5c 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -75,7 +75,9 @@ class AppDebugLogScreen extends StatelessWidget { entry.formattedTime, style: TextStyle( fontSize: 10, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ); @@ -95,7 +97,9 @@ class AppDebugLogScreen extends StatelessWidget { context.l10n.debugLog_noEntries, style: TextStyle( fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -103,7 +107,9 @@ class AppDebugLogScreen extends StatelessWidget { context.l10n.debugLog_enableInSettings, style: TextStyle( fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ], diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 76ce1f07..d1ccfd09 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -359,113 +359,129 @@ class AppSettingsScreen extends StatelessWidget { const Divider(height: 1), ListTile( title: Text(context.l10n.appSettings_maxRouteWeight), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.appSettings_maxRouteWeightSubtitle), - Slider( - value: settingsService.settings.maxRouteWeight, - min: 1, - max: 10, - divisions: 9, - label: settingsService.settings.maxRouteWeight - .round() - .toString(), - onChanged: (value) => - settingsService.setMaxRouteWeight(value), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_maxRouteWeightSubtitle), + Slider( + value: settingsService.settings.maxRouteWeight, + min: 1, + max: 10, + divisions: 9, + label: settingsService.settings.maxRouteWeight + .round() + .toString(), + onChanged: (value) => + settingsService.setMaxRouteWeight(value), + ), + ], + ), ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_initialRouteWeight), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.appSettings_initialRouteWeightSubtitle), - Slider( - value: settingsService.settings.initialRouteWeight, - min: 0.5, - max: 5.0, - divisions: 9, - label: settingsService.settings.initialRouteWeight - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setInitialRouteWeight(value), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_initialRouteWeight), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_initialRouteWeightSubtitle, + ), + Slider( + value: settingsService.settings.initialRouteWeight, + min: 0.5, + max: 5.0, + divisions: 9, + label: settingsService.settings.initialRouteWeight + .toStringAsFixed(1), + onChanged: (value) => + settingsService.setInitialRouteWeight(value), + ), + ], + ), ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_routeWeightSuccessIncrement), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context - .l10n - .appSettings_routeWeightSuccessIncrementSubtitle, + const Divider(height: 1), + ListTile( + title: Text( + context.l10n.appSettings_routeWeightSuccessIncrement, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightSuccessIncrementSubtitle, + ), + Slider( + value: settingsService + .settings + .routeWeightSuccessIncrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService + .settings + .routeWeightSuccessIncrement + .toStringAsFixed(1), + onChanged: (value) => settingsService + .setRouteWeightSuccessIncrement(value), + ), + ], + ), ), - Slider( - value: settingsService.settings.routeWeightSuccessIncrement, - min: 0.1, - max: 2.0, - divisions: 19, - label: settingsService.settings.routeWeightSuccessIncrement - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setRouteWeightSuccessIncrement(value), + const Divider(height: 1), + ListTile( + title: Text( + context.l10n.appSettings_routeWeightFailureDecrement, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightFailureDecrementSubtitle, + ), + Slider( + value: settingsService + .settings + .routeWeightFailureDecrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService + .settings + .routeWeightFailureDecrement + .toStringAsFixed(1), + onChanged: (value) => settingsService + .setRouteWeightFailureDecrement(value), + ), + ], + ), ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_routeWeightFailureDecrement), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context - .l10n - .appSettings_routeWeightFailureDecrementSubtitle, + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxMessageRetries), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_maxMessageRetriesSubtitle, + ), + Slider( + value: settingsService.settings.maxMessageRetries + .toDouble(), + min: 2, + max: 10, + divisions: 8, + label: settingsService.settings.maxMessageRetries + .toString(), + onChanged: (value) => settingsService + .setMaxMessageRetries(value.toInt()), + ), + ], + ), ), - Slider( - value: settingsService.settings.routeWeightFailureDecrement, - min: 0.1, - max: 2.0, - divisions: 19, - label: settingsService.settings.routeWeightFailureDecrement - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setRouteWeightFailureDecrement(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxMessageRetries), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.appSettings_maxMessageRetriesSubtitle), - Slider( - value: settingsService.settings.maxMessageRetries - .toDouble(), - min: 2, - max: 10, - divisions: 8, - label: settingsService.settings.maxMessageRetries - .toString(), - onChanged: (value) => - settingsService.setMaxMessageRetries(value.toInt()), - ), - ], - ), - ), ], ), ), @@ -610,15 +626,25 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.auto_awesome_outlined, - color: translationEnabled ? null : Theme.of(context).disabledColor, + color: translationEnabled + ? null + : Theme.of(context).disabledColor, ), title: Text( context.l10n.translation_autoIncomingTitle, - style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), + style: TextStyle( + color: translationEnabled + ? null + : Theme.of(context).disabledColor, + ), ), subtitle: Text( context.l10n.translation_autoIncomingSubtitle, - style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), + style: TextStyle( + color: translationEnabled + ? null + : Theme.of(context).disabledColor, + ), ), value: settings.autoTranslateIncomingMessages, onChanged: translationEnabled @@ -629,15 +655,25 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.outgoing_mail, - color: translationEnabled ? null : Theme.of(context).disabledColor, + color: translationEnabled + ? null + : Theme.of(context).disabledColor, ), title: Text( context.l10n.translation_composerTitle, - style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), + style: TextStyle( + color: translationEnabled + ? null + : Theme.of(context).disabledColor, + ), ), subtitle: Text( context.l10n.translation_composerSubtitle, - style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor), + style: TextStyle( + color: translationEnabled + ? null + : Theme.of(context).disabledColor, + ), ), value: settings.composerTranslationEnabled, onChanged: translationEnabled diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index c43c6c67..e2c63c35 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -512,149 +512,133 @@ class _ChannelChatScreenState extends State { const bodyFontSize = 14.0; final messageBody = LayoutBuilder( builder: (context, constraints) => Column( - crossAxisAlignment: isOutgoing - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: isOutgoing - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - _buildAvatar(message.senderName), - const SizedBox(width: 8), - ], - Flexible( - child: GestureDetector( - onLongPress: () => _showMessageActions(message), - onSecondaryTapUp: PlatformInfo.isDesktop - ? (_) => _showMessageActions(message) - : null, - child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - constraints: BoxConstraints( - maxWidth: constraints.maxWidth * 0.65, - ), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - top: 4, - bottom: 4, - ) - : EdgeInsets.zero, - child: Text( + crossAxisAlignment: isOutgoing + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + _buildAvatar(message.senderName), + const SizedBox(width: 8), + ], + Flexible( + child: GestureDetector( + onLongPress: () => _showMessageActions(message), + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => _showMessageActions(message) + : null, + child: Container( + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.65, + ), + decoration: BoxDecoration( + color: isOutgoing + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + if (gifId == null) const SizedBox(height: 4), + ], + if (message.replyToMessageId != null) ...[ + _buildReplyPreview(message, textScale), + const SizedBox(height: 8), + ], + if (poi != null) + _buildPoiMessage( + context, + poi, + isOutgoing, + textScale, message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - if (gifId == null) const SizedBox(height: 4), - ], - if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message, textScale), - const SizedBox(height: 8), - ], - if (poi != null) - _buildPoiMessage( - context, - poi, - isOutgoing, - textScale, - message.senderName, - ) - else if (gifId != null) - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), - ), - ], - ) - else - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: TranslatedMessageContent( - displayText: translatedDisplayText, - originalText: originalDisplayText, - style: TextStyle( - fontSize: bodyFontSize * textScale, - ), - originalStyle: TextStyle( - fontSize: bodyFontSize * textScale, - fontStyle: FontStyle.italic, - color: Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.72), + ) + else if (gifId != null) + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), ), ), - ), - ], - ), - if (enableTracing && displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.channels_via( - _formatPathPrefixes(displayPath), - ), - style: TextStyle( - fontSize: 11 * textScale, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), + ], + ) + else + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: TranslatedMessageContent( + displayText: translatedDisplayText, + originalText: originalDisplayText, + style: TextStyle( + fontSize: bodyFontSize * textScale, + ), + originalStyle: TextStyle( + fontSize: bodyFontSize * textScale, + fontStyle: FontStyle.italic, + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.72), + ), + ), + ), + ], ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(context, message.timestamp), + if (enableTracing && displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.channels_via( + _formatPathPrefixes(displayPath), + ), style: TextStyle( fontSize: 11 * textScale, color: Theme.of( @@ -662,18 +646,22 @@ class _ChannelChatScreenState extends State { ).colorScheme.onSurfaceVariant, ), ), - if (enableTracing && message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12 * textScale, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 2), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', + _formatTime(context, message.timestamp), style: TextStyle( fontSize: 11 * textScale, color: Theme.of( @@ -681,43 +669,62 @@ class _ChannelChatScreenState extends State { ).colorScheme.onSurfaceVariant, ), ), + if (enableTracing && message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent, + isRepeated: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isPending: + message.status == + ChannelMessageStatus.pending, + isFailed: + message.status == + ChannelMessageStatus.failed, + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent, - isRepeated: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isPending: - message.status == - ChannelMessageStatus.pending, - isFailed: - message.status == - ChannelMessageStatus.failed, - ), - ], - ], + ), ), - ), - ], + ], + ), ), ), ), + ], + ), + if (message.reactions.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), + child: _buildReactionsDisplay(message), ), ], - ), - if (message.reactions.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), - child: _buildReactionsDisplay(message), - ), ], - ], ), ); @@ -922,7 +929,9 @@ class _ChannelChatScreenState extends State { padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 40, minHeight: 40), onPressed: () { - final selfName = context.read().selfName ?? context.l10n.chat_me; + final selfName = + context.read().selfName ?? + context.l10n.chat_me; final fromName = isOutgoing ? selfName : senderName; final key = buildSharedMarkerKey( sourceId: 'channel:${widget.channel.index}', diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 6e9f3882..e7f21459 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -106,7 +106,9 @@ class ChannelMessagePathScreen extends StatelessWidget { if (!hasHopDetails) Text( l10n.channelPath_noHopDetails, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ) else ..._buildHopTiles(context, hops), @@ -131,7 +133,11 @@ class ChannelMessagePathScreen extends StatelessWidget { style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), - _buildDetailRow(context, l10n.channelPath_senderLabel, message.senderName), + _buildDetailRow( + context, + l10n.channelPath_senderLabel, + message.senderName, + ), _buildDetailRow( context, l10n.channelPath_timeLabel, @@ -149,7 +155,11 @@ class ChannelMessagePathScreen extends StatelessWidget { _formatPathLabel(message.pathLength, l10n), ), if (observedLabel != null) - _buildDetailRow(context, l10n.channelPath_observedLabel, observedLabel), + _buildDetailRow( + context, + l10n.channelPath_observedLabel, + observedLabel, + ), ], ), ), @@ -261,7 +271,12 @@ class ChannelMessagePathScreen extends StatelessWidget { children: [ SizedBox( width: 70, - child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)), + child: Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), Expanded(child: Text(value)), ], @@ -596,7 +611,9 @@ class _ChannelMessagePathMapScreenState if (points.isEmpty) Center( child: Card( - color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9), + color: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.9), child: Padding( padding: EdgeInsets.all(12), child: Text( @@ -667,7 +684,10 @@ class _ChannelMessagePathMapScreenState label, _formatPathPrefixes(selectedPath.pathBytes), ), - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), ), ], ), @@ -816,10 +836,10 @@ class _ChannelMessagePathMapScreenState } Widget _colorDot(Color color) => Container( - width: 10, - height: 10, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ); + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ); Widget _buildLegendCard( BuildContext context, @@ -855,7 +875,10 @@ class _ChannelMessagePathMapScreenState children: [ _colorDot(Colors.green), const SizedBox(width: 4), - Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)), + Text( + l10n.pathTrace_legendGpsConfirmed, + style: const TextStyle(fontSize: 11), + ), ], ), ], diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index b725cc61..36e82049 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -253,8 +253,7 @@ class _ChannelsScreenState extends State ), child: EmptyState( icon: Icons.search_off, - title: - context.l10n.channels_noChannelsFound, + title: context.l10n.channels_noChannelsFound, ), ), ], @@ -953,7 +952,11 @@ class _ChannelsScreenState extends State Channel.publicChannelPsk, ); Navigator.pop(dialogContext); - connector.setChannel(nextIndex, context.l10n.channels_public, psk); + connector.setChannel( + nextIndex, + context.l10n.channels_public, + psk, + ); if (context.mounted) { showDismissibleSnackBar( context, @@ -1240,7 +1243,8 @@ class _ChannelsScreenState extends State child: FilledButton( onPressed: () async { final name = nameController.text.trim(); - final publicLabel = context.l10n.channels_public; + final publicLabel = + context.l10n.channels_public; if (name.isEmpty) { showDismissibleSnackBar( context, @@ -1727,7 +1731,9 @@ class _ChannelsScreenState extends State ), title: Text(community.name), subtitle: Text( - context.l10n.channels_communityShortId(community.shortCommunityId), + context.l10n.channels_communityShortId( + community.shortCommunityId, + ), style: TextStyle( fontSize: 12, color: Theme.of( diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index b96095bf..89310bbd 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -187,7 +187,11 @@ class _ChatScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), + Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => @@ -1251,202 +1255,202 @@ class _MessageBubble extends StatelessWidget { children: [ LayoutBuilder( builder: (context, constraints) => GestureDetector( - onLongPress: onLongPress, - onSecondaryTapUp: PlatformInfo.isDesktop - ? (_) => onLongPress?.call() - : null, - child: Row( - mainAxisAlignment: isOutgoing - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - _buildAvatar(senderName, colorScheme), - const SizedBox(width: 8), - ], - Flexible( - child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - constraints: BoxConstraints( - maxWidth: constraints.maxWidth * 0.65, - ), - decoration: BoxDecoration( - color: bubbleColor, - borderRadius: BorderRadius.circular(16), - border: isFailed - ? Border.all(color: colorScheme.error) - : null, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ + onLongPress: onLongPress, + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => onLongPress?.call() + : null, + child: Row( + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + _buildAvatar(senderName, colorScheme), + const SizedBox(width: 8), + ], + Flexible( + child: Container( + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.65, + ), + decoration: BoxDecoration( + color: bubbleColor, + borderRadius: BorderRadius.circular(16), + border: isFailed + ? Border.all(color: colorScheme.error) + : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + ), + if (gifId == null) const SizedBox(height: 4), + ], + if (poi != null) + _buildPoiMessage( + context, + poi, + textColor, + metaColor, + textScale, + senderName, + ) + else if (gifId != null) + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), + ), + ), + ], + ) + else + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: TranslatedMessageContent( + displayText: translatedDisplayText, + originalText: originalDisplayText, + style: TextStyle( + color: textColor, + fontSize: bodyFontSize * textScale, + ), + originalStyle: TextStyle( + color: textColor.withValues(alpha: 0.78), + fontSize: bodyFontSize * textScale, + ), + ), + ), + ], + ), + if (enableTracing && + isOutgoing && + message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + context + .read() + .settings + .maxMessageRetries, + ), + style: TextStyle( + fontSize: 10 * textScale, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), Padding( padding: gifId != null ? const EdgeInsets.only( left: 8, - top: 4, + right: 8, bottom: 4, ) : EdgeInsets.zero, - child: Text( - senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.primary, - ), - ), - ), - if (gifId == null) const SizedBox(height: 4), - ], - if (poi != null) - _buildPoiMessage( - context, - poi, - textColor, - metaColor, - textScale, - senderName, - ) - else if (gifId != null) - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues( - alpha: 0.7, - ), - ), - ), - ], - ) - else - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: TranslatedMessageContent( - displayText: translatedDisplayText, - originalText: originalDisplayText, - style: TextStyle( - color: textColor, - fontSize: bodyFontSize * textScale, - ), - originalStyle: TextStyle( - color: textColor.withValues(alpha: 0.78), - fontSize: bodyFontSize * textScale, - ), - ), - ), - ], - ), - if (enableTracing && - isOutgoing && - message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - context - .read() - .settings - .maxMessageRetries, - ), - style: TextStyle( - fontSize: 10 * textScale, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10 * textScale, - color: metaColor, - ), - ), - if (isOutgoing) ...[ - const SizedBox(width: 4), - MessageStatusIcon( - size: 12 * textScale, - onColor: metaColor, - isAcked: - message.status == - MessageStatus.delivered, - isPending: - message.status == MessageStatus.pending, - isFailed: - message.status == MessageStatus.failed, - ), - ], - if (enableTracing && - message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10 * textScale, - color: isOutgoing - ? metaColor - : Theme.of( - context, - ).colorScheme.tertiary, - ), + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + _formatTime(message.timestamp), style: TextStyle( - fontSize: 9 * textScale, + fontSize: 10 * textScale, + color: metaColor, + ), + ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + MessageStatusIcon( + size: 12 * textScale, + onColor: metaColor, + isAcked: + message.status == + MessageStatus.delivered, + isPending: + message.status == MessageStatus.pending, + isFailed: + message.status == MessageStatus.failed, + ), + ], + if (enableTracing && + message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10 * textScale, color: isOutgoing ? metaColor : Theme.of( context, ).colorScheme.tertiary, ), - ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9 * textScale, + color: isOutgoing + ? metaColor + : Theme.of( + context, + ).colorScheme.tertiary, + ), + ), + ], ], - ], + ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ), - ), if (message.reactions.isNotEmpty) ...[ const SizedBox(height: 4), Padding( @@ -1476,7 +1480,9 @@ class _MessageBubble extends StatelessWidget { padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 40, minHeight: 40), onPressed: () async { - final selfName = context.read().selfName ?? context.l10n.chat_me; + final selfName = + context.read().selfName ?? + context.l10n.chat_me; final fromName = message.isOutgoing ? selfName : senderName; final key = buildSharedMarkerKey( sourceId: sourceId, diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart index 53271d8f..cdf3c938 100644 --- a/lib/screens/chrome_required_screen.dart +++ b/lib/screens/chrome_required_screen.dart @@ -55,12 +55,18 @@ class ChromeRequiredScreen extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.secondaryContainer.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(30), - border: Border.all(color: colorScheme.outline.withValues(alpha: 0.4)), + border: Border.all( + color: colorScheme.outline.withValues(alpha: 0.4), + ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.info_outline, size: 20, color: colorScheme.secondary), + Icon( + Icons.info_outline, + size: 20, + color: colorScheme.secondary, + ), const SizedBox(width: 12), Text( l10n.chrome_bluetoothRequiresChromium, diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d64b8dd1..95cbb55d 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -705,9 +705,7 @@ class _ContactsScreenState extends State action: FilledButton.icon( onPressed: () => Navigator.push( context, - MaterialPageRoute( - builder: (context) => const DiscoveryScreen(), - ), + MaterialPageRoute(builder: (context) => const DiscoveryScreen()), ), icon: const Icon(Icons.person_add_rounded), label: Text(context.l10n.discoveredContacts_Title), @@ -1496,9 +1494,7 @@ class _ContactsScreenState extends State ), title: Text( context.l10n.contacts_deleteContact, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), onTap: () { Navigator.pop(sheetContext); diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 736e8f36..1bc392bf 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -189,8 +189,7 @@ class _DiscoveryScreenState extends State { ), action: SnackBarAction( label: context.l10n.common_undo, - onPressed: () => - connector.removeContact(contact), + onPressed: () => connector.removeContact(contact), ), ); }, @@ -444,7 +443,6 @@ class _DiscoveryScreenState extends State { } } - String _formatLastSeen(BuildContext context, DateTime lastSeen) { final now = DateTime.now(); final diff = now.difference(lastSeen); diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 1d63b910..af80ca22 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -507,7 +507,9 @@ class _LineOfSightMapScreenState extends State { bottom: 12, child: DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(8), ), child: Padding( @@ -517,7 +519,10 @@ class _LineOfSightMapScreenState extends State { ), child: Text( context.l10n.losElevationAttribution, - style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurface), + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), ), @@ -631,7 +636,10 @@ class _LineOfSightMapScreenState extends State { const SizedBox(height: 4), Text( context.l10n.losBlockedSpotsHint, - style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 6), Wrap( @@ -700,7 +708,9 @@ class _LineOfSightMapScreenState extends State { '${_selectedObstruction!.point.longitude.toStringAsFixed(5)}', style: TextStyle( fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ], @@ -726,7 +736,10 @@ class _LineOfSightMapScreenState extends State { const SizedBox(width: 8), Text( '${displayFrequencyMHz.toStringAsFixed(3)} MHz', - style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), if (kFactorUsed != null) ...[ const SizedBox(width: 8), @@ -734,7 +747,9 @@ class _LineOfSightMapScreenState extends State { 'k=${kFactorUsed.toStringAsFixed(3)}', style: TextStyle( fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 4), @@ -758,7 +773,10 @@ class _LineOfSightMapScreenState extends State { ), Text( context.l10n.losElevationAttribution, - style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 6), ExpansionTile( diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 6bc62018..e627bd58 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -458,7 +458,9 @@ class _MapCacheScreenState extends State { padding: const EdgeInsets.only(top: 8), child: Text( l10n.mapCache_failedDownloads(_failedTiles), - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ), ], diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index dd6462b7..3e4193c0 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -486,7 +486,10 @@ class _MapScreenState extends State { PopupMenuItem( child: Row( children: [ - Icon(Icons.logout, color: Theme.of(context).colorScheme.error), + Icon( + Icons.logout, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 8), Text(context.l10n.common_disconnect), ], @@ -1223,7 +1226,9 @@ class _MapScreenState extends State { Icon( Icons.location_on, size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), Text( ": $nodeCount", @@ -1239,7 +1244,9 @@ class _MapScreenState extends State { Icon( Icons.wrong_location, size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), Text( ": ${nodeCountAll - nodeCount}", @@ -1255,7 +1262,9 @@ class _MapScreenState extends State { Icon( Icons.add_outlined, size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), Text( ": $nodeCountAll", @@ -1612,51 +1621,56 @@ class _MapScreenState extends State { return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - _getNodeIcon(contact.type), - color: _getNodeColor(contact.type), - ), - const SizedBox(width: 8), - Expanded(child: SelectableText(contact.name)), - ], - ), - const SizedBox(height: 8), - _buildInfoRow( - context.l10n.map_type, - contact.typeLabel(context.l10n), - ), - _buildInfoRow( - context.l10n.map_path, - contact.pathLabel(context.l10n), - ), - if (contact.hasLocation) - _buildInfoRow( - context.l10n.map_location, - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ) - else if (guessedPosition != null) - _buildInfoRow( - context.l10n.map_estLocation, - '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + _getNodeIcon(contact.type), + color: _getNodeColor(contact.type), + ), + const SizedBox(width: 8), + Expanded(child: SelectableText(contact.name)), + ], ), - _buildInfoRow( - context.l10n.map_lastSeen, - _formatLastSeen(contact.lastSeen), - ), - _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex), - const SizedBox(height: 16), - ...actions, - TextButton( - onPressed: () => Navigator.pop(sheetContext), - child: Text(context.l10n.common_close), - ), - ], + const SizedBox(height: 8), + _buildInfoRow( + context.l10n.map_type, + contact.typeLabel(context.l10n), + ), + _buildInfoRow( + context.l10n.map_path, + contact.pathLabel(context.l10n), + ), + if (contact.hasLocation) + _buildInfoRow( + context.l10n.map_location, + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + context.l10n.map_estLocation, + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), + _buildInfoRow( + context.l10n.map_lastSeen, + _formatLastSeen(contact.lastSeen), + ), + _buildInfoRow( + context.l10n.map_publicKey, + contact.publicKeyHex, + ), + const SizedBox(height: 16), + ...actions, + TextButton( + onPressed: () => Navigator.pop(sheetContext), + child: Text(context.l10n.common_close), + ), + ], + ), ), ), ); @@ -1844,9 +1858,7 @@ class _MapScreenState extends State { ); await connector.refreshDeviceInfo(); if (!mounted) return; - messenger.showSnackBar( - SnackBar(content: Text(successMsg)), - ); + messenger.showSnackBar(SnackBar(content: Text(successMsg))); }, ), ListTile( @@ -2235,7 +2247,10 @@ class _MapScreenState extends State { const SizedBox(height: 8), Text( _getTimeFilterLabel(settings.mapTimeFilterHours), - style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), Slider( value: _hoursToSliderValue(settings.mapTimeFilterHours), @@ -2387,7 +2402,10 @@ class _MapScreenState extends State { if (_pathTrace.isNotEmpty) Text( "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", - style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), SelectableText( _pathTrace diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index e92a5cc0..c34af7f1 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -326,10 +326,7 @@ class _NeighborsScreenState extends State { if (!_isLoaded && !_hasData && (_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) - EmptyState( - icon: Icons.wifi_find, - title: l10n.neighbors_noData, - ), + EmptyState(icon: Icons.wifi_find, title: l10n.neighbors_noData), if (_isLoaded || _hasData && !(_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 81082b9e..99bc4deb 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -274,8 +274,8 @@ class _PathTraceMapScreenState extends State { final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, - 0, //flags 0, //auth + 0, //flag payload: path, ); connector.sendFrame(frame); @@ -350,11 +350,16 @@ class _PathTraceMapScreenState extends State { try { buffer.skipBytes(2); // Skip push code and reserved byte int pathLength = buffer.readUInt8(); - buffer.skipBytes(5); // Skip Flag byte and tag data + final int flags = buffer + .readUInt8(); // path_sz = flags & 0x03 (path-hash mode, fw v1.11+) + buffer.skipBytes(4); // Skip tag data buffer.skipBytes(4); // Skip auth code + final int pathSz = flags & 0x03; Uint8List pathData = buffer.readBytes(pathLength); + // Firmware emits (path_len >> path_sz) hop SNRs plus 1 final SNR (to this node). + final int snrCount = (pathLength >> pathSz) + 1; List snrData = buffer - .readRemainingBytes() + .readBytes(snrCount) .map((snr) => snr.toSigned(8).toDouble() / 4) .toList(); @@ -616,7 +621,9 @@ class _PathTraceMapScreenState extends State { !_failed2Loaded) Center( child: Card( - color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9), + color: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.9), child: Padding( padding: EdgeInsets.all(12), child: Text( @@ -963,10 +970,10 @@ class _PathTraceMapScreenState extends State { } Widget _colorDot(Color color) => Container( - width: 10, - height: 10, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ); + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ); Widget _buildLegendCard( BuildContext context, @@ -1002,11 +1009,17 @@ class _PathTraceMapScreenState extends State { children: [ _colorDot(Colors.green), const SizedBox(width: 4), - Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)), + Text( + l10n.pathTrace_legendGpsConfirmed, + style: const TextStyle(fontSize: 11), + ), const SizedBox(width: 12), _colorDot(Colors.orange), const SizedBox(width: 4), - Text(l10n.pathTrace_legendInferred, style: const TextStyle(fontSize: 11)), + Text( + l10n.pathTrace_legendInferred, + style: const TextStyle(fontSize: 11), + ), ], ), ], diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 93720f28..80269061 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -385,16 +385,26 @@ class _RepeaterCliScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.terminal, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), + Icon( + Icons.terminal, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), const SizedBox(height: 16), Text( l10n.repeater_noCommandsSent, - style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 8), Text( l10n.repeater_typeCommandOrUseQuick, - style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 337d148c..2aebcf03 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -72,11 +72,15 @@ class RepeaterHubScreen extends StatelessWidget { children: [ CircleAvatar( radius: 40, - backgroundColor: Theme.of(context).colorScheme.tertiaryContainer, + backgroundColor: Theme.of( + context, + ).colorScheme.tertiaryContainer, child: Icon( Icons.cell_tower, size: 40, - color: Theme.of(context).colorScheme.onTertiaryContainer, + color: Theme.of( + context, + ).colorScheme.onTertiaryContainer, ), ), const SizedBox(height: 16), @@ -90,12 +94,18 @@ class RepeaterHubScreen extends StatelessWidget { const SizedBox(height: 8), Text( repeater.shortPubKeyHex, - style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 8), Text( repeater.pathLabel(context.l10n), - style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), if (repeater.hasLocation) ...[ const SizedBox(height: 4), @@ -105,14 +115,18 @@ class RepeaterHubScreen extends StatelessWidget { Icon( Icons.location_on, size: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( '${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', style: TextStyle( fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ], @@ -329,12 +343,18 @@ class RepeaterHubScreen extends StatelessWidget { const SizedBox(height: 4), Text( subtitle, - style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), ), - Icon(Icons.chevron_right, color: Theme.of(context).colorScheme.onSurfaceVariant), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ], ), ), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 1d8ae3bc..e04cfb70 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -459,7 +459,9 @@ class _RepeaterSettingsScreenState extends State { ? l10n.repeater_refreshed(label) : l10n.repeater_errorRefreshing(label), ), - backgroundColor: successCount > 0 ? null : Theme.of(context).colorScheme.error, + backgroundColor: successCount > 0 + ? null + : Theme.of(context).colorScheme.error, ); setState(() => setRefreshing(false)); } @@ -2229,7 +2231,9 @@ class _RepeaterSettingsScreenState extends State { onConfirm(); }, style: isDestructive - ? FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error) + ? FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ) : null, child: Text(l10n.repeater_confirm), ), diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index e665cbc7..1fd2c53c 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -113,9 +113,9 @@ class _ScannerScreenState extends State { 'USB selected, opening UsbScreen', tag: 'ScannerScreen', ); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const UsbScreen()), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const UsbScreen())); }, ), if (!PlatformInfo.isWeb) @@ -123,9 +123,9 @@ class _ScannerScreenState extends State { icon: const Icon(Icons.lan), tooltip: context.l10n.connectionChoiceTcpLabel, onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const TcpScreen()), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const TcpScreen())); }, ), ], @@ -167,7 +167,9 @@ class _ScannerScreenState extends State { ) : const Icon(Icons.bluetooth_searching), label: Text( - isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan, + isScanning + ? context.l10n.scanner_stop + : context.l10n.scanner_scan, ), ); }, @@ -256,8 +258,7 @@ class _ScannerScreenState extends State { ); } - final isConnecting = - connector.state == MeshCoreConnectionState.connecting; + final isConnecting = connector.state == MeshCoreConnectionState.connecting; return ListView.separated( padding: const EdgeInsets.all(8), itemCount: connector.scanResults.length, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 0fe077cb..b6eefb05 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -343,7 +343,10 @@ class _SettingsScreenState extends State { ), ), ListTile( - leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), + leading: Icon( + Icons.delete_outline, + color: Theme.of(context).colorScheme.error, + ), title: Text(l10n.settings_deleteAllPaths), subtitle: Text( l10n.settings_deleteAllPathsSubtitle, @@ -367,7 +370,10 @@ class _SettingsScreenState extends State { ), const Divider(height: 1), ListTile( - leading: Icon(Icons.restart_alt, color: Theme.of(context).colorScheme.tertiary), + leading: Icon( + Icons.restart_alt, + color: Theme.of(context).colorScheme.tertiary, + ), title: Text(l10n.settings_rebootDevice), subtitle: Text(l10n.settings_rebootDeviceSubtitle), onTap: () => _confirmReboot(context, connector), diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index 755a4298..517b39a6 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -224,10 +224,7 @@ class _TcpScreenState extends State { statusText, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), ), ), ], diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 6634a6af..4559adac 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -385,7 +385,10 @@ class _TelemetryScreenState extends State { Center( child: Text( l10n.telemetry_noData, - style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), if ((_isLoaded || _hasData) && diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 331de5e3..ec551c6f 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -203,10 +203,7 @@ class _UsbScreenState extends State { statusText, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), ), ), ], @@ -222,11 +219,18 @@ class _UsbScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.usb, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), + Icon( + Icons.usb, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), const SizedBox(height: 16), Text( l10n.usbStatus_searching, - style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), @@ -238,12 +242,19 @@ class _UsbScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.usb, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), + Icon( + Icons.usb, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), const SizedBox(height: 16), Text( l10n.usbScreenEmptyState, textAlign: TextAlign.center, - style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index d8e70ce3..36027917 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -304,7 +304,8 @@ class NotificationService { try { await _notifications.show( - id: channelIndex?.hashCode ?? + id: + channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF, title: channelName, body: body, diff --git a/lib/widgets/gif_picker.dart b/lib/widgets/gif_picker.dart index df27ec68..90b6261b 100644 --- a/lib/widgets/gif_picker.dart +++ b/lib/widgets/gif_picker.dart @@ -180,7 +180,10 @@ class _GifPickerState extends State { const SizedBox(height: 8), Text( context.l10n.gifPicker_poweredBy, - style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), @@ -197,11 +200,18 @@ class _GifPickerState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), + Icon( + Icons.error_outline, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), const SizedBox(height: 16), Text( _error!, - style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 16), ElevatedButton.icon( @@ -219,11 +229,18 @@ class _GifPickerState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.search_off, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), + Icon( + Icons.search_off, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), const SizedBox(height: 16), Text( context.l10n.gifPicker_noGifsFound, - style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), diff --git a/lib/widgets/path_editor_sheet.dart b/lib/widgets/path_editor_sheet.dart index 66c1fa4b..ee6ba006 100644 --- a/lib/widgets/path_editor_sheet.dart +++ b/lib/widgets/path_editor_sheet.dart @@ -119,7 +119,9 @@ class _PathEditorSheetState extends State { _hexError = null; _hops ..clear() - ..addAll(tokens.map((t) => _Hop(_nextHopId++, int.parse(t, radix: 16)))); + ..addAll( + tokens.map((t) => _Hop(_nextHopId++, int.parse(t, radix: 16))), + ); }); } @@ -287,10 +289,7 @@ class _PathEditorSheetState extends State { ), const Divider(), const SizedBox(height: 8), - Text( - l10n.pathEditor_addHops, - style: theme.textTheme.titleSmall, - ), + Text(l10n.pathEditor_addHops, style: theme.textTheme.titleSmall), const SizedBox(height: 8), TextField( onChanged: (value) => setState(() => _search = value), diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 9ba8efd9..5b45037d 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -466,7 +466,10 @@ class _RepeaterLoginDialogState extends State { const SizedBox(height: 4), Text( repeater.pathLabel(context.l10n), - style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 8), Align( diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 8a737130..a6475cfb 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -395,7 +395,10 @@ class _RoomLoginDialogState extends State { const SizedBox(height: 4), Text( repeater.pathLabel(context.l10n), - style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 8), Align( diff --git a/lib/widgets/routing_sheet.dart b/lib/widgets/routing_sheet.dart index ddb0bb04..71fe2a4d 100644 --- a/lib/widgets/routing_sheet.dart +++ b/lib/widgets/routing_sheet.dart @@ -150,10 +150,7 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { ); } - Future _forgetPath( - MeshCoreConnector connector, - Contact contact, - ) async { + Future _forgetPath(MeshCoreConnector connector, Contact contact) async { await connector.clearContactPath(contact); if (!mounted) return; setState(() => _syncStatus = context.l10n.chat_pathCleared); @@ -235,7 +232,8 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { case _RoutingMode.auto: if (contact.pathLength < 0) return l10n.routing_noPathYet; if (contact.pathLength == 0) return l10n.routing_directNoHops; - if (contact.path.isEmpty) return l10n.chat_hopsCount(contact.pathLength); + if (contact.path.isEmpty) + return l10n.chat_hopsCount(contact.pathLength); return PathHelper.resolvePathNames(contact.path, connector.allContacts); } } @@ -325,7 +323,8 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { int failureCount, int lastTripTimeMs, DateTime? lastUsed, - })? floodStats, + })? + floodStats, ) { final l10n = context.l10n; final theme = Theme.of(context); @@ -347,7 +346,10 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { color: scheme.primary, ), const SizedBox(width: 8), - Text(l10n.routing_currentRoute, style: theme.textTheme.titleSmall), + Text( + l10n.routing_currentRoute, + style: theme.textTheme.titleSmall, + ), ], ), const SizedBox(height: 8), @@ -418,7 +420,8 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { int failureCount, int lastTripTimeMs, DateTime? lastUsed, - }) stats, + }) + stats, ) { final l10n = context.l10n; final parts = [ @@ -441,7 +444,8 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { int failureCount, int lastTripTimeMs, DateTime? lastUsed, - }) stats, + }) + stats, ) { final l10n = context.l10n; final scheme = Theme.of(context).colorScheme; @@ -487,7 +491,10 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { final scheme = theme.colorScheme; final (Color bg, Color fg) = switch (quality) { - _PathQuality.strong => (scheme.primaryContainer, scheme.onPrimaryContainer), + _PathQuality.strong => ( + scheme.primaryContainer, + scheme.onPrimaryContainer, + ), _PathQuality.good => ( scheme.secondaryContainer, scheme.onSecondaryContainer, @@ -504,7 +511,8 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { }; final hasBytes = record.pathBytes.isNotEmpty; - final inUse = hasBytes && + final inUse = + hasBytes && ((mode == _RoutingMode.manual && listEquals(record.pathBytes, contact.pathOverrideBytes)) || (mode == _RoutingMode.auto && @@ -596,19 +604,24 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { final rankedRepeaters = List.of(connector.directRepeaters) ..sort((a, b) => b.ranking.compareTo(a.ranking)); - final entries = pathService - .getRecentPaths(contact.publicKeyHex) - .map((r) => (quality: _qualityOf(r, rankedRepeaters), record: r)) - .toList() - ..sort((a, b) { - final byQuality = a.quality.index.compareTo(b.quality.index); - if (byQuality != 0) return byQuality; - final aTime = - a.record.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0); - final bTime = - b.record.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0); - return bTime.compareTo(aTime); - }); + final entries = + pathService + .getRecentPaths(contact.publicKeyHex) + .map( + (r) => (quality: _qualityOf(r, rankedRepeaters), record: r), + ) + .toList() + ..sort((a, b) { + final byQuality = a.quality.index.compareTo(b.quality.index); + if (byQuality != 0) return byQuality; + final aTime = + a.record.timestamp ?? + DateTime.fromMillisecondsSinceEpoch(0); + final bTime = + b.record.timestamp ?? + DateTime.fromMillisecondsSinceEpoch(0); + return bTime.compareTo(aTime); + }); return ListView( controller: widget.scrollController, diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index ecd33ee9..4c6b8bd2 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -160,15 +160,9 @@ void main() { final context = tester.element(find.byType(ScannerScreen)); final l10n = AppLocalizations.of(context); if (PlatformInfo.supportsUsbSerial) { - expect( - find.byTooltip(l10n.connectionChoiceUsbLabel), - findsOneWidget, - ); + expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsOneWidget); } else { - expect( - find.byTooltip(l10n.connectionChoiceUsbLabel), - findsNothing, - ); + expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsNothing); } // ScannerScreen.dispose() schedules disconnect work that debounces notify. diff --git a/untranslated.json b/untranslated.json index c3a9394b..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,987 +1 @@ -{ - "bg": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "de": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "es": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "fr": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "hu": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "it": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "ja": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "ko": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "nl": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "pl": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "pt": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "ru": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "sk": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "sl": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "sv": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "uk": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ], - - "zh": [ - "common_undo", - "messageStatus_sent", - "messageStatus_delivered", - "messageStatus_pending", - "messageStatus_failed", - "messageStatus_repeated", - "contacts_moreOptions", - "contacts_searchOpen", - "contacts_searchClose", - "routing_title", - "routing_modeAuto", - "routing_modeFlood", - "routing_modeManual", - "routing_modeAutoHint", - "routing_modeFloodHint", - "routing_modeManualHint", - "routing_currentRoute", - "routing_directNoHops", - "routing_noPathYet", - "routing_floodBroadcast", - "routing_editPath", - "routing_forgetPath", - "routing_knownPaths", - "routing_knownPathsHint", - "routing_inUse", - "routing_qualityStrong", - "routing_qualityGood", - "routing_qualityFair", - "routing_qualityWorked", - "routing_qualityFlood", - "routing_qualityUntested", - "routing_lastWorked", - "routing_neverWorked", - "routing_deliveryCounts", - "routing_floodDelivery", - "pathEditor_title", - "pathEditor_hopCounter", - "pathEditor_noHops", - "pathEditor_addHops", - "pathEditor_searchRepeaters", - "pathEditor_advancedHex", - "pathEditor_hexLabel", - "pathEditor_hexHelper", - "pathEditor_invalidTokens", - "pathEditor_tooManyHops", - "pathEditor_usePath", - "pathEditor_removeHop", - "pathEditor_unknownHop", - "map_zoomIn", - "map_zoomOut", - "map_centerMap", - "chrome_bluetoothRequiresChromium", - "channels_communityShortId", - "pathTrace_legendGpsConfirmed", - "pathTrace_legendInferred" - ] -} +{} \ No newline at end of file From 6a31d304d398a724079687133df4cb2a80c393a7 Mon Sep 17 00:00:00 2001 From: Zach Date: Thu, 11 Jun 2026 10:08:31 -0700 Subject: [PATCH 07/16] feat: update license type to nonprofit and add jni to plugin lists; bump dependencies for flutter_local_notifications, package_info_plus, share_plus, flutter_blue_plus_platform_interface, and llamadart --- lib/connector/meshcore_connector.dart | 6 +-- lib/widgets/routing_sheet.dart | 3 +- linux/flutter/generated_plugins.cmake | 1 + macos/Podfile.lock | 52 ------------------- macos/Runner.xcodeproj/project.pbxproj | 22 ++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 18 +++++++ pubspec.yaml | 10 ++-- windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 52 insertions(+), 61 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c9f11dd8..579d8257 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1883,7 +1883,7 @@ class MeshCoreConnector extends ChangeNotifier { .connect( timeout: connectTimeout, mtu: null, - license: License.free, + license: License.nonprofit, ) .timeout( connectTimeout + const Duration(seconds: 2), @@ -1979,7 +1979,7 @@ class MeshCoreConnector extends ChangeNotifier { return device.connect( timeout: connectTimeout, mtu: null, - license: License.free, + license: License.nonprofit, ); } @@ -2088,7 +2088,7 @@ class MeshCoreConnector extends ChangeNotifier { await device.connect( timeout: const Duration(seconds: 15), mtu: null, - license: License.free, + license: License.nonprofit, ); services = await device.discoverServices(); } else { diff --git a/lib/widgets/routing_sheet.dart b/lib/widgets/routing_sheet.dart index 71fe2a4d..4c1a26f6 100644 --- a/lib/widgets/routing_sheet.dart +++ b/lib/widgets/routing_sheet.dart @@ -232,8 +232,9 @@ class _RoutingSheetBodyState extends State<_RoutingSheetBody> { case _RoutingMode.auto: if (contact.pathLength < 0) return l10n.routing_noPathYet; if (contact.pathLength == 0) return l10n.routing_directNoHops; - if (contact.path.isEmpty) + if (contact.path.isEmpty) { return l10n.chat_hopsCount(contact.pathLength); + } return PathHelper.resolvePathNames(contact.path, connector.allContacts); } } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36fa..93e46829 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 481f1bcc..0f1561a5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,73 +1,21 @@ PODS: - flserial (0.0.1): - FlutterMacOS - - flutter_blue_plus_darwin (0.0.2): - - Flutter - - FlutterMacOS - - flutter_local_notifications (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (7.0.0): - - Flutter - - FlutterMacOS - - package_info_plus (0.0.1): - - FlutterMacOS - - share_plus (0.0.1): - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS DEPENDENCIES: - flserial (from `Flutter/ephemeral/.symlinks/plugins/flserial/macos`) - - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: flserial: :path: Flutter/ephemeral/.symlinks/plugins/flserial/macos - flutter_blue_plus_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin - flutter_local_notifications: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos FlutterMacOS: :path: Flutter/ephemeral - mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin - package_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos - share_plus: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6 - flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 - flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index defe6932..10f8906b 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 99C5B380294D2DE19A818101 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */; }; D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -88,6 +89,7 @@ BEFF4DDC60AFB628205F8E82 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -103,6 +105,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -164,6 +167,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -231,6 +235,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -256,6 +263,9 @@ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -796,6 +806,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fd43b250..e082e324 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + =0.6.8 <0.7.0' + llamadart: ^0.8.0 flutter_langdetect: ^0.0.1 hooks: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f4..533a1712 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES) From 51d62109201b88f557eb600f86097c9c9850fedf Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 12 Jun 2026 21:04:02 -0700 Subject: [PATCH 08/16] Add shared UI components for mesh application - Introduced `mesh_ui.dart` with reusable widgets including SectionHeader, MeshCard, StatusChip, StatTile, AvatarCircle, SignalBars, RouteChip, PulseDot, BottomSheetHeader, ErrorRetryCard, and ListEntrance. - Implemented `path_map_ui.dart` for path map screens, featuring path distance calculations, playback controls, and a summary list of observed paths. - Created `themed_map_tile_layer.dart` for shared cached map tiles with automatic dark-mode treatment. --- lib/helpers/snack_bar_builder.dart | 14 +- lib/l10n/app_en.arb | 95 +- lib/l10n/app_localizations.dart | 228 ++ lib/l10n/app_localizations_bg.dart | 140 + lib/l10n/app_localizations_de.dart | 140 + lib/l10n/app_localizations_en.dart | 140 + lib/l10n/app_localizations_es.dart | 140 + lib/l10n/app_localizations_fr.dart | 140 + lib/l10n/app_localizations_hu.dart | 140 + lib/l10n/app_localizations_it.dart | 140 + lib/l10n/app_localizations_ja.dart | 140 + lib/l10n/app_localizations_ko.dart | 140 + lib/l10n/app_localizations_nl.dart | 140 + lib/l10n/app_localizations_pl.dart | 140 + lib/l10n/app_localizations_pt.dart | 140 + lib/l10n/app_localizations_ru.dart | 140 + lib/l10n/app_localizations_sk.dart | 140 + lib/l10n/app_localizations_sl.dart | 140 + lib/l10n/app_localizations_sv.dart | 140 + lib/l10n/app_localizations_uk.dart | 140 + lib/l10n/app_localizations_zh.dart | 140 + lib/models/display_path.dart | 69 + lib/models/path_playback.dart | 177 ++ lib/screens/app_debug_log_screen.dart | 113 +- lib/screens/app_settings_screen.dart | 2491 ++++++++++------- lib/screens/ble_debug_log_screen.dart | 132 +- lib/screens/channel_chat_screen.dart | 521 ++-- lib/screens/channel_message_path_screen.dart | 1251 +++++++-- lib/screens/channels_screen.dart | 994 ++++--- lib/screens/chat_screen.dart | 413 +-- lib/screens/chrome_required_screen.dart | 155 +- lib/screens/community_qr_scanner_screen.dart | 289 +- lib/screens/companion_radio_stats_screen.dart | 143 +- lib/screens/contacts_screen.dart | 314 ++- lib/screens/discovery_screen.dart | 298 +- lib/screens/line_of_sight_map_screen.dart | 1666 +++++++---- lib/screens/map_cache_screen.dart | 294 +- lib/screens/map_screen.dart | 2354 +++++++++++----- lib/screens/neighbors_screen.dart | 160 +- lib/screens/path_trace_map.dart | 1097 ++++++-- lib/screens/repeater_cli_screen.dart | 1091 +++----- lib/screens/repeater_hub_screen.dart | 457 ++- lib/screens/repeater_settings_screen.dart | 1500 +++++----- lib/screens/repeater_status_screen.dart | 458 ++- lib/screens/scanner_screen.dart | 223 +- lib/screens/settings_screen.dart | 932 +++--- lib/screens/tcp_screen.dart | 156 +- lib/screens/telemetry_screen.dart | 93 +- lib/screens/usb_screen.dart | 222 +- lib/theme/mesh_theme.dart | 258 +- lib/widgets/battery_indicator.dart | 23 +- lib/widgets/device_tile.dart | 112 +- lib/widgets/elements_ui.dart | 83 +- lib/widgets/emoji_picker.dart | 14 +- lib/widgets/empty_state.dart | 112 +- lib/widgets/jump_to_bottom_button.dart | 36 +- lib/widgets/mesh_ui.dart | 643 +++++ lib/widgets/message_status_icon.dart | 12 +- lib/widgets/path_map_ui.dart | 659 +++++ lib/widgets/quick_switch_bar.dart | 27 +- lib/widgets/radio_stats_entry.dart | 13 +- lib/widgets/repeater_login_dialog.dart | 45 +- lib/widgets/room_login_dialog.dart | 41 +- lib/widgets/signal_ui.dart | 11 +- lib/widgets/snr_indicator.dart | 57 +- lib/widgets/telemetry_location_map.dart | 9 +- lib/widgets/themed_map_tile_layer.dart | 60 + lib/widgets/unread_badge.dart | 15 +- lib/widgets/unread_divider.dart | 37 +- linux/flutter/generated_plugins.cmake | 1 - untranslated.json | 699 ++++- windows/flutter/generated_plugins.cmake | 1 - 72 files changed, 16778 insertions(+), 7110 deletions(-) create mode 100644 lib/models/display_path.dart create mode 100644 lib/models/path_playback.dart create mode 100644 lib/widgets/mesh_ui.dart create mode 100644 lib/widgets/path_map_ui.dart create mode 100644 lib/widgets/themed_map_tile_layer.dart diff --git a/lib/helpers/snack_bar_builder.dart b/lib/helpers/snack_bar_builder.dart index d7409b6d..913e783f 100644 --- a/lib/helpers/snack_bar_builder.dart +++ b/lib/helpers/snack_bar_builder.dart @@ -25,7 +25,19 @@ void showDismissibleSnackBar( DismissDirection? dismissDirection, Clip? clipBehavior, }) { - final messenger = ScaffoldMessenger.of(context); + // Callers often reach here after an async gap; the context may already be + // unmounted, or deactivated (popped but not yet disposed) — ancestor + // lookups on a deactivated element throw. Showing nothing is the right + // outcome in both cases. + if (!context.mounted) return; + var isActive = true; + assert(() { + isActive = (context as Element).debugIsActive; + return true; + }()); + if (!isActive) return; + final messenger = ScaffoldMessenger.maybeOf(context); + if (messenger == null) return; messenger.showSnackBar( SnackBar( key: key, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 58613d02..37308ba5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -900,6 +900,17 @@ }, "chat_invalidLink": "Invalid link format", "map_title": "Node Map", + "map_searchHint": "Search node name or ID", + "map_activity": "Activity", + "map_online": "Online", + "map_recent": "Recent", + "map_stale": "Stale", + "map_visible": "Visible", + "map_hidden": "Hidden", + "map_centerOnNode": "Center on node", + "map_details": "Details", + "map_noGps": "No GPS", + "map_noResults": "No matching nodes", "map_lineOfSight": "Line of Sight", "map_losScreenTitle": "Line of Sight", "map_noNodesWithLocation": "No nodes with location data", @@ -2501,5 +2512,87 @@ } }, "pathTrace_legendGpsConfirmed": "GPS confirmed", - "pathTrace_legendInferred": "Inferred position" + "pathTrace_legendInferred": "Inferred position", + "pathMap_viewSingle": "Single", + "pathMap_viewCombined": "Combined", + "pathMap_play": "Play", + "pathMap_pause": "Pause", + "pathMap_replay": "Replay", + "pathMap_stepBack": "Previous hop", + "pathMap_stepForward": "Next hop", + "pathMap_animationOn": "Show packet animation", + "pathMap_animationOff": "Hide packet animation", + "pathMap_hopOf": "Hop {current} of {total}", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "pathMap_observedPaths": "Observed paths: {count}", + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "pathMap_primary": "Primary", + "pathMap_alternate": "Alt {index}", + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "pathMap_hopCount": "{count, plural, =1{1 hop} other{{count} hops}}", + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "pathMap_gpsCount": "{confirmed}/{total} GPS", + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "pathMap_legendShared": "Shared segment", + "pathMap_legendEstimated": "Estimated segment", + "pathMap_sharedNodeCount": "Used by {count} paths", + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "pathMap_partialAnimation": "{count, plural, =1{1 hop has no location — the shown path is partial} other{{count} hops have no location — the shown path is partial}}", + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "pathMap_showAllPaths": "Show all", + "pathMap_hidePath": "Hide path", + "pathMap_showPath": "Show path", + "pathMap_collapsePanel": "Collapse panel", + "pathMap_expandPanel": "Expand panel", + "pathMap_noLocation": "No location", + "pathMap_followPacket": "Lock view to packet", + "pathMap_unfollowPacket": "Unlock view from packet" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f55fc02c..8b513be5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3154,6 +3154,72 @@ abstract class AppLocalizations { /// **'Node Map'** String get map_title; + /// No description provided for @map_searchHint. + /// + /// In en, this message translates to: + /// **'Search node name or ID'** + String get map_searchHint; + + /// No description provided for @map_activity. + /// + /// In en, this message translates to: + /// **'Activity'** + String get map_activity; + + /// No description provided for @map_online. + /// + /// In en, this message translates to: + /// **'Online'** + String get map_online; + + /// No description provided for @map_recent. + /// + /// In en, this message translates to: + /// **'Recent'** + String get map_recent; + + /// No description provided for @map_stale. + /// + /// In en, this message translates to: + /// **'Stale'** + String get map_stale; + + /// No description provided for @map_visible. + /// + /// In en, this message translates to: + /// **'Visible'** + String get map_visible; + + /// No description provided for @map_hidden. + /// + /// In en, this message translates to: + /// **'Hidden'** + String get map_hidden; + + /// No description provided for @map_centerOnNode. + /// + /// In en, this message translates to: + /// **'Center on node'** + String get map_centerOnNode; + + /// No description provided for @map_details. + /// + /// In en, this message translates to: + /// **'Details'** + String get map_details; + + /// No description provided for @map_noGps. + /// + /// In en, this message translates to: + /// **'No GPS'** + String get map_noGps; + + /// No description provided for @map_noResults. + /// + /// In en, this message translates to: + /// **'No matching nodes'** + String get map_noResults; + /// No description provided for @map_lineOfSight. /// /// In en, this message translates to: @@ -7701,6 +7767,168 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Inferred position'** String get pathTrace_legendInferred; + + /// No description provided for @pathMap_viewSingle. + /// + /// In en, this message translates to: + /// **'Single'** + String get pathMap_viewSingle; + + /// No description provided for @pathMap_viewCombined. + /// + /// In en, this message translates to: + /// **'Combined'** + String get pathMap_viewCombined; + + /// No description provided for @pathMap_play. + /// + /// In en, this message translates to: + /// **'Play'** + String get pathMap_play; + + /// No description provided for @pathMap_pause. + /// + /// In en, this message translates to: + /// **'Pause'** + String get pathMap_pause; + + /// No description provided for @pathMap_replay. + /// + /// In en, this message translates to: + /// **'Replay'** + String get pathMap_replay; + + /// No description provided for @pathMap_stepBack. + /// + /// In en, this message translates to: + /// **'Previous hop'** + String get pathMap_stepBack; + + /// No description provided for @pathMap_stepForward. + /// + /// In en, this message translates to: + /// **'Next hop'** + String get pathMap_stepForward; + + /// No description provided for @pathMap_animationOn. + /// + /// In en, this message translates to: + /// **'Show packet animation'** + String get pathMap_animationOn; + + /// No description provided for @pathMap_animationOff. + /// + /// In en, this message translates to: + /// **'Hide packet animation'** + String get pathMap_animationOff; + + /// No description provided for @pathMap_hopOf. + /// + /// In en, this message translates to: + /// **'Hop {current} of {total}'** + String pathMap_hopOf(int current, int total); + + /// No description provided for @pathMap_observedPaths. + /// + /// In en, this message translates to: + /// **'Observed paths: {count}'** + String pathMap_observedPaths(int count); + + /// No description provided for @pathMap_primary. + /// + /// In en, this message translates to: + /// **'Primary'** + String get pathMap_primary; + + /// No description provided for @pathMap_alternate. + /// + /// In en, this message translates to: + /// **'Alt {index}'** + String pathMap_alternate(int index); + + /// No description provided for @pathMap_hopCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 hop} other{{count} hops}}'** + String pathMap_hopCount(int count); + + /// No description provided for @pathMap_gpsCount. + /// + /// In en, this message translates to: + /// **'{confirmed}/{total} GPS'** + String pathMap_gpsCount(int confirmed, int total); + + /// No description provided for @pathMap_legendShared. + /// + /// In en, this message translates to: + /// **'Shared segment'** + String get pathMap_legendShared; + + /// No description provided for @pathMap_legendEstimated. + /// + /// In en, this message translates to: + /// **'Estimated segment'** + String get pathMap_legendEstimated; + + /// No description provided for @pathMap_sharedNodeCount. + /// + /// In en, this message translates to: + /// **'Used by {count} paths'** + String pathMap_sharedNodeCount(int count); + + /// No description provided for @pathMap_partialAnimation. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 hop has no location — the shown path is partial} other{{count} hops have no location — the shown path is partial}}'** + String pathMap_partialAnimation(int count); + + /// No description provided for @pathMap_showAllPaths. + /// + /// In en, this message translates to: + /// **'Show all'** + String get pathMap_showAllPaths; + + /// No description provided for @pathMap_hidePath. + /// + /// In en, this message translates to: + /// **'Hide path'** + String get pathMap_hidePath; + + /// No description provided for @pathMap_showPath. + /// + /// In en, this message translates to: + /// **'Show path'** + String get pathMap_showPath; + + /// No description provided for @pathMap_collapsePanel. + /// + /// In en, this message translates to: + /// **'Collapse panel'** + String get pathMap_collapsePanel; + + /// No description provided for @pathMap_expandPanel. + /// + /// In en, this message translates to: + /// **'Expand panel'** + String get pathMap_expandPanel; + + /// No description provided for @pathMap_noLocation. + /// + /// In en, this message translates to: + /// **'No location'** + String get pathMap_noLocation; + + /// No description provided for @pathMap_followPacket. + /// + /// In en, this message translates to: + /// **'Lock view to packet'** + String get pathMap_followPacket; + + /// No description provided for @pathMap_unfollowPacket. + /// + /// In en, this message translates to: + /// **'Unlock view from packet'** + String get pathMap_unfollowPacket; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index d22848c3..48d0ea2a 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1738,6 +1738,39 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_title => 'Карта на възлите'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Линия на видимост'; @@ -4479,4 +4512,111 @@ class AppLocalizationsBg extends AppLocalizations { @override String get pathTrace_legendInferred => 'Извлечена позиция'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 98d25a6d..10cc0baf 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1737,6 +1737,39 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_title => 'Karte'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Sichtlinie'; @@ -4500,4 +4533,111 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_legendInferred => 'Abgeleitete Position'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index eecfbc41..f851b914 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1703,6 +1703,39 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_title => 'Node Map'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Line of Sight'; @@ -4405,4 +4438,111 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pathTrace_legendInferred => 'Inferred position'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e0268b4f..c6e8ad2f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1734,6 +1734,39 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_title => 'Mapa de Nodos'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Línea de visión'; @@ -4486,4 +4519,111 @@ class AppLocalizationsEs extends AppLocalizations { @override String get pathTrace_legendInferred => 'Posición inferida'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7bb38a0d..3fd7839d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1741,6 +1741,39 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_title => 'Carte des nœuds'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Ligne de vue'; @@ -4512,4 +4545,111 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_legendInferred => 'Position déduite'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index eef91d09..0a09c402 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1744,6 +1744,39 @@ class AppLocalizationsHu extends AppLocalizations { @override String get map_title => 'Grafikus ábrázás'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Látási vonal'; @@ -4500,4 +4533,111 @@ class AppLocalizationsHu extends AppLocalizations { @override String get pathTrace_legendInferred => 'Feltehető helyzet'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 89fb77e7..b748de9e 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1738,6 +1738,39 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_title => 'Mappa Nodi'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Linea di vista'; @@ -4492,4 +4525,111 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_legendInferred => 'Posizione dedotta'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index c60816ee..150fef5f 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1661,6 +1661,39 @@ class AppLocalizationsJa extends AppLocalizations { @override String get map_title => 'ノードマップ'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => '視界'; @@ -4257,4 +4290,111 @@ class AppLocalizationsJa extends AppLocalizations { @override String get pathTrace_legendInferred => '推測される位置'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 812c2f94..783990cc 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1656,6 +1656,39 @@ class AppLocalizationsKo extends AppLocalizations { @override String get map_title => '노드 매핑'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => '시야'; @@ -4254,4 +4287,111 @@ class AppLocalizationsKo extends AppLocalizations { @override String get pathTrace_legendInferred => '추론된 위치'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 94d5e1e3..087a0204 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1721,6 +1721,39 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_title => 'Kaart van de knopen'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Zichtlijn'; @@ -4463,4 +4496,111 @@ class AppLocalizationsNl extends AppLocalizations { @override String get pathTrace_legendInferred => 'Afgeleide positie'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index b95dcbfc..ae102624 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1750,6 +1750,39 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_title => 'Mapa węzłów'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Linia wzroku'; @@ -4501,4 +4534,111 @@ class AppLocalizationsPl extends AppLocalizations { @override String get pathTrace_legendInferred => 'Wywnioskowana pozycja'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 09a4fa79..55dccf17 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1734,6 +1734,39 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_title => 'Mapa de Nós'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Linha de visão'; @@ -4480,4 +4513,111 @@ class AppLocalizationsPt extends AppLocalizations { @override String get pathTrace_legendInferred => 'Posição inferida'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 6503c5c1..a66e99d4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1739,6 +1739,39 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_title => 'Карта нод'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Линия видимости'; @@ -4496,4 +4529,111 @@ class AppLocalizationsRu extends AppLocalizations { @override String get pathTrace_legendInferred => 'Выведенная позиция'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 3bb7ac5f..349c4c6e 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1726,6 +1726,39 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_title => 'Mapa uzlov'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Úroveň výhľadu'; @@ -4462,4 +4495,111 @@ class AppLocalizationsSk extends AppLocalizations { @override String get pathTrace_legendInferred => 'Odvodená poloha'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e1efed05..f79b0d2e 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1719,6 +1719,39 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_title => 'Mapa omrežja'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Linija vida'; @@ -4461,4 +4494,111 @@ class AppLocalizationsSl extends AppLocalizations { @override String get pathTrace_legendInferred => 'Izpeljana lokacija'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a95e8903..8a354354 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1712,6 +1712,39 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_title => 'Nodkarta'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Synlinje'; @@ -4435,4 +4468,111 @@ class AppLocalizationsSv extends AppLocalizations { @override String get pathTrace_legendInferred => 'Antagen position'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 989acdb8..8379a505 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1732,6 +1732,39 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_title => 'Карта вузлів'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => 'Пряма видимість'; @@ -4495,4 +4528,111 @@ class AppLocalizationsUk extends AppLocalizations { @override String get pathTrace_legendInferred => 'Висновок щодо положення'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index bf4131b8..d2643bd9 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1634,6 +1634,39 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_title => '节点地图'; + @override + String get map_searchHint => 'Search node name or ID'; + + @override + String get map_activity => 'Activity'; + + @override + String get map_online => 'Online'; + + @override + String get map_recent => 'Recent'; + + @override + String get map_stale => 'Stale'; + + @override + String get map_visible => 'Visible'; + + @override + String get map_hidden => 'Hidden'; + + @override + String get map_centerOnNode => 'Center on node'; + + @override + String get map_details => 'Details'; + + @override + String get map_noGps => 'No GPS'; + + @override + String get map_noResults => 'No matching nodes'; + @override String get map_lineOfSight => '视线'; @@ -4145,4 +4178,111 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_legendInferred => '推测的位置'; + + @override + String get pathMap_viewSingle => 'Single'; + + @override + String get pathMap_viewCombined => 'Combined'; + + @override + String get pathMap_play => 'Play'; + + @override + String get pathMap_pause => 'Pause'; + + @override + String get pathMap_replay => 'Replay'; + + @override + String get pathMap_stepBack => 'Previous hop'; + + @override + String get pathMap_stepForward => 'Next hop'; + + @override + String get pathMap_animationOn => 'Show packet animation'; + + @override + String get pathMap_animationOff => 'Hide packet animation'; + + @override + String pathMap_hopOf(int current, int total) { + return 'Hop $current of $total'; + } + + @override + String pathMap_observedPaths(int count) { + return 'Observed paths: $count'; + } + + @override + String get pathMap_primary => 'Primary'; + + @override + String pathMap_alternate(int index) { + return 'Alt $index'; + } + + @override + String pathMap_hopCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops', + one: '1 hop', + ); + return '$_temp0'; + } + + @override + String pathMap_gpsCount(int confirmed, int total) { + return '$confirmed/$total GPS'; + } + + @override + String get pathMap_legendShared => 'Shared segment'; + + @override + String get pathMap_legendEstimated => 'Estimated segment'; + + @override + String pathMap_sharedNodeCount(int count) { + return 'Used by $count paths'; + } + + @override + String pathMap_partialAnimation(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hops have no location — the shown path is partial', + one: '1 hop has no location — the shown path is partial', + ); + return '$_temp0'; + } + + @override + String get pathMap_showAllPaths => 'Show all'; + + @override + String get pathMap_hidePath => 'Hide path'; + + @override + String get pathMap_showPath => 'Show path'; + + @override + String get pathMap_collapsePanel => 'Collapse panel'; + + @override + String get pathMap_expandPanel => 'Expand panel'; + + @override + String get pathMap_noLocation => 'No location'; + + @override + String get pathMap_followPacket => 'Lock view to packet'; + + @override + String get pathMap_unfollowPacket => 'Unlock view from packet'; } diff --git a/lib/models/display_path.dart b/lib/models/display_path.dart new file mode 100644 index 00000000..71f8ed53 --- /dev/null +++ b/lib/models/display_path.dart @@ -0,0 +1,69 @@ +import 'dart:ui'; + +import 'package:latlong2/latlong.dart'; + +import 'path_history.dart'; + +/// One observed route rendered on the path map — the live traced path +/// (primary) or an alternate from the contact's path history — resolved to +/// map coordinates with per-hop confidence flags. +class DisplayPath { + final String id; + final String label; + final Color color; + final bool isPrimary; + + /// Outbound hop bytes, including hops that could not be placed on the map. + final List hopBytes; + + /// Resolved map points: self, each locatable hop, then the target when its + /// position is known. Hops with no position are skipped here but still + /// counted in [unresolvedHops]. + final List points; + + /// Display name for each entry of [points]. + final List pointLabels; + + /// Whether each entry of [points] is a GPS-grade position (vs inferred). + final List pointConfirmed; + + /// Per segment (length points-1): true when either endpoint is inferred or + /// unlocatable hops were skipped in between — rendered dashed. + final List segmentEstimated; + + /// Per segment: the transmission ordinal of the segment's destination, + /// used to highlight the matching hop-list row during animation. + final List rowForSegment; + + /// Total transmissions on the full route (including unlocatable hops). + final int totalTransmissions; + + /// True when the route ends with a chat-target endpoint row. + final bool hasTargetEndpoint; + + final int gpsConfirmedHops; + final int unresolvedHops; + final double distanceMeters; + + /// History metadata; null for the live traced (primary) path. + final PathRecord? record; + + const DisplayPath({ + required this.id, + required this.label, + required this.color, + required this.isPrimary, + required this.hopBytes, + required this.points, + required this.pointLabels, + required this.pointConfirmed, + required this.segmentEstimated, + required this.rowForSegment, + required this.totalTransmissions, + required this.hasTargetEndpoint, + required this.gpsConfirmedHops, + required this.unresolvedHops, + required this.distanceMeters, + this.record, + }); +} diff --git a/lib/models/path_playback.dart b/lib/models/path_playback.dart new file mode 100644 index 00000000..89fdff0b --- /dev/null +++ b/lib/models/path_playback.dart @@ -0,0 +1,177 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:latlong2/latlong.dart'; + +/// Timeline state for the packet-flow animation on the path map. +/// +/// The packet travels each segment over [segmentMs] (scaled by [speed]), +/// then dwells at the reached hop for [dwellMs] so the hop visibly lights up. +/// Overlay layers listen to this controller directly; [activeSegment] only +/// fires when the segment index changes so list highlights rebuild cheaply. +class PathPlaybackController extends ChangeNotifier { + static const double segmentMs = 1100; + static const double dwellMs = 380; + static const List speedSteps = [0.5, 1.0, 2.0]; + + late final Ticker _ticker; + List _points = const []; + double _timelineMs = 0; + Duration _lastTick = Duration.zero; + bool _playing = false; + bool _started = false; + double _speed = 1.0; + + /// Segment currently being traveled (clamped to the last segment), or -1 + /// while the animation has not been started — listeners use this for + /// hop-list highlighting without rebuilding every tick. + final ValueNotifier activeSegment = ValueNotifier(-1); + + PathPlaybackController(TickerProvider vsync) { + _ticker = vsync.createTicker(_onTick); + } + + List get points => _points; + bool get hasPath => _points.length >= 2; + int get segmentCount => hasPath ? _points.length - 1 : 0; + bool get playing => _playing; + double get speed => _speed; + + /// True once the user has started or stepped the animation; the packet + /// overlay renders only in this state. + bool get started => _started; + + double get _slotMs => segmentMs + dwellMs; + double get _totalMs => segmentCount * _slotMs; + bool get isComplete => hasPath && _timelineMs >= _totalMs; + + int get currentSegment { + if (!hasPath) return 0; + return (_timelineMs / _slotMs).floor().clamp(0, segmentCount - 1); + } + + /// Travel progress through [currentSegment]; 1.0 while dwelling at its end. + double get segmentProgress { + if (!hasPath) return 0; + final within = _timelineMs - currentSegment * _slotMs; + return (within / segmentMs).clamp(0.0, 1.0); + } + + /// Dwell progress (0..1) at the reached hop, or null while traveling. + double? get dwellProgress { + if (!hasPath || isComplete) return null; + final within = _timelineMs - currentSegment * _slotMs; + if (within < segmentMs) return null; + return ((within - segmentMs) / dwellMs).clamp(0.0, 1.0); + } + + /// Index of the point the packet has most recently reached. + int get reachedPointIndex { + if (!hasPath) return 0; + if (isComplete) return _points.length - 1; + return segmentProgress >= 1.0 ? currentSegment + 1 : currentSegment; + } + + LatLng get position { + if (!hasPath) return const LatLng(0, 0); + final seg = currentSegment; + final a = _points[seg]; + final b = _points[seg + 1]; + final t = segmentProgress; + return LatLng( + a.latitude + (b.latitude - a.latitude) * t, + a.longitude + (b.longitude - a.longitude) * t, + ); + } + + /// Replaces the path and resets the animation to the start. + void setPath(List points) { + _ticker.stop(); + _points = List.unmodifiable(points); + _timelineMs = 0; + _playing = false; + _started = false; + activeSegment.value = -1; + notifyListeners(); + } + + void play() { + if (!hasPath) return; + if (isComplete) _timelineMs = 0; + _started = true; + _playing = true; + activeSegment.value = currentSegment; + if (!_ticker.isActive) { + _lastTick = Duration.zero; + _ticker.start(); + } + notifyListeners(); + } + + void pause() { + _ticker.stop(); + _playing = false; + notifyListeners(); + } + + void togglePlay() => _playing ? pause() : play(); + + void replay() { + if (!hasPath) return; + _timelineMs = 0; + activeSegment.value = 0; + play(); + } + + /// Stops playback and hides the packet overlay. + void stop() { + _ticker.stop(); + _playing = false; + _started = false; + _timelineMs = 0; + activeSegment.value = -1; + notifyListeners(); + } + + void stepForward() => _jumpToPoint(reachedPointIndex + 1); + + void stepBack() => _jumpToPoint(reachedPointIndex - 1); + + void cycleSpeed() { + final index = speedSteps.indexOf(_speed); + _speed = speedSteps[(index + 1) % speedSteps.length]; + notifyListeners(); + } + + void _jumpToPoint(int index) { + if (!hasPath) return; + _ticker.stop(); + _playing = false; + _started = true; + final clamped = index.clamp(0, _points.length - 1); + // Land at the start of the dwell window so the hop pulse plays. + _timelineMs = clamped == 0 ? 0 : (clamped - 1) * _slotMs + segmentMs; + activeSegment.value = currentSegment; + notifyListeners(); + } + + void _onTick(Duration elapsed) { + final dtMs = (elapsed - _lastTick).inMicroseconds / 1000.0; + _lastTick = elapsed; + _timelineMs = (_timelineMs + dtMs * _speed).clamp(0.0, _totalMs); + if (_timelineMs >= _totalMs) { + _ticker.stop(); + _playing = false; + } + if (activeSegment.value != currentSegment) { + activeSegment.value = currentSegment; + } + notifyListeners(); + } + + @override + void dispose() { + _ticker.dispose(); + activeSegment.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index 522a2c5c..4b00c70c 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../services/app_debug_log_service.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../helpers/snack_bar_builder.dart'; @@ -58,27 +59,57 @@ class AppDebugLogScreen extends StatelessWidget { child: hasEntries ? ListView.separated( itemCount: entries.length, - separatorBuilder: (_, _) => const Divider(height: 1), + separatorBuilder: (_, _) => + const Divider(height: 1, color: MeshPalette.line), itemBuilder: (context, index) { final entry = entries[index]; - return ListTile( - dense: true, - leading: _buildLevelIcon(context, entry.level), - title: Text( - '[${entry.tag}] ${entry.message}', - style: const TextStyle( - fontSize: 12, - fontFamily: 'monospace', - ), + return Container( + color: MeshPalette.bg, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), - subtitle: Text( - entry.formattedTime, - style: TextStyle( - fontSize: 10, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLevelIcon(context, entry.level), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '[${entry.tag}] ', + style: MeshTheme.mono( + fontSize: 11.5, + color: _levelColor(entry.level), + ), + ), + TextSpan( + text: entry.message, + style: MeshTheme.mono( + fontSize: 11.5, + color: MeshPalette.ink2, + ), + ), + ], + ), + ), + const SizedBox(height: 2), + Text( + entry.formattedTime, + style: MeshTheme.mono( + fontSize: 9.5, + color: MeshPalette.ink4, + ), + ), + ], + ), + ), + ], ), ); }, @@ -87,29 +118,25 @@ class AppDebugLogScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.bug_report_outlined, size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: MeshPalette.ink3, ), const SizedBox(height: 16), Text( context.l10n.debugLog_noEntries, - style: TextStyle( + style: const TextStyle( fontSize: 16, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + color: MeshPalette.ink3, ), ), const SizedBox(height: 8), Text( context.l10n.debugLog_enableInSettings, - style: TextStyle( + style: const TextStyle( fontSize: 12, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + color: MeshPalette.ink3, ), ), ], @@ -121,19 +148,37 @@ class AppDebugLogScreen extends StatelessWidget { ); } - Widget _buildLevelIcon(BuildContext context, AppDebugLogLevel level) { - final colorScheme = Theme.of(context).colorScheme; + Color _levelColor(AppDebugLogLevel level) { switch (level) { case AppDebugLogLevel.info: - return Icon(Icons.info_outline, size: 18, color: colorScheme.primary); + return MeshPalette.blue; case AppDebugLogLevel.warning: - return Icon( + return MeshPalette.warn; + case AppDebugLogLevel.error: + return MeshPalette.alert; + } + } + + Widget _buildLevelIcon(BuildContext context, AppDebugLogLevel level) { + switch (level) { + case AppDebugLogLevel.info: + return const Icon( + Icons.info_outline, + size: 18, + color: MeshPalette.blue, + ); + case AppDebugLogLevel.warning: + return const Icon( Icons.warning_amber_outlined, size: 18, - color: colorScheme.tertiary, + color: MeshPalette.warn, ); case AppDebugLogLevel.error: - return Icon(Icons.error_outline, size: 18, color: colorScheme.error); + return const Icon( + Icons.error_outline, + size: 18, + color: MeshPalette.alert, + ); } } } diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index d1ccfd09..3d3af8b9 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -10,7 +10,9 @@ import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; import '../services/notification_service.dart'; import '../services/translation_service.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/sync_progress_overlay.dart'; import '../helpers/snack_bar_builder.dart'; import 'map_cache_screen.dart'; @@ -28,73 +30,123 @@ class AppSettingsScreen extends StatelessWidget { ), body: SafeArea( top: false, - child: - Consumer3< - AppSettingsService, - MeshCoreConnector, - TranslationService - >( - builder: - ( + child: Consumer3( + builder: ( + context, + settingsService, + connector, + translationService, + child, + ) { + return ListView( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 24), + children: [ + // APPEARANCE + SectionHeader(context.l10n.appSettings_appearance), + MeshCard( + padding: EdgeInsets.zero, + child: _buildAppearanceContent(context, settingsService), + ), + + // NOTIFICATIONS + SectionHeader(context.l10n.appSettings_notifications), + MeshCard( + padding: EdgeInsets.zero, + child: _buildNotificationsContent(context, settingsService), + ), + + // MESSAGING + SectionHeader(context.l10n.appSettings_messaging), + MeshCard( + padding: EdgeInsets.zero, + child: _buildMessagingContent(context, settingsService), + ), + + // BATTERY + SectionHeader(context.l10n.appSettings_battery), + MeshCard( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), + child: _buildBatteryContent( context, settingsService, connector, - translationService, - child, - ) { - return ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildAppearanceCard(context, settingsService), - const SizedBox(height: 16), - _buildNotificationsCard(context, settingsService), - const SizedBox(height: 16), - _buildMessagingCard(context, settingsService), - const SizedBox(height: 16), - if (!kIsWeb) ...[ - _buildTranslationCard( - context, - settingsService, - translationService, - ), - const SizedBox(height: 16), - ], - _buildBatteryCard(context, settingsService, connector), - const SizedBox(height: 16), - _buildMapSettingsCard(context, settingsService), - const SizedBox(height: 16), - _buildCyr2LatCard(context, settingsService), - const SizedBox(height: 16), - _buildDebugCard(context, settingsService), - ], - ); - }, - ), + ), + ), + + // MAP + SectionHeader(context.l10n.appSettings_mapDisplay), + MeshCard( + padding: EdgeInsets.zero, + child: _buildMapContent(context, settingsService), + ), + + // TRANSLATION (non-web only) + if (!kIsWeb) ...[ + SectionHeader(context.l10n.translation_title), + MeshCard( + padding: EdgeInsets.zero, + child: _buildTranslationContent( + context, + settingsService, + translationService, + ), + ), + ], + + // CYR2LAT + SectionHeader(context.l10n.channels_cyr2latSettingsHeading), + MeshCard( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), + child: _buildCyr2LatContent(context, settingsService), + ), + + // DEBUG + SectionHeader(context.l10n.appSettings_debugCard), + MeshCard( + padding: EdgeInsets.zero, + child: _buildDebugContent(context, settingsService), + ), + ], + ); + }, + ), ), ); } - Widget _buildAppearanceCard( + Widget _buildAppearanceContent( BuildContext context, AppSettingsService settingsService, ) { - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.appSettings_appearance, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - ListTile( - leading: const Icon(Icons.brightness_6_outlined), - title: Text(context.l10n.appSettings_theme), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8), - child: SegmentedButton( + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.brightness_6_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Text( + context.l10n.appSettings_theme, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 10), + SegmentedButton( segments: [ ButtonSegment( value: 'system', @@ -114,713 +166,1016 @@ class AppSettingsScreen extends StatelessWidget { settingsService.setThemeMode(selection.first); }, ), + ], + ), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showLanguageSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.language_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_language, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + _languageLabel( + context, + settingsService.settings.languageOverride, + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.language_outlined), - title: Text(context.l10n.appSettings_language), - subtitle: Text( - _languageLabel( - context, - settingsService.settings.languageOverride, - ), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () => _showLanguageDialog(context, settingsService), - ), - ], - ), + ), + ], ); } - Widget _buildNotificationsCard( + Widget _buildNotificationsContent( BuildContext context, AppSettingsService settingsService, ) { - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.appSettings_notifications, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + final notifEnabled = settingsService.settings.notificationsEnabled; + return Column( + children: [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.notifications_outlined, size: 20), + title: Text(context.l10n.appSettings_enableNotifications), + subtitle: Text(context.l10n.appSettings_enableNotificationsSubtitle), + value: settingsService.settings.notificationsEnabled, + onChanged: (value) async { + if (value) { + final granted = await NotificationService().requestPermissions(); + if (!granted) { + if (context.mounted) { + showDismissibleSnackBar( + context, + content: Text( + context.l10n.appSettings_notificationPermissionDenied, + ), + duration: const Duration(seconds: 2), + ); + } + return; + } + } + await settingsService.setNotificationsEnabled(value); + if (context.mounted) { + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_notificationsEnabled + : context.l10n.appSettings_notificationsDisabled, + ), + duration: const Duration(seconds: 2), + ); + } + }, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: Icon( + Icons.message_outlined, + size: 20, + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + title: Text( + context.l10n.appSettings_messageNotifications, + style: TextStyle( + color: notifEnabled ? null : Theme.of(context).disabledColor, ), ), - SwitchListTile( - secondary: const Icon(Icons.notifications_outlined), - title: Text(context.l10n.appSettings_enableNotifications), - subtitle: Text( - context.l10n.appSettings_enableNotificationsSubtitle, + subtitle: Text( + context.l10n.appSettings_messageNotificationsSubtitle, + style: TextStyle( + color: notifEnabled ? null : Theme.of(context).disabledColor, ), - value: settingsService.settings.notificationsEnabled, - onChanged: (value) async { - if (value) { - // Request permission when enabling - final granted = await NotificationService() - .requestPermissions(); - if (!granted) { - if (context.mounted) { - showDismissibleSnackBar( - context, - content: Text( - context.l10n.appSettings_notificationPermissionDenied, + ), + value: settingsService.settings.notifyOnNewMessage, + onChanged: notifEnabled + ? (value) => settingsService.setNotifyOnNewMessage(value) + : null, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: Icon( + Icons.forum_outlined, + size: 20, + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + title: Text( + context.l10n.appSettings_channelMessageNotifications, + style: TextStyle( + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + ), + subtitle: Text( + context.l10n.appSettings_channelMessageNotificationsSubtitle, + style: TextStyle( + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + ), + value: settingsService.settings.notifyOnNewChannelMessage, + onChanged: notifEnabled + ? (value) => + settingsService.setNotifyOnNewChannelMessage(value) + : null, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: Icon( + Icons.cell_tower, + size: 20, + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + title: Text( + context.l10n.appSettings_advertisementNotifications, + style: TextStyle( + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + ), + subtitle: Text( + context.l10n.appSettings_advertisementNotificationsSubtitle, + style: TextStyle( + color: notifEnabled ? null : Theme.of(context).disabledColor, + ), + ), + value: settingsService.settings.notifyOnNewAdvert, + onChanged: notifEnabled + ? (value) => settingsService.setNotifyOnNewAdvert(value) + : null, + ), + ], + ); + } + + Widget _buildMessagingContent( + BuildContext context, + AppSettingsService settingsService, + ) { + final autoRouteEnabled = settingsService.settings.autoRouteRotationEnabled; + return Column( + children: [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.refresh_outlined, size: 20), + title: Text(context.l10n.appSettings_clearPathOnMaxRetry), + subtitle: Text( + context.l10n.appSettings_clearPathOnMaxRetrySubtitle, + ), + value: settingsService.settings.clearPathOnMaxRetry, + onChanged: (value) { + settingsService.setClearPathOnMaxRetry(value); + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_pathsWillBeCleared + : context.l10n.appSettings_pathsWillNotBeCleared, + ), + duration: const Duration(seconds: 2), + ); + }, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.vertical_align_top, size: 20), + title: Text(context.l10n.appSettings_jumpToOldestUnread), + subtitle: Text(context.l10n.appSettings_jumpToOldestUnreadSubtitle), + value: settingsService.settings.jumpToOldestUnread, + onChanged: settingsService.setJumpToOldestUnread, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.alt_route, size: 20), + title: Text(context.l10n.appSettings_autoRouteRotation), + subtitle: Text(context.l10n.appSettings_autoRouteRotationSubtitle), + value: autoRouteEnabled, + onChanged: (value) { + settingsService.setAutoRouteRotationEnabled(value); + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_autoRouteRotationEnabled + : context.l10n.appSettings_autoRouteRotationDisabled, + ), + duration: const Duration(seconds: 2), + ); + }, + ), + // AnimatedSize sub-options for auto-route rotation + AnimatedSize( + duration: const Duration(milliseconds: 200), + alignment: Alignment.topCenter, + child: autoRouteEnabled + ? Container( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + padding: const EdgeInsets.only(left: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxRouteWeight), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_maxRouteWeightSubtitle, + ), + Slider( + value: settingsService.settings.maxRouteWeight, + min: 1, + max: 10, + divisions: 9, + label: settingsService.settings.maxRouteWeight + .round() + .toString(), + onChanged: (value) => + settingsService.setMaxRouteWeight(value), + ), + ], + ), ), - duration: const Duration(seconds: 2), + const Divider(height: 1), + ListTile( + title: Text( + context.l10n.appSettings_initialRouteWeight, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_initialRouteWeightSubtitle, + ), + Slider( + value: + settingsService.settings.initialRouteWeight, + min: 0.5, + max: 5.0, + divisions: 9, + label: settingsService + .settings + .initialRouteWeight + .toStringAsFixed(1), + onChanged: (value) => + settingsService.setInitialRouteWeight(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text( + context + .l10n + .appSettings_routeWeightSuccessIncrement, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightSuccessIncrementSubtitle, + ), + Slider( + value: settingsService + .settings + .routeWeightSuccessIncrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService + .settings + .routeWeightSuccessIncrement + .toStringAsFixed(1), + onChanged: (value) => settingsService + .setRouteWeightSuccessIncrement(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text( + context + .l10n + .appSettings_routeWeightFailureDecrement, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightFailureDecrementSubtitle, + ), + Slider( + value: settingsService + .settings + .routeWeightFailureDecrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService + .settings + .routeWeightFailureDecrement + .toStringAsFixed(1), + onChanged: (value) => settingsService + .setRouteWeightFailureDecrement(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text( + context.l10n.appSettings_maxMessageRetries, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_maxMessageRetriesSubtitle, + ), + Slider( + value: settingsService + .settings + .maxMessageRetries + .toDouble(), + min: 2, + max: 10, + divisions: 8, + label: settingsService + .settings + .maxMessageRetries + .toString(), + onChanged: (value) => settingsService + .setMaxMessageRetries(value.toInt()), + ), + ], + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.location_searching, size: 20), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), + ], + ); + } + + Widget _buildBatteryContent( + BuildContext context, + AppSettingsService settingsService, + MeshCoreConnector connector, + ) { + final deviceId = connector.deviceId; + final isConnected = connector.isConnected && deviceId != null; + final selection = isConnected + ? settingsService.batteryChemistryForDevice(deviceId) + : 'nmc'; + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 4), + child: Row( + children: [ + Icon( + Icons.battery_full, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_batteryChemistry, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + isConnected + ? context.l10n.appSettings_batteryChemistryPerDevice( + connector.deviceDisplayName, + ) + : context + .l10n + .appSettings_batteryChemistryConnectFirst, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + initialValue: selection, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, + ), + onChanged: isConnected + ? (value) { + if (value != null) { + settingsService.setBatteryChemistryForDevice( + deviceId, + value, ); } - return; } - } - - await settingsService.setNotificationsEnabled(value); - if (context.mounted) { - showDismissibleSnackBar( - context, - content: Text( - value - ? context.l10n.appSettings_notificationsEnabled - : context.l10n.appSettings_notificationsDisabled, - ), - duration: const Duration(seconds: 2), - ); - } - }, - ), - const Divider(height: 1), - SwitchListTile( - secondary: Icon( - Icons.message_outlined, - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, + : null, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(context.l10n.appSettings_batteryNmc), ), - title: Text( - context.l10n.appSettings_messageNotifications, - style: TextStyle( - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(context.l10n.appSettings_batteryLifepo4), ), - subtitle: Text( - context.l10n.appSettings_messageNotificationsSubtitle, - style: TextStyle( - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), + DropdownMenuItem( + value: 'lipo', + child: Text(context.l10n.appSettings_batteryLipo), ), - value: settingsService.settings.notifyOnNewMessage, - onChanged: settingsService.settings.notificationsEnabled - ? (value) { - settingsService.setNotifyOnNewMessage(value); - } - : null, - ), - const Divider(height: 1), - SwitchListTile( - secondary: Icon( - Icons.forum_outlined, - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), - title: Text( - context.l10n.appSettings_channelMessageNotifications, - style: TextStyle( - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - subtitle: Text( - context.l10n.appSettings_channelMessageNotificationsSubtitle, - style: TextStyle( - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - value: settingsService.settings.notifyOnNewChannelMessage, - onChanged: settingsService.settings.notificationsEnabled - ? (value) { - settingsService.setNotifyOnNewChannelMessage(value); - } - : null, - ), - const Divider(height: 1), - SwitchListTile( - secondary: Icon( - Icons.cell_tower, - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), - title: Text( - context.l10n.appSettings_advertisementNotifications, - style: TextStyle( - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - subtitle: Text( - context.l10n.appSettings_advertisementNotificationsSubtitle, - style: TextStyle( - color: settingsService.settings.notificationsEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - value: settingsService.settings.notifyOnNewAdvert, - onChanged: settingsService.settings.notificationsEnabled - ? (value) { - settingsService.setNotifyOnNewAdvert(value); - } - : null, - ), - ], - ), + ], + ), + ], ); } - Widget _buildMessagingCard( + Widget _buildMapContent( BuildContext context, AppSettingsService settingsService, ) { - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.appSettings_messaging, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return Column( + children: [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, ), - SwitchListTile( - secondary: const Icon(Icons.refresh_outlined), - title: Text(context.l10n.appSettings_clearPathOnMaxRetry), - subtitle: Text( - context.l10n.appSettings_clearPathOnMaxRetrySubtitle, - ), - value: settingsService.settings.clearPathOnMaxRetry, - onChanged: (value) { - settingsService.setClearPathOnMaxRetry(value); - showDismissibleSnackBar( - context, - content: Text( - value - ? context.l10n.appSettings_pathsWillBeCleared - : context.l10n.appSettings_pathsWillNotBeCleared, + secondary: const Icon(Icons.router_outlined, size: 20), + title: Text(context.l10n.appSettings_showRepeaters), + subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle), + value: settingsService.settings.mapShowRepeaters, + onChanged: (value) => settingsService.setMapShowRepeaters(value), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.chat_outlined, size: 20), + title: Text(context.l10n.appSettings_showChatNodes), + subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle), + value: settingsService.settings.mapShowChatNodes, + onChanged: (value) => settingsService.setMapShowChatNodes(value), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.people_outline, size: 20), + title: Text(context.l10n.appSettings_showOtherNodes), + subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle), + value: settingsService.settings.mapShowOtherNodes, + onChanged: (value) => settingsService.setMapShowOtherNodes(value), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showTimeFilterSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.timer_outlined, + size: 20, + color: scheme.onSurfaceVariant, ), - duration: const Duration(seconds: 2), - ); - }, - ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.vertical_align_top), - title: Text(context.l10n.appSettings_jumpToOldestUnread), - subtitle: Text(context.l10n.appSettings_jumpToOldestUnreadSubtitle), - value: settingsService.settings.jumpToOldestUnread, - onChanged: settingsService.setJumpToOldestUnread, - ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.alt_route), - title: Text(context.l10n.appSettings_autoRouteRotation), - subtitle: Text(context.l10n.appSettings_autoRouteRotationSubtitle), - value: settingsService.settings.autoRouteRotationEnabled, - onChanged: (value) { - settingsService.setAutoRouteRotationEnabled(value); - showDismissibleSnackBar( - context, - content: Text( - value - ? context.l10n.appSettings_autoRouteRotationEnabled - : context.l10n.appSettings_autoRouteRotationDisabled, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_timeFilter, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.mapTimeFilterHours == 0 + ? context.l10n.appSettings_timeFilterShowAll + : context.l10n.appSettings_timeFilterShowLast( + settingsService + .settings + .mapTimeFilterHours + .toInt(), + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), ), - duration: const Duration(seconds: 2), - ); - }, - ), - if (settingsService.settings.autoRouteRotationEnabled) - Container( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - padding: const EdgeInsets.only(left: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxRouteWeight), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.appSettings_maxRouteWeightSubtitle), - Slider( - value: settingsService.settings.maxRouteWeight, - min: 1, - max: 10, - divisions: 9, - label: settingsService.settings.maxRouteWeight - .round() - .toString(), - onChanged: (value) => - settingsService.setMaxRouteWeight(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_initialRouteWeight), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.appSettings_initialRouteWeightSubtitle, - ), - Slider( - value: settingsService.settings.initialRouteWeight, - min: 0.5, - max: 5.0, - divisions: 9, - label: settingsService.settings.initialRouteWeight - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setInitialRouteWeight(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text( - context.l10n.appSettings_routeWeightSuccessIncrement, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context - .l10n - .appSettings_routeWeightSuccessIncrementSubtitle, - ), - Slider( - value: settingsService - .settings - .routeWeightSuccessIncrement, - min: 0.1, - max: 2.0, - divisions: 19, - label: settingsService - .settings - .routeWeightSuccessIncrement - .toStringAsFixed(1), - onChanged: (value) => settingsService - .setRouteWeightSuccessIncrement(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text( - context.l10n.appSettings_routeWeightFailureDecrement, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context - .l10n - .appSettings_routeWeightFailureDecrementSubtitle, - ), - Slider( - value: settingsService - .settings - .routeWeightFailureDecrement, - min: 0.1, - max: 2.0, - divisions: 19, - label: settingsService - .settings - .routeWeightFailureDecrement - .toStringAsFixed(1), - onChanged: (value) => settingsService - .setRouteWeightFailureDecrement(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxMessageRetries), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.appSettings_maxMessageRetriesSubtitle, - ), - Slider( - value: settingsService.settings.maxMessageRetries - .toDouble(), - min: 2, - max: 10, - divisions: 8, - label: settingsService.settings.maxMessageRetries - .toString(), - onChanged: (value) => settingsService - .setMaxMessageRetries(value.toInt()), - ), - ], - ), - ), - ], - ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.location_searching), - title: Text(context.l10n.appSettings_enableMessageTracing), - subtitle: Text( - context.l10n.appSettings_enableMessageTracingSubtitle, - ), - value: settingsService.settings.enableMessageTracing, - onChanged: (value) { - settingsService.setEnableMessageTracing(value); - }, ), - ], - ), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showUnitsSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.straighten, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_unitsTitle, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.unitSystem == + UnitSystem.imperial + ? context.l10n.appSettings_unitsImperial + : context.l10n.appSettings_unitsMetric, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], + ), + ), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MapCacheScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.download_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_offlineMapCache, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.mapCacheBounds == null + ? context.l10n.appSettings_noAreaSelected + : context.l10n.appSettings_areaSelectedZoom( + settingsService.settings.mapCacheMinZoom, + settingsService.settings.mapCacheMaxZoom, + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], + ), + ), + ), + ], ); } - Widget _buildMapSettingsCard( - BuildContext context, - AppSettingsService settingsService, - ) { - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.appSettings_mapDisplay, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - SwitchListTile( - secondary: const Icon(Icons.router_outlined), - title: Text(context.l10n.appSettings_showRepeaters), - subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle), - value: settingsService.settings.mapShowRepeaters, - onChanged: (value) { - settingsService.setMapShowRepeaters(value); - }, - ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.chat_outlined), - title: Text(context.l10n.appSettings_showChatNodes), - subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle), - value: settingsService.settings.mapShowChatNodes, - onChanged: (value) { - settingsService.setMapShowChatNodes(value); - }, - ), - const Divider(height: 1), - SwitchListTile( - secondary: const Icon(Icons.people_outline), - title: Text(context.l10n.appSettings_showOtherNodes), - subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle), - value: settingsService.settings.mapShowOtherNodes, - onChanged: (value) { - settingsService.setMapShowOtherNodes(value); - }, - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.timer_outlined), - title: Text(context.l10n.appSettings_timeFilter), - subtitle: Text( - settingsService.settings.mapTimeFilterHours == 0 - ? context.l10n.appSettings_timeFilterShowAll - : context.l10n.appSettings_timeFilterShowLast( - settingsService.settings.mapTimeFilterHours.toInt(), - ), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () => _showTimeFilterDialog(context, settingsService), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.straighten), - title: Text(context.l10n.appSettings_unitsTitle), - subtitle: Text( - settingsService.settings.unitSystem == UnitSystem.imperial - ? context.l10n.appSettings_unitsImperial - : context.l10n.appSettings_unitsMetric, - ), - trailing: const Icon(Icons.chevron_right), - onTap: () => _showUnitsDialog(context, settingsService), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.download_outlined), - title: Text(context.l10n.appSettings_offlineMapCache), - subtitle: Text( - settingsService.settings.mapCacheBounds == null - ? context.l10n.appSettings_noAreaSelected - : context.l10n.appSettings_areaSelectedZoom( - settingsService.settings.mapCacheMinZoom, - settingsService.settings.mapCacheMaxZoom, - ), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const MapCacheScreen()), - ); - }, - ), - ], - ), - ); - } - - Widget _buildTranslationCard( + Widget _buildTranslationContent( BuildContext context, AppSettingsService settingsService, TranslationService translationService, ) { final settings = settingsService.settings; final translationEnabled = settings.translationEnabled; - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.translation_title, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: const Icon(Icons.translate, size: 20), + title: Text(context.l10n.translation_enableTitle), + subtitle: Text(context.l10n.translation_enableSubtitle), + value: settings.translationEnabled, + onChanged: settingsService.setTranslationEnabled, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + secondary: Icon( + Icons.auto_awesome_outlined, + size: 20, + color: translationEnabled ? null : Theme.of(context).disabledColor, + ), + title: Text( + context.l10n.translation_autoIncomingTitle, + style: TextStyle( + color: + translationEnabled ? null : Theme.of(context).disabledColor, ), ), - SwitchListTile( - secondary: const Icon(Icons.translate), - title: Text(context.l10n.translation_enableTitle), - subtitle: Text(context.l10n.translation_enableSubtitle), - value: settings.translationEnabled, - onChanged: settingsService.setTranslationEnabled, + subtitle: Text( + context.l10n.translation_autoIncomingSubtitle, + style: TextStyle( + color: + translationEnabled ? null : Theme.of(context).disabledColor, + ), ), - const Divider(height: 1), - SwitchListTile( - secondary: Icon( - Icons.auto_awesome_outlined, - color: translationEnabled - ? null - : Theme.of(context).disabledColor, - ), - title: Text( - context.l10n.translation_autoIncomingTitle, - style: TextStyle( - color: translationEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - subtitle: Text( - context.l10n.translation_autoIncomingSubtitle, - style: TextStyle( - color: translationEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - value: settings.autoTranslateIncomingMessages, - onChanged: translationEnabled - ? settingsService.setAutoTranslateIncomingMessages - : null, + value: settings.autoTranslateIncomingMessages, + onChanged: translationEnabled + ? settingsService.setAutoTranslateIncomingMessages + : null, + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, ), - const Divider(height: 1), - SwitchListTile( - secondary: Icon( - Icons.outgoing_mail, - color: translationEnabled - ? null - : Theme.of(context).disabledColor, - ), - title: Text( - context.l10n.translation_composerTitle, - style: TextStyle( - color: translationEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - subtitle: Text( - context.l10n.translation_composerSubtitle, - style: TextStyle( - color: translationEnabled - ? null - : Theme.of(context).disabledColor, - ), - ), - value: settings.composerTranslationEnabled, - onChanged: translationEnabled - ? settingsService.setComposerTranslationEnabled - : null, + secondary: Icon( + Icons.outgoing_mail, + size: 20, + color: translationEnabled ? null : Theme.of(context).disabledColor, ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.language), - title: Text(context.l10n.translation_targetLanguage), - subtitle: Text( - _translationLanguageLabel( - context, - settings.translationTargetLanguageCode, - ), + title: Text( + context.l10n.translation_composerTitle, + style: TextStyle( + color: + translationEnabled ? null : Theme.of(context).disabledColor, ), - trailing: const Icon(Icons.chevron_right), - onTap: () => - _showTranslationLanguageDialog(context, settingsService), ), - const Divider(height: 1), - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), - child: DropdownButtonFormField( - initialValue: settings.translationSelectedModelId, - isExpanded: true, - decoration: InputDecoration( - labelText: context.l10n.translation_downloadedModelLabel, - border: const OutlineInputBorder(), - ), - items: [ - for (final model in settings.translationDownloadedModels) - DropdownMenuItem( - value: model.id, - child: Text(translationModelFriendlyName(model)), + subtitle: Text( + context.l10n.translation_composerSubtitle, + style: TextStyle( + color: + translationEnabled ? null : Theme.of(context).disabledColor, + ), + ), + value: settings.composerTranslationEnabled, + onChanged: translationEnabled + ? settingsService.setComposerTranslationEnabled + : null, + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => + _showTranslationLanguageDialog(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.language, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.translation_targetLanguage, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + _translationLanguageLabel( + context, + settings.translationTargetLanguageCode, + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), ], - onChanged: settings.translationDownloadedModels.isEmpty - ? null - : (value) { - settingsService.setTranslationSelectedModelId(value); - }, ), ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), - child: DropdownButtonFormField( - initialValue: null, - isExpanded: true, - decoration: InputDecoration( - labelText: context.l10n.translation_presetModelLabel, - border: const OutlineInputBorder(), - ), - items: [ - for (final preset in translationPresetModels) - DropdownMenuItem( - value: preset.sourceUrl, - child: Text(translationModelFriendlyName(preset)), - ), - ], - onChanged: translationService.isBusy - ? null - : (value) async { - if (value == null) return; - final preset = translationPresetModels.firstWhere( - (entry) => entry.sourceUrl == value, - ); - await _downloadTranslationModel( + ), + const Divider(height: 1, indent: 16), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: DropdownButtonFormField( + initialValue: settings.translationSelectedModelId, + isExpanded: true, + decoration: InputDecoration( + labelText: context.l10n.translation_downloadedModelLabel, + border: const OutlineInputBorder(), + ), + items: [ + for (final model in settings.translationDownloadedModels) + DropdownMenuItem( + value: model.id, + child: Text(translationModelFriendlyName(model)), + ), + ], + onChanged: settings.translationDownloadedModels.isEmpty + ? null + : (value) { + settingsService.setTranslationSelectedModelId(value); + }, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: DropdownButtonFormField( + initialValue: null, + isExpanded: true, + decoration: InputDecoration( + labelText: context.l10n.translation_presetModelLabel, + border: const OutlineInputBorder(), + ), + items: [ + for (final preset in translationPresetModels) + DropdownMenuItem( + value: preset.sourceUrl, + child: Text(translationModelFriendlyName(preset)), + ), + ], + onChanged: translationService.isBusy + ? null + : (value) async { + if (value == null) return; + final preset = translationPresetModels.firstWhere( + (entry) => entry.sourceUrl == value, + ); + await _downloadTranslationModel( + context, + translationService, + settingsService, + sourceUrl: preset.sourceUrl, + fileName: preset.name, + id: preset.id, + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _TranslationUrlField( + initialValue: settings.translationModelSourceUrl ?? '', + onChanged: settingsService.setTranslationModelSourceUrl, + onDownload: translationService.isBusy + ? null + : (url) => _downloadTranslationModel( context, translationService, settingsService, - sourceUrl: preset.sourceUrl, - fileName: preset.name, - id: preset.id, - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), - child: Column( - children: [ - _TranslationUrlField( - initialValue: settings.translationModelSourceUrl ?? '', - onChanged: settingsService.setTranslationModelSourceUrl, - onDownload: translationService.isBusy - ? null - : (url) => _downloadTranslationModel( - context, - translationService, - settingsService, - sourceUrl: url, - ), - downloadLabel: translationService.isDownloading - ? context.l10n.translation_downloading - : translationService.isBusy - ? context.l10n.translation_working - : context.l10n.translation_downloadModel, - isDownloading: translationService.isDownloading, - onCancel: translationService.cancelDownload, - labelText: context.l10n.translation_manualUrlLabel, - stopLabel: context.l10n.translation_stop, - ), - if (translationService.isDownloading) ...[ - const SizedBox(height: 12), - LinearProgressIndicator( - value: - translationService.downloadFileName == + sourceUrl: url, + ), + downloadLabel: translationService.isDownloading + ? context.l10n.translation_downloading + : translationService.isBusy + ? context.l10n.translation_working + : context.l10n.translation_downloadModel, + isDownloading: translationService.isDownloading, + onCancel: translationService.cancelDownload, + labelText: context.l10n.translation_manualUrlLabel, + stopLabel: context.l10n.translation_stop, + ), + if (translationService.isDownloading) ...[ + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(2), + child: LinearProgressIndicator( + value: translationService.downloadFileName == 'Merging chunks...' ? null : translationService.downloadProgress, ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerLeft, - child: Text( - _downloadProgressLabel(context, translationService), - style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerLeft, + child: Text( + _downloadProgressLabel(context, translationService), + style: MeshTheme.mono( + fontSize: 12, + color: scheme.onSurfaceVariant, ), ), - ], - if (settings.translationDownloadedModels.isNotEmpty) ...[ - const SizedBox(height: 16), - Align( - alignment: Alignment.centerLeft, - child: Text( - context.l10n.translation_downloadedModels, - style: Theme.of(context).textTheme.titleSmall, - ), + ), + ], + if (settings.translationDownloadedModels.isNotEmpty) ...[ + const SizedBox(height: 16), + Align( + alignment: Alignment.centerLeft, + child: Text( + context.l10n.translation_downloadedModels, + style: Theme.of(context).textTheme.titleSmall, ), - const SizedBox(height: 8), - for (final model in settings.translationDownloadedModels) - Card.outlined( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - leading: Icon( + ), + const SizedBox(height: 8), + for (final model in settings.translationDownloadedModels) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Icon( model.id == settings.translationSelectedModelId ? Icons.check_circle : Icons.memory_outlined, + size: 20, + color: model.id == settings.translationSelectedModelId + ? scheme.primary + : scheme.onSurfaceVariant, ), - title: Text(translationModelFriendlyName(model)), - subtitle: Text(_downloadedModelLabel(model)), - trailing: IconButton( + const SizedBox(width: 12), + Expanded( + child: InkWell( + borderRadius: + BorderRadius.circular(MeshRadii.xs), + onTap: () => settingsService + .setTranslationSelectedModelId(model.id), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translationModelFriendlyName(model), + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + _downloadedModelLabel(model), + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + IconButton( tooltip: context.l10n.translation_deleteModel, onPressed: translationService.isBusy ? null @@ -831,105 +1186,119 @@ class AppSettingsScreen extends StatelessWidget { ), icon: const Icon(Icons.delete_outline), ), - onTap: () => settingsService - .setTranslationSelectedModelId(model.id), - ), - ), - ], - if (translationService.lastError != null) ...[ - const SizedBox(height: 8), - Text( - translationService.lastError!, - style: TextStyle( - color: Theme.of(context).colorScheme.error, + ], ), ), - ], ], - ), + if (translationService.lastError != null) ...[ + const SizedBox(height: 8), + Text( + translationService.lastError!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ], + ], ), - ], - ), + ), + ], ); } - // Fixed rendering issues - Widget _buildBatteryCard( + Widget _buildCyr2LatContent( BuildContext context, AppSettingsService settingsService, - MeshCoreConnector connector, ) { - final deviceId = connector.deviceId; - final isConnected = connector.isConnected && deviceId != null; - final selection = isConnected - ? settingsService.batteryChemistryForDevice(deviceId) - : 'nmc'; - - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.appSettings_battery, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), + final selectedProfile = settingsService.getSelectedCyr2LatProfile(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + DropdownButtonFormField( + initialValue: settingsService.settings.selectedCyr2latProfileId, + decoration: InputDecoration( + labelText: context.l10n.channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), ), - - // Main tile (icon + text only) - ListTile( - leading: const Icon(Icons.battery_full), - title: Text(context.l10n.appSettings_batteryChemistry), - subtitle: Text( - isConnected - ? context.l10n.appSettings_batteryChemistryPerDevice( - connector.deviceDisplayName, - ) - : context.l10n.appSettings_batteryChemistryConnectFirst, - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), - - // Dropdown (separate full-width row) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: DropdownButtonFormField( - initialValue: selection, - isExpanded: true, - decoration: const InputDecoration( - border: UnderlineInputBorder(), - isDense: true, + items: settingsService.settings.cyr2latProfiles.map((profile) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + settingsService.setSelectedCyr2LatProfile(value); + } + }, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => + _showAddCyr2LatProfileDialog(context, settingsService), + icon: const Icon(Icons.add), + label: Text(context.l10n.common_add), ), - onChanged: isConnected - ? (value) { - if (value != null) { - settingsService.setBatteryChemistryForDevice( - deviceId, - value, - ); - } - } - : null, - items: [ - DropdownMenuItem( - value: 'nmc', - child: Text(context.l10n.appSettings_batteryNmc), - ), - DropdownMenuItem( - value: 'lifepo4', - child: Text(context.l10n.appSettings_batteryLifepo4), - ), - DropdownMenuItem( - value: 'lipo', - child: Text(context.l10n.appSettings_batteryLipo), - ), - ], ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: () => _showEditCyr2LatProfileDialog( + context, + settingsService, + selectedProfile, + ), + icon: const Icon(Icons.edit), + label: Text(context.l10n.common_edit), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: settingsService.settings.cyr2latProfiles.length > 1 + ? () => _showDeleteCyr2LatProfileDialog( + context, + settingsService, + selectedProfile, + ) + : null, + icon: const Icon(Icons.delete), + label: Text(context.l10n.common_delete), + ), + ), + ], + ), + ], + ); + } + + Widget _buildDebugContent( + BuildContext context, + AppSettingsService settingsService, + ) { + return SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.bug_report_outlined, size: 20), + title: Text(context.l10n.appSettings_appDebugLogging), + subtitle: Text(context.l10n.appSettings_appDebugLoggingSubtitle), + value: settingsService.settings.appDebugLogEnabled, + onChanged: (value) async { + await settingsService.setAppDebugLogEnabled(value); + if (!context.mounted) return; + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_appDebugLoggingEnabled + : context.l10n.appSettings_appDebugLoggingDisabled, ), - ], - ), + duration: const Duration(seconds: 2), + ); + }, ); } @@ -976,208 +1345,363 @@ class AppSettingsScreen extends StatelessWidget { } } - void _showLanguageDialog( + void _showLanguageSheet( BuildContext context, AppSettingsService settingsService, ) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.appSettings_language), - content: SingleChildScrollView( - child: RadioGroup( - groupValue: settingsService.settings.languageOverride, - onChanged: (value) { - settingsService.setLanguageOverride(value); - Navigator.pop(context); - }, - child: Column( - mainAxisSize: MainAxisSize.min, + showMeshSheet( + context, + builder: (ctx) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHeader(title: context.l10n.appSettings_language), + SizedBox( + height: 400, + child: ListView( children: [ - RadioListTile( - title: Text(context.l10n.appSettings_languageSystem), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageSystem, value: null, + selected: settingsService.settings.languageOverride == null, + onTap: () { + settingsService.setLanguageOverride(null); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageEn), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageEn, value: 'en', + selected: + settingsService.settings.languageOverride == 'en', + onTap: () { + settingsService.setLanguageOverride('en'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageFr), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageFr, value: 'fr', + selected: + settingsService.settings.languageOverride == 'fr', + onTap: () { + settingsService.setLanguageOverride('fr'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageEs), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageEs, value: 'es', + selected: + settingsService.settings.languageOverride == 'es', + onTap: () { + settingsService.setLanguageOverride('es'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageDe), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageDe, value: 'de', + selected: + settingsService.settings.languageOverride == 'de', + onTap: () { + settingsService.setLanguageOverride('de'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languagePl), + _sheetOption( + ctx, + label: context.l10n.appSettings_languagePl, value: 'pl', + selected: + settingsService.settings.languageOverride == 'pl', + onTap: () { + settingsService.setLanguageOverride('pl'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageSl), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageSl, value: 'sl', + selected: + settingsService.settings.languageOverride == 'sl', + onTap: () { + settingsService.setLanguageOverride('sl'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languagePt), + _sheetOption( + ctx, + label: context.l10n.appSettings_languagePt, value: 'pt', + selected: + settingsService.settings.languageOverride == 'pt', + onTap: () { + settingsService.setLanguageOverride('pt'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageIt), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageIt, value: 'it', + selected: + settingsService.settings.languageOverride == 'it', + onTap: () { + settingsService.setLanguageOverride('it'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageZh), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageZh, value: 'zh', + selected: + settingsService.settings.languageOverride == 'zh', + onTap: () { + settingsService.setLanguageOverride('zh'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageSv), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageSv, value: 'sv', + selected: + settingsService.settings.languageOverride == 'sv', + onTap: () { + settingsService.setLanguageOverride('sv'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageNl), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageNl, value: 'nl', + selected: + settingsService.settings.languageOverride == 'nl', + onTap: () { + settingsService.setLanguageOverride('nl'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageSk), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageSk, value: 'sk', + selected: + settingsService.settings.languageOverride == 'sk', + onTap: () { + settingsService.setLanguageOverride('sk'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageBg), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageBg, value: 'bg', + selected: + settingsService.settings.languageOverride == 'bg', + onTap: () { + settingsService.setLanguageOverride('bg'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageRu), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageRu, value: 'ru', + selected: + settingsService.settings.languageOverride == 'ru', + onTap: () { + settingsService.setLanguageOverride('ru'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageUk), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageUk, value: 'uk', + selected: + settingsService.settings.languageOverride == 'uk', + onTap: () { + settingsService.setLanguageOverride('uk'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageHu), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageHu, value: 'hu', + selected: + settingsService.settings.languageOverride == 'hu', + onTap: () { + settingsService.setLanguageOverride('hu'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageJa), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageJa, value: 'ja', + selected: + settingsService.settings.languageOverride == 'ja', + onTap: () { + settingsService.setLanguageOverride('ja'); + Navigator.pop(ctx); + }, ), - RadioListTile( - title: Text(context.l10n.appSettings_languageKo), + _sheetOption( + ctx, + label: context.l10n.appSettings_languageKo, value: 'ko', + selected: + settingsService.settings.languageOverride == 'ko', + onTap: () { + settingsService.setLanguageOverride('ko'); + Navigator.pop(ctx); + }, ), + SizedBox(height: MediaQuery.paddingOf(ctx).bottom + 8), ], ), ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), - ), ], ), ); } - void _showTimeFilterDialog( + void _showTimeFilterSheet( BuildContext context, AppSettingsService settingsService, ) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.appSettings_mapTimeFilter), - content: RadioGroup( - groupValue: settingsService.settings.mapTimeFilterHours, - onChanged: (value) { - if (value != null) { - settingsService.setMapTimeFilterHours(value); - Navigator.pop(context); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(context.l10n.appSettings_showNodesDiscoveredWithin), - const SizedBox(height: 16), - RadioListTile( - title: Text(context.l10n.appSettings_allTime), - value: 0, - ), - RadioListTile( - title: Text(context.l10n.appSettings_lastHour), - value: 1, - ), - RadioListTile( - title: Text(context.l10n.appSettings_last6Hours), - value: 6, - ), - RadioListTile( - title: Text(context.l10n.appSettings_last24Hours), - value: 24, - ), - RadioListTile( - title: Text(context.l10n.appSettings_lastWeek), - value: 168, - ), - ], + showMeshSheet( + context, + builder: (ctx) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHeader(title: context.l10n.appSettings_mapTimeFilter), + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), + child: Text(context.l10n.appSettings_showNodesDiscoveredWithin), ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), + _sheetOption( + ctx, + label: context.l10n.appSettings_allTime, + value: 0, + selected: settingsService.settings.mapTimeFilterHours == 0, + onTap: () { + settingsService.setMapTimeFilterHours(0); + Navigator.pop(ctx); + }, ), + _sheetOption( + ctx, + label: context.l10n.appSettings_lastHour, + value: 1, + selected: settingsService.settings.mapTimeFilterHours == 1, + onTap: () { + settingsService.setMapTimeFilterHours(1); + Navigator.pop(ctx); + }, + ), + _sheetOption( + ctx, + label: context.l10n.appSettings_last6Hours, + value: 6, + selected: settingsService.settings.mapTimeFilterHours == 6, + onTap: () { + settingsService.setMapTimeFilterHours(6); + Navigator.pop(ctx); + }, + ), + _sheetOption( + ctx, + label: context.l10n.appSettings_last24Hours, + value: 24, + selected: settingsService.settings.mapTimeFilterHours == 24, + onTap: () { + settingsService.setMapTimeFilterHours(24); + Navigator.pop(ctx); + }, + ), + _sheetOption( + ctx, + label: context.l10n.appSettings_lastWeek, + value: 168, + selected: settingsService.settings.mapTimeFilterHours == 168, + onTap: () { + settingsService.setMapTimeFilterHours(168); + Navigator.pop(ctx); + }, + ), + SizedBox(height: MediaQuery.paddingOf(ctx).bottom + 8), ], ), ); } - void _showUnitsDialog( + void _showUnitsSheet( BuildContext context, AppSettingsService settingsService, ) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.appSettings_unitsTitle), - content: RadioGroup( - groupValue: settingsService.settings.unitSystem, - onChanged: (value) { - if (value != null) { - settingsService.setUnitSystem(value); - Navigator.pop(context); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: Text(context.l10n.appSettings_unitsMetric), - value: UnitSystem.metric, - ), - RadioListTile( - title: Text(context.l10n.appSettings_unitsImperial), - value: UnitSystem.imperial, - ), - ], + showMeshSheet( + context, + builder: (ctx) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHeader(title: context.l10n.appSettings_unitsTitle), + _sheetOption( + ctx, + label: context.l10n.appSettings_unitsMetric, + value: UnitSystem.metric, + selected: + settingsService.settings.unitSystem == UnitSystem.metric, + onTap: () { + settingsService.setUnitSystem(UnitSystem.metric); + Navigator.pop(ctx); + }, ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), + _sheetOption( + ctx, + label: context.l10n.appSettings_unitsImperial, + value: UnitSystem.imperial, + selected: + settingsService.settings.unitSystem == UnitSystem.imperial, + onTap: () { + settingsService.setUnitSystem(UnitSystem.imperial); + Navigator.pop(ctx); + }, ), + SizedBox(height: MediaQuery.paddingOf(ctx).bottom + 8), ], ), ); } + Widget _sheetOption( + BuildContext context, { + required String label, + required T value, + required bool selected, + required VoidCallback onTap, + }) { + final scheme = Theme.of(context).colorScheme; + return ListTile( + leading: Icon( + selected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: selected ? scheme.primary : scheme.onSurfaceVariant, + ), + title: Text(label), + onTap: onTap, + ); + } + void _showTranslationLanguageDialog( BuildContext context, AppSettingsService settingsService, @@ -1303,90 +1827,6 @@ class AppSettingsScreen extends StatelessWidget { return '${sizeMb.toStringAsFixed(1)} MB • $source'; } - Widget _buildCyr2LatCard( - BuildContext context, - AppSettingsService settingsService, - ) { - final selectedProfile = settingsService.getSelectedCyr2LatProfile(); - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.channels_cyr2latSettingsHeading, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DropdownButtonFormField( - initialValue: settingsService.settings.selectedCyr2latProfileId, - decoration: InputDecoration( - labelText: context.l10n.channels_cyr2latSettingsSubheading, - border: const OutlineInputBorder(), - ), - items: settingsService.settings.cyr2latProfiles.map((profile) { - return DropdownMenuItem( - value: profile.id, - child: Text(profile.name), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - settingsService.setSelectedCyr2LatProfile(value); - } - }, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () => - _showAddCyr2LatProfileDialog(context, settingsService), - icon: const Icon(Icons.add), - label: Text(context.l10n.common_add), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton.icon( - onPressed: () => _showEditCyr2LatProfileDialog( - context, - settingsService, - selectedProfile, - ), - icon: const Icon(Icons.edit), - label: Text(context.l10n.common_edit), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton.icon( - onPressed: - settingsService.settings.cyr2latProfiles.length > 1 - ? () => _showDeleteCyr2LatProfileDialog( - context, - settingsService, - selectedProfile, - ) - : null, - icon: const Icon(Icons.delete), - label: Text(context.l10n.common_delete), - ), - ), - ], - ), - ), - ], - ), - ); - } - void _showAddCyr2LatProfileDialog( BuildContext context, AppSettingsService settingsService, @@ -1434,7 +1874,9 @@ class AppSettingsScreen extends StatelessWidget { if (nameController.text.isEmpty) { showDismissibleSnackBar( context, - content: Text(context.l10n.settings_cyr2latProfileNameEmpty), + content: Text( + context.l10n.settings_cyr2latProfileNameEmpty, + ), ); return; } @@ -1522,7 +1964,9 @@ class AppSettingsScreen extends StatelessWidget { if (nameController.text.isEmpty) { showDismissibleSnackBar( context, - content: Text(context.l10n.settings_cyr2latProfileNameEmpty), + content: Text( + context.l10n.settings_cyr2latProfileNameEmpty, + ), ); return; } @@ -1594,45 +2038,6 @@ class AppSettingsScreen extends StatelessWidget { ), ); } - - Widget _buildDebugCard( - BuildContext context, - AppSettingsService settingsService, - ) { - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - context.l10n.appSettings_debugCard, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - SwitchListTile( - secondary: const Icon(Icons.bug_report_outlined), - title: Text(context.l10n.appSettings_appDebugLogging), - subtitle: Text(context.l10n.appSettings_appDebugLoggingSubtitle), - value: settingsService.settings.appDebugLogEnabled, - onChanged: (value) async { - await settingsService.setAppDebugLogEnabled(value); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text( - value - ? context.l10n.appSettings_appDebugLoggingEnabled - : context.l10n.appSettings_appDebugLoggingDisabled, - ), - duration: const Duration(seconds: 2), - ); - }, - ), - ], - ), - ); - } } /// Owns the [TextEditingController] for the manual model URL field so it diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 6d186970..c229c2e5 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import '../l10n/l10n.dart'; import '../services/ble_debug_log_service.dart'; import '../connector/meshcore_protocol.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../helpers/snack_bar_builder.dart'; @@ -32,6 +33,7 @@ class _BleDebugLogScreenState extends State { return Scaffold( appBar: AppBar( title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle), + centerTitle: true, actions: [ IconButton( tooltip: context.l10n.debugLog_copyLog, @@ -101,23 +103,14 @@ class _BleDebugLogScreenState extends State { itemCount: showingFrames ? entries.length : rawEntries.length, - separatorBuilder: (_, _) => const Divider(height: 1), + separatorBuilder: (_, _) => + const Divider(height: 1, color: MeshPalette.line), itemBuilder: (context, index) { if (showingFrames) { final entry = entries[index]; final time = '${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}'; - return ListTile( - dense: true, - title: Text(entry.description), - subtitle: Text('${entry.hexPreview}\n$time'), - isThreeLine: true, - leading: Icon( - entry.outgoing - ? Icons.upload - : Icons.download, - size: 18, - ), + return GestureDetector( onLongPress: () async { await Clipboard.setData( ClipboardData( @@ -131,6 +124,60 @@ class _BleDebugLogScreenState extends State { ), ); }, + child: Container( + color: MeshPalette.bg, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + entry.outgoing + ? Icons.upload + : Icons.download, + size: 18, + color: entry.outgoing + ? MeshPalette.blue + : MeshPalette.signal, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + entry.description, + style: MeshTheme.mono( + fontSize: 11.5, + color: MeshPalette.ink, + ), + ), + const SizedBox(height: 2), + Text( + entry.hexPreview, + style: MeshTheme.mono( + fontSize: 10, + color: MeshPalette.ink3, + ), + ), + const SizedBox(height: 2), + Text( + time, + style: MeshTheme.mono( + fontSize: 9.5, + color: MeshPalette.ink4, + ), + ), + ], + ), + ), + ], + ), + ), ); } @@ -138,18 +185,65 @@ class _BleDebugLogScreenState extends State { final info = _decodeRawPacket(entry.payload); final time = '${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}'; - return ListTile( - dense: true, - title: Text(info.title), - subtitle: Text('${info.summary}\n$time'), - isThreeLine: true, - leading: const Icon(Icons.download, size: 18), + return GestureDetector( onTap: () => _showRawDialog(context, info), + child: Container( + color: MeshPalette.bg, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.download, + size: 18, + color: MeshPalette.signal, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + info.title, + style: MeshTheme.mono( + fontSize: 11.5, + color: MeshPalette.ink, + ), + ), + const SizedBox(height: 2), + Text( + info.summary, + style: MeshTheme.mono( + fontSize: 10, + color: MeshPalette.ink3, + ), + ), + const SizedBox(height: 2), + Text( + time, + style: MeshTheme.mono( + fontSize: 9.5, + color: MeshPalette.ink4, + ), + ), + ], + ), + ), + ], + ), + ), ); }, ) : Center( - child: Text(context.l10n.debugLog_noBleActivity), + child: Text( + context.l10n.debugLog_noBleActivity, + style: const TextStyle(color: MeshPalette.ink3), + ), ), ), ], diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index e2c63c35..94101ef3 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -25,7 +25,6 @@ import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; -import '../helpers/contact_ui.dart'; import '../widgets/byte_count_input.dart'; import '../widgets/empty_state.dart'; import '../widgets/chat_zoom_wrapper.dart'; @@ -39,6 +38,8 @@ import '../widgets/radio_stats_entry.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/translated_message_content.dart'; import '../widgets/unread_divider.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -108,7 +109,7 @@ class _ChannelChatScreenState extends State { totalMessages: messages.length, onJumped: () { if (!mounted) return; - _scrollToMessage(anchor!.messageId); + _scrollToMessage(anchor!.messageId, quiet: true); }, ); }); @@ -193,9 +194,12 @@ class _ChannelChatScreenState extends State { }); } - Future _scrollToMessage(String messageId) async { + Future _scrollToMessage(String messageId, {bool quiet = false}) async { final key = _messageKeys[messageId]; if (key == null) { + // The auto unread-jump can resolve a frame after navigating away; + // a deactivated context can't host a snackbar. + if (quiet || !mounted || !context.mounted) return; showDismissibleSnackBar( context, content: Text(context.l10n.chat_originalMessageNotFound), @@ -491,6 +495,7 @@ class _ChannelChatScreenState extends State { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; + final scheme = Theme.of(context).colorScheme; final gifId = GifHelper.parseGif(message.text); final poi = parseMarkerText(message.text); final translatedDisplayText = @@ -507,9 +512,34 @@ class _ChannelChatScreenState extends State { ? message.pathVariants.first : Uint8List(0)); + // Bubble colors — outgoing uses MeshPalette.me / meBorder / meInk. + final bubbleColor = isOutgoing + ? MeshPalette.me + : scheme.surfaceContainerLow; + final bubbleBorder = isOutgoing + ? MeshPalette.meBorder + : scheme.outlineVariant; + final textColor = isOutgoing ? MeshPalette.meInk : scheme.onSurface; + final metaColor = textColor.withValues(alpha: 0.65); + const bodyFontSize = 14.0; + + // Asymmetric radius matching chat_screen bubbles. + final borderRadius = isOutgoing + ? const BorderRadius.only( + topLeft: Radius.circular(MeshRadii.lg), + topRight: Radius.circular(MeshRadii.lg), + bottomLeft: Radius.circular(MeshRadii.lg), + bottomRight: Radius.circular(MeshRadii.xs), + ) + : const BorderRadius.only( + topLeft: Radius.circular(MeshRadii.xs), + topRight: Radius.circular(MeshRadii.lg), + bottomLeft: Radius.circular(MeshRadii.lg), + bottomRight: Radius.circular(MeshRadii.lg), + ); + const maxSwipeOffset = 64.0; const replySwipeThreshold = 64.0; - const bodyFontSize = 14.0; final messageBody = LayoutBuilder( builder: (context, constraints) => Column( crossAxisAlignment: isOutgoing @@ -520,11 +550,11 @@ class _ChannelChatScreenState extends State { mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, children: [ if (!isOutgoing) ...[ _buildAvatar(message.senderName), - const SizedBox(width: 8), + const SizedBox(width: 6), ], Flexible( child: GestureDetector( @@ -540,15 +570,12 @@ class _ChannelChatScreenState extends State { vertical: 8, ), constraints: BoxConstraints( - maxWidth: constraints.maxWidth * 0.65, + maxWidth: constraints.maxWidth * 0.72, ), decoration: BoxDecoration( - color: isOutgoing - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), + color: bubbleColor, + borderRadius: borderRadius, + border: Border.all(color: bubbleBorder, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -564,14 +591,14 @@ class _ChannelChatScreenState extends State { : EdgeInsets.zero, child: Text( message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w700, + color: _colorForName(message.senderName), ), ), ), - if (gifId == null) const SizedBox(height: 4), + if (gifId == null) const SizedBox(height: 2), ], if (message.replyToMessageId != null) ...[ _buildReplyPreview(message, textScale), @@ -594,13 +621,9 @@ class _ChannelChatScreenState extends State { url: 'https://media.giphy.com/media/$gifId/giphy.gif', backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), ), ), ], @@ -615,40 +638,48 @@ class _ChannelChatScreenState extends State { displayText: translatedDisplayText, originalText: originalDisplayText, style: TextStyle( + color: textColor, fontSize: bodyFontSize * textScale, ), originalStyle: TextStyle( fontSize: bodyFontSize * textScale, fontStyle: FontStyle.italic, - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.72), + color: textColor.withValues(alpha: 0.72), ), ), ), ], ), if (enableTracing && displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), + const SizedBox(height: 3), Padding( padding: gifId != null ? const EdgeInsets.symmetric(horizontal: 8) : EdgeInsets.zero, - child: Text( - context.l10n.channels_via( - _formatPathPrefixes(displayPath), - ), - style: TextStyle( - fontSize: 11 * textScale, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + RouteChip( + isDirect: (message.pathLength ?? -1) >= 0, + hops: (message.pathLength ?? -1) >= 0 + ? message.pathLength + : null, + ), + const SizedBox(width: 4), + Text( + context.l10n.channels_via( + _formatPathPrefixes(displayPath), + ), + style: MeshTheme.mono( + fontSize: 9.5 * textScale, + color: metaColor, + ), + ), + ], ), ), ], - const SizedBox(height: 4), + const SizedBox(height: 3), Padding( padding: gifId != null ? const EdgeInsets.only( @@ -662,30 +693,24 @@ class _ChannelChatScreenState extends State { children: [ Text( _formatTime(context, message.timestamp), - style: TextStyle( - fontSize: 11 * textScale, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + style: MeshTheme.mono( + fontSize: 10 * textScale, + color: metaColor, ), ), if (enableTracing && message.repeatCount > 0) ...[ const SizedBox(width: 6), Icon( Icons.repeat, - size: 12 * textScale, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + size: 11 * textScale, + color: metaColor, ), const SizedBox(width: 2), Text( '${message.repeatCount}', - style: TextStyle( - fontSize: 11 * textScale, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + style: MeshTheme.mono( + fontSize: 10 * textScale, + color: metaColor, ), ), ], @@ -705,6 +730,7 @@ class _ChannelChatScreenState extends State { isFailed: message.status == ChannelMessageStatus.failed, + onColor: metaColor, ), ], ], @@ -720,7 +746,7 @@ class _ChannelChatScreenState extends State { if (message.reactions.isNotEmpty) ...[ const SizedBox(height: 4), Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), + padding: EdgeInsets.only(left: isOutgoing ? 0 : 42), child: _buildReactionsDisplay(message), ), ], @@ -739,7 +765,7 @@ class _ChannelChatScreenState extends State { ); } else { return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(vertical: 3), child: messageBody, ); } @@ -836,7 +862,7 @@ class _ChannelChatScreenState extends State { padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(MeshRadii.sm), border: Border( left: BorderSide(color: colorScheme.primary, width: 3), ), @@ -849,9 +875,7 @@ class _ChannelChatScreenState extends State { style: TextStyle( fontSize: 11 * textScale, fontWeight: FontWeight.bold, - color: isOwnNode - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, + color: isOwnNode ? colorScheme.primary : colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -863,6 +887,7 @@ class _ChannelChatScreenState extends State { } Widget _buildReactionsDisplay(ChannelMessage message) { + final scheme = Theme.of(context).colorScheme; return Wrap( spacing: 6, runSpacing: 6, @@ -873,27 +898,29 @@ class _ChannelChatScreenState extends State { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withValues(alpha: 0.3), - width: 1, - ), + color: scheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(MeshRadii.pill), + border: Border.all(color: scheme.outlineVariant, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(emoji, style: const TextStyle(fontSize: 16)), + Text( + emoji, + style: MeshTheme.emoji(fontSize: 16), + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + ), if (count > 1) ...[ const SizedBox(width: 4), Text( '$count', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSecondaryContainer, + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w700, + color: scheme.onSurface, ), ), ], @@ -912,20 +939,15 @@ class _ChannelChatScreenState extends State { String senderName, { Widget? trailing, }) { - final colorScheme = Theme.of(context).colorScheme; - final textColor = isOutgoing - ? colorScheme.onPrimaryContainer - : colorScheme.onSurface; + final scheme = Theme.of(context).colorScheme; + final textColor = isOutgoing ? MeshPalette.meInk : scheme.onSurface; final metaColor = textColor.withValues(alpha: 0.7); - final channelColor = widget.channel.isPublicChannel - ? Colors.orange - : Colors.blue; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( - icon: Icon(Icons.location_on_outlined, color: channelColor), + icon: Icon(Icons.location_on_outlined, color: scheme.primary), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 40, minHeight: 40), onPressed: () { @@ -991,41 +1013,24 @@ class _ChannelChatScreenState extends State { } Widget _buildAvatar(String senderName) { - final initial = firstCharacterOrEmoji(senderName); - final color = colorForName(senderName); - - return CircleAvatar( - radius: 18, - backgroundColor: color.withValues(alpha: 0.2), - child: Text( - initial, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: color, - ), - ), - ); + return AvatarCircle(name: senderName, size: 32); } Widget _buildReplyBanner(double textScale) { final message = _replyingToMessage!; + final scheme = Theme.of(context).colorScheme; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, + color: scheme.surfaceContainerHigh, border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1), + bottom: BorderSide(color: scheme.outlineVariant, width: 1), ), ), child: Row( children: [ - Icon( - Icons.reply, - size: 18, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), + Icon(Icons.reply, size: 18, color: scheme.primary), const SizedBox(width: 8), Expanded( child: Column( @@ -1033,10 +1038,10 @@ class _ChannelChatScreenState extends State { children: [ Text( context.l10n.chat_replyingTo(message.senderName), - style: TextStyle( - fontSize: 12 * textScale, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSecondaryContainer, + style: MeshTheme.mono( + fontSize: 11 * textScale, + fontWeight: FontWeight.w700, + color: scheme.primary, ), ), Text( @@ -1045,9 +1050,7 @@ class _ChannelChatScreenState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11 * textScale, - color: Theme.of( - context, - ).colorScheme.onSecondaryContainer.withValues(alpha: 0.7), + color: scheme.onSurfaceVariant, ), ), ], @@ -1056,7 +1059,7 @@ class _ChannelChatScreenState extends State { IconButton( icon: const Icon(Icons.close, size: 18), onPressed: _cancelReply, - color: Theme.of(context).colorScheme.onSecondaryContainer, + color: scheme.onSurfaceVariant, constraints: const BoxConstraints(minWidth: 44, minHeight: 44), ), ], @@ -1068,6 +1071,7 @@ class _ChannelChatScreenState extends State { final connector = context.watch(); final maxBytes = maxChannelMessageBytes(connector.selfName); final settings = context.watch().settings; + final scheme = Theme.of(context).colorScheme; return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1081,123 +1085,166 @@ class _ChannelChatScreenState extends State { }, ), Container( - padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 4, - offset: const Offset(0, -2), - ), - ], + color: scheme.surface, + border: Border( + top: BorderSide(color: scheme.outlineVariant, width: 1), + ), ), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.gif_box), - onPressed: () => _showGifPicker(context), - tooltip: context.l10n.chat_sendGif, - ), - if (settings.translationEnabled) - MessageTranslationButton( - enabled: settings.composerTranslationEnabled, - languageCode: settings.translationTargetLanguageCode, - onPressed: _showTranslationOptions, - ), - Expanded( - child: ValueListenableBuilder( - valueListenable: _textController, - builder: (context, value, child) { - final gifId = GifHelper.parseGif(value.text); - if (gifId != null) { - return Focus( - autofocus: true, - onKeyEvent: (node, event) { - if (event is KeyDownEvent && - (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == - LogicalKeyboardKey.numpadEnter)) { - _sendMessage(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.6), - maxSize: 160, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.gif_box), + onPressed: () => _showGifPicker(context), + tooltip: context.l10n.chat_sendGif, + ), + if (settings.translationEnabled) + MessageTranslationButton( + enabled: settings.composerTranslationEnabled, + languageCode: settings.translationTargetLanguageCode, + onPressed: _showTranslationOptions, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, child) { + final gifId = GifHelper.parseGif(value.text); + if (gifId != null) { + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == + LogicalKeyboardKey.enter || + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { + _sendMessage(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + scheme.surfaceContainerHighest, + fallbackTextColor: scheme.onSurface + .withValues(alpha: 0.6), + maxSize: 160, + ), + ), ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), + ); + } + return ByteCountedTextField( + maxBytes: maxBytes, + controller: _textController, + focusNode: _textFieldFocusNode, + hintText: context.l10n.chat_typeMessage, + onSubmitted: (_) => _sendMessage(), + encoder: + (connector.isChannelSmazEnabled( + widget.channel.index, + ) || + connector.isChannelCyr2LatEnabled( + widget.channel.index, + )) + ? (text) => connector.prepareChannelOutboundText( + widget.channel.index, + text, + ) + : null, + decoration: InputDecoration( + hintText: context.l10n.chat_typeMessage, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + MeshRadii.pill, + ), + borderSide: BorderSide( + color: scheme.outlineVariant, ), ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _textController.clear(); - _textFieldFocusNode.requestFocus(); - }, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + MeshRadii.pill, + ), + borderSide: BorderSide( + color: scheme.outlineVariant, + ), ), - ], + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + MeshRadii.pill, + ), + borderSide: BorderSide( + color: scheme.primary, + width: 1.5, + ), + ), + filled: true, + fillColor: scheme.surfaceContainerLow, + contentPadding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), + ), + ); + }, + ), + ), + const SizedBox(width: 6), + ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, _) { + final hasText = value.text.trim().isNotEmpty; + return AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeInOut, + child: IconButton.filled( + icon: const Icon(Icons.send, size: 20), + tooltip: context.l10n.chat_sendMessage, + style: IconButton.styleFrom( + backgroundColor: hasText + ? scheme.primary + : scheme.surfaceContainerHighest, + foregroundColor: hasText + ? scheme.onPrimary + : scheme.onSurfaceVariant, + minimumSize: const Size(40, 40), + shape: const CircleBorder(), + ), + onPressed: hasText + ? () { + HapticFeedback.lightImpact(); + _sendMessage(); + } + : null, ), ); - } - return ByteCountedTextField( - maxBytes: maxBytes, - controller: _textController, - focusNode: _textFieldFocusNode, - hintText: context.l10n.chat_typeMessage, - onSubmitted: (_) => _sendMessage(), - encoder: - (connector.isChannelSmazEnabled( - widget.channel.index, - ) || - connector.isChannelCyr2LatEnabled( - widget.channel.index, - )) - ? (text) => connector.prepareChannelOutboundText( - widget.channel.index, - text, - ) - : null, - decoration: InputDecoration( - hintText: context.l10n.chat_typeMessage, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - ), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerLow, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 14, - ), - ), - ); - }, - ), + }, + ), + ], ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.send), - tooltip: context.l10n.chat_sendMessage, - onPressed: _sendMessage, - color: Theme.of(context).colorScheme.primary, - ), - ], + ), ), ), ], @@ -1338,12 +1385,20 @@ class _ChannelChatScreenState extends State { ) && (message.translatedText?.trim().isEmpty ?? true); - showModalBottomSheet( - context: context, + showMeshSheet( + context, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ + BottomSheetHeader( + title: message.text.length > 40 + ? '${message.text.substring(0, 40)}…' + : message.text, + subtitle: message.senderName.isNotEmpty + ? message.senderName + : null, + ), ListTile( leading: const Icon(Icons.reply), title: Text(context.l10n.chat_reply), @@ -1402,7 +1457,7 @@ class _ChannelChatScreenState extends State { _markAsUnread(message); }, ), - const Divider(), + const Divider(height: 1), ListTile( leading: Icon( Icons.delete_outline, @@ -1417,6 +1472,7 @@ class _ChannelChatScreenState extends State { await _deleteMessage(message); }, ), + const SizedBox(height: 8), ], ), ), @@ -1499,6 +1555,23 @@ class _ChannelChatScreenState extends State { .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) .join(','); } + + /// Deterministic name-to-hue mapping consistent with [AvatarCircle]. + Color _colorForName(String name) { + const hues = [ + MeshPalette.blue, + MeshPalette.magenta, + MeshPalette.signal, + MeshPalette.warn, + Color(0xFF8FA8F0), + Color(0xFF6FD9CE), + ]; + var h = 0; + for (final c in name.codeUnits) { + h = (h * 31 + c) & 0x7fffffff; + } + return hues[h % hues.length]; + } } class _SwipeReplyBubble extends StatefulWidget { @@ -1640,7 +1713,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { onPointerUp: (event) => _handleSwipePointerUp(event.position), onPointerCancel: (_) => _resetSwipe(), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 8), child: Stack( alignment: Alignment.center, children: [ diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index e7f21459..13bc0c3c 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -16,7 +16,13 @@ import '../l10n/l10n.dart'; import '../models/channel_message.dart'; import '../models/app_settings.dart'; import '../models/contact.dart'; +import '../models/display_path.dart'; +import '../models/path_playback.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/mesh_ui.dart'; +import '../widgets/path_map_ui.dart'; +import '../widgets/themed_map_tile_layer.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; @@ -85,33 +91,25 @@ class ChannelMessagePathScreen extends StatelessWidget { body: SafeArea( top: false, child: ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 8), children: [ _buildSummaryCard(context, observedLabel: observedLabel), - const SizedBox(height: 16), if (extraPaths.isNotEmpty) ...[ - Text( + SectionHeader( l10n.channelPath_otherObservedPaths, - style: Theme.of(context).textTheme.titleSmall, + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), ), - const SizedBox(height: 8), _buildPathVariants(context, extraPaths), - const SizedBox(height: 16), ], - Text( + SectionHeader( l10n.channelPath_repeaterHops, - style: Theme.of(context).textTheme.titleSmall, + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), ), - const SizedBox(height: 8), if (!hasHopDetails) - Text( - l10n.channelPath_noHopDetails, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) + _buildNoHopCard(context, l10n) else - ..._buildHopTiles(context, hops), + _buildHopTimeline(context, hops, l10n), + const SizedBox(height: 16), ], ), ), @@ -122,46 +120,64 @@ class ChannelMessagePathScreen extends StatelessWidget { Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.channelPath_messageDetails, - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - _buildDetailRow( - context, - l10n.channelPath_senderLabel, - message.senderName, - ), - _buildDetailRow( - context, - l10n.channelPath_timeLabel, - _formatTime(message.timestamp, l10n), - ), - if (message.repeatCount > 0) - _buildDetailRow( - context, - l10n.channelPath_repeatsLabel, - message.repeatCount.toString(), + final scheme = Theme.of(context).colorScheme; + final routeChip = message.pathLength == null + ? null + : message.pathLength! < 0 + ? const RouteChip(isDirect: false) + : RouteChip(isDirect: true, hops: message.pathLength); + + return MeshCard( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: SectionHeader( + l10n.channelPath_messageDetails, + padding: EdgeInsets.zero, + ), ), + ?routeChip, + ], + ), + const SizedBox(height: 10), + _buildDetailRow( + context, + l10n.channelPath_senderLabel, + message.senderName, + scheme: scheme, + ), + _buildDetailRow( + context, + l10n.channelPath_timeLabel, + _formatTime(message.timestamp, l10n), + scheme: scheme, + ), + if (message.repeatCount > 0) _buildDetailRow( context, - l10n.channelPath_pathLabelTitle, - _formatPathLabel(message.pathLength, l10n), + l10n.channelPath_repeatsLabel, + message.repeatCount.toString(), + scheme: scheme, ), - if (observedLabel != null) - _buildDetailRow( - context, - l10n.channelPath_observedLabel, - observedLabel, - ), - ], - ), + _buildDetailRow( + context, + l10n.channelPath_pathLabelTitle, + _formatPathLabel(message.pathLength, l10n), + scheme: scheme, + ), + if (observedLabel != null) + _buildDetailRow( + context, + l10n.channelPath_observedLabel, + observedLabel, + scheme: scheme, + ), + ], ), ); } @@ -172,54 +188,199 @@ class ChannelMessagePathScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ for (int i = 0; i < variants.length; i++) - Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - title: Text( - l10n.channelPath_observedPathTitle( - i + 1, - _formatHopCount(variants[i].length, l10n), + MeshCard( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + onTap: () => _openPathMap( + context, + initialPath: variants[i], + channelMessage: channelMessage, + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.channelPath_observedPathTitle( + i + 1, + _formatHopCount(variants[i].length, l10n), + ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + _formatPathPrefixes(variants[i]), + style: MeshTheme.mono( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), - ), - subtitle: Text(_formatPathPrefixes(variants[i])), - trailing: const Icon(Icons.map_outlined, size: 20), - onTap: () => _openPathMap( - context, - initialPath: variants[i], - channelMessage: channelMessage, - ), + const SizedBox(width: 8), + Icon( + Icons.map_outlined, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], ), ), ], ); } - List _buildHopTiles(BuildContext context, List<_PathHop> hops) { - final l10n = context.l10n; - return [ - for (final hop in hops) - Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 14, - child: Text( - hop.index.toString(), - style: const TextStyle(fontSize: 12), - ), - ), - title: Text(hop.displayLabel), - subtitle: Text( - hop.hasLocation - ? '${hop.position!.latitude.toStringAsFixed(5)}, ' - '${hop.position!.longitude.toStringAsFixed(5)}' - : l10n.channelPath_noLocationData, + Widget _buildNoHopCard(BuildContext context, AppLocalizations l10n) { + final scheme = Theme.of(context).colorScheme; + return MeshCard( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.all(14), + child: Row( + children: [ + Icon(Icons.route_outlined, size: 20, color: scheme.onSurfaceVariant), + const SizedBox(width: 10), + Expanded( + child: Text( + l10n.channelPath_noHopDetails, + style: TextStyle(color: scheme.onSurfaceVariant), ), ), - ), - ]; + ], + ), + ); + } + + Widget _buildHopTimeline( + BuildContext context, + List<_PathHop> hops, + AppLocalizations l10n, + ) { + if (hops.isEmpty) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + for (int i = 0; i < hops.length; i++) + ListEntrance( + index: i, + child: _buildTimelineNode( + context, + hops[i], + l10n, + isLast: i == hops.length - 1, + ), + ), + ], + ), + ); + } + + Widget _buildTimelineNode( + BuildContext context, + _PathHop hop, + AppLocalizations l10n, { + required bool isLast, + }) { + final scheme = Theme.of(context).colorScheme; + final hexPrefix = _formatPrefix(hop.prefix); + final locationText = hop.hasLocation + ? '${hop.position!.latitude.toStringAsFixed(5)}, ' + '${hop.position!.longitude.toStringAsFixed(5)}' + : l10n.channelPath_noLocationData; + + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: 48, + child: Column( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + AvatarCircle(name: hop.displayLabel, size: 36), + Positioned( + right: -4, + top: -4, + child: Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: MeshPalette.blueDim, + shape: BoxShape.circle, + border: Border.all( + color: scheme.surfaceContainerLow, + width: 1.5, + ), + ), + alignment: Alignment.center, + child: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + if (!isLast) + Expanded( + child: Container( + width: 2, + margin: const EdgeInsets.symmetric(vertical: 4), + color: MeshPalette.blueLine, + ), + ) + else + const SizedBox(height: 12), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 16, top: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + hop.displayLabel, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + hexPrefix, + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + locationText, + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ], + ), + ); } String _formatTime(DateTime time, AppLocalizations l10n) { @@ -263,21 +424,25 @@ class ChannelMessagePathScreen extends StatelessWidget { return l10n.channelPath_observedSomeOf(observedCount, pathLength); } - Widget _buildDetailRow(BuildContext context, String label, String value) { + Widget _buildDetailRow( + BuildContext context, + String label, + String value, { + required ColorScheme scheme, + }) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(vertical: 3), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 70, + width: 72, child: Text( - label, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + label.toUpperCase(), + style: MeshTheme.accentLabel(color: scheme.onSurfaceVariant), ), ), + const SizedBox(width: 8), Expanded(child: Text(value)), ], ), @@ -320,7 +485,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget { } class _ChannelMessagePathMapScreenState - extends State { + extends State + with SingleTickerProviderStateMixin { static const double _labelZoomThreshold = 8.5; static const double _mapMinZoom = 2.0; static const double _mapMaxZoom = 18.0; @@ -332,10 +498,39 @@ class _ChannelMessagePathMapScreenState bool _didReceivePositionUpdate = false; int? _focusedHopIndex; + // Packet-flow animation + multi-path view state. + late final PathPlaybackController _playback; + PathViewMode _viewMode = PathViewMode.single; + final Set _hiddenPathIds = {}; + bool _panelCollapsed = false; + bool _animationEnabled = true; + bool _followPacket = false; + @override void initState() { super.initState(); _selectedPath = widget.initialPath; + _playback = PathPlaybackController(this); + _playback.addListener(_followPacketCamera); + } + + /// Keeps the camera centered on the packet while the follow lock is on. + void _followPacketCamera() { + if (!_followPacket || + !_animationEnabled || + !_playback.started || + !_playback.hasPath || + !mounted) { + return; + } + _mapController.move(_playback.position, _mapController.camera.zoom); + } + + void _toggleFollowPacket() { + setState(() { + _followPacket = !_followPacket; + }); + _followPacketCamera(); } @override @@ -352,10 +547,142 @@ class _ChannelMessagePathMapScreenState @override void dispose() { + _playback.dispose(); _mapController.dispose(); super.dispose(); } + /// Builds a renderable [DisplayPath] for one observed route, oriented in + /// the direction the packet traveled (sender first, receiver last). + DisplayPath? _buildDisplayPath({ + required int index, + required bool isPrimary, + required Uint8List orientedBytes, + required List<_PathHop> hops, + required MeshCoreConnector connector, + }) { + final l10n = context.l10n; + final selfLat = connector.selfLatitude; + final selfLon = connector.selfLongitude; + + final points = []; + final labels = []; + final confirmed = []; + final rowIdx = []; + final gapBefore = []; + var pendingGap = false; + var locatedHops = 0; + + void addSelf() { + if (selfLat == null || selfLon == null) return; + points.add(LatLng(selfLat, selfLon)); + labels.add(l10n.pathTrace_you); + confirmed.add(true); + rowIdx.add(-1); + gapBefore.add(pendingGap); + pendingGap = false; + } + + final selfFirst = widget.message.isOutgoing; + if (selfFirst) addSelf(); + for (var i = 0; i < hops.length; i++) { + final hop = hops[i]; + if (!hop.hasLocation) { + pendingGap = true; + continue; + } + locatedHops++; + points.add(hop.position!); + labels.add(hop.contact?.name ?? _formatPrefix(hop.prefix)); + confirmed.add(true); + rowIdx.add(i); + gapBefore.add(pendingGap); + pendingGap = false; + } + if (!selfFirst) addSelf(); + + if (points.length < 2) return null; + + final segmentEstimated = []; + final rowForSegment = []; + for (var i = 0; i < points.length - 1; i++) { + segmentEstimated.add(gapBefore[i + 1]); + final dest = rowIdx[i + 1]; + rowForSegment.add(dest >= 0 ? dest : (rowIdx[i] >= 0 ? rowIdx[i] : 0)); + } + + return DisplayPath( + id: 'op-$index', + label: isPrimary ? l10n.pathMap_primary : l10n.pathMap_alternate(index), + color: isPrimary + ? kPrimaryPathColor + : kAlternatePathColors[(index - 1) % kAlternatePathColors.length], + isPrimary: isPrimary, + hopBytes: List.from(orientedBytes), + points: points, + pointLabels: labels, + pointConfirmed: confirmed, + segmentEstimated: segmentEstimated, + rowForSegment: rowForSegment, + totalTransmissions: hops.length, + hasTargetEndpoint: false, + gpsConfirmedHops: locatedHops, + unresolvedHops: hops.length - locatedHops, + distanceMeters: getPathDistanceMeters(points), + record: null, + ); + } + + /// Updates the playback path after this frame, but only when the selected + /// path's geometry actually changed, so rebuilds don't reset a running + /// animation. + void _schedulePlaybackSync(DisplayPath? selected) { + final points = selected?.points ?? const []; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (points.length == _playback.points.length) { + var same = true; + for (var i = 0; i < points.length; i++) { + if (points[i] != _playback.points[i]) { + same = false; + break; + } + } + if (same) return; + } + _playback.setPath(points); + }); + } + + void _selectEntry(_ObservedPathEntry entry) { + setState(() { + _selectedPath = entry.observedBytes; + _hiddenPathIds.remove(entry.display.id); + _focusedHopIndex = null; + }); + } + + void _togglePathVisibility( + DisplayPath path, + List<_ObservedPathEntry> entries, + DisplayPath? selected, + ) { + setState(() { + if (!_hiddenPathIds.remove(path.id)) { + _hiddenPathIds.add(path.id); + if (path.id == selected?.id) { + final visible = entries.where( + (e) => !_hiddenPathIds.contains(e.display.id), + ); + if (visible.isNotEmpty) { + _selectedPath = visible.first.observedBytes; + _focusedHopIndex = null; + } + } + } + }); + } + bool _isDesktopPlatform(TargetPlatform platform) { return platform == TargetPlatform.linux || platform == TargetPlatform.windows || @@ -460,6 +787,7 @@ class _ChannelMessagePathMapScreenState final settings = context.watch().settings; final isImperial = settings.unitSystem == UnitSystem.imperial; final tileCache = context.read(); + final mapScheme = Theme.of(context).colorScheme; final primaryPath = _selectPrimaryPath( widget.message.pathBytes, widget.message.pathVariants, @@ -475,15 +803,57 @@ class _ChannelMessagePathMapScreenState primaryPath, ); - final selectedPath = - ((!widget.message.isOutgoing && !widget.channelMessage) || - (widget.message.isOutgoing && widget.channelMessage)) - ? Uint8List.fromList(selectedPathTmp.reversed.toList()) - : selectedPathTmp; + final selectedPath = _orientPath(selectedPathTmp); - final selectedIndex = _indexForPath(selectedPath, observedPaths); + // Match on the unoriented bytes — observedPaths stores them as + // recorded, while selectedPath may be reversed for display. + final selectedIndex = _indexForPath(selectedPathTmp, observedPaths); final hops = _buildPathHops(selectedPath, connector, context.l10n); + // Renderable paths for the animation and combined view. + final entries = <_ObservedPathEntry>[]; + for (var i = 0; i < observedPaths.length; i++) { + final oriented = _orientPath(observedPaths[i].pathBytes); + final pathHops = i == selectedIndex + ? hops + : _buildPathHops(oriented, connector, context.l10n); + final display = _buildDisplayPath( + index: i, + isPrimary: observedPaths[i].isPrimary, + orientedBytes: oriented, + hops: pathHops, + connector: connector, + ); + if (display != null) { + entries.add( + _ObservedPathEntry( + index: i, + observedBytes: observedPaths[i].pathBytes, + display: display, + hops: pathHops, + ), + ); + } + } + final effectiveMode = entries.length > 1 + ? _viewMode + : PathViewMode.single; + _ObservedPathEntry? selectedEntry; + for (final entry in entries) { + if (entry.index == selectedIndex) { + selectedEntry = entry; + break; + } + } + final selectedDisplay = selectedEntry?.display; + final visibleEntries = effectiveMode == PathViewMode.single + ? [?selectedEntry] + : entries + .where((e) => !_hiddenPathIds.contains(e.display.id)) + .toList(); + final visibleDisplays = visibleEntries.map((e) => e.display).toList(); + _schedulePlaybackSync(selectedDisplay); + final points = []; if ((widget.message.isOutgoing && !widget.channelMessage) || @@ -507,7 +877,7 @@ class _ChannelMessagePathMapScreenState Polyline( points: points, strokeWidth: 4, - color: Colors.blueAccent, + color: MeshPalette.blue, ), ] : []; @@ -564,10 +934,16 @@ class _ChannelMessagePathMapScreenState : const KeyboardOptions.disabled(), ), onPositionChanged: (camera, hasGesture) { + if (!mounted) return; + // A manual pan/zoom releases the follow lock. + if (hasGesture && _followPacket) { + setState(() { + _followPacket = false; + }); + } final shouldShow = camera.zoom >= _labelZoomThreshold; if (!_didReceivePositionUpdate || shouldShow != _showNodeLabels) { - if (!mounted) return; setState(() { _didReceivePositionUpdate = true; _showNodeLabels = shouldShow; @@ -576,20 +952,65 @@ class _ChannelMessagePathMapScreenState }, ), children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, + ThemedMapTileLayer(tileCache: tileCache), + AnimatedBuilder( + animation: _playback, + builder: (context, _) { + List lines; + if (visibleDisplays.isEmpty) { + lines = polylines; + } else { + final animating = + _animationEnabled && + _playback.started && + _playback.hasPath; + lines = buildMultiPathPolylines( + visible: visibleDisplays, + selected: selectedDisplay, + combined: + effectiveMode == PathViewMode.combined, + animating: animating, + ); + if (animating && selectedDisplay != null) { + lines.addAll( + buildPacketTrailPolylines( + _playback, + selectedDisplay.color, + ), + ); + } + } + if (lines.isEmpty) return const SizedBox.shrink(); + return PolylineLayer(polylines: lines); + }, ), - if (polylines.isNotEmpty) - PolylineLayer(polylines: polylines), - MarkerLayer( - markers: _buildHopMarkers( - hops, - showLabels: _showNodeLabels, + if (effectiveMode == PathViewMode.combined) + MarkerLayer( + markers: _buildCombinedHopMarkers( + visibleEntries, + showLabels: _showNodeLabels, + ), + ) + else + MarkerLayer( + markers: _buildHopMarkers( + hops, + showLabels: _showNodeLabels, + ), ), + AnimatedBuilder( + animation: _playback, + builder: (context, _) { + if (!_animationEnabled || selectedDisplay == null) { + return const SizedBox.shrink(); + } + final markers = buildPacketMarkers( + _playback, + selectedDisplay.color, + ); + if (markers.isEmpty) return const SizedBox.shrink(); + return MarkerLayer(markers: markers); + }, ), ], ), @@ -599,30 +1020,46 @@ class _ChannelMessagePathMapScreenState initialZoom: initialZoom, bounds: bounds, ), - if (observedPaths.length > 1) - _buildPathSelector(context, observedPaths, selectedIndex, ( - index, - ) { - setState(() { - _selectedPath = observedPaths[index].pathBytes; - _focusedHopIndex = null; - }); - }), + if (entries.length > 1) + PathViewModeToggle( + mode: effectiveMode, + onChanged: (mode) => setState(() => _viewMode = mode), + ), + if (observedPaths.length > 1 && + effectiveMode == PathViewMode.single) + _buildPathSelector( + context, + observedPaths, + selectedIndex, + (index) { + setState(() { + _selectedPath = observedPaths[index].pathBytes; + _focusedHopIndex = null; + }); + }, + topOffset: entries.length > 1 ? 60.0 : 16.0, + ), if (points.isEmpty) Center( - child: Card( - color: Theme.of( - context, - ).colorScheme.surface.withValues(alpha: 0.9), - child: Padding( - padding: EdgeInsets.all(12), - child: Text( - context.l10n.channelPath_noRepeaterLocations, - ), + child: Container( + margin: const EdgeInsets.all(24), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: mapScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: mapScheme.outlineVariant), ), + child: Text(context.l10n.channelPath_noRepeaterLocations), ), ), - _buildLegendCard(context, hops, isImperial), + _buildLegendCard( + context, + hops, + isImperial, + entries: entries, + selectedEntry: selectedEntry, + effectiveMode: effectiveMode, + ), ], ), ), @@ -635,8 +1072,9 @@ class _ChannelMessagePathMapScreenState BuildContext context, List<_ObservedPath> paths, int selectedIndex, - ValueChanged onSelected, - ) { + ValueChanged onSelected, { + double topOffset = 16, + }) { final l10n = context.l10n; final selectedPath = paths[selectedIndex]; final label = selectedPath.isPrimary @@ -645,7 +1083,7 @@ class _ChannelMessagePathMapScreenState return Positioned( left: 16, right: 16, - top: 16, + top: topOffset, child: SafeArea( child: Card( child: Padding( @@ -715,7 +1153,7 @@ class _ChannelMessagePathMapScreenState width: 35, height: 35, decoration: BoxDecoration( - color: Colors.green, + color: MeshPalette.blue, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ @@ -749,40 +1187,163 @@ class _ChannelMessagePathMapScreenState } } + markers.addAll(_buildSelfMarkers(showLabels: showLabels)); + + return markers; + } + + List _buildSelfMarkers({required bool showLabels}) { final selfLat = context.read().selfLatitude; final selfLon = context.read().selfLongitude; - if (selfLat != null && selfLon != null) { - final selfPoint = LatLng(selfLat, selfLon); + if (selfLat == null || selfLon == null) return const []; + final markers = []; + final selfPoint = LatLng(selfLat, selfLon); + markers.add( + Marker( + point: selfPoint, + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + decoration: BoxDecoration( + color: MeshPalette.signal, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: selfPoint, + label: context.l10n.pathTrace_you, + ), + ); + } + return markers; + } + + /// Markers for the union of located hops across all visible paths, with a + /// badge on repeaters used by more than one path. + List _buildCombinedHopMarkers( + List<_ObservedPathEntry> visibleEntries, { + required bool showLabels, + }) { + final markers = []; + + final nodes = {}; + for (final entry in visibleEntries) { + final seenInPath = {}; + for (final hop in entry.hops) { + if (!hop.hasLocation) continue; + final key = + '${hop.prefix}|${hop.position!.latitude.toStringAsFixed(5)},' + '${hop.position!.longitude.toStringAsFixed(5)}'; + if (!seenInPath.add(key)) continue; + nodes.putIfAbsent(key, () => _SharedNode(hop)).paths.add(entry.display); + } + } + + for (final node in nodes.values) { + final hop = node.hop; + final point = hop.position!; + final label = _formatPrefix(hop.prefix); + final shared = node.paths.length > 1; + markers.add( Marker( - point: selfPoint, + point: point, width: 48, height: 48, - child: Center( - child: Container( - width: 35, - height: 35, - decoration: BoxDecoration( - color: Colors.teal, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), + child: GestureDetector( + onTap: () => showSharedNodeSheet( + context, + title: '$label: ${_resolveName(hop.contact, context.l10n)}', + paths: node.paths, + onSelect: (display) { + for (final entry in visibleEntries) { + if (entry.display.id == display.id) { + _selectEntry(entry); + break; + } + } + }, + ), + child: Stack( alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + children: [ + Container( + width: 35, + height: 35, + decoration: BoxDecoration( + color: MeshPalette.blue, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: shared ? 2.5 : 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + label, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), ), - ), + if (shared) + Positioned( + top: 0, + right: 0, + child: Container( + width: 17, + height: 17, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MeshPalette.bg1, + border: Border.all(color: MeshPalette.line3), + ), + alignment: Alignment.center, + child: Text( + '${node.paths.length}', + style: MeshTheme.mono( + fontSize: 9, + fontWeight: FontWeight.w700, + color: MeshPalette.ink, + ), + ), + ), + ), + ], ), ), ), @@ -790,16 +1351,26 @@ class _ChannelMessagePathMapScreenState if (showLabels) { markers.add( _buildNodeLabelMarker( - point: selfPoint, - label: context.l10n.pathTrace_you, + point: point, + label: hop.contact?.name ?? label, ), ); } } + markers.addAll(_buildSelfMarkers(showLabels: showLabels)); + return markers; } + /// Orients recorded path bytes in the direction the packet traveled. + Uint8List _orientPath(Uint8List bytes) { + final reverse = + (!widget.message.isOutgoing && !widget.channelMessage) || + (widget.message.isOutgoing && widget.channelMessage); + return reverse ? Uint8List.fromList(bytes.reversed.toList()) : bytes; + } + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { return Marker( point: point, @@ -835,21 +1406,38 @@ class _ChannelMessagePathMapScreenState ); } - Widget _colorDot(Color color) => Container( - width: 10, - height: 10, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ); - Widget _buildLegendCard( BuildContext context, List<_PathHop> hops, - bool isImperial, - ) { + bool isImperial, { + required List<_ObservedPathEntry> entries, + required _ObservedPathEntry? selectedEntry, + required PathViewMode effectiveMode, + }) { final l10n = context.l10n; - final maxHeight = MediaQuery.of(context).size.height * 0.35; - final estimatedHeight = 72.0 + (hops.length * 56.0); - final cardHeight = max(96.0, min(maxHeight, estimatedHeight)); + final combined = effectiveMode == PathViewMode.combined; + final selectedDisplay = selectedEntry?.display; + final maxHeight = + MediaQuery.of(context).size.height * (combined ? 0.45 : 0.35); + + double cardHeight; + if (_panelCollapsed) { + cardHeight = 128; + } else { + final summaryHeight = combined ? 34.0 + entries.length * 36.0 : 0; + final estimatedHeight = 132.0 + summaryHeight + hops.length * 56.0; + cardHeight = max(176.0, min(maxHeight, estimatedHeight)); + } + + final hopUseCount = {}; + if (combined) { + for (final entry in entries) { + if (_hiddenPathIds.contains(entry.display.id)) continue; + for (final prefix in entry.hops.map((h) => h.prefix).toSet()) { + hopUseCount.update(prefix, (v) => v + 1, ifAbsent: () => 1); + } + } + } return Positioned( left: 16, @@ -857,80 +1445,269 @@ class _ChannelMessagePathMapScreenState bottom: 16, child: SizedBox( height: cardHeight, - child: Card( + child: Container( + decoration: BoxDecoration( + color: MeshPalette.bg1, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MeshPalette.line2), + ), + clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(12), - child: Column( + padding: const EdgeInsets.fromLTRB(12, 8, 4, 0), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', - style: const TextStyle(fontWeight: FontWeight.w600), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + l10n.channelPath_repeaterHops, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + formatDistance( + selectedDisplay?.distanceMeters ?? + _pathDistance, + isImperial: isImperial, + ), + style: MeshTheme.mono( + fontSize: 12, + color: MeshPalette.ink2, + ), + ), + ], + ), + const SizedBox(height: 4), + PathMiniLegend( + combined: combined, + showInferred: false, + ), + ], + ), ), - const SizedBox(height: 6), - Row( - children: [ - _colorDot(Colors.green), - const SizedBox(width: 4), - Text( - l10n.pathTrace_legendGpsConfirmed, - style: const TextStyle(fontSize: 11), - ), - ], + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + _panelCollapsed + ? Icons.expand_less + : Icons.expand_more, + size: 20, + ), + tooltip: _panelCollapsed + ? l10n.pathMap_expandPanel + : l10n.pathMap_collapsePanel, + onPressed: () => setState( + () => _panelCollapsed = !_panelCollapsed, + ), ), ], ), ), - const Divider(height: 1), - Expanded( - child: hops.isEmpty - ? Center( - child: Text(l10n.channelPath_noHopDetailsAvailable), - ) - : ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: hops.length, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final hop = hops[index]; - final isFocused = _focusedHopIndex == hop.index; - return ListTile( - dense: true, - enabled: hop.hasLocation, - selected: isFocused, - selectedTileColor: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.12), - onTap: hop.hasLocation - ? () => _onHopTapped(hop) - : null, - leading: CircleAvatar( - radius: 14, - child: Text( - hop.index.toString(), - style: const TextStyle(fontSize: 12), - ), - ), - title: Text(hop.displayLabel), - subtitle: Text( - hop.hasLocation - ? '${hop.position!.latitude.toStringAsFixed(5)}, ' - '${hop.position!.longitude.toStringAsFixed(5)}' - : l10n.channelPath_noLocationData, - ), - ); - }, - ), + PathAnimationControls( + playback: _playback, + selected: selectedDisplay, + animationEnabled: _animationEnabled, + onToggleAnimation: () => setState(() { + _animationEnabled = !_animationEnabled; + if (!_animationEnabled) _playback.stop(); + }), + followEnabled: _followPacket, + onToggleFollow: _toggleFollowPacket, ), + if (!_panelCollapsed) ...[ + if (selectedDisplay != null && + selectedDisplay.unresolvedHops > 0) + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), + child: Text( + l10n.pathMap_partialAnimation( + selectedDisplay.unresolvedHops, + ), + style: TextStyle(fontSize: 10.5, color: MeshPalette.warn), + ), + ), + if (combined) + PathSummaryList( + paths: entries.map((e) => e.display).toList(), + selectedId: selectedDisplay?.id ?? '', + hiddenIds: _hiddenPathIds, + isImperial: isImperial, + onSelect: (display) { + for (final entry in entries) { + if (entry.display.id == display.id) { + _selectEntry(entry); + break; + } + } + }, + onToggleVisibility: (display) => _togglePathVisibility( + display, + entries, + selectedDisplay, + ), + onShowAll: () => setState(_hiddenPathIds.clear), + ), + const Divider(height: 1), + Expanded( + child: _buildHopListView( + hops, + selectedDisplay, + hopUseCount, + ), + ), + ], ], ), ), ), ); } + + Widget _buildHopListView( + List<_PathHop> hops, + DisplayPath? selectedDisplay, + Map hopUseCount, + ) { + final l10n = context.l10n; + if (hops.isEmpty) { + return Center(child: Text(l10n.channelPath_noHopDetailsAvailable)); + } + return ValueListenableBuilder( + valueListenable: _playback.activeSegment, + builder: (context, activeSegment, _) { + int highlightRow = -1; + if (_animationEnabled && + selectedDisplay != null && + activeSegment >= 0 && + activeSegment < selectedDisplay.rowForSegment.length) { + highlightRow = selectedDisplay.rowForSegment[activeSegment]; + } + final highlightColor = (selectedDisplay?.color ?? MeshPalette.blue) + .withValues(alpha: 0.14); + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: hops.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final hop = hops[index]; + final isFocused = _focusedHopIndex == hop.index; + final sharedCount = hopUseCount[hop.prefix] ?? 0; + return InkWell( + onTap: hop.hasLocation ? () => _onHopTapped(hop) : null, + child: Container( + color: index == highlightRow + ? highlightColor + : isFocused + ? MeshPalette.blueBg + : Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: MeshPalette.blueDim.withValues( + alpha: 0.3, + ), + shape: BoxShape.circle, + border: Border.all( + color: MeshPalette.blueDim.withValues( + alpha: 0.5, + ), + ), + ), + alignment: Alignment.center, + child: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + hop.displayLabel, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + [ + hop.hasLocation + ? '${hop.position!.latitude.toStringAsFixed(5)}, ' + '${hop.position!.longitude.toStringAsFixed(5)}' + : context + .l10n + .channelPath_noLocationData, + if (sharedCount > 1) + context.l10n.pathMap_sharedNodeCount( + sharedCount, + ), + ].join(' · '), + style: MeshTheme.mono( + fontSize: 10, + color: MeshPalette.ink3, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} + +/// One observed route paired with its renderable form and resolved hops. +class _ObservedPathEntry { + final int index; + final Uint8List observedBytes; + final DisplayPath display; + final List<_PathHop> hops; + + const _ObservedPathEntry({ + required this.index, + required this.observedBytes, + required this.display, + required this.hops, + }); +} + +/// A located hop shared across one or more visible paths. +class _SharedNode { + final _PathHop hop; + final List paths = []; + + _SharedNode(this.hop); } class _PathHop { diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 36e82049..080eef8e 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:meshcore_open/storage/channel_message_store.dart'; import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; @@ -16,11 +16,13 @@ import '../services/ui_view_state_service.dart'; import '../models/channel.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; +import '../theme/mesh_theme.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; import '../utils/route_transitions.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/qr_code_display.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/sync_progress_overlay.dart'; @@ -82,6 +84,14 @@ class _ChannelsScreenState extends State super.dispose(); } + String _relativeTime(DateTime t) { + final diff = DateTime.now().difference(t); + if (diff.inMinutes < 1) return 'now'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m'; + if (diff.inHours < 24) return '${diff.inHours}h'; + return '${diff.inDays}d'; + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -107,16 +117,19 @@ class _ChannelsScreenState extends State bottom: const SyncProgressAppBarBottom(), actions: [ PopupMenuButton( - itemBuilder: (context) => [ + // onTap handlers run after the menu route pops, so they must + // capture the screen's context — not the itemBuilder's menu + // context, which is deactivated by then. + itemBuilder: (menuContext) => [ PopupMenuItem( child: Row( children: [ Icon( Icons.logout, - color: Theme.of(context).colorScheme.error, + color: Theme.of(menuContext).colorScheme.error, ), const SizedBox(width: 8), - Text(context.l10n.common_disconnect), + Text(menuContext.l10n.common_disconnect), ], ), onTap: () => _disconnect(context), @@ -126,7 +139,7 @@ class _ChannelsScreenState extends State children: [ const Icon(Icons.groups), const SizedBox(width: 8), - Text(context.l10n.community_manageCommunities), + Text(menuContext.l10n.community_manageCommunities), ], ), onTap: () => _showManageCommunitiesDialog(context), @@ -136,7 +149,7 @@ class _ChannelsScreenState extends State children: [ const Icon(Icons.settings), const SizedBox(width: 8), - Text(context.l10n.settings_title), + Text(menuContext.l10n.settings_title), ], ), onTap: () => Navigator.push( @@ -219,9 +232,6 @@ class _ChannelsScreenState extends State _buildFilterButton(viewState), ], ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, @@ -264,8 +274,8 @@ class _ChannelsScreenState extends State viewState.channelsSearchText.isEmpty) ? ReorderableListView.builder( padding: const EdgeInsets.only( - left: 16, - right: 16, + left: 0, + right: 0, top: 8, bottom: 88, ), @@ -294,13 +304,14 @@ class _ChannelsScreenState extends State channel, showDragHandle: true, dragIndex: index, + listIndex: index, ); }, ) : ListView.builder( padding: const EdgeInsets.only( - left: 16, - right: 16, + left: 0, + right: 0, top: 8, bottom: 88, ), @@ -312,6 +323,7 @@ class _ChannelsScreenState extends State connector, channelMessageStore, channel, + listIndex: index, ); }, ), @@ -346,16 +358,17 @@ class _ChannelsScreenState extends State Channel channel, { bool showDragHandle = false, int? dragIndex, + int listIndex = 0, }) { final unreadCount = connector.getUnreadCountForChannel(channel); final isMuted = context.watch().isChannelMuted( channel.name, ); + final scheme = Theme.of(context).colorScheme; // Determine icon and colors based on channel type IconData icon; Color iconColor; - Color bgColor; final ChannelType channelType = Channel.getChannelType( channel, _communityIndex, @@ -364,131 +377,194 @@ class _ChannelsScreenState extends State switch (channelType) { case ChannelType.communityPublic: icon = Icons.groups; - iconColor = Colors.purple; - bgColor = Colors.purple.withValues(alpha: 0.2); + iconColor = MeshPalette.magenta; case ChannelType.communityHashtag: - icon = Icons.tag; - iconColor = Colors.purple; - bgColor = Colors.purple.withValues(alpha: 0.2); + icon = Icons.groups; + iconColor = MeshPalette.magenta; case ChannelType.public: icon = Icons.public; - iconColor = Colors.green; - bgColor = Colors.green.withValues(alpha: 0.2); + iconColor = MeshPalette.signal; case ChannelType.hashtag: icon = Icons.tag; - iconColor = Colors.blue; - bgColor = Colors.blue.withValues(alpha: 0.2); + iconColor = MeshPalette.blue; case ChannelType.private: icon = Icons.lock; - iconColor = Colors.blue; - bgColor = Colors.blue.withValues(alpha: 0.2); + iconColor = MeshPalette.blue; } - return Card( - key: ValueKey('channel_${channel.index}'), - margin: const EdgeInsets.only(bottom: 12), - child: GestureDetector( - onSecondaryTapUp: PlatformInfo.isDesktop - ? (_) => _showChannelActions( - context, - connector, - channelMessageStore, - channel, - ) - : null, - child: ListTile( - dense: true, - minVerticalPadding: 14, - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - visualDensity: const VisualDensity(vertical: -2), - leading: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 3), - child: CircleAvatar( - backgroundColor: bgColor, - child: Icon(icon, color: iconColor), - ), + // Last message preview + final messages = connector.getChannelMessages(channel); + final lastMessage = messages.isNotEmpty ? messages.last : null; + final lastPreview = lastMessage?.text ?? ''; + final lastTime = lastMessage?.timestamp; + + final channelLabel = channel.name.isEmpty + ? context.l10n.channels_channelIndex(channel.index) + : channel.name; + + return ListEntrance( + key: ValueKey('channel_entrance_${channel.index}'), + index: dragIndex ?? listIndex, + child: MeshCard( + key: ValueKey('channel_${channel.index}'), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + onTap: () { + HapticFeedback.selectionClick(); + final unread = connector.getUnreadCountForChannelIndex(channel.index); + connector.markChannelRead(channel.index); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelChatScreen( + channel: channel, + initialUnreadCount: unread, ), - if (isCommunityChannel) - Positioned( - right: 0, - bottom: 0, - child: Container( - width: 14, - height: 14, - decoration: BoxDecoration( - color: Colors.purple, - shape: BoxShape.circle, - border: Border.all( - color: Theme.of(context).cardColor, - width: 2, + ), + ); + }, + onLongPress: () => _showChannelActions( + this.context, + connector, + channelMessageStore, + channel, + ), + child: GestureDetector( + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => _showChannelActions( + this.context, + connector, + channelMessageStore, + channel, + ) + : null, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Leading avatar with optional community badge + Stack( + clipBehavior: Clip.none, + children: [ + AvatarCircle( + name: channelLabel, + size: 42, + color: iconColor, + icon: icon, + ), + if (isCommunityChannel) + Positioned( + right: -2, + bottom: -2, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: MeshPalette.magenta, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerLow, + width: 2, + ), + ), + child: const Icon( + Icons.people, + size: 8, + color: Colors.white, + ), ), ), - child: const Icon( - Icons.people, - size: 8, - color: Colors.white, + ], + ), + const SizedBox(width: 12), + // Title + subtitle + ch chip + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + channelLabel, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 6), + StatusChip( + label: 'CH ${channel.index}', + color: MeshPalette.blue, + fontSize: 10, + ), + ], ), + if (lastPreview.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + lastPreview, + style: MeshTheme.mono( + fontSize: 11.5, + color: scheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + const SizedBox(width: 8), + // Right side: time + unread badge + muted + drag handle + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (lastTime != null) + Text( + _relativeTime(lastTime), + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isMuted) ...[ + Icon( + Icons.notifications_off, + size: 14, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + ], + if (unreadCount > 0) + UnreadBadge(count: unreadCount), + ], ), - ), - ], - ), - title: Text( - channel.name.isEmpty - ? context.l10n.channels_channelIndex(channel.index) - : channel.name, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isMuted) ...[ - Icon( - Icons.notifications_off, - size: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + ], + ), + if (showDragHandle && dragIndex != null) ...[ const SizedBox(width: 4), - ], - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(width: 4), - ], - if (showDragHandle && dragIndex != null) ReorderableDragStartListener( index: dragIndex, child: Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(8), child: Icon( Icons.drag_handle, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, ), ), ), + ], ], ), - onTap: () { - final unread = connector.getUnreadCountForChannelIndex( - channel.index, - ); - connector.markChannelRead(channel.index); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChannelChatScreen( - channel: channel, - initialUnreadCount: unread, - ), - ), - ); - }, - onLongPress: () => _showChannelActions( - context, - connector, - channelMessageStore, - channel, - ), ), ), ); @@ -512,7 +588,7 @@ class _ChannelsScreenState extends State children: [ ListTile( leading: const Icon(Icons.edit_outlined), - title: Text(context.l10n.channels_editChannel), + title: Text(sheetContext.l10n.channels_editChannel), onTap: () async { Navigator.pop(sheetContext); await Future.delayed(const Duration(milliseconds: 100)); @@ -529,8 +605,8 @@ class _ChannelsScreenState extends State ), title: Text( isMuted - ? context.l10n.channels_unmuteChannel - : context.l10n.channels_muteChannel, + ? sheetContext.l10n.channels_unmuteChannel + : sheetContext.l10n.channels_muteChannel, ), onTap: () async { Navigator.pop(sheetContext); @@ -547,7 +623,7 @@ class _ChannelsScreenState extends State color: Theme.of(sheetContext).colorScheme.error, ), title: Text( - context.l10n.channels_deleteChannel, + sheetContext.l10n.channels_deleteChannel, style: TextStyle( color: Theme.of(sheetContext).colorScheme.error, ), @@ -557,7 +633,7 @@ class _ChannelsScreenState extends State await Future.delayed(const Duration(milliseconds: 100)); if (parentContext.mounted) { _confirmDeleteChannel( - context, + parentContext, connector, channelMessageStore, channel, @@ -714,11 +790,11 @@ class _ChannelsScreenState extends State _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; - showDialog( - context: context, - builder: (dialogContext) => StatefulBuilder( - builder: (dialogContext, setDialogState) { - Widget buildOptionTile({ + showMeshSheet( + context, + builder: (sheetContext) => StatefulBuilder( + builder: (sheetContext, setSheetState) { + Widget buildOptionCard({ required int optionIndex, required IconData icon, required String title, @@ -726,51 +802,19 @@ class _ChannelsScreenState extends State bool enabled = true, }) { final isSelected = selectedOption == optionIndex; - return ListTile( - leading: CircleAvatar( - backgroundColor: enabled - ? (isSelected - ? Theme.of(dialogContext).colorScheme.primaryContainer - : null) - : Theme.of( - dialogContext, - ).colorScheme.onSurface.withValues(alpha: 0.12), - child: Icon( - icon, - color: enabled - ? (isSelected - ? Theme.of(dialogContext).colorScheme.primary - : null) - : Theme.of( - dialogContext, - ).colorScheme.onSurface.withValues(alpha: 0.38), - ), - ), - title: Text( - title, - style: TextStyle( - color: enabled - ? null - : Theme.of( - dialogContext, - ).colorScheme.onSurface.withValues(alpha: 0.38), - ), - ), - subtitle: Text( - subtitle, - style: TextStyle( - color: enabled - ? null - : Theme.of( - dialogContext, - ).colorScheme.onSurface.withValues(alpha: 0.38), - ), - ), - trailing: enabled ? const Icon(Icons.chevron_right) : null, - selected: isSelected, + final cardScheme = Theme.of(sheetContext).colorScheme; + return MeshCard( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + borderColor: isSelected && enabled + ? MeshPalette.blueLine + : null, + color: isSelected && enabled + ? MeshPalette.blueBg + : null, onTap: enabled ? () { - setDialogState(() { + setSheetState(() { selectedOption = optionIndex; nameController.clear(); pskController.clear(); @@ -778,6 +822,49 @@ class _ChannelsScreenState extends State }); } : null, + child: Row( + children: [ + AvatarCircle( + name: title, + size: 38, + color: enabled + ? (isSelected ? MeshPalette.blue : cardScheme.onSurfaceVariant) + : cardScheme.outline, + icon: icon, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: enabled ? null : cardScheme.outline, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: Theme.of(sheetContext).textTheme.bodySmall?.copyWith( + color: enabled ? cardScheme.onSurfaceVariant : cardScheme.outline, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + if (enabled) + Icon( + Icons.chevron_right, + color: isSelected ? MeshPalette.blue : cardScheme.onSurfaceVariant, + size: 20, + ), + ], + ), ); } @@ -796,7 +883,7 @@ class _ChannelsScreenState extends State child: TextField( controller: nameController, decoration: InputDecoration( - labelText: dialogContext.l10n.channels_channelName, + labelText: sheetContext.l10n.channels_channelName, border: const OutlineInputBorder(), ), maxLength: 31, @@ -814,7 +901,7 @@ class _ChannelsScreenState extends State showDismissibleSnackBar( context, content: Text( - dialogContext + sheetContext .l10n .channels_enterChannelName, ), @@ -826,7 +913,7 @@ class _ChannelsScreenState extends State for (int i = 0; i < 16; i++) { psk[i] = random.nextInt(256); } - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); await connector.setChannel( nextIndex, name, @@ -844,12 +931,13 @@ class _ChannelsScreenState extends State ); } }, - child: Text(dialogContext.l10n.common_create), + child: Text(sheetContext.l10n.common_create), ), ), ], ), ), + const SizedBox(height: 8), ], ); @@ -864,7 +952,7 @@ class _ChannelsScreenState extends State child: TextField( controller: nameController, decoration: InputDecoration( - labelText: dialogContext.l10n.channels_channelName, + labelText: sheetContext.l10n.channels_channelName, border: const OutlineInputBorder(), ), maxLength: 31, @@ -878,7 +966,7 @@ class _ChannelsScreenState extends State child: TextField( controller: pskController, decoration: InputDecoration( - labelText: dialogContext.l10n.channels_pskHex, + labelText: sheetContext.l10n.channels_pskHex, border: const OutlineInputBorder(), ), ), @@ -896,7 +984,7 @@ class _ChannelsScreenState extends State showDismissibleSnackBar( context, content: Text( - dialogContext + sheetContext .l10n .channels_enterChannelName, ), @@ -910,14 +998,14 @@ class _ChannelsScreenState extends State showDismissibleSnackBar( context, content: Text( - dialogContext + sheetContext .l10n .channels_pskMustBe32Hex, ), ); return; } - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); connector.setChannel(nextIndex, name, psk); if (context.mounted) { showDismissibleSnackBar( @@ -928,12 +1016,13 @@ class _ChannelsScreenState extends State ); } }, - child: Text(dialogContext.l10n.common_add), + child: Text(sheetContext.l10n.common_add), ), ), ], ), ), + const SizedBox(height: 8), ], ); @@ -951,7 +1040,7 @@ class _ChannelsScreenState extends State final psk = Channel.parsePskHex( Channel.publicChannelPsk, ); - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); connector.setChannel( nextIndex, context.l10n.channels_public, @@ -966,7 +1055,7 @@ class _ChannelsScreenState extends State ); } }, - child: Text(dialogContext.l10n.common_add), + child: Text(sheetContext.l10n.common_add), ), ), ], @@ -980,7 +1069,7 @@ class _ChannelsScreenState extends State if (_communities.isNotEmpty) ...[ RadioGroup( groupValue: isRegularHashtag, - onChanged: (v) => setDialogState(() { + onChanged: (v) => setSheetState(() { if (v == null) return; isRegularHashtag = v; if (isRegularHashtag) { @@ -995,20 +1084,20 @@ class _ChannelsScreenState extends State RadioListTile( value: true, title: Text( - dialogContext.l10n.community_regularHashtag, + sheetContext.l10n.community_regularHashtag, ), subtitle: Text( - dialogContext.l10n.community_regularHashtagDesc, + sheetContext.l10n.community_regularHashtagDesc, ), dense: true, ), RadioListTile( value: false, title: Text( - dialogContext.l10n.community_communityHashtag, + sheetContext.l10n.community_communityHashtag, ), subtitle: Text( - dialogContext + sheetContext .l10n .community_communityHashtagDesc, ), @@ -1036,10 +1125,10 @@ class _ChannelsScreenState extends State ) .toList(), onChanged: (c) => - setDialogState(() => selectedCommunity = c), + setSheetState(() => selectedCommunity = c), decoration: InputDecoration( labelText: - dialogContext.l10n.community_selectCommunity, + sheetContext.l10n.community_selectCommunity, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.groups), ), @@ -1054,8 +1143,8 @@ class _ChannelsScreenState extends State child: TextField( controller: hashtagController, decoration: InputDecoration( - labelText: dialogContext.l10n.channels_enterHashtag, - hintText: dialogContext.l10n.channels_hashtagHint, + labelText: sheetContext.l10n.channels_enterHashtag, + hintText: sheetContext.l10n.channels_hashtagHint, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.tag), ), @@ -1067,11 +1156,11 @@ class _ChannelsScreenState extends State Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - dialogContext.l10n.community_hashtagPrivacyHint, + sheetContext.l10n.community_hashtagPrivacyHint, style: TextStyle( fontSize: 12, color: Theme.of( - dialogContext, + sheetContext, ).colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), @@ -1092,7 +1181,7 @@ class _ChannelsScreenState extends State showDismissibleSnackBar( context, content: Text( - dialogContext + sheetContext .l10n .channels_enterChannelName, ), @@ -1115,9 +1204,9 @@ class _ChannelsScreenState extends State // Community hashtag - HMAC derivation from community secret if (selectedCommunity == null) { showDismissibleSnackBar( - dialogContext, + sheetContext, content: Text( - dialogContext + sheetContext .l10n .community_selectCommunity, ), @@ -1136,8 +1225,8 @@ class _ChannelsScreenState extends State _loadCommunities(); } - if (dialogContext.mounted) { - Navigator.pop(dialogContext); + if (sheetContext.mounted) { + Navigator.pop(sheetContext); } connector.setChannel( nextIndex, @@ -1155,7 +1244,7 @@ class _ChannelsScreenState extends State ); } }, - child: Text(dialogContext.l10n.common_add), + child: Text(sheetContext.l10n.common_add), ), ), ], @@ -1175,7 +1264,7 @@ class _ChannelsScreenState extends State Expanded( child: FilledButton.icon( onPressed: () async { - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); if (context.mounted) { final result = await Navigator.push( context, @@ -1191,7 +1280,7 @@ class _ChannelsScreenState extends State } }, icon: const Icon(Icons.qr_code_scanner), - label: Text(dialogContext.l10n.community_scanQr), + label: Text(sheetContext.l10n.community_scanQr), ), ), ], @@ -1209,8 +1298,8 @@ class _ChannelsScreenState extends State child: TextField( controller: nameController, decoration: InputDecoration( - labelText: dialogContext.l10n.community_name, - hintText: dialogContext.l10n.community_enterName, + labelText: sheetContext.l10n.community_name, + hintText: sheetContext.l10n.community_enterName, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.groups), ), @@ -1220,15 +1309,15 @@ class _ChannelsScreenState extends State CheckboxListTile( value: addPublicChannel, onChanged: (value) { - setDialogState(() { + setSheetState(() { addPublicChannel = value ?? true; }); }, title: Text( - dialogContext.l10n.community_addPublicChannel, + sheetContext.l10n.community_addPublicChannel, ), subtitle: Text( - dialogContext.l10n.community_addPublicChannelHint, + sheetContext.l10n.community_addPublicChannelHint, ), controlAffinity: ListTileControlAffinity.leading, contentPadding: const EdgeInsets.symmetric( @@ -1249,7 +1338,7 @@ class _ChannelsScreenState extends State showDismissibleSnackBar( context, content: Text( - dialogContext.l10n.community_enterName, + sheetContext.l10n.community_enterName, ), ); return; @@ -1277,8 +1366,8 @@ class _ChannelsScreenState extends State ); } - if (dialogContext.mounted) { - Navigator.pop(dialogContext); + if (sheetContext.mounted) { + Navigator.pop(sheetContext); } // Refresh communities list @@ -1307,12 +1396,13 @@ class _ChannelsScreenState extends State ); } }, - child: Text(dialogContext.l10n.common_create), + child: Text(sheetContext.l10n.common_create), ), ), ], ), ), + const SizedBox(height: 8), ], ); @@ -1321,84 +1411,80 @@ class _ChannelsScreenState extends State } } - return AlertDialog( - title: Text(dialogContext.l10n.channels_addChannel), - contentPadding: const EdgeInsets.symmetric(vertical: 16), - content: SizedBox( - width: double.maxFinite, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - buildOptionTile( - optionIndex: 0, - icon: Icons.add, - title: dialogContext.l10n.channels_createPrivateChannel, - subtitle: - dialogContext.l10n.channels_createPrivateChannelDesc, - ), - if (selectedOption == 0) - buildExpandedContent(_channelMessageStore)!, - const Divider(height: 1), - buildOptionTile( - optionIndex: 1, - icon: Icons.lock, - title: dialogContext.l10n.channels_joinPrivateChannel, - subtitle: - dialogContext.l10n.channels_joinPrivateChannelDesc, - ), - if (selectedOption == 1) - buildExpandedContent(_channelMessageStore)!, - if (!hasPublicChannel) ...[ - const Divider(height: 1), - buildOptionTile( - optionIndex: 2, - icon: Icons.public, - title: dialogContext.l10n.channels_joinPublicChannel, + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.7, + minChildSize: 0.4, + maxChildSize: 0.95, + builder: (_, scrollController) => Column( + children: [ + BottomSheetHeader( + title: sheetContext.l10n.channels_addChannel, + ), + Expanded( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.only(bottom: 24), + children: [ + buildOptionCard( + optionIndex: 0, + icon: Icons.add, + title: sheetContext.l10n.channels_createPrivateChannel, subtitle: - dialogContext.l10n.channels_joinPublicChannelDesc, + sheetContext.l10n.channels_createPrivateChannelDesc, ), - if (selectedOption == 2) + if (selectedOption == 0) + buildExpandedContent(_channelMessageStore)!, + buildOptionCard( + optionIndex: 1, + icon: Icons.lock, + title: sheetContext.l10n.channels_joinPrivateChannel, + subtitle: + sheetContext.l10n.channels_joinPrivateChannelDesc, + ), + if (selectedOption == 1) + buildExpandedContent(_channelMessageStore)!, + if (!hasPublicChannel) ...[ + buildOptionCard( + optionIndex: 2, + icon: Icons.public, + title: sheetContext.l10n.channels_joinPublicChannel, + subtitle: + sheetContext.l10n.channels_joinPublicChannelDesc, + ), + if (selectedOption == 2) + buildExpandedContent(_channelMessageStore)!, + ], + buildOptionCard( + optionIndex: 3, + icon: Icons.tag, + title: sheetContext.l10n.channels_joinHashtagChannel, + subtitle: + sheetContext.l10n.channels_joinHashtagChannelDesc, + ), + if (selectedOption == 3) + buildExpandedContent(_channelMessageStore)!, + buildOptionCard( + optionIndex: 4, + icon: Icons.qr_code_scanner, + title: sheetContext.l10n.community_scanQr, + subtitle: sheetContext.l10n.community_join, + ), + if (selectedOption == 4) + buildExpandedContent(_channelMessageStore)!, + buildOptionCard( + optionIndex: 5, + icon: Icons.groups, + title: sheetContext.l10n.community_create, + subtitle: sheetContext.l10n.community_createDesc, + ), + if (selectedOption == 5) buildExpandedContent(_channelMessageStore)!, ], - const Divider(height: 1), - buildOptionTile( - optionIndex: 3, - icon: Icons.tag, - title: dialogContext.l10n.channels_joinHashtagChannel, - subtitle: - dialogContext.l10n.channels_joinHashtagChannelDesc, - ), - if (selectedOption == 3) - buildExpandedContent(_channelMessageStore)!, - const Divider(height: 1), - buildOptionTile( - optionIndex: 4, - icon: Icons.qr_code_scanner, - title: dialogContext.l10n.community_scanQr, - subtitle: dialogContext.l10n.community_join, - ), - if (selectedOption == 4) - buildExpandedContent(_channelMessageStore)!, - const Divider(height: 1), - buildOptionTile( - optionIndex: 5, - icon: Icons.groups, - title: dialogContext.l10n.community_create, - subtitle: dialogContext.l10n.community_createDesc, - ), - if (selectedOption == 5) - buildExpandedContent(_channelMessageStore)!, - ], + ), ), - ), + ], ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(dialogContext.l10n.common_close), - ), - ], ); }, ), @@ -1422,154 +1508,175 @@ class _ChannelsScreenState extends State channel.index, ); - showDialog( - context: context, - builder: (dialogContext) => StatefulBuilder( - builder: (dialogContext, setState) => AlertDialog( - title: Text( - dialogContext.l10n.channels_editChannelTitle(channel.index), - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - decoration: InputDecoration( - labelText: dialogContext.l10n.channels_channelName, - border: const OutlineInputBorder(), - ), - maxLength: 31, - ), - const SizedBox(height: 16), - TextField( - controller: pskController, - decoration: InputDecoration( - labelText: dialogContext.l10n.channels_pskHex, - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: const Icon(Icons.casino), - tooltip: dialogContext.l10n.channels_generateRandomPsk, - onPressed: () { - final random = Random.secure(); - final bytes = Uint8List(16); - for (int i = 0; i < 16; i++) { - bytes[i] = random.nextInt(256); - } - pskController.text = Channel.formatPskHex(bytes); - }, - ), - ), - ), - const SizedBox(height: 16), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(dialogContext.l10n.channels_smazCompression), - value: smazEnabled, - onChanged: (value) => setState(() { - smazEnabled = value; - if (smazEnabled) { - cyr2latEnabled = false; - } - }), - ), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(dialogContext.l10n.channels_cyr2latCompression), - subtitle: Text( - dialogContext.l10n.channels_cyr2latCompressionDscr, - ), - value: cyr2latEnabled, - onChanged: (value) => setState(() { - cyr2latEnabled = value; - if (cyr2latEnabled) { - smazEnabled = false; - } - }), - ), - if (cyr2latEnabled) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: DropdownButtonFormField( - initialValue: selectedCyr2LatProfileId, + showMeshSheet( + context, + builder: (sheetContext) => StatefulBuilder( + builder: (sheetContext, setSheetState) => DraggableScrollableSheet( + expand: false, + initialChildSize: 0.65, + minChildSize: 0.4, + maxChildSize: 0.95, + builder: (_, scrollController) => Column( + children: [ + BottomSheetHeader( + title: sheetContext.l10n.channels_editChannelTitle(channel.index), + ), + Expanded( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + const SizedBox(height: 8), + TextField( + controller: nameController, decoration: InputDecoration( - labelText: dialogContext - .l10n - .channels_cyr2latSettingsSubheading, + labelText: sheetContext.l10n.channels_channelName, border: const OutlineInputBorder(), ), - items: appSettingsService.settings.cyr2latProfiles.map(( - profile, - ) { - return DropdownMenuItem( - value: profile.id, - child: Text(profile.name), - ); - }).toList(), - onChanged: (value) => setState(() { - selectedCyr2LatProfileId = value; + maxLength: 31, + ), + const SizedBox(height: 16), + TextField( + controller: pskController, + decoration: InputDecoration( + labelText: sheetContext.l10n.channels_pskHex, + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.casino), + tooltip: sheetContext.l10n.channels_generateRandomPsk, + onPressed: () { + final random = Random.secure(); + final bytes = Uint8List(16); + for (int i = 0; i < 16; i++) { + bytes[i] = random.nextInt(256); + } + pskController.text = Channel.formatPskHex(bytes); + }, + ), + ), + ), + const SizedBox(height: 16), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(sheetContext.l10n.channels_smazCompression), + value: smazEnabled, + onChanged: (value) => setSheetState(() { + smazEnabled = value; + if (smazEnabled) { + cyr2latEnabled = false; + } }), ), - ), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(dialogContext.l10n.common_cancel), - ), - FilledButton( - onPressed: () async { - final name = nameController.text.trim(); - final pskHex = pskController.text.trim(); - - Uint8List psk; - try { - psk = Channel.parsePskHex(pskHex); - } on FormatException { - showDismissibleSnackBar( - dialogContext, - content: Text(dialogContext.l10n.channels_pskMustBe32Hex), - ); - return; - } - - Navigator.pop(dialogContext); - try { - await connector.setChannel(channel.index, name, psk); - await connector.setChannelSmazEnabled( - channel.index, - smazEnabled, - ); - await connector.setChannelCyr2LatEnabled( - channel.index, - cyr2latEnabled, - ); - await connector.setChannelCyr2LatProfileId( - channel.index, - selectedCyr2LatProfileId, - ); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(context.l10n.channels_channelUpdated(name)), - ); - } catch (e, st) { - debugPrint(st.toString()); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text( - context.l10n.channels_channelUpdateFailed('$e'), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(sheetContext.l10n.channels_cyr2latCompression), + subtitle: Text( + sheetContext.l10n.channels_cyr2latCompressionDscr, + ), + value: cyr2latEnabled, + onChanged: (value) => setSheetState(() { + cyr2latEnabled = value; + if (cyr2latEnabled) { + smazEnabled = false; + } + }), ), - ); - } - }, - child: Text(dialogContext.l10n.common_save), - ), - ], + if (cyr2latEnabled) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: DropdownButtonFormField( + initialValue: selectedCyr2LatProfileId, + decoration: InputDecoration( + labelText: sheetContext + .l10n + .channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), + ), + items: appSettingsService.settings.cyr2latProfiles.map(( + profile, + ) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) => setSheetState(() { + selectedCyr2LatProfileId = value; + }), + ), + ), + ], + const SizedBox(height: 24), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(sheetContext), + child: Text(sheetContext.l10n.common_cancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: () async { + final name = nameController.text.trim(); + final pskHex = pskController.text.trim(); + + Uint8List psk; + try { + psk = Channel.parsePskHex(pskHex); + } on FormatException { + showDismissibleSnackBar( + sheetContext, + content: Text(sheetContext.l10n.channels_pskMustBe32Hex), + ); + return; + } + + Navigator.pop(sheetContext); + try { + await connector.setChannel(channel.index, name, psk); + await connector.setChannelSmazEnabled( + channel.index, + smazEnabled, + ); + await connector.setChannelCyr2LatEnabled( + channel.index, + cyr2latEnabled, + ); + await connector.setChannelCyr2LatProfileId( + channel.index, + selectedCyr2LatProfileId, + ); + if (!context.mounted) return; + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_channelUpdated(name)), + ); + } catch (e, st) { + debugPrint(st.toString()); + if (!context.mounted) return; + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelUpdateFailed('$e'), + ), + ); + } + }, + child: Text(sheetContext.l10n.common_save), + ), + ), + ], + ), + ), + ], + ), ), ), ); @@ -1625,7 +1732,7 @@ class _ChannelsScreenState extends State }, child: Text( dialogContext.l10n.common_delete, - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle(color: Theme.of(dialogContext).colorScheme.error), ), ), ], @@ -1651,9 +1758,8 @@ class _ChannelsScreenState extends State } void _showManageCommunitiesDialog(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, + showMeshSheet( + context, builder: (sheetContext) => DraggableScrollableSheet( initialChildSize: 0.5, minChildSize: 0.3, @@ -1661,18 +1767,8 @@ class _ChannelsScreenState extends State expand: false, builder: (_, scrollController) => Column( children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const Icon(Icons.groups, size: 28), - const SizedBox(width: 12), - Text( - context.l10n.community_manageCommunities, - style: Theme.of(context).textTheme.titleLarge, - ), - ], - ), + BottomSheetHeader( + title: sheetContext.l10n.community_manageCommunities, ), const Divider(height: 1), Expanded( @@ -1684,27 +1780,27 @@ class _ChannelsScreenState extends State Icon( Icons.groups_outlined, size: 64, - color: Theme.of(context) + color: Theme.of(sheetContext) .colorScheme .onSurfaceVariant .withValues(alpha: 0.6), ), const SizedBox(height: 16), Text( - context.l10n.community_noCommunities, + sheetContext.l10n.community_noCommunities, style: TextStyle( fontSize: 16, color: Theme.of( - context, + sheetContext, ).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( - context.l10n.community_scanOrCreate, + sheetContext.l10n.community_scanOrCreate, style: TextStyle( fontSize: 14, - color: Theme.of(context) + color: Theme.of(sheetContext) .colorScheme .onSurfaceVariant .withValues(alpha: 0.8), @@ -1721,12 +1817,10 @@ class _ChannelsScreenState extends State final community = _communities[index]; return ListTile( leading: CircleAvatar( - backgroundColor: Colors.purple.withValues( - alpha: 0.2, - ), + backgroundColor: MeshPalette.magentaBg, child: const Icon( Icons.groups, - color: Colors.purple, + color: MeshPalette.magenta, ), ), title: Text(community.name), @@ -1744,10 +1838,12 @@ class _ChannelsScreenState extends State trailing: PopupMenuButton( onSelected: (value) { Navigator.pop(sheetContext); + // Use the screen's context: the sheet item's + // context is deactivated once the sheet pops. if (value == 'share') { - _showCommunityQrDialog(context, community); + _showCommunityQrDialog(this.context, community); } else if (value == 'leave') { - _confirmLeaveCommunity(context, community); + _confirmLeaveCommunity(this.context, community); } }, itemBuilder: (context) => [ @@ -1879,7 +1975,7 @@ class _ChannelsScreenState extends State }, child: Text( dialogContext.l10n.community_delete, - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle(color: Theme.of(dialogContext).colorScheme.error), ), ), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 89310bbd..3b0a2441 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -30,7 +30,6 @@ import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/byte_count_input.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; -import '../helpers/contact_ui.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; @@ -43,6 +42,8 @@ import '../widgets/translated_message_content.dart'; import '../l10n/l10n.dart'; import '../helpers/snack_bar_builder.dart'; import '../widgets/unread_divider.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import 'telemetry_screen.dart'; class ChatScreen extends StatefulWidget { @@ -448,119 +449,157 @@ class _ChatScreenState extends State { Widget _buildInputBar(MeshCoreConnector connector) { final maxBytes = maxContactMessageBytes(); - final colorScheme = Theme.of(context).colorScheme; + final scheme = Theme.of(context).colorScheme; final settings = context.watch().settings; return Container( - padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: colorScheme.surface, - border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), + color: scheme.surface, + border: Border(top: BorderSide(color: scheme.outlineVariant, width: 1)), ), child: SafeArea( - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.gif_box), - onPressed: () => _showGifPicker(context), - tooltip: context.l10n.chat_sendGif, - ), - if (settings.translationEnabled) - MessageTranslationButton( - enabled: settings.composerTranslationEnabled, - languageCode: settings.translationTargetLanguageCode, - onPressed: _showTranslationOptions, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.gif_box), + onPressed: () => _showGifPicker(context), + tooltip: context.l10n.chat_sendGif, ), - Expanded( - child: ValueListenableBuilder( - valueListenable: _textController, - builder: (context, value, child) { - final gifId = GifHelper.parseGif(value.text); - if (gifId != null) { - return Focus( - autofocus: true, - onKeyEvent: (node, event) { - if (event is KeyDownEvent && - (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == - LogicalKeyboardKey.numpadEnter)) { - _sendMessage(connector); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - colorScheme.surfaceContainerHighest, - fallbackTextColor: colorScheme.onSurface - .withValues(alpha: 0.6), - maxSize: 160, + if (settings.translationEnabled) + MessageTranslationButton( + enabled: settings.composerTranslationEnabled, + languageCode: settings.translationTargetLanguageCode, + onPressed: _showTranslationOptions, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, child) { + final gifId = GifHelper.parseGif(value.text); + if (gifId != null) { + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { + _sendMessage(connector); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + scheme.surfaceContainerHighest, + fallbackTextColor: scheme.onSurface + .withValues(alpha: 0.6), + maxSize: 160, + ), ), ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), + ); + } + return ByteCountedTextField( + maxBytes: maxBytes, + controller: _textController, + focusNode: _textFieldFocusNode, + hintText: context.l10n.chat_typeMessage, + onSubmitted: (_) => _sendMessage(connector), + encoder: + (connector.isContactSmazEnabled( + widget.contact.publicKeyHex, + ) || + connector.isContactCyr2LatEnabled( + widget.contact.publicKeyHex, + )) + ? (text) => connector.prepareContactOutboundText( + widget.contact, + text, + ) + : null, + decoration: InputDecoration( + hintText: context.l10n.chat_typeMessage, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + borderSide: BorderSide(color: scheme.outlineVariant), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + borderSide: BorderSide(color: scheme.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + borderSide: BorderSide( + color: scheme.primary, + width: 1.5, ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _textController.clear(); - _textFieldFocusNode.requestFocus(); - }, - ), - ], + ), + filled: true, + fillColor: scheme.surfaceContainerLow, + contentPadding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), ), ); - } - return ByteCountedTextField( - maxBytes: maxBytes, - controller: _textController, - focusNode: _textFieldFocusNode, - hintText: context.l10n.chat_typeMessage, - onSubmitted: (_) => _sendMessage(connector), - encoder: - (connector.isContactSmazEnabled( - widget.contact.publicKeyHex, - ) || - connector.isContactCyr2LatEnabled( - widget.contact.publicKeyHex, - )) - ? (text) => connector.prepareContactOutboundText( - widget.contact, - text, - ) - : null, - decoration: InputDecoration( - hintText: context.l10n.chat_typeMessage, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + }, + ), + ), + const SizedBox(width: 6), + ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, _) { + final hasText = value.text.trim().isNotEmpty; + return AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeInOut, + child: IconButton.filled( + icon: const Icon(Icons.send, size: 20), + tooltip: context.l10n.chat_sendMessageTo( + _resolveContact(connector).name, ), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerLow, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 14, + style: IconButton.styleFrom( + backgroundColor: hasText + ? scheme.primary + : scheme.surfaceContainerHighest, + foregroundColor: hasText + ? scheme.onPrimary + : scheme.onSurfaceVariant, + minimumSize: const Size(40, 40), + shape: const CircleBorder(), ), + onPressed: hasText + ? () { + HapticFeedback.lightImpact(); + _sendMessage(connector); + } + : null, ), ); }, ), - ), - const SizedBox(width: 8), - IconButton.filled( - icon: const Icon(Icons.send), - tooltip: context.l10n.chat_sendMessageTo( - _resolveContact(connector).name, - ), - onPressed: () => _sendMessage(connector), - ), - ], + ], + ), ), ), ); @@ -1043,12 +1082,17 @@ class _ChatScreenState extends State { ) && (message.translatedText?.trim().isEmpty ?? true); - showModalBottomSheet( - context: context, + showMeshSheet( + context, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ + BottomSheetHeader( + title: message.text.length > 40 + ? '${message.text.substring(0, 40)}…' + : message.text, + ), // Can't react to your own messages if (!message.isOutgoing) ListTile( @@ -1118,7 +1162,7 @@ class _ChatScreenState extends State { _openChat(context, contact); }, ), - const Divider(), + const Divider(height: 1), ListTile( leading: Icon( Icons.delete_outline, @@ -1133,6 +1177,7 @@ class _ChatScreenState extends State { await _deleteMessage(message); }, ), + const SizedBox(height: 8), ], ), ), @@ -1221,20 +1266,45 @@ class _MessageBubble extends StatelessWidget { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; - final colorScheme = Theme.of(context).colorScheme; + final scheme = Theme.of(context).colorScheme; final gifId = GifHelper.parseGif(message.text); final poi = parseMarkerText(message.text); final isFailed = message.status == MessageStatus.failed; + + // Bubble colors — outgoing uses MeshPalette.me / meBorder / meInk. final bubbleColor = isFailed - ? colorScheme.errorContainer - : (isOutgoing - ? colorScheme.primary - : colorScheme.surfaceContainerHighest); + ? scheme.errorContainer + : isOutgoing + ? MeshPalette.me + : scheme.surfaceContainerLow; + final bubbleBorder = isFailed + ? scheme.error + : isOutgoing + ? MeshPalette.meBorder + : scheme.outlineVariant; final textColor = isFailed - ? colorScheme.onErrorContainer - : (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface); - final metaColor = textColor.withValues(alpha: 0.7); + ? scheme.onErrorContainer + : isOutgoing + ? MeshPalette.meInk + : scheme.onSurface; + final metaColor = textColor.withValues(alpha: 0.65); const bodyFontSize = 14.0; + + // Asymmetric radius: outgoing — top-left large, others also large; outgoing bottom-right tight. + final borderRadius = isOutgoing + ? const BorderRadius.only( + topLeft: Radius.circular(MeshRadii.lg), + topRight: Radius.circular(MeshRadii.lg), + bottomLeft: Radius.circular(MeshRadii.lg), + bottomRight: Radius.circular(MeshRadii.xs), + ) + : const BorderRadius.only( + topLeft: Radius.circular(MeshRadii.xs), + topRight: Radius.circular(MeshRadii.lg), + bottomLeft: Radius.circular(MeshRadii.lg), + bottomRight: Radius.circular(MeshRadii.lg), + ); + // Do not strip room-server author bytes here: the parser stores them in // fourByteRoomContactKey, so message.text is safe to render as-is. final messageText = message.text; @@ -1246,8 +1316,9 @@ class _MessageBubble extends StatelessWidget { final originalDisplayText = isOutgoing ? message.originalText : (translatedDisplayText != messageText ? messageText : null); + return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(vertical: 3), child: Column( crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end @@ -1263,11 +1334,11 @@ class _MessageBubble extends StatelessWidget { mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, children: [ if (!isOutgoing) ...[ - _buildAvatar(senderName, colorScheme), - const SizedBox(width: 8), + _buildAvatar(senderName), + const SizedBox(width: 6), ], Flexible( child: Container( @@ -1278,14 +1349,12 @@ class _MessageBubble extends StatelessWidget { vertical: 8, ), constraints: BoxConstraints( - maxWidth: constraints.maxWidth * 0.65, + maxWidth: constraints.maxWidth * 0.72, ), decoration: BoxDecoration( color: bubbleColor, - borderRadius: BorderRadius.circular(16), - border: isFailed - ? Border.all(color: colorScheme.error) - : null, + borderRadius: borderRadius, + border: Border.all(color: bubbleBorder, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1301,14 +1370,14 @@ class _MessageBubble extends StatelessWidget { : EdgeInsets.zero, child: Text( senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.primary, + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w700, + color: _colorForName(senderName), ), ), ), - if (gifId == null) const SizedBox(height: 4), + if (gifId == null) const SizedBox(height: 2), ], if (poi != null) _buildPoiMessage( @@ -1349,7 +1418,7 @@ class _MessageBubble extends StatelessWidget { fontSize: bodyFontSize * textScale, ), originalStyle: TextStyle( - color: textColor.withValues(alpha: 0.78), + color: textColor.withValues(alpha: 0.72), fontSize: bodyFontSize * textScale, ), ), @@ -1359,7 +1428,7 @@ class _MessageBubble extends StatelessWidget { if (enableTracing && isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), + const SizedBox(height: 3), Padding( padding: gifId != null ? const EdgeInsets.symmetric(horizontal: 8) @@ -1372,15 +1441,15 @@ class _MessageBubble extends StatelessWidget { .settings .maxMessageRetries, ), - style: TextStyle( - fontSize: 10 * textScale, + style: MeshTheme.mono( + fontSize: 9.5 * textScale, color: metaColor, - fontWeight: FontWeight.w500, ), ), ), ], - const SizedBox(height: 4), + const SizedBox(height: 3), + // Meta row: timestamp + status icon + optional tracing Padding( padding: gifId != null ? const EdgeInsets.only( @@ -1395,13 +1464,13 @@ class _MessageBubble extends StatelessWidget { children: [ Text( _formatTime(message.timestamp), - style: TextStyle( + style: MeshTheme.mono( fontSize: 10 * textScale, color: metaColor, ), ), if (isOutgoing) ...[ - const SizedBox(width: 4), + const SizedBox(width: 2), MessageStatusIcon( size: 12 * textScale, onColor: metaColor, @@ -1418,25 +1487,21 @@ class _MessageBubble extends StatelessWidget { message.tripTimeMs != null && message.status == MessageStatus.delivered) ...[ - const SizedBox(width: 4), + const SizedBox(width: 2), Icon( Icons.speed, size: 10 * textScale, color: isOutgoing ? metaColor - : Theme.of( - context, - ).colorScheme.tertiary, + : scheme.tertiary, ), Text( '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( + style: MeshTheme.mono( fontSize: 9 * textScale, color: isOutgoing ? metaColor - : Theme.of( - context, - ).colorScheme.tertiary, + : scheme.tertiary, ), ), ], @@ -1454,8 +1519,8 @@ class _MessageBubble extends StatelessWidget { if (message.reactions.isNotEmpty) ...[ const SizedBox(height: 4), Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), - child: _buildReactionsDisplay(context, message, colorScheme), + padding: EdgeInsets.only(left: isOutgoing ? 0 : 42), + child: _buildReactionsDisplay(context, message, scheme), ), ], ], @@ -1532,7 +1597,7 @@ class _MessageBubble extends StatelessWidget { Widget _buildReactionsDisplay( BuildContext context, Message message, - ColorScheme colorScheme, + ColorScheme scheme, ) { return Wrap( spacing: 6, @@ -1555,28 +1620,33 @@ class _MessageBubble extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isFailed - ? colorScheme.errorContainer - : colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(12), + ? scheme.errorContainer + : scheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(MeshRadii.pill), border: Border.all( - color: isFailed - ? colorScheme.error - : colorScheme.outline.withValues(alpha: 0.3), + color: isFailed ? scheme.error : scheme.outlineVariant, width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(emoji, style: const TextStyle(fontSize: 16)), + Text( + emoji, + style: MeshTheme.emoji(fontSize: 16), + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + ), if (count > 1) ...[ const SizedBox(width: 4), Text( '$count', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.onSecondaryContainer, + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w700, + color: scheme.onSurface, ), ), ], @@ -1587,13 +1657,13 @@ class _MessageBubble extends StatelessWidget { height: 8, child: CircularProgressIndicator( strokeWidth: 1.5, - color: colorScheme.onSecondaryContainer, + color: scheme.primary, ), ), ], if (isFailed) ...[ const SizedBox(width: 2), - Icon(Icons.replay, size: 10, color: colorScheme.error), + Icon(Icons.replay, size: 10, color: scheme.error), ], ], ), @@ -1604,22 +1674,8 @@ class _MessageBubble extends StatelessWidget { ); } - Widget _buildAvatar(String senderName, ColorScheme colorScheme) { - final initial = firstCharacterOrEmoji(senderName); - final color = colorForName(senderName); - - return CircleAvatar( - radius: 18, - backgroundColor: color.withValues(alpha: 0.2), - child: Text( - initial, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: color, - ), - ), - ); + Widget _buildAvatar(String senderName) { + return AvatarCircle(name: senderName, size: 32); } String _formatTime(DateTime time) { @@ -1628,3 +1684,20 @@ class _MessageBubble extends StatelessWidget { return '$hour:$minute'; } } + +/// Deterministic name-to-hue mapping consistent with [AvatarCircle]. +Color _colorForName(String name) { + const hues = [ + MeshPalette.blue, + MeshPalette.magenta, + MeshPalette.signal, + MeshPalette.warn, + Color(0xFF8FA8F0), + Color(0xFF6FD9CE), + ]; + var h = 0; + for (final c in name.codeUnits) { + h = (h * 31 + c) & 0x7fffffff; + } + return hues[h % hues.length]; +} diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart index cdf3c938..4507b10f 100644 --- a/lib/screens/chrome_required_screen.dart +++ b/lib/screens/chrome_required_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; class ChromeRequiredScreen extends StatelessWidget { const ChromeRequiredScreen({super.key}); @@ -7,78 +9,95 @@ class ChromeRequiredScreen extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; + final scheme = Theme.of(context).colorScheme; + return Scaffold( - body: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 32), - color: colorScheme.surface, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: Icon( - Icons.browser_not_supported_rounded, - size: 80, - color: colorScheme.tertiary, - ), - ), - const SizedBox(height: 32), - Text( - l10n.scanner_chromeRequired, - textAlign: TextAlign.center, - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 16), - Text( - l10n.scanner_chromeRequiredMessage, - textAlign: TextAlign.center, - style: theme.textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - height: 1.5, - ), - ), - const SizedBox(height: 48), - // We can't really "fix" it for them other than telling them to use Chrome - // but we can provide a nice visual. - Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: colorScheme.outline.withValues(alpha: 0.4), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.info_outline, - size: 20, - color: colorScheme.secondary, - ), - const SizedBox(width: 12), - Text( - l10n.chrome_bluetoothRequiresChromium, - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w500, + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Icon in tinted circle + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: scheme.tertiary.withValues(alpha: 0.10), + border: Border.all( + color: scheme.tertiary.withValues(alpha: 0.25), + width: 1.5, ), ), - ], - ), + child: Icon( + Icons.browser_not_supported_rounded, + size: 42, + color: scheme.tertiary, + ), + ), + const SizedBox(height: 28), + + // Title + Text( + l10n.scanner_chromeRequired, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + color: scheme.onSurface, + letterSpacing: -0.3, + ), + ), + const SizedBox(height: 12), + + // Body text + Text( + l10n.scanner_chromeRequiredMessage, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: scheme.onSurfaceVariant, + height: 1.55, + ), + ), + const SizedBox(height: 32), + + // Info chip + MeshCard( + margin: EdgeInsets.zero, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + color: scheme.secondaryContainer.withValues(alpha: 0.35), + borderColor: scheme.outline.withValues(alpha: 0.3), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.info_outline, + size: 18, + color: scheme.secondary, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + l10n.chrome_bluetoothRequiresChromium, + style: MeshTheme.mono( + fontSize: 12, + color: scheme.onSecondaryContainer, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ); diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index 6b71715f..c7941900 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -1,14 +1,18 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; import '../connector/meshcore_connector.dart'; +import '../helpers/snack_bar_builder.dart'; import '../l10n/l10n.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/qr_scanner_widget.dart'; -import '../helpers/snack_bar_builder.dart'; /// Screen for scanning community QR codes to join communities. /// @@ -35,16 +39,90 @@ class _CommunityQrScannerScreenState extends State { centerTitle: true, ), body: _isProcessing - ? const Center(child: CircularProgressIndicator()) + ? Container( + color: Theme.of(context).colorScheme.surface, + child: const Center(child: CircularProgressIndicator()), + ) : QrScannerWidget( onScanned: (data) => _handleScannedData(context, data), validator: Community.isValidQrData, onValidationFailed: (_) => _showInvalidQrError(context), instructions: context.l10n.community_scanInstructions, + overlay: _buildThemedOverlay(context), ), ); } + Widget _buildThemedOverlay(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + // Dark semi-transparent background with cutout + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withValues(alpha: 0.5), + BlendMode.srcOut, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Container( + decoration: const BoxDecoration( + color: Colors.black, + backgroundBlendMode: BlendMode.dstOut, + ), + ), + Center( + child: Container( + height: 250, + width: 250, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ], + ), + ), + // Corner brackets on top + const ScannerCornerOverlay( + scanWindowSize: 250, + borderColor: MeshPalette.blue, + borderWidth: 2, + cornerLength: 24, + ), + // Instructions pill below the scan window + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 250 + 24), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.72), + borderRadius: BorderRadius.circular(MeshRadii.pill), + ), + child: Text( + context.l10n.community_scanInstructions, + style: const TextStyle( + color: MeshPalette.ink2, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ); + } + Future _handleScannedData(BuildContext context, String data) async { if (_isProcessing) return; @@ -80,7 +158,7 @@ class _CommunityQrScannerScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.community_invalidQrCode), - backgroundColor: Colors.red, + backgroundColor: MeshPalette.alert, ); } } finally { @@ -96,29 +174,74 @@ class _CommunityQrScannerScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.community_invalidQrCode), - backgroundColor: Colors.orange, + backgroundColor: MeshPalette.warn, duration: const Duration(seconds: 2), ); } void _showAlreadyMemberDialog(BuildContext context, Community community) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(context.l10n.community_alreadyMember), - content: Text( - context.l10n.community_alreadyMemberMessage(community.name), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(dialogContext); - Navigator.pop(context); - }, - child: Text(context.l10n.common_ok), + showMeshSheet( + context, + builder: (sheetContext) { + final sheetScheme = Theme.of(sheetContext).colorScheme; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BottomSheetHeader(title: context.l10n.community_alreadyMember), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 4), + child: Text( + context.l10n.community_alreadyMemberMessage(community.name), + style: TextStyle(color: sheetScheme.onSurfaceVariant), + ), + ), + MeshCard( + child: Row( + children: [ + const Icon( + Icons.groups, + color: MeshPalette.magenta, + size: 32, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + community.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + Text( + 'ID: ${community.shortCommunityId}...', + style: MeshTheme.mono( + fontSize: 11.5, + color: sheetScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: FilledButton( + onPressed: () { + Navigator.pop(sheetContext); + Navigator.pop(context); + }, + child: Text(context.l10n.common_ok), + ), ), ], - ), + ); + }, ); } @@ -127,38 +250,51 @@ class _CommunityQrScannerScreenState extends State { Community community, ) async { bool addPublicChannel = true; + final completer = Completer(); - final result = await showDialog( - context: context, - builder: (dialogContext) => StatefulBuilder( - builder: (dialogContext, setDialogState) => AlertDialog( - title: Text(context.l10n.community_joinTitle), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.community_joinConfirmation(community.name)), - const SizedBox(height: 16), - Row( + await showMeshSheet( + context, + builder: (sheetContext) => StatefulBuilder( + builder: (sheetContext, setSheetState) { + final joinScheme = Theme.of(sheetContext).colorScheme; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BottomSheetHeader(title: context.l10n.community_joinTitle), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 4), + child: Text( + context.l10n.community_joinConfirmation(community.name), + style: TextStyle(color: joinScheme.onSurfaceVariant), + ), + ), + MeshCard( + child: Row( children: [ - Icon( - Icons.groups, - color: Theme.of(dialogContext).colorScheme.primary, + AvatarCircle( + name: community.name, + icon: Icons.groups, + color: MeshPalette.magenta, + size: 44, ), - const SizedBox(width: 12), + const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( community.name, - style: const TextStyle(fontWeight: FontWeight.bold), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), ), Text( 'ID: ${community.shortCommunityId}...', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + style: MeshTheme.mono( + fontSize: 11.5, + color: joinScheme.onSurfaceVariant, ), ), ], @@ -166,38 +302,59 @@ class _CommunityQrScannerScreenState extends State { ), ], ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 8), - CheckboxListTile( - value: addPublicChannel, - onChanged: (value) { - setDialogState(() { - addPublicChannel = value ?? true; - }); - }, - title: Text(context.l10n.community_addPublicChannel), - subtitle: Text(context.l10n.community_addPublicChannelHint), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext, false), - child: Text(context.l10n.common_cancel), ), - FilledButton( - onPressed: () => Navigator.pop(dialogContext, true), - child: Text(context.l10n.community_join), + CheckboxListTile( + value: addPublicChannel, + onChanged: (value) { + setSheetState(() { + addPublicChannel = value ?? true; + }); + }, + title: Text(context.l10n.community_addPublicChannel), + subtitle: Text(context.l10n.community_addPublicChannelHint), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + completer.complete(false); + Navigator.pop(sheetContext); + }, + child: Text(context.l10n.common_cancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: () { + completer.complete(true); + Navigator.pop(sheetContext); + }, + child: Text(context.l10n.community_join), + ), + ), + ], + ), ), ], - ), + ); + }, ), ); - if (result == true && context.mounted) { + // If sheet was dismissed without a button press, treat as cancel + if (!completer.isCompleted) { + completer.complete(false); + } + + final result = await completer.future; + + if (result && context.mounted) { await _joinCommunity(context, community, addPublicChannel); } else if (context.mounted) { // User cancelled - go back @@ -231,7 +388,7 @@ class _CommunityQrScannerScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.community_joined(community.name)), - backgroundColor: Colors.green, + backgroundColor: MeshPalette.signal, ); // Return to previous screen diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index f666254c..a36a2a6c 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/models/companion_radio_stats.dart'; import 'package:meshcore_open/l10n/l10n.dart'; +import 'package:meshcore_open/theme/mesh_theme.dart'; +import 'package:meshcore_open/widgets/mesh_ui.dart'; import 'package:provider/provider.dart'; class CompanionRadioStatsScreen extends StatefulWidget { @@ -49,6 +51,25 @@ class _CompanionRadioStatsScreenState extends State { super.dispose(); } + Widget _tile(String text, IconData icon, Color color) { + final scheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 10), + Expanded( + child: Text( + text, + style: MeshTheme.mono(fontSize: 13, color: scheme.onSurface), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -85,44 +106,108 @@ class _CompanionRadioStatsScreenState extends State { valueListenable: connector.radioStatsNotifier, builder: (context, stats, _) { return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 8), children: [ if (stats != null) ...[ - Text( - l10n.radioStats_noiseFloor(stats.noiseFloorDbm), - style: tt.titleMedium, + const SectionHeader( + 'Signal', + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), ), - const SizedBox(height: 4), - Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)), - Text( - l10n.radioStats_lastSnr( - stats.lastSnrDb.toStringAsFixed(1), + MeshCard( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _tile( + l10n.radioStats_noiseFloor(stats.noiseFloorDbm), + Icons.noise_aware, + scheme.onSurfaceVariant, + ), + const Divider(height: 1), + _tile( + l10n.radioStats_lastRssi(stats.lastRssiDbm), + Icons.wifi_tethering, + scheme.onSurfaceVariant, + ), + const Divider(height: 1), + _tile( + l10n.radioStats_lastSnr( + stats.lastSnrDb.toStringAsFixed(1), + ), + Icons.signal_cellular_alt, + MeshTheme.snrColor( + stats.lastSnrDb, + blocked: false, + ), + ), + ], ), ), - Text(l10n.radioStats_txAir(stats.txAirSecs)), - Text(l10n.radioStats_rxAir(stats.rxAirSecs)), - const SizedBox(height: 16), - ] else - Text(l10n.radioStats_waiting), - const SizedBox(height: 16), - SizedBox( - height: 200, - child: CustomPaint( - painter: _NoiseChartPainter( - samples: List.from(_noiseHistory), - colorScheme: scheme, - textTheme: tt, + const SectionHeader( + 'Airtime', + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + ), + MeshCard( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _tile( + l10n.radioStats_txAir(stats.txAirSecs), + Icons.upload, + MeshPalette.blue, + ), + const Divider(height: 1), + _tile( + l10n.radioStats_rxAir(stats.rxAirSecs), + Icons.download, + MeshPalette.blue, + ), + ], + ), + ), + ] else ...[ + const SizedBox(height: 80), + Center( + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 8), + Center( + child: Text( + l10n.radioStats_waiting, + style: TextStyle(color: scheme.onSurfaceVariant), + ), + ), + ], + SectionHeader( + l10n.radioStats_chartCaption, + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 200, + child: CustomPaint( + painter: _NoiseChartPainter( + samples: List.from(_noiseHistory), + colorScheme: scheme, + textTheme: tt, + ), + child: const SizedBox.expand(), ), - child: const SizedBox.expand(), ), ), const SizedBox(height: 8), - Text( - l10n.radioStats_chartCaption, - style: tt.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - ), - ), ], ); }, diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 95cbb55d..69fb67e0 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -16,6 +16,7 @@ import '../models/contact.dart'; import '../l10n/contact_localization.dart'; import '../models/contact_group.dart'; import '../services/ui_view_state_service.dart'; +import '../theme/mesh_theme.dart'; import '../utils/contact_search.dart'; import '../storage/contact_group_store.dart'; import '../utils/dialog_utils.dart'; @@ -24,12 +25,12 @@ import '../utils/emoji_utils.dart'; import '../utils/route_transitions.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/unread_badge.dart'; -import '../helpers/contact_ui.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; @@ -472,12 +473,13 @@ class _ContactsScreenState extends State } void _showAddContactSheet(BuildContext context) { - showModalBottomSheet( - context: context, + showMeshSheet( + context, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ + BottomSheetHeader(title: context.l10n.contacts_title), ListTile( leading: const Icon(Icons.paste), title: Text(context.l10n.contacts_addContactFromClipboard), @@ -499,6 +501,7 @@ class _ContactsScreenState extends State ); }, ), + const SizedBox(height: 8), ], ), ), @@ -909,7 +912,8 @@ class _ContactsScreenState extends State final unreadCount = connector.getUnreadCountForContact( contact, ); - return _ContactTile( + return _ContactTileEntrance( + index: index, contact: contact, lastSeen: _resolveLastSeen(contact), unreadCount: unreadCount, @@ -1343,17 +1347,22 @@ class _ContactsScreenState extends State final isRoom = contact.type == advTypeRoom; final isFavorite = contact.isFavorite; - showModalBottomSheet( - context: context, + showMeshSheet( + context, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ + BottomSheetHeader( + title: contact.name, + subtitle: contact.typeLabel(context.l10n), + ), if (isRepeater) ...[ ListTile( - leading: const Icon(Icons.radar, color: Colors.green), + leading: Icon(Icons.radar, color: MeshPalette.signal), title: Text(context.l10n.contacts_ping), onTap: () { + Navigator.pop(sheetContext); final hw = context .read() .pathHashByteWidth; @@ -1371,7 +1380,7 @@ class _ContactsScreenState extends State }, ), ListTile( - leading: const Icon(Icons.cell_tower, color: Colors.orange), + leading: Icon(Icons.cell_tower, color: MeshPalette.warn), title: Text(context.l10n.contacts_manageRepeater), onTap: () { Navigator.pop(sheetContext); @@ -1380,9 +1389,10 @@ class _ContactsScreenState extends State ), ] else if (isRoom) ...[ ListTile( - leading: const Icon(Icons.radar, color: Colors.green), + leading: Icon(Icons.radar, color: MeshPalette.signal), title: Text(context.l10n.contacts_pathTrace), onTap: () { + Navigator.pop(sheetContext); final hw = context .read() .pathHashByteWidth; @@ -1405,7 +1415,7 @@ class _ContactsScreenState extends State }, ), ListTile( - leading: const Icon(Icons.room, color: Colors.blue), + leading: Icon(Icons.meeting_room, color: MeshPalette.blue), title: Text(context.l10n.contacts_roomLogin), onTap: () { Navigator.pop(sheetContext); @@ -1413,10 +1423,7 @@ class _ContactsScreenState extends State }, ), ListTile( - leading: const Icon( - Icons.room_preferences, - color: Colors.orange, - ), + leading: Icon(Icons.room_preferences, color: MeshPalette.warn), title: Text(context.l10n.room_management), onTap: () { Navigator.pop(sheetContext); @@ -1430,9 +1437,10 @@ class _ContactsScreenState extends State ] else ...[ if (contact.pathLength > 0) ListTile( - leading: const Icon(Icons.radar, color: Colors.green), + leading: Icon(Icons.radar, color: MeshPalette.signal), title: Text(context.l10n.contacts_chatTraceRoute), onTap: () { + Navigator.pop(sheetContext); final hw = context .read() .pathHashByteWidth; @@ -1456,7 +1464,7 @@ class _ContactsScreenState extends State ListTile( leading: Icon( isFavorite ? Icons.star : Icons.star_border, - color: Colors.amber[700], + color: MeshPalette.warn, ), title: Text( isFavorite @@ -1501,6 +1509,7 @@ class _ContactsScreenState extends State _confirmDelete(context, connector, contact); }, ), + const SizedBox(height: 8), ], ), ), @@ -1555,82 +1564,173 @@ class _ContactTile extends StatelessWidget { required this.onLongPress, }); - @override - Widget build(BuildContext context) { - return GestureDetector( - onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null, - child: ListTile( - leading: CircleAvatar( - backgroundColor: contactTypeColor(contact.type), - child: _buildContactAvatar(contact), - ), - title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), - subtitle: Text( - contact.pathLabel(context.l10n), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - // Clamp text scaling in trailing section to prevent overflow while - // maintaining accessibility. Primary content (title/subtitle) scales normally. - trailing: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear( - MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), - ), - ), - child: SizedBox( - width: 96, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(height: 4), - ], - Text( - _formatLastSeen(context, lastSeen), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isFavorite) - Icon(Icons.star, size: 14, color: Colors.amber[700]), - if (isFavorite && contact.hasLocation) - const SizedBox(width: 2), - if (contact.hasLocation) - Icon( - Icons.location_on, - size: 14, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), - ), - ], - ), - ], - ), - ), - ), - onTap: onTap, - onLongPress: onLongPress, - ), - ); + /// Node-type avatar color per design language. + Color _avatarColor() { + switch (contact.type) { + case advTypeRepeater: + return MeshPalette.warn; + case advTypeRoom: + return MeshPalette.magenta; + case advTypeSensor: + return const Color(0xFF4ACCC4); // teal + default: + return MeshPalette.blue; // chat — AvatarCircle handles deterministic hue + } } - Widget _buildContactAvatar(Contact contact) { - final emoji = firstEmoji(contact.name); - if (emoji != null) { - return Text(emoji, style: const TextStyle(fontSize: 18)); + /// Node-type avatar icon. Returns null for chat nodes so AvatarCircle shows initials. + IconData? _avatarIcon() { + switch (contact.type) { + case advTypeRepeater: + return Icons.cell_tower; + case advTypeRoom: + return Icons.meeting_room; + case advTypeSensor: + return Icons.sensors; + default: + return null; // chat uses initials } - return Icon(contactTypeIcon(contact.type), color: Colors.white, size: 20); + } + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final emoji = firstEmoji(contact.name); + final isChat = contact.type == advTypeChat; + final pathLen = contact.pathBytesForDisplay.length; + final isDirect = contact.pathLength >= 0; + final hasPath = pathLen > 0 || contact.pathLength == 0; + + return GestureDetector( + onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null, + child: MeshCard( + onTap: onTap, + onLongPress: onLongPress, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + children: [ + // Avatar + if (emoji != null) + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: scheme.surfaceContainerHigh, + border: Border.all(color: scheme.outlineVariant), + ), + alignment: Alignment.center, + child: Text(emoji, style: const TextStyle(fontSize: 20)), + ) + else + AvatarCircle( + name: contact.name, + size: 42, + color: isChat ? null : _avatarColor(), + icon: _avatarIcon(), + ), + const SizedBox(width: 12), + // Main content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Name row + route chip + Row( + children: [ + Expanded( + child: Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: unreadCount > 0 + ? FontWeight.w700 + : FontWeight.w500, + fontSize: 15, + color: scheme.onSurface, + ), + ), + ), + if (isFavorite) ...[ + const SizedBox(width: 4), + Icon(Icons.star, size: 13, color: MeshPalette.warn), + ], + if (contact.hasLocation) ...[ + const SizedBox(width: 4), + Icon( + Icons.location_on, + size: 13, + color: scheme.onSurfaceVariant.withValues(alpha: 0.55), + ), + ], + ], + ), + const SizedBox(height: 3), + // Path / subtitle row + Row( + children: [ + Expanded( + child: Text( + contact.pathLabel(context.l10n), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: scheme.onSurfaceVariant, + ), + ), + ), + if (hasPath) ...[ + const SizedBox(width: 6), + RouteChip( + isDirect: isDirect, + hops: isDirect ? contact.pathLength : null, + ), + ], + ], + ), + ], + ), + ), + const SizedBox(width: 10), + // Trailing: time + unread badge + // Clamp text scale to prevent overflow in trailing section. + MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(height: 4), + ], + Text( + _formatLastSeen(context, lastSeen), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: MeshTheme.mono( + fontSize: 11, + color: unreadCount > 0 + ? MeshPalette.blue + : scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ); } String _formatLastSeen(BuildContext context, DateTime lastSeen) { @@ -1655,3 +1755,39 @@ class _ContactTile extends StatelessWidget { : context.l10n.contacts_lastSeenDaysAgo(days); } } + +// Wrap each contact tile with staggered entrance. +class _ContactTileEntrance extends StatelessWidget { + final int index; + final Contact contact; + final DateTime lastSeen; + final int unreadCount; + final bool isFavorite; + final VoidCallback onTap; + final VoidCallback onLongPress; + + const _ContactTileEntrance({ + required this.index, + required this.contact, + required this.lastSeen, + required this.unreadCount, + required this.isFavorite, + required this.onTap, + required this.onLongPress, + }); + + @override + Widget build(BuildContext context) { + return ListEntrance( + index: index, + child: _ContactTile( + contact: contact, + lastSeen: lastSeen, + unreadCount: unreadCount, + isFavorite: isFavorite, + onTap: onTap, + onLongPress: onLongPress, + ), + ); + } +} diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 1bc392bf..8fb4cf95 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -7,12 +7,14 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; +import '../l10n/contact_localization.dart'; import '../models/contact.dart'; +import '../theme/mesh_theme.dart'; import '../utils/contact_search.dart'; import '../utils/platform_info.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; -import '../helpers/contact_ui.dart'; +import '../widgets/mesh_ui.dart'; import '../helpers/snack_bar_builder.dart'; enum DiscoverySortOption { lastSeen, name, type } @@ -47,6 +49,34 @@ class _DiscoveryScreenState extends State { : contact.lastSeen; } + /// Node-type avatar color per design language. + Color _avatarColor(int type) { + switch (type) { + case advTypeRepeater: + return MeshPalette.warn; + case advTypeRoom: + return MeshPalette.magenta; + case advTypeSensor: + return const Color(0xFF4ACCC4); // teal + default: + return MeshPalette.blue; + } + } + + /// Node-type avatar icon; null = show initials for chat nodes. + IconData? _avatarIcon(int type) { + switch (type) { + case advTypeRepeater: + return Icons.cell_tower; + case advTypeRoom: + return Icons.meeting_room; + case advTypeSensor: + return Icons.sensors; + default: + return null; + } + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -93,121 +123,167 @@ class _DiscoveryScreenState extends State { children: [ _buildFilters(filteredAndSorted, connector), Expanded( - child: discoveredContacts.isEmpty - ? Center(child: Text(l10n.contacts_noContacts)) - : filteredAndSorted.isEmpty - ? Center(child: Text(l10n.discoveredContacts_noMatching)) - : ListView.builder( - itemCount: filteredAndSorted.length, - itemBuilder: (context, index) { - final contact = filteredAndSorted[index]; - final tile = ListTile( - leading: CircleAvatar( - backgroundColor: contactTypeColor(contact.type), - child: Icon( - contactTypeIcon(contact.type), - color: Colors.white, - size: 20, - ), - ), - title: Text( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 220), + child: discoveredContacts.isEmpty + ? Center( + key: const ValueKey('empty_all'), + child: Text(l10n.contacts_noContacts), + ) + : filteredAndSorted.isEmpty + ? Center( + key: const ValueKey('empty_filtered'), + child: Text(l10n.discoveredContacts_noMatching), + ) + : ListView.builder( + key: const ValueKey('list'), + padding: const EdgeInsets.only(bottom: 24), + itemCount: filteredAndSorted.length, + itemBuilder: (context, index) { + final contact = filteredAndSorted[index]; + final tile = _buildDiscoveryTile( + context, + contact, + connector, + index, + ); + if (PlatformInfo.isDesktop) { + return GestureDetector( + onSecondaryTapUp: (_) => + _showContactContextMenu(contact, connector), + child: tile, + ); + } + return tile; + }, + ), + ), + ), + ], + ), + ); + } + + Widget _buildDiscoveryTile( + BuildContext context, + Contact contact, + MeshCoreConnector connector, + int index, + ) { + final scheme = Theme.of(context).colorScheme; + final isChat = contact.type == advTypeChat; + + return ListEntrance( + index: index, + child: MeshCard( + onTap: () { + connector.importDiscoveredContact(contact); + showDismissibleSnackBar( + context, + content: Text( + context.l10n.discoveredContacts_contactAdded, + ), + action: SnackBarAction( + label: context.l10n.common_undo, + onPressed: () => connector.removeContact(contact), + ), + ); + }, + onLongPress: () => _showContactContextMenu(contact, connector), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + children: [ + AvatarCircle( + name: contact.name, + size: 42, + color: isChat ? null : _avatarColor(contact.type), + icon: _avatarIcon(contact.type), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Name + type chip + Row( + children: [ + Expanded( + child: Text( contact.name, maxLines: 1, overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), ), - subtitle: Text( + ), + const SizedBox(width: 6), + StatusChip( + label: contact.typeLabel(context.l10n).toUpperCase(), + color: _avatarColor(contact.type), + icon: _avatarIcon(contact.type), + ), + ], + ), + const SizedBox(height: 3), + // Short pub key + Row( + children: [ + Expanded( + child: Text( contact.shortPubKeyHex, maxLines: 1, overflow: TextOverflow.ellipsis, - ), - // Clamp text scaling in trailing section to prevent overflow while - // maintaining accessibility. Primary content (title/subtitle) scales normally. - trailing: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear( - MediaQuery.textScalerOf( - context, - ).scale(1.0).clamp(1.0, 1.3), - ), - ), - child: SizedBox( - width: 120, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - _formatLastSeen( - context, - _resolveLastSeen(contact), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 12, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (contact.hasLocation) - Icon( - Icons.location_on, - size: 14, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant - .withValues(alpha: 0.6), - ), - if (contact.rawPacket != null) - const SizedBox(width: 2), - if (contact.rawPacket != null) - Icon( - Icons.cell_tower, - size: 14, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant - .withValues(alpha: 0.6), - ), - ], - ), - ], - ), + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, ), ), - onTap: () { - connector.importDiscoveredContact(contact); - showDismissibleSnackBar( - context, - content: Text( - context.l10n.discoveredContacts_contactAdded, - ), - action: SnackBarAction( - label: context.l10n.common_undo, - onPressed: () => connector.removeContact(contact), - ), - ); - }, - onLongPress: () => - _showContactContextMenu(contact, connector), - ); - if (PlatformInfo.isDesktop) { - return GestureDetector( - onSecondaryTapUp: (_) => - _showContactContextMenu(contact, connector), - child: tile, - ); - } - return tile; - }, + ), + if (contact.hasLocation) ...[ + const SizedBox(width: 6), + Icon( + Icons.location_on, + size: 13, + color: scheme.onSurfaceVariant.withValues(alpha: 0.55), + ), + ], + if (contact.rawPacket != null) ...[ + const SizedBox(width: 4), + Icon( + Icons.cell_tower, + size: 13, + color: scheme.onSurfaceVariant.withValues(alpha: 0.55), + ), + ], + ], ), - ), - ], + ], + ), + ), + const SizedBox(width: 10), + // Last seen time + MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: Text( + _formatLastSeen(context, _resolveLastSeen(contact)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + ), + ], + ), ), ); } @@ -216,15 +292,18 @@ class _DiscoveryScreenState extends State { Contact contact, MeshCoreConnector connector, ) async { - final action = await showModalBottomSheet( - context: context, - showDragHandle: true, + final action = await showMeshSheet( + context, builder: (sheetContext) { final l10n = context.l10n; return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ + BottomSheetHeader( + title: contact.name, + subtitle: contact.typeLabel(l10n), + ), ListTile( leading: const Icon(Icons.copy), title: Text(l10n.discoveredContacts_copyContact), @@ -235,6 +314,7 @@ class _DiscoveryScreenState extends State { title: Text(l10n.discoveredContacts_deleteContact), onTap: () => Navigator.of(sheetContext).pop('delete_contact'), ), + const SizedBox(height: 8), ], ), ); diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index af80ca22..57d7603f 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -19,6 +19,8 @@ import '../connector/meshcore_connector.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; import '../icons/los_icon.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/themed_map_tile_layer.dart'; class LineOfSightEndpoint { final String label; @@ -30,7 +32,7 @@ class LineOfSightEndpoint { const LineOfSightEndpoint({ required this.label, required this.point, - this.color = Colors.green, + this.color = LosPalette.clear, this.icon = Icons.location_on, this.isCustom = false, }); @@ -59,9 +61,12 @@ class _LineOfSightMapScreenState extends State { static const double _labelZoomThreshold = 8.5; static const double _mapMinZoom = 2.0; static const double _mapMaxZoom = 18.0; + static const double _marginalClearanceMeters = 5.0; final LineOfSightService _lineOfSightService = LineOfSightService(); final MapController _mapController = MapController(); + final DraggableScrollableController _panelController = + DraggableScrollableController(); bool _loading = false; String? _error; @@ -79,6 +84,7 @@ class _LineOfSightMapScreenState extends State { bool _didReceivePositionUpdate = false; int _losRequestNonce = 0; bool _initialLosScheduled = false; + bool _showTerrainLayer = true; @override void initState() { @@ -104,6 +110,7 @@ class _LineOfSightMapScreenState extends State { @override void dispose() { _mapController.dispose(); + _panelController.dispose(); _lineOfSightService.dispose(); super.dispose(); } @@ -140,48 +147,6 @@ class _LineOfSightMapScreenState extends State { _mapController.move(initialCenter, initialZoom); } - Widget _buildDesktopMapControls({ - required LatLng initialCenter, - required double initialZoom, - required LatLngBounds? bounds, - }) { - final screenHeight = MediaQuery.of(context).size.height; - final topOffset = _showHud - ? math.min(screenHeight * 0.52 + 24, screenHeight - 220) - : 12.0; - return Positioned( - top: topOffset, - left: 12, - child: Card( - elevation: 4, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.add), - tooltip: context.l10n.map_zoomIn, - onPressed: () => _zoomMapBy(1), - ), - IconButton( - icon: const Icon(Icons.remove), - tooltip: context.l10n.map_zoomOut, - onPressed: () => _zoomMapBy(-1), - ), - IconButton( - icon: const Icon(Icons.my_location), - tooltip: context.l10n.map_centerMap, - onPressed: () => _resetMapView( - initialCenter: initialCenter, - initialZoom: initialZoom, - bounds: bounds, - ), - ), - ], - ), - ), - ); - } - Future _runLos() async { final start = _start; final end = _end; @@ -224,7 +189,7 @@ class _LineOfSightMapScreenState extends State { setState(() { _result = result; _selectedObstruction = _defaultObstructionFor(result); - _menuExpanded = true; + _menuExpanded = false; }); } catch (e) { if (!mounted) return; @@ -288,7 +253,7 @@ class _LineOfSightMapScreenState extends State { final endpoint = LineOfSightEndpoint( label: context.l10n.losCustomPointLabel(_customEndpoints.length + 1), point: point, - color: Colors.orange, + color: LosPalette.marginal, icon: Icons.push_pin, isCustom: true, ); @@ -415,16 +380,23 @@ class _LineOfSightMapScreenState extends State { title: AppBarTitle(widget.title), centerTitle: true, actions: [ - IconButton( - icon: _loading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.delete_outline), - onPressed: _loading ? null : _clearAllPoints, - tooltip: context.l10n.losClearAllPoints, + PopupMenuButton( + onSelected: (value) { + if (value == 'clear') _clearAllPoints(); + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'clear', + enabled: !_loading, + child: Row( + children: [ + const Icon(Icons.delete_outline), + const SizedBox(width: 10), + Text(context.l10n.losClearAllPoints), + ], + ), + ), + ], ), ], ), @@ -470,11 +442,9 @@ class _LineOfSightMapScreenState extends State { }, ), children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: MapTileCacheService.userAgentPackageName, - maxZoom: 19, + ThemedMapTileLayer( + tileCache: tileCache, + opacity: _showTerrainLayer ? 1 : 0.72, ), if (_result != null && _result!.segments.isNotEmpty) PolylineLayer(polylines: _buildSegmentPolylines(_result!)), @@ -483,70 +453,38 @@ class _LineOfSightMapScreenState extends State { ), ], ), - if (isDesktop) - _buildDesktopMapControls( - initialCenter: initialCenter, - initialZoom: initialZoom, - bounds: bounds, - ), + _buildLinkBanner(isImperial), + _buildMapControlRail( + initialCenter: initialCenter, + initialZoom: initialZoom, + bounds: bounds, + isImperial: isImperial, + ), if (_showHud) - Positioned( - left: 12, - right: 12, - top: 12, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.52, - ), - child: _buildControlPanel(isImperial), - ), - ), - if (!_showHud && _result != null && _result!.segments.isNotEmpty) - Positioned( - left: 12, - bottom: 12, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(8), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - child: Text( - context.l10n.losElevationAttribution, - style: TextStyle( - fontSize: 10, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), + DraggableScrollableSheet( + controller: _panelController, + initialChildSize: 0.43, + minChildSize: 0.14, + maxChildSize: 0.88, + snap: true, + snapSizes: const [0.14, 0.43, 0.88], + builder: (context, scrollController) => Theme( + data: MeshTheme.dark(), + child: _buildControlPanel(isImperial, scrollController), ), ), if (_loading) - const Positioned( + Positioned( left: 0, right: 0, top: 0, - child: LinearProgressIndicator(), + child: LinearProgressIndicator( + color: LosPalette.selected, + backgroundColor: LosPalette.chartBackground, + ), ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - _showHud = !_showHud; - }); - }, - tooltip: _showHud - ? context.l10n.losHidePanelTooltip - : context.l10n.losShowPanelTooltip, - child: Icon(_showHud ? Icons.visibility_off : Icons.tune), - ), bottomNavigationBar: SafeArea( top: false, child: QuickSwitchBar( @@ -558,12 +496,477 @@ class _LineOfSightMapScreenState extends State { channelsUnreadCount: context .watch() .getTotalChannelsUnreadCount(), + highContrast: true, ), ), ); } - Widget _buildControlPanel(bool isImperial) { + Widget _buildLinkBanner(bool isImperial) { + final connector = context.watch(); + final segment = _primarySegmentResult(); + final status = _losStatusFor(segment); + final battery = connector.batteryPercent; + final snr = connector.latestRadioStats?.lastSnrDb; + return Positioned( + top: 10, + left: 12, + right: 12, + child: IgnorePointer( + ignoring: false, + child: Material( + color: LosPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.md), + shadowColor: LosPalette.shadow, + elevation: 4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: _statusColorFor(status).withValues(alpha: 0.18), + shape: BoxShape.circle, + border: Border.all(color: _statusColorFor(status)), + ), + child: Icon( + _statusIcon(status), + color: _statusColorFor(status), + size: 20, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${_start?.label ?? 'A'} → ${_end?.label ?? 'B'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: LosPalette.text, + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + segment == null + ? _statusText() + : '${_statusLabel(status)} • ' + '${_formatDistanceValue(segment.totalDistanceMeters, isImperial)} ' + '${isImperial ? 'mi' : 'km'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: LosPalette.textMuted, + fontSize: 11.5, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + _headerMetric( + Icons.battery_5_bar, + battery == null ? '--' : '$battery%', + ), + const SizedBox(width: 10), + _headerMetric( + Icons.network_cell, + snr == null ? '--' : '${snr.toStringAsFixed(1)} dB', + ), + ], + ), + ), + ), + ), + ); + } + + Widget _headerMetric(IconData icon, String value) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: LosPalette.textMuted, size: 16), + const SizedBox(height: 2), + Text( + value, + style: MeshTheme.mono( + color: LosPalette.text, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ], + ); + } + + Widget _buildMapControlRail({ + required LatLng initialCenter, + required double initialZoom, + required LatLngBounds? bounds, + required bool isImperial, + }) { + return Positioned( + right: 12, + top: 92, + child: Material( + color: LosPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.md), + clipBehavior: Clip.antiAlias, + elevation: 4, + shadowColor: LosPalette.shadow, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + color: LosPalette.text, + icon: const Icon(Icons.add), + tooltip: context.l10n.map_zoomIn, + onPressed: () => _zoomMapBy(1), + ), + IconButton( + color: LosPalette.text, + icon: const Icon(Icons.remove), + tooltip: context.l10n.map_zoomOut, + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + color: LosPalette.text, + icon: const Icon(Icons.center_focus_strong), + tooltip: context.l10n.map_centerMap, + onPressed: () => _resetMapView( + initialCenter: initialCenter, + initialZoom: initialZoom, + bounds: bounds, + ), + ), + IconButton( + color: _showTerrainLayer + ? LosPalette.selected + : LosPalette.textMuted, + icon: const Icon(Icons.layers_outlined), + tooltip: 'Map detail', + onPressed: () => + setState(() => _showTerrainLayer = !_showTerrainLayer), + ), + IconButton( + color: LosPalette.text, + icon: Text( + isImperial ? 'ft' : 'm', + style: const TextStyle( + color: LosPalette.text, + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + tooltip: 'Units', + onPressed: () => context.read().setUnitSystem( + isImperial ? UnitSystem.metric : UnitSystem.imperial, + ), + ), + IconButton( + color: _showHud ? LosPalette.selected : LosPalette.text, + icon: Icon( + _showHud ? Icons.keyboard_arrow_down : Icons.analytics_outlined, + ), + tooltip: _showHud + ? context.l10n.losHidePanelTooltip + : context.l10n.losShowPanelTooltip, + onPressed: () => setState(() => _showHud = !_showHud), + ), + ], + ), + ), + ); + } + + Widget _buildResultSummary(LineOfSightResult? segment, bool isImperial) { + final status = _losStatusFor(segment); + final color = _statusColorFor(status); + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + final worst = _defaultObstructionFor(_result); + final minClearance = segment == null || segment.samples.isEmpty + ? null + : segment.samples + .map((sample) => sample.clearanceMeters) + .reduce(math.min); + final amount = segment == null + ? '--' + : segment.isClear + ? '${_formatHeightValue(minClearance ?? 0, isImperial)} $heightUnit' + : '${_formatHeightValue(segment.maxObstructionMeters, isImperial)} $heightUnit'; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: color.withValues(alpha: 0.8)), + ), + child: Column( + children: [ + Row( + children: [ + Icon(_statusIcon(status), color: color, size: 24), + const SizedBox(width: 9), + Expanded( + child: Text( + _statusLabel(status), + style: TextStyle( + color: color, + fontSize: 19, + fontWeight: FontWeight.w900, + ), + ), + ), + if (_loading) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + _summaryMetric( + 'Distance', + segment == null + ? '--' + : '${_formatDistanceValue(segment.totalDistanceMeters, isImperial)} $distanceUnit', + ), + _summaryMetric( + segment?.isClear == true ? 'Clearance' : 'Blocked by', + amount, + valueColor: color, + ), + _summaryMetric( + 'Obstruction', + worst == null + ? '--' + : '${_formatDistanceValue(worst.distanceMeters, isImperial)} $distanceUnit from A', + ), + ], + ), + ], + ), + ); + } + + Widget _summaryMetric(String label, String value, {Color? valueColor}) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label.toUpperCase(), + style: const TextStyle( + color: LosPalette.textMuted, + fontSize: 9, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 3), + Text( + value, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: MeshTheme.mono( + color: valueColor ?? LosPalette.text, + fontSize: 11.5, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ), + ); + } + + Widget _buildObstructionCard( + LineOfSightObstruction obstruction, + bool isImperial, { + required bool isWorst, + }) { + final selected = + _selectedObstruction?.sampleIndex == obstruction.sampleIndex; + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + return InkWell( + onTap: () => _centerOnObstruction(obstruction), + borderRadius: BorderRadius.circular(MeshRadii.sm), + child: Container( + width: 154, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: selected + ? LosPalette.selected.withValues(alpha: 0.18) + : LosPalette.chartBackground, + borderRadius: BorderRadius.circular(MeshRadii.sm), + border: Border.all( + color: selected ? LosPalette.selected : LosPalette.border, + width: selected ? 2 : 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.warning_amber_rounded, + color: LosPalette.blocked, + size: 17, + ), + const SizedBox(width: 5), + Expanded( + child: Text( + '${_formatDistanceValue(obstruction.distanceMeters, isImperial)} $distanceUnit', + style: const TextStyle( + color: LosPalette.text, + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + ), + if (isWorst) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: LosPalette.blocked, + borderRadius: BorderRadius.circular(99), + ), + child: const Text( + 'WORST', + style: TextStyle( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.w900, + ), + ), + ), + ], + ), + const Spacer(), + Text( + 'Blocked ${_formatHeightValue(obstruction.obstructionMeters, isImperial)} $heightUnit', + style: const TextStyle( + color: LosPalette.textMuted, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + Widget _buildSelectedObstructionCard( + LineOfSightObstruction obstruction, + LineOfSightResult segment, + bool isImperial, + ) { + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: LosPalette.selected.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: LosPalette.selected), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Selected obstruction', + style: TextStyle( + color: LosPalette.text, + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 10), + Wrap( + spacing: 18, + runSpacing: 10, + children: [ + _detailValue( + 'Blocked by', + '${_formatHeightValue(obstruction.obstructionMeters, isImperial)} $heightUnit', + ), + _detailValue( + 'From A', + '${_formatDistanceValue(obstruction.distanceMeters, isImperial)} $distanceUnit', + ), + _detailValue( + 'From B', + '${_formatDistanceValue(segment.totalDistanceMeters - obstruction.distanceMeters, isImperial)} $distanceUnit', + ), + _detailValue( + 'Elevation', + '${_formatHeightValue(obstruction.terrainMeters, isImperial)} $heightUnit', + ), + ], + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + onPressed: () => _centerOnObstruction(obstruction), + icon: const Icon(Icons.center_focus_strong, size: 17), + label: const Text('Center on map'), + ), + ), + ], + ), + ); + } + + Widget _detailValue(String label, String value) { + return SizedBox( + width: 120, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label.toUpperCase(), + style: const TextStyle( + color: LosPalette.textMuted, + fontSize: 9, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: MeshTheme.mono( + color: LosPalette.text, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } + + Widget _buildControlPanel( + bool isImperial, + ScrollController scrollController, + ) { _sanitizeSelection(); final segment = _primarySegmentResult(); final connector = context.read(); @@ -583,363 +986,265 @@ class _LineOfSightMapScreenState extends State { final antennaBDisplay = _toDisplayHeight(antennaBMeters, isImperial); final antennaSliderMax = isImperial ? _maxAntennaFeet : _maxAntennaMeters; final antennaSliderDivisions = isImperial ? 400 : 122; - return Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + final worst = _defaultObstructionFor(_result); + return Material( + color: LosPalette.panelDark, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(MeshRadii.lg), + ), + clipBehavior: Clip.antiAlias, + child: ListView( + controller: scrollController, + padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), + children: [ + Center( + child: Container( + width: 44, + height: 5, + decoration: BoxDecoration( + color: LosPalette.textMuted.withValues(alpha: 0.55), + borderRadius: BorderRadius.circular(99), + ), + ), + ), + const SizedBox(height: 10), + _buildResultSummary(segment, isImperial), + if (segment != null) ...[ + const SizedBox(height: 14), + _buildProfileView(segment, distanceUnit, heightUnit, isImperial), + const SizedBox(height: 10), + _LosLegend( + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, + ), + ], + if (obstructions.isNotEmpty) ...[ + const SizedBox(height: 18), + Text( + context.l10n.losBlockedSpotsTitle, + style: const TextStyle( + color: LosPalette.text, + fontSize: 15, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.losBlockedSpotsHint, + style: const TextStyle(color: LosPalette.textMuted, fontSize: 12), + ), + const SizedBox(height: 10), + SizedBox( + height: 86, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: obstructions.length, + separatorBuilder: (_, _) => const SizedBox(width: 8), + itemBuilder: (context, index) { + final obstruction = obstructions[index]; + return _buildObstructionCard( + obstruction, + isImperial, + isWorst: obstruction.sampleIndex == worst?.sampleIndex, + ); + }, + ), + ), + ], + if (_selectedObstruction != null && segment != null) ...[ + const SizedBox(height: 14), + _buildSelectedObstructionCard( + _selectedObstruction!, + segment, + isImperial, + ), + ], + const SizedBox(height: 12), + ExpansionTile( + initiallyExpanded: _menuExpanded, + onExpansionChanged: (value) => + setState(() => _menuExpanded = value), + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + iconColor: LosPalette.text, + collapsedIconColor: LosPalette.textMuted, + title: Text( + context.l10n.losMenuTitle, + style: const TextStyle( + color: LosPalette.text, + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + subtitle: Text( + context.l10n.losMenuSubtitle, + style: const TextStyle(color: LosPalette.textMuted, fontSize: 11), + ), children: [ - if (segment != null) - _buildProfileView(segment, distanceUnit, heightUnit, isImperial) - else - SizedBox( - height: 44, - child: Center( - child: Text( - context.l10n.losRunToViewElevationProfile, - style: const TextStyle(fontSize: 11), - ), - ), + SwitchListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text( + context.l10n.losShowDisplayNodes, + style: const TextStyle(fontSize: 12), ), - if (segment != null) ...[ - const SizedBox(height: 8), - _LosLegend( - terrainLabel: context.l10n.losLegendTerrain, - losBeamLabel: context.l10n.losLegendLosBeam, - radioHorizonLabel: context.l10n.losLegendRadioHorizon, - ), - ], - const SizedBox(height: 8), - Text( - segment != null - ? _profileStats(segment, isImperial) - : _statusText(), - style: TextStyle( - fontSize: 12, - color: segment != null - ? (segment.isClear ? Colors.green : Colors.red) - : _statusColor(), - fontWeight: FontWeight.w600, - ), - ), - if (obstructions.isNotEmpty) ...[ - const SizedBox(height: 8), - Text( - context.l10n.losBlockedSpotsTitle, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - Text( - context.l10n.losBlockedSpotsHint, - style: TextStyle( - fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 6), - Wrap( - spacing: 6, - runSpacing: 6, - children: [ - for (final obstruction in obstructions) - ChoiceChip( - label: Text( - _obstructionChipLabel(obstruction, isImperial), - style: const TextStyle(fontSize: 11), - ), - selected: - _selectedObstruction?.sampleIndex == - obstruction.sampleIndex, - onSelected: (_) => _selectObstruction(obstruction), - ), - ], - ), - if (_selectedObstruction != null) ...[ - const SizedBox(height: 8), - DecoratedBox( - decoration: BoxDecoration( - color: Colors.orange.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Colors.deepOrangeAccent.withValues(alpha: 0.45), - ), - ), - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.losSelectedObstructionTitle, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - Text( - context.l10n.losSelectedObstructionDetails( - _formatHeightValue( - _selectedObstruction!.obstructionMeters, - isImperial, - ), - heightUnit, - _formatDistanceValue( - _selectedObstruction!.distanceMeters, - isImperial, - ), - distanceUnit, - _formatDistanceValue( - segment!.totalDistanceMeters - - _selectedObstruction!.distanceMeters, - isImperial, - ), - ), - style: const TextStyle(fontSize: 11), - ), - const SizedBox(height: 4), - Text( - '${_selectedObstruction!.point.latitude.toStringAsFixed(5)}, ' - '${_selectedObstruction!.point.longitude.toStringAsFixed(5)}', - style: TextStyle( - fontSize: 11, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ], - ], - const SizedBox(height: 4), - if (displayFrequencyMHz != null) - Padding( - padding: const EdgeInsets.only(top: 2, bottom: 4), - child: Row( - children: [ - Text( - context.l10n.losFrequencyLabel, - style: TextStyle( - fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 8), - Text( - '${displayFrequencyMHz.toStringAsFixed(3)} MHz', - style: TextStyle( - fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - if (kFactorUsed != null) ...[ - const SizedBox(width: 8), - Text( - 'k=${kFactorUsed.toStringAsFixed(3)}', - style: TextStyle( - fontSize: 11, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: 4), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: const Icon(Icons.info_outline, size: 16), - color: Theme.of(context).colorScheme.onSurfaceVariant, - tooltip: context.l10n.losFrequencyInfoTooltip, - onPressed: () { - _showFrequencyInfoDialog( - context, - displayFrequencyMHz, - kFactorUsed, - ); - }, - ), - ], - ], - ), - ), - Text( - context.l10n.losElevationAttribution, - style: TextStyle( - fontSize: 10, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 6), - ExpansionTile( - initiallyExpanded: _menuExpanded, - onExpansionChanged: (value) { + value: _showDisplayNodes, + onChanged: (value) { setState(() { - _menuExpanded = value; + _showDisplayNodes = value; + _sanitizeSelection(); + _result = null; + _selectedObstruction = null; }); }, - tilePadding: EdgeInsets.zero, - childrenPadding: EdgeInsets.zero, - title: Text( - context.l10n.losMenuTitle, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - ), + ), + if (_customEndpoints.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + context.l10n.losCustomPoints, + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), - subtitle: Text( - context.l10n.losMenuSubtitle, - style: const TextStyle(fontSize: 11), - ), - children: [ - SwitchListTile( + for (final point in _customEndpoints) + ListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text( - context.l10n.losShowDisplayNodes, + point.label, style: const TextStyle(fontSize: 12), ), - value: _showDisplayNodes, - onChanged: (value) { - setState(() { - _showDisplayNodes = value; - _sanitizeSelection(); - _result = null; - _selectedObstruction = null; - }); - }, - ), - if (_customEndpoints.isNotEmpty) ...[ - const SizedBox(height: 6), - Text( - context.l10n.losCustomPoints, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - ), + subtitle: Text( + '${point.point.latitude.toStringAsFixed(5)}, ${point.point.longitude.toStringAsFixed(5)}', + style: const TextStyle(fontSize: 11), ), - for (final point in _customEndpoints) - ListTile( - dense: true, - contentPadding: EdgeInsets.zero, - title: Text( - point.label, - style: const TextStyle(fontSize: 12), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit, size: 18), + onPressed: () => _renameCustomPoint(point), + tooltip: context.l10n.common_edit, ), - subtitle: Text( - '${point.point.latitude.toStringAsFixed(5)}, ${point.point.longitude.toStringAsFixed(5)}', - style: const TextStyle(fontSize: 11), + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: () => _deleteCustomPoint(point), + tooltip: context.l10n.common_delete, ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit, size: 18), - onPressed: () => _renameCustomPoint(point), - tooltip: context.l10n.common_edit, - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 18), - onPressed: () => _deleteCustomPoint(point), - tooltip: context.l10n.common_delete, - ), - ], - ), - ), - ], - const SizedBox(height: 8), - _buildEndpointRow( - label: context.l10n.losPointA, - value: _start, - candidates: endpoints, - onChanged: (value) { - setState(() { - _start = value; - _result = null; - _selectedObstruction = null; - }); - if (_start != null && _end != null) { - _runLos(); - } - }, - ), - const SizedBox(height: 8), - _buildEndpointRow( - label: context.l10n.losPointB, - value: _end, - candidates: endpoints, - onChanged: (value) { - setState(() { - _end = value; - _result = null; - _selectedObstruction = null; - }); - if (_start != null && _end != null) { - _runLos(); - } - }, - ), - const SizedBox(height: 10), - Text( - context.l10n.losAntennaA( - antennaADisplay.toStringAsFixed(1), - heightUnit, - ), - style: const TextStyle(fontSize: 12), - ), - Slider( - value: antennaADisplay, - min: 0, - max: antennaSliderMax, - divisions: antennaSliderDivisions, - onChanged: (value) { - setState(() { - _startAntennaHeight = _toMetersHeight( - value, - isImperial, - ); - }); - }, - ), - Text( - context.l10n.losAntennaB( - antennaBDisplay.toStringAsFixed(1), - heightUnit, - ), - style: const TextStyle(fontSize: 12), - ), - Slider( - value: antennaBDisplay, - min: 0, - max: antennaSliderMax, - divisions: antennaSliderDivisions, - onChanged: (value) { - setState(() { - _endAntennaHeight = _toMetersHeight(value, isImperial); - }); - }, - ), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: _loading ? null : _runLos, - icon: const LosIcon(), - label: Text(context.l10n.losRun), + ], ), ), - ], + ], + const SizedBox(height: 8), + _buildEndpointRow( + label: context.l10n.losPointA, + value: _start, + candidates: endpoints, + onChanged: (value) { + setState(() { + _start = value; + _result = null; + _selectedObstruction = null; + }); + if (_start != null && _end != null) { + _runLos(); + } + }, + ), + const SizedBox(height: 8), + _buildEndpointRow( + label: context.l10n.losPointB, + value: _end, + candidates: endpoints, + onChanged: (value) { + setState(() { + _end = value; + _result = null; + _selectedObstruction = null; + }); + if (_start != null && _end != null) { + _runLos(); + } + }, + ), + const SizedBox(height: 10), + Text( + context.l10n.losAntennaA( + antennaADisplay.toStringAsFixed(1), + heightUnit, + ), + style: const TextStyle(fontSize: 12), + ), + Slider( + value: antennaADisplay, + min: 0, + max: antennaSliderMax, + divisions: antennaSliderDivisions, + onChanged: (value) { + setState(() { + _startAntennaHeight = _toMetersHeight(value, isImperial); + }); + }, + ), + Text( + context.l10n.losAntennaB( + antennaBDisplay.toStringAsFixed(1), + heightUnit, + ), + style: const TextStyle(fontSize: 12), + ), + Slider( + value: antennaBDisplay, + min: 0, + max: antennaSliderMax, + divisions: antennaSliderDivisions, + onChanged: (value) { + setState(() { + _endAntennaHeight = _toMetersHeight(value, isImperial); + }); + }, + ), + Align( + alignment: Alignment.centerRight, + child: FilledButton.icon( + onPressed: _loading ? null : _runLos, + icon: const LosIcon(), + label: Text(context.l10n.losRun), + ), ), ], ), - ), + if (displayFrequencyMHz != null) ...[ + const SizedBox(height: 10), + Row( + children: [ + Text( + '${context.l10n.losFrequencyLabel}: ' + '${displayFrequencyMHz.toStringAsFixed(3)} MHz' + '${kFactorUsed == null ? '' : ' k=${kFactorUsed.toStringAsFixed(3)}'}', + style: const TextStyle( + color: LosPalette.textMuted, + fontSize: 11, + ), + ), + if (kFactorUsed != null) + IconButton( + icon: const Icon(Icons.info_outline, size: 17), + color: LosPalette.textMuted, + tooltip: context.l10n.losFrequencyInfoTooltip, + onPressed: () => _showFrequencyInfoDialog( + context, + displayFrequencyMHz, + kFactorUsed, + ), + ), + ], + ), + ], + Text( + context.l10n.losElevationAttribution, + style: const TextStyle(color: LosPalette.textMuted, fontSize: 10), + ), + ], ), ); } @@ -1002,6 +1307,14 @@ class _LineOfSightMapScreenState extends State { }); } + void _centerOnObstruction(LineOfSightObstruction obstruction) { + _selectObstruction(obstruction); + _mapController.move( + obstruction.point, + math.max(_mapController.camera.zoom, 15), + ); + } + String _formatDistanceValue(double meters, bool isImperial) { final value = isImperial ? (meters / 1000.0) * _kmToMiles : meters / 1000.0; return value.toStringAsFixed(2); @@ -1034,7 +1347,7 @@ class _LineOfSightMapScreenState extends State { ) { if (segment.samples.length < 2) { return SizedBox( - height: 160, + height: 190, width: double.infinity, child: CustomPaint( painter: _LosProfilePainter( @@ -1043,12 +1356,12 @@ class _LineOfSightMapScreenState extends State { heightUnit: heightUnit, badgeTextStyle: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.white70, + color: LosPalette.textMuted, fontSize: 10, fontWeight: FontWeight.w600, ) ?? const TextStyle( - color: Colors.white70, + color: LosPalette.textMuted, fontSize: 10, fontWeight: FontWeight.w600, ), @@ -1061,11 +1374,11 @@ class _LineOfSightMapScreenState extends State { ); } return SizedBox( - height: 160, + height: 190, width: double.infinity, child: LayoutBuilder( builder: (context, constraints) { - final size = Size(constraints.maxWidth, 160); + final size = Size(constraints.maxWidth, 190); final geometry = _LosProfileGeometry( samples: segment.samples, size: size, @@ -1081,12 +1394,12 @@ class _LineOfSightMapScreenState extends State { heightUnit: heightUnit, badgeTextStyle: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.white70, + color: LosPalette.textMuted, fontSize: 10, fontWeight: FontWeight.w600, ) ?? const TextStyle( - color: Colors.white70, + color: LosPalette.textMuted, fontSize: 10, fontWeight: FontWeight.w600, ), @@ -1121,23 +1434,26 @@ class _LineOfSightMapScreenState extends State { child: Tooltip( message: _obstructionChipLabel(obstruction, isImperial), child: GestureDetector( - onTap: () => _selectObstruction(obstruction), + onTap: () => _centerOnObstruction(obstruction), child: Container( width: markerSize, height: markerSize, decoration: BoxDecoration( color: isSelected - ? Colors.amberAccent - : Colors.deepOrangeAccent, + ? LosPalette.selected + : LosPalette.blocked, shape: BoxShape.circle, border: Border.all( color: isSelected - ? Colors.white - : Colors.black87, + ? LosPalette.text + : LosPalette.chartBackground, width: isSelected ? 2 : 1.5, ), boxShadow: const [ - BoxShadow(color: Colors.black45, blurRadius: 4), + BoxShadow( + color: LosPalette.shadow, + blurRadius: 4, + ), ], ), ), @@ -1153,51 +1469,19 @@ class _LineOfSightMapScreenState extends State { ); } - String _profileStats(LineOfSightResult result, bool isImperial) { - final distance = isImperial - ? (result.totalDistanceMeters / 1000.0) * _kmToMiles - : result.totalDistanceMeters / 1000.0; - final distanceUnit = isImperial ? 'mi' : 'km'; - final heightUnit = isImperial ? 'ft' : 'm'; - final minClearance = result.samples.isEmpty - ? 0.0 - : result.samples.map((s) => s.clearanceMeters).reduce(math.min); - final minClearanceDisplay = isImperial - ? minClearance * _metersToFeet - : minClearance; - final maxObstructionDisplay = isImperial - ? result.maxObstructionMeters * _metersToFeet - : result.maxObstructionMeters; - if (!result.hasData) { - return _localizedLosError(result.errorMessage); - } - if (result.isClear) { - return context.l10n.losProfileClear( - distance.toStringAsFixed(1), - distanceUnit, - minClearanceDisplay.toStringAsFixed(1), - heightUnit, - ); - } - return context.l10n.losProfileBlocked( - distance.toStringAsFixed(1), - distanceUnit, - maxObstructionDisplay.toStringAsFixed(1), - heightUnit, - ); - } - List _buildSegmentPolylines(LineOfSightPathResult result) { final polylines = []; for (final segment in result.segments) { final color = !segment.result.hasData - ? Colors.grey - : (segment.result.isClear ? Colors.green : Colors.red); + ? LosPalette.textMuted + : _statusColorFor(_losStatusFor(segment.result)); polylines.add( Polyline( points: [segment.start, segment.end], - strokeWidth: 4, + strokeWidth: 5, color: color, + borderStrokeWidth: 2, + borderColor: Colors.white, ), ); } @@ -1215,7 +1499,7 @@ class _LineOfSightMapScreenState extends State { width: 52, height: 52, child: GestureDetector( - onTap: () => _selectObstruction(obstruction), + onTap: () => _centerOnObstruction(obstruction), child: Center( child: Container( width: @@ -1233,16 +1517,20 @@ class _LineOfSightMapScreenState extends State { color: _selectedObstruction?.sampleIndex == obstruction.sampleIndex - ? Colors.amberAccent - : Colors.deepOrangeAccent, + ? LosPalette.selected + : LosPalette.blocked, width: _selectedObstruction?.sampleIndex == obstruction.sampleIndex ? 4 : 3, ), - boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 6), + boxShadow: [ + const BoxShadow( + color: LosPalette.shadow, + blurRadius: 8, + offset: Offset(0, 2), + ), ], ), ), @@ -1258,17 +1546,34 @@ class _LineOfSightMapScreenState extends State { onTap: () => _selectFromMap(endpoint), child: Container( decoration: BoxDecoration( - color: endpoint.color, shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), + color: (endpoint == _start || endpoint == _end) + ? endpoint.color + : LosPalette.panelDark, + border: Border.all( + color: (endpoint == _start || endpoint == _end) + ? Colors.white + : endpoint.color.withValues(alpha: 0.75), + width: (endpoint == _start || endpoint == _end) ? 2.5 : 1.5, + ), boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 4), + BoxShadow( + color: LosPalette.shadow, + blurRadius: 7, + offset: Offset(0, 2), + ), ], ), child: Stack( children: [ Center( - child: Icon(endpoint.icon, color: Colors.white, size: 16), + child: Icon( + endpoint.icon, + color: endpoint == _start || endpoint == _end + ? Colors.white + : endpoint.color, + size: 17, + ), ), if (endpoint == _start || endpoint == _end) Positioned( @@ -1278,17 +1583,17 @@ class _LineOfSightMapScreenState extends State { width: 14, height: 14, decoration: BoxDecoration( - color: Colors.black87, + color: LosPalette.chartBackground, borderRadius: BorderRadius.circular(7), - border: Border.all(color: Colors.white, width: 1), + border: Border.all(color: endpoint.color, width: 1), ), alignment: Alignment.center, child: Text( endpoint == _start ? 'A' : 'B', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, + style: MeshTheme.mono( fontSize: 9, + fontWeight: FontWeight.w700, + color: endpoint.color, ), ), ), @@ -1316,18 +1621,19 @@ class _LineOfSightMapScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), + color: LosPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.xs), + border: Border.all(color: LosPalette.border), ), alignment: Alignment.center, child: Text( endpoint.label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, + style: MeshTheme.mono( + fontSize: 10, + fontWeight: FontWeight.w700, + color: LosPalette.text, ), ), ), @@ -1354,13 +1660,55 @@ class _LineOfSightMapScreenState extends State { ); } - Color _statusColor() { - if (_error != null) return Colors.red; - if (_loading) return Colors.orange; - if (_result == null) return Colors.grey; - if (_result!.blockedSegments > 0) return Colors.red; - if (_result!.clearSegments > 0) return Colors.green; - return Colors.grey; + _LosDisplayStatus _losStatusFor(LineOfSightResult? result) { + if (result == null || !result.hasData) return _LosDisplayStatus.unknown; + if (!result.isClear) return _LosDisplayStatus.blocked; + if (result.samples.isEmpty) return _LosDisplayStatus.clear; + final minClearance = result.samples + .map((sample) => sample.clearanceMeters) + .reduce(math.min); + return minClearance <= _marginalClearanceMeters + ? _LosDisplayStatus.marginal + : _LosDisplayStatus.clear; + } + + String _statusLabel(_LosDisplayStatus status) { + switch (status) { + case _LosDisplayStatus.clear: + return 'Clear'; + case _LosDisplayStatus.marginal: + return 'Marginal'; + case _LosDisplayStatus.blocked: + return 'Blocked'; + case _LosDisplayStatus.unknown: + return _loading ? 'Checking' : 'No result'; + } + } + + Color _statusColorFor(_LosDisplayStatus status) { + switch (status) { + case _LosDisplayStatus.clear: + return LosPalette.clear; + case _LosDisplayStatus.marginal: + return LosPalette.marginal; + case _LosDisplayStatus.blocked: + return LosPalette.blocked; + case _LosDisplayStatus.unknown: + return LosPalette.textMuted; + } + } + + IconData _statusIcon(_LosDisplayStatus status) { + switch (status) { + case _LosDisplayStatus.clear: + return Icons.check_circle; + case _LosDisplayStatus.marginal: + return Icons.warning_amber_rounded; + case _LosDisplayStatus.blocked: + return Icons.block; + case _LosDisplayStatus.unknown: + return Icons.help_outline; + } } double _toDisplayHeight(double meters, bool isImperial) { @@ -1371,16 +1719,6 @@ class _LineOfSightMapScreenState extends State { return isImperial ? displayHeight / _metersToFeet : displayHeight; } - String _localizedLosError(String? message) { - if (message == LineOfSightService.errorElevationUnavailable) { - return context.l10n.losErrorElevationUnavailable; - } - if (message == LineOfSightService.errorInvalidInput) { - return context.l10n.losErrorInvalidInput; - } - return context.l10n.losNoElevationData; - } - void _handleQuickSwitch(int index, BuildContext context) { if (index == 2) { Navigator.pop(context); @@ -1437,9 +1775,13 @@ class _LineOfSightMapScreenState extends State { } } +enum _LosDisplayStatus { clear, marginal, blocked, unknown } + class _LosProfileGeometry { - static const horizontalPadding = 12.0; - static const verticalPadding = 12.0; + static const leftPadding = 38.0; + static const rightPadding = 14.0; + static const topPadding = 20.0; + static const bottomPadding = 28.0; final List samples; final Size size; @@ -1463,20 +1805,20 @@ class _LosProfileGeometry { late final double maxDist = math.max(1.0, samples.last.distanceMeters); late final double chartWidth = math.max( 1.0, - size.width - horizontalPadding * 2, + size.width - leftPadding - rightPadding, ); late final double chartHeight = math.max( 1.0, - size.height - verticalPadding * 2, + size.height - topPadding - bottomPadding, ); _LosProfileGeometry({required this.samples, required this.size}); Offset mapPoint(double distanceMeters, double elevationMeters) { - final px = horizontalPadding + (distanceMeters / maxDist) * chartWidth; + final px = leftPadding + (distanceMeters / maxDist) * chartWidth; final py = size.height - - verticalPadding - + bottomPadding - ((elevationMeters - minY) / ySpan) * chartHeight; return Offset(px, py); } @@ -1505,7 +1847,7 @@ class _LosProfilePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final bg = Paint()..color = const Color(0xFF243A63); + final bg = Paint()..color = LosPalette.chartBackground; canvas.drawRect(Offset.zero & size, bg); _drawUnitBadge(canvas, size); @@ -1529,18 +1871,52 @@ class _LosProfilePainter extends CustomPainter { .reduce(math.max); final ySpan = math.max(1.0, maxY - minY); final maxDist = math.max(1.0, samples.last.distanceMeters); - const horizontalPadding = 12.0; - const verticalPadding = 12.0; - final chartWidth = math.max(1.0, size.width - horizontalPadding * 2); - final chartHeight = math.max(1.0, size.height - verticalPadding * 2); + const leftPadding = _LosProfileGeometry.leftPadding; + const rightPadding = _LosProfileGeometry.rightPadding; + const topPadding = _LosProfileGeometry.topPadding; + const bottomPadding = _LosProfileGeometry.bottomPadding; + final chartWidth = math.max(1.0, size.width - leftPadding - rightPadding); + final chartHeight = math.max(1.0, size.height - topPadding - bottomPadding); Offset mapPoint(double x, double y) { - final px = horizontalPadding + (x / maxDist) * chartWidth; + final px = leftPadding + (x / maxDist) * chartWidth; final py = - size.height - verticalPadding - ((y - minY) / ySpan) * chartHeight; + size.height - bottomPadding - ((y - minY) / ySpan) * chartHeight; return Offset(px, py); } + final gridPaint = Paint() + ..color = LosPalette.textMuted.withValues(alpha: 0.16) + ..strokeWidth = 1; + for (var i = 0; i <= 4; i++) { + final x = leftPadding + chartWidth * i / 4; + final y = topPadding + chartHeight * i / 4; + canvas.drawLine( + Offset(x, topPadding), + Offset(x, size.height - bottomPadding), + gridPaint, + ); + canvas.drawLine( + Offset(leftPadding, y), + Offset(size.width - rightPadding, y), + gridPaint, + ); + final distance = maxDist * i / 4; + _paintLabel( + canvas, + _displayDistance(distance).toStringAsFixed(i == 0 ? 0 : 1), + Offset(x, size.height - bottomPadding + 7), + center: true, + ); + final elevation = maxY - ySpan * i / 4; + _paintLabel( + canvas, + _displayHeight(elevation).toStringAsFixed(0), + Offset(leftPadding - 6, y - 6), + alignRight: true, + ); + } + final firstTerrainPoint = mapPoint( samples.first.distanceMeters, samples.first.terrainMeters, @@ -1551,13 +1927,13 @@ class _LosProfilePainter extends CustomPainter { ); double distanceForCanvasX(double x) { - final normalized = ((x - horizontalPadding) / chartWidth).clamp(0.0, 1.0); + final normalized = ((x - leftPadding) / chartWidth).clamp(0.0, 1.0); return normalized * maxDist; } double elevationToPixel(double elevation) { final normalized = ((elevation - minY) / ySpan).clamp(0.0, 1.0); - return size.height - verticalPadding - normalized * chartHeight; + return size.height - bottomPadding - normalized * chartHeight; } double extrapolateTerrain(double distance, bool isLeft) { @@ -1599,10 +1975,10 @@ class _LosProfilePainter extends CustomPainter { ..lineTo(size.width, size.height) ..close(); - const terrainFillColor = Color(0xCC7C6F5D); - const terrainLineColor = Color(0xFF9FE870); - const losLineColor = Color(0xFFE0E7FF); - canvas.drawPath(terrainPath, Paint()..color = terrainFillColor); + canvas.drawPath( + terrainPath, + Paint()..color = LosPalette.terrain.withValues(alpha: 0.18), + ); final terrainLine = ui.Path()..moveTo(leftEdgePoint.dx, leftEdgePoint.dy); for (final sample in samples) { @@ -1613,9 +1989,9 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( terrainLine, Paint() - ..color = terrainLineColor + ..color = LosPalette.terrain ..style = PaintingStyle.stroke - ..strokeWidth = 2, + ..strokeWidth = 2.5, ); final losLine = ui.Path(); @@ -1633,12 +2009,11 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( losLine, Paint() - ..color = losLineColor + ..color = LosPalette.beam ..style = PaintingStyle.stroke - ..strokeWidth = 2, + ..strokeWidth = 2.5, ); - const refractedLineColor = Color(0xFFFFD57F); final refractedLine = ui.Path(); for (int i = 0; i < samples.length; i++) { final p = mapPoint( @@ -1654,7 +2029,7 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( refractedLine, Paint() - ..color = refractedLineColor + ..color = LosPalette.horizon ..style = PaintingStyle.stroke ..strokeWidth = 1.5, ); @@ -1679,14 +2054,53 @@ class _LosProfilePainter extends CustomPainter { capPath.lineTo(p.dx, p.dy); } capPath.close(); - const horizonFillColor = Color(0x40FFD57F); canvas.drawPath( capPath, Paint() - ..color = horizonFillColor + ..color = LosPalette.horizon.withValues(alpha: 0.10) ..style = PaintingStyle.fill, ); + for (var i = 0; i < samples.length - 1; i++) { + if (samples[i].clearanceMeters >= 0 && + samples[i + 1].clearanceMeters >= 0) { + continue; + } + final terrainA = mapPoint( + samples[i].distanceMeters, + samples[i].terrainMeters, + ); + final terrainB = mapPoint( + samples[i + 1].distanceMeters, + samples[i + 1].terrainMeters, + ); + final lineB = mapPoint( + samples[i + 1].distanceMeters, + samples[i + 1].lineHeightMeters, + ); + final lineA = mapPoint( + samples[i].distanceMeters, + samples[i].lineHeightMeters, + ); + final blockedArea = ui.Path() + ..moveTo(terrainA.dx, terrainA.dy) + ..lineTo(terrainB.dx, terrainB.dy) + ..lineTo(lineB.dx, lineB.dy) + ..lineTo(lineA.dx, lineA.dy) + ..close(); + canvas.drawPath( + blockedArea, + Paint()..color = LosPalette.blocked.withValues(alpha: 0.42), + ); + } + + _paintEndpoint(canvas, mapPoint(0, samples.first.lineHeightMeters), 'A'); + _paintEndpoint( + canvas, + mapPoint(maxDist, samples.last.lineHeightMeters), + 'B', + ); + if (selectedSampleIndex != null && selectedSampleIndex! >= 0 && selectedSampleIndex! < samples.length) { @@ -1696,24 +2110,103 @@ class _LosProfilePainter extends CustomPainter { selectedSample.terrainMeters, ); canvas.drawLine( - Offset(selectedPoint.dx, verticalPadding), - Offset(selectedPoint.dx, size.height - verticalPadding), + Offset(selectedPoint.dx, topPadding), + Offset(selectedPoint.dx, size.height - bottomPadding), Paint() - ..color = Colors.amberAccent.withValues(alpha: 0.7) - ..strokeWidth = 1.5, + ..color = LosPalette.selected + ..strokeWidth = 2, ); - canvas.drawCircle(selectedPoint, 7, Paint()..color = Colors.amberAccent); + canvas.drawCircle(selectedPoint, 7, Paint()..color = LosPalette.selected); canvas.drawCircle( selectedPoint, 8.5, Paint() - ..color = Colors.white + ..color = LosPalette.text ..style = PaintingStyle.stroke ..strokeWidth = 1.5, ); + final labelY = math.max(topPadding + 2, selectedPoint.dy - 27); + _paintPill( + canvas, + 'Selected', + Offset( + selectedPoint.dx.clamp(42.0, size.width - 42).toDouble(), + labelY, + ), + ); } } + double _displayDistance(double meters) { + return distanceUnit == 'mi' + ? (meters / 1000.0) * 0.621371 + : meters / 1000.0; + } + + double _displayHeight(double meters) { + return heightUnit == 'ft' ? meters * 3.28084 : meters; + } + + void _paintLabel( + Canvas canvas, + String text, + Offset offset, { + bool center = false, + bool alignRight = false, + }) { + final painter = TextPainter( + text: TextSpan( + text: text, + style: const TextStyle( + color: LosPalette.textMuted, + fontSize: 9, + fontWeight: FontWeight.w600, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + var dx = offset.dx; + if (center) dx -= painter.width / 2; + if (alignRight) dx -= painter.width; + painter.paint(canvas, Offset(dx, offset.dy)); + } + + void _paintEndpoint(Canvas canvas, Offset point, String label) { + canvas.drawCircle(point, 9, Paint()..color = LosPalette.chartBackground); + canvas.drawCircle( + point, + 9, + Paint() + ..color = LosPalette.beam + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + _paintLabel(canvas, label, Offset(point.dx, point.dy - 5), center: true); + } + + void _paintPill(Canvas canvas, String text, Offset center) { + final painter = TextPainter( + text: TextSpan( + text: text, + style: const TextStyle( + color: LosPalette.text, + fontSize: 9, + fontWeight: FontWeight.w800, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + final rect = RRect.fromRectAndRadius( + Rect.fromCenter(center: center, width: painter.width + 12, height: 20), + const Radius.circular(10), + ); + canvas.drawRRect(rect, Paint()..color = LosPalette.selected); + painter.paint( + canvas, + Offset(center.dx - painter.width / 2, center.dy - painter.height / 2), + ); + } + @override bool shouldRepaint(covariant _LosProfilePainter oldDelegate) { return oldDelegate.samples != samples || @@ -1738,10 +2231,6 @@ class _LosProfilePainter extends CustomPainter { } class _LosLegend extends StatelessWidget { - static const _terrainColor = Color(0xFF9FE870); - static const _losColor = Color(0xFFE0E7FF); - static const _radioColor = Color(0xFFFFD57F); - final String terrainLabel; final String losBeamLabel; final String radioHorizonLabel; @@ -1756,23 +2245,24 @@ class _LosLegend extends StatelessWidget { Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 11, - fontWeight: FontWeight.w500, + color: LosPalette.text, + fontSize: 12, + fontWeight: FontWeight.w700, ) ?? - TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 11, - fontWeight: FontWeight.w500, + const TextStyle( + color: LosPalette.text, + fontSize: 12, + fontWeight: FontWeight.w700, ); final entries = [ - _LegendEntry(terrainLabel, _terrainColor), - _LegendEntry(losBeamLabel, _losColor), - _LegendEntry(radioHorizonLabel, _radioColor), + _LegendEntry(terrainLabel, LosPalette.terrain), + _LegendEntry(losBeamLabel, LosPalette.beam), + _LegendEntry(radioHorizonLabel, LosPalette.horizon), + const _LegendEntry('Blocked', LosPalette.blocked), ]; - const swatchSize = 10.0; + const swatchSize = 12.0; return Wrap( spacing: 16, diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index e627bd58..6455c15c 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -10,6 +10,9 @@ import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../helpers/snack_bar_builder.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; +import '../widgets/themed_map_tile_layer.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -76,27 +79,34 @@ class _MapCacheScreenState extends State { return Positioned( top: 12, left: 12, - child: Card( - elevation: 4, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.add), - tooltip: context.l10n.map_zoomIn, - onPressed: () => _zoomMapBy(1), - ), - IconButton( - icon: const Icon(Icons.remove), - tooltip: context.l10n.map_zoomOut, - onPressed: () => _zoomMapBy(-1), - ), - IconButton( - icon: const Icon(Icons.my_location), - tooltip: context.l10n.map_centerMap, - onPressed: _resetMapView, - ), - ], + child: DecoratedBox( + decoration: BoxDecoration( + color: MeshPalette.bg1.withValues(alpha: 0.90), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MeshPalette.line2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(MeshRadii.md), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: context.l10n.map_zoomIn, + onPressed: () => _zoomMapBy(1), + ), + IconButton( + icon: const Icon(Icons.remove), + tooltip: context.l10n.map_zoomOut, + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + icon: const Icon(Icons.my_location), + tooltip: context.l10n.map_centerMap, + onPressed: _resetMapView, + ), + ], + ), ), ), ); @@ -281,6 +291,7 @@ class _MapCacheScreenState extends State { final tileCache = context.read(); final selectedBounds = _selectedBounds; final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final progressValue = _estimatedTiles == 0 ? 0.0 @@ -318,13 +329,7 @@ class _MapCacheScreenState extends State { ), ), children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), + ThemedMapTileLayer(tileCache: tileCache), if (selectedBounds != null) PolygonLayer( polygons: [ @@ -342,14 +347,25 @@ class _MapCacheScreenState extends State { Positioned( top: 12, right: 12, - child: Card( + child: DecoratedBox( + decoration: BoxDecoration( + color: MeshPalette.bg1.withValues(alpha: 0.93), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MeshPalette.line2), + ), child: Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), child: Text( selectedBounds == null ? l10n.mapCache_noAreaSelected : _formatBounds(selectedBounds, l10n), - style: const TextStyle(fontSize: 12), + style: MeshTheme.mono( + fontSize: 11, + color: MeshPalette.ink2, + ), ), ), ), @@ -359,111 +375,133 @@ class _MapCacheScreenState extends State { ), SafeArea( top: false, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.mapCache_cacheArea, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + child: DecoratedBox( + decoration: BoxDecoration( + color: scheme.surfaceContainerLow, + border: Border(top: BorderSide(color: scheme.outlineVariant)), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SectionHeader( + l10n.mapCache_cacheArea, + padding: const EdgeInsets.fromLTRB(0, 12, 0, 8), ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - icon: const Icon(Icons.crop_free), - label: Text(l10n.mapCache_useCurrentView), - onPressed: _isDownloading ? null : _setBoundsFromView, + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.crop_free), + label: Text(l10n.mapCache_useCurrentView), + onPressed: _isDownloading + ? null + : _setBoundsFromView, + ), ), - ), - const SizedBox(width: 12), - TextButton( - onPressed: _isDownloading || selectedBounds == null - ? null - : _clearBounds, - child: Text(l10n.common_clear), - ), - ], - ), - const SizedBox(height: 12), - Text( - l10n.mapCache_zoomRange, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - RangeSlider( - values: RangeValues( - _minZoom.toDouble(), - _maxZoom.toDouble(), - ), - min: 3, - max: 18, - divisions: 15, - labels: RangeLabels('$_minZoom', '$_maxZoom'), - onChanged: _isDownloading - ? null - : (values) { - setState(() { - _minZoom = values.start.round(); - _maxZoom = values.end.round(); - }); - }, - onChangeEnd: _isDownloading - ? null - : (_) { - _saveZoomRange(); - }, - ), - Text(l10n.mapCache_estimatedTiles(_estimatedTiles)), - if (_isDownloading) ...[ - const SizedBox(height: 8), - LinearProgressIndicator(value: progressValue), - const SizedBox(height: 4), - Text( - l10n.mapCache_downloadedTiles( - _completedTiles, - _estimatedTiles, - ), - ), - ], - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - icon: const Icon(Icons.download), - label: Text(l10n.mapCache_downloadTilesButton), + const SizedBox(width: 12), + TextButton( onPressed: _isDownloading || selectedBounds == null ? null - : _startDownload, + : _clearBounds, + child: Text(l10n.common_clear), ), + ], + ), + const SizedBox(height: 12), + SectionHeader( + l10n.mapCache_zoomRange, + padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), + ), + RangeSlider( + values: RangeValues( + _minZoom.toDouble(), + _maxZoom.toDouble(), ), - const SizedBox(width: 12), - OutlinedButton( - onPressed: _isDownloading ? null : _clearCache, - child: Text(l10n.mapCache_clearCacheButton), - ), - ], - ), - if (_failedTiles > 0 && !_isDownloading) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - l10n.mapCache_failedDownloads(_failedTiles), - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), + min: 3, + max: 18, + divisions: 15, + labels: RangeLabels('$_minZoom', '$_maxZoom'), + onChanged: _isDownloading + ? null + : (values) { + setState(() { + _minZoom = values.start.round(); + _maxZoom = values.end.round(); + }); + }, + onChangeEnd: _isDownloading + ? null + : (_) { + _saveZoomRange(); + }, + ), + Text( + l10n.mapCache_estimatedTiles(_estimatedTiles), + style: MeshTheme.mono( + fontSize: 12, + color: scheme.onSurfaceVariant, ), ), - ], + if (_isDownloading) ...[ + const SizedBox(height: 8), + LinearProgressIndicator( + value: progressValue, + color: MeshPalette.blue, + backgroundColor: scheme.surfaceContainerHighest, + ), + const SizedBox(height: 4), + Text( + l10n.mapCache_downloadedTiles( + _completedTiles, + _estimatedTiles, + ), + style: MeshTheme.mono( + fontSize: 12, + color: scheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.download), + label: Text(l10n.mapCache_downloadTilesButton), + onPressed: _isDownloading || selectedBounds == null + ? null + : _startDownload, + ), + ), + const SizedBox(width: 12), + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: MeshPalette.alert, + side: const BorderSide( + color: MeshPalette.alertLine, + ), + ), + onPressed: _isDownloading ? null : _clearCache, + child: Text(l10n.mapCache_clearCacheButton), + ), + ], + ), + if (_failedTiles > 0 && !_isDownloading) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + l10n.mapCache_failedDownloads(_failedTiles), + style: MeshTheme.mono( + fontSize: 12, + color: MeshPalette.alert, + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 3e4193c0..f14a1820 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; @@ -21,13 +22,17 @@ import '../services/path_history_service.dart'; import '../services/map_marker_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; +import '../utils/battery_utils.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/sync_progress_overlay.dart'; +import '../widgets/themed_map_tile_layer.dart'; import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import '../helpers/snack_bar_builder.dart'; @@ -58,6 +63,11 @@ class MapScreen extends StatefulWidget { class _MapScreenState extends State { // Zoom level at which node labels start to appear static const double _labelZoomThreshold = 14.0; + // Below this zoom, nearby nodes collapse into clusters. + static const double _clusterOffZoom = 12.5; + // Guessed (estimated) locations only render at closer zooms to avoid a + // carpet of approximate markers at city-wide scale. + static const double _guessedZoomThreshold = 12.0; static const double _mapMinZoom = 2.0; static const double _mapMaxZoom = 18.0; @@ -73,11 +83,51 @@ class _MapScreenState extends State { final List _pathTraceContacts = []; final List _points = []; final List _polylines = []; - bool _legendExpanded = false; + bool _statsExpanded = false; bool _showNodeLabels = true; + double _zoom = 10.0; + String? _selectedKey; + LatLng? _selectedGuessPos; + _Freshness _freshness = _Freshness.all; + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocus = FocusNode(); + String _searchQuery = ''; List<_GuessedLocation> _cachedGuessedLocations = []; String _guessedLocationsCacheKey = ''; + @override + void dispose() { + _searchController.dispose(); + _searchFocus.dispose(); + _mapController.dispose(); + super.dispose(); + } + + _NodeAge _ageOf(Contact contact) { + final d = DateTime.now().difference(contact.lastSeen); + if (d.inMinutes <= 60) return _NodeAge.online; + if (d.inHours <= 24) return _NodeAge.recent; + return _NodeAge.stale; + } + + void _selectNode(Contact contact, {LatLng? guessedPosition}) { + HapticFeedback.selectionClick(); + setState(() { + _selectedKey = contact.publicKeyHex; + _selectedGuessPos = guessedPosition; + _searchQuery = ''; + _searchController.clear(); + _searchFocus.unfocus(); + }); + } + + void _clearSelection() { + setState(() { + _selectedKey = null; + _selectedGuessPos = null; + }); + } + @override void initState() { super.initState(); @@ -166,38 +216,67 @@ class _MapScreenState extends State { _mapController.move(camera.center, nextZoom); } - Widget _buildDesktopMapControls( + Widget _buildControlRail( BuildContext context, { required LatLng center, required double zoom, - required bool hasPathSelector, + required MeshCoreConnector connector, }) { + final hasSelf = + connector.selfLatitude != null && connector.selfLongitude != null; return Positioned( - left: 16, - top: hasPathSelector ? null : 16, - bottom: hasPathSelector ? 16 : null, - child: Card( - elevation: 4, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.add), - tooltip: context.l10n.map_zoomIn, - onPressed: () => _zoomMapBy(1), - ), - IconButton( - icon: const Icon(Icons.remove), - tooltip: context.l10n.map_zoomOut, - onPressed: () => _zoomMapBy(-1), - ), - IconButton( - icon: const Icon(Icons.my_location), - tooltip: context.l10n.map_centerMap, - onPressed: () => _mapController.move(center, zoom), + left: 12, + bottom: 96, + child: DecoratedBox( + decoration: BoxDecoration( + color: MapPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MapPalette.border), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 8, + offset: Offset(0, 3), ), ], ), + child: ClipRRect( + borderRadius: BorderRadius.circular(MeshRadii.md), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + color: MapPalette.textPrimary, + icon: const Icon(Icons.add), + visualDensity: VisualDensity.standard, + tooltip: context.l10n.map_zoomIn, + onPressed: () => _zoomMapBy(1), + ), + IconButton( + color: MapPalette.textPrimary, + icon: const Icon(Icons.remove), + tooltip: context.l10n.map_zoomOut, + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + color: MapPalette.textPrimary, + icon: const Icon(Icons.crop_free), + tooltip: context.l10n.map_centerMap, + onPressed: () => _mapController.move(center, zoom), + ), + if (hasSelf) + IconButton( + color: MapPalette.selected, + icon: const Icon(Icons.my_location), + tooltip: context.l10n.map_setAsMyLocation, + onPressed: () => _mapController.move( + LatLng(connector.selfLatitude!, connector.selfLongitude!), + max(_zoom, 14), + ), + ), + ], + ), + ), ), ); } @@ -235,16 +314,27 @@ class _MapScreenState extends State { return hoursSinceLastSeen <= settings.mapTimeFilterHours; }).toList(); + // Quick activity filter (search bar chips) + final filteredByFreshness = switch (_freshness) { + _Freshness.all => filteredByTime, + _Freshness.online => + filteredByTime.where((c) => _ageOf(c) == _NodeAge.online).toList(), + _Freshness.recent => + filteredByTime.where((c) => _ageOf(c) != _NodeAge.stale).toList(), + _Freshness.stale => + filteredByTime.where((c) => _ageOf(c) == _NodeAge.stale).toList(), + }; + // Filter by key prefix final keyPrefix = settings.mapKeyPrefix.trim(); final filteredByKeyPrefix = (settings.mapKeyPrefixEnabled && keyPrefix.isNotEmpty) - ? filteredByTime.where((c) { + ? filteredByFreshness.where((c) { return c.publicKeyHex.toLowerCase().startsWith( keyPrefix.toLowerCase(), ); }).toList() - : filteredByTime; + : filteredByFreshness; // Filter by location final contactsWithLocation = filteredByKeyPrefix.where((c) { @@ -292,7 +382,7 @@ class _MapScreenState extends State { Polyline( points: _points, strokeWidth: 4, - color: Colors.blueAccent, + color: MapPalette.selected, ), ] : [], @@ -308,8 +398,10 @@ class _MapScreenState extends State { Polyline( points: points, color: marker.isChannel - ? (marker.isPublicChannel ? Colors.orange : Colors.purple) - : Colors.blue, + ? (marker.isPublicChannel + ? MapPalette.cluster + : MapPalette.router) + : MapPalette.shared, strokeWidth: 3, ), ); @@ -397,6 +489,7 @@ class _MapScreenState extends State { if (!_hasInitializedMap && _removedMarkersLoaded) { _hasInitializedMap = true; _showNodeLabels = initialZoom >= _labelZoomThreshold; + _zoom = initialZoom; if (hasMapContent) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -408,10 +501,34 @@ class _MapScreenState extends State { final allowBack = !connector.isConnected; + final visibleContacts = _filterContactsBySettings( + contactsWithLocation, + settings, + ); + Contact? selectedContact; + if (_selectedKey != null) { + for (final c in allContacts) { + if (c.publicKeyHex == _selectedKey) { + selectedContact = c; + break; + } + } + } + final locatedTotal = allContacts.where((c) => c.hasLocation).length; + final hiddenCount = max(0, locatedTotal - visibleContacts.length); + final onlineCount = visibleContacts + .where((c) => _ageOf(c) == _NodeAge.online) + .length; + final repeaterCount = visibleContacts + .where((c) => c.type == advTypeRepeater) + .length; + return PopScope( canPop: allowBack, child: Scaffold( appBar: AppBar( + backgroundColor: MapPalette.panelDark, + foregroundColor: MapPalette.textPrimary, title: AppBarTitle(context.l10n.map_title), centerTitle: true, automaticallyImplyLeading: false, @@ -457,7 +574,7 @@ class _MapScreenState extends State { connector.selfLatitude!, connector.selfLongitude!, ), - color: Colors.teal, + color: MapPalette.selected, icon: Icons.person_pin_circle, ), ); @@ -550,6 +667,17 @@ class _MapScreenState extends State { defaultLabel: context.l10n.map_pointOfInterest, flags: 'poi', ); + return; + } + // Tapping empty map dismisses selection + search. + if (_selectedKey != null || _searchQuery.isNotEmpty) { + setState(() { + _selectedKey = null; + _selectedGuessPos = null; + _searchQuery = ''; + _searchController.clear(); + _searchFocus.unfocus(); + }); } }, onLongPress: (_, latLng) { @@ -573,22 +701,21 @@ class _MapScreenState extends State { ); }, onPositionChanged: (camera, hasGesture) { + // Track zoom in half-step buckets so cluster/marker + // detail levels update without rebuilding every frame. + final bucket = (camera.zoom * 2).roundToDouble() / 2; final shouldShow = camera.zoom >= _labelZoomThreshold; - if (shouldShow != _showNodeLabels && mounted) { + if ((bucket != _zoom || shouldShow != _showNodeLabels) && + mounted) { setState(() { + _zoom = bucket; _showNodeLabels = shouldShow; }); } }, ), children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), + ThemedMapTileLayer(tileCache: tileCache), if (_polylines.isNotEmpty && _isBuildingPathTrace) PolylineLayer(polylines: _polylines), if (sharedMarkerPolylines.isNotEmpty) @@ -598,25 +725,45 @@ class _MapScreenState extends State { if (highlightPosition != null) Marker( point: highlightPosition, - width: 40, - height: 40, + width: 44, + height: 44, child: IgnorePointer( - child: Icon( - Icons.location_on_outlined, - color: Colors.red[600], - size: 34, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MapPalette.batteryLow, + border: Border.all( + color: MapPalette.markerOutline, + width: 3, + ), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 8, + offset: Offset(0, 3), + ), + ], + ), + child: const Icon( + Icons.location_on, + color: Colors.white, + size: 25, + ), ), ), ), - if (!settings.mapShowOverlaps) + if (!settings.mapShowOverlaps && + (_zoom >= _guessedZoomThreshold || + _isBuildingPathTrace)) ..._buildGuessedMarker( guessedLocations, showLabels: _showNodeLabels, ), - ..._buildMarkers( - contactsWithLocation, + ..._buildNodeMarkers( + visibleContacts, settings, showLabels: _showNodeLabels, + selectedContact: selectedContact, ), ...sharedMarkers.map(_buildSharedMarker), if (connector.selfLatitude != null && @@ -631,29 +778,28 @@ class _MapScreenState extends State { child: IgnorePointer( ignoring: true, child: Container( - padding: const EdgeInsets.all(4), + width: 36, + height: 36, decoration: BoxDecoration( - color: Colors.teal, shape: BoxShape.circle, + color: MapPalette.panelDark, border: Border.all( - color: Colors.white, - width: 2, + color: MapPalette.markerOutline, + width: 2.5, ), boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.3, - ), - blurRadius: 4, - offset: const Offset(0, 2), + const BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 8, + offset: Offset(0, 2), ), ], ), alignment: Alignment.center, child: const Icon( Icons.person_pin_circle, - color: Colors.white, - size: 20, + color: MapPalette.selected, + size: 22, ), ), ), @@ -672,22 +818,34 @@ class _MapScreenState extends State { ), ], ), - if (!_isBuildingPathTrace) - _buildLegend( - contacts, - contactsWithLocation, - settings, - sharedMarkers.length, - guessedLocations.length, - ), - if (isDesktop) - _buildDesktopMapControls( + if (selectedContact == null) + _buildControlRail( context, center: center, zoom: initialZoom, - hasPathSelector: _isBuildingPathTrace, + connector: connector, + ), + if (!_isBuildingPathTrace) + _buildTopOverlay( + context, + connector: connector, + settingsService: settingsService, + allContacts: allContacts, + guessedLocations: guessedLocations, + visibleCount: + visibleContacts.length + + ((settings.mapShowGuessedLocations && + _zoom >= _guessedZoomThreshold) + ? guessedLocations.length + : 0), + onlineCount: onlineCount, + repeaterCount: repeaterCount, + hiddenCount: hiddenCount, + pinCount: sharedMarkers.length, ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), + if (selectedContact != null && !_isBuildingPathTrace) + _buildSelectedNodeCard(context, selectedContact, connector), ], ), bottomNavigationBar: SafeArea( @@ -698,13 +856,17 @@ class _MapScreenState extends State { _handleQuickSwitch(index, context), contactsUnreadCount: connector.getTotalContactsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + highContrast: true, ), ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showFilterDialog(context, settingsService), - tooltip: context.l10n.map_filterNodes, - child: const Icon(Icons.filter_list), - ), + floatingActionButton: + (selectedContact == null && !_isBuildingPathTrace) + ? FloatingActionButton( + onPressed: () => _showFilterSheet(context, settingsService), + tooltip: context.l10n.map_filterNodes, + child: const Icon(Icons.filter_list), + ) + : null, ), ); }, @@ -932,32 +1094,31 @@ class _MapScreenState extends State { : null, onTap: () => _isBuildingPathTrace ? _addToPath(context, guess.contact, position: guess.position) - : _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), + : _selectNode(guess.contact, guessedPosition: guess.position), child: Center( child: Container( - padding: const EdgeInsets.all(4), + width: 36, + height: 36, decoration: BoxDecoration( - color: color.withValues( - alpha: guess.highConfidence ? 0.55 : 0.30, - ), shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ + color: MapPalette.panelDark, + border: Border.all( + color: guess.highConfidence ? color : MapPalette.textMuted, + width: guess.highConfidence ? 2.5 : 2, + ), + boxShadow: const [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + color: MapPalette.markerShadow, + blurRadius: 7, + offset: Offset(0, 2), ), ], ), - child: const Icon( + alignment: Alignment.center, + child: Icon( Icons.not_listed_location, - color: Colors.white, - size: 20, + color: MapPalette.textPrimary, + size: 19, ), ), ), @@ -1040,57 +1201,24 @@ class _MapScreenState extends State { return filtered; } - List _buildMarkers( + List _buildNodeMarkers( List contacts, settings, { required bool showLabels, + Contact? selectedContact, }) { final markers = []; - final filteredContacts = _filterContactsBySettings(contacts, settings); - for (final contact in filteredContacts) { - final marker = Marker( - point: LatLng(contact.latitude!, contact.longitude!), - width: 48, - height: 48, - child: GestureDetector( - onLongPress: () => - _isBuildingPathTrace ? _showNodeInfo(context, contact) : null, - onTap: () => _isBuildingPathTrace - ? _addToPath(context, contact) - : _showNodeInfo(context, contact), - child: Center( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: settings.mapShowOverlaps && !_isBuildingPathTrace - ? Colors.red - : _getNodeColor(contact.type), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Icon( - _getNodeIcon(contact.type), - color: Colors.white, - size: 20, - ), - ), - ), - ), - ); + final overlapsMode = settings.mapShowOverlaps && !_isBuildingPathTrace; + final selectedKey = selectedContact?.publicKeyHex; + final items = contacts.where((c) => c.publicKeyHex != selectedKey).toList(); - markers.add(marker); + void addNode(Contact contact, {bool dot = false}) { + markers.add(_nodeMarker(contact, overlapsMode: overlapsMode, dot: dot)); if (showLabels) { markers.add( _buildNodeLabelMarker( point: LatLng(contact.latitude!, contact.longitude!), - label: settings.mapShowOverlaps && !_isBuildingPathTrace + label: overlapsMode ? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}" : contact.name, ), @@ -1098,9 +1226,196 @@ class _MapScreenState extends State { } } + if (_zoom >= _clusterOffZoom || overlapsMode || _isBuildingPathTrace) { + for (final contact in items) { + addNode(contact); + } + } else { + // Grid clustering: bucket markers into ~64px screen cells at the + // current zoom; cells with 2+ nodes render as a numbered cluster. + final cellDeg = 360.0 / (256.0 * pow(2.0, _zoom)) * 64.0; + final cells = >{}; + for (final contact in items) { + final key = + '${(contact.latitude! / cellDeg).floor()}:${(contact.longitude! / cellDeg).floor()}'; + (cells[key] ??= []).add(contact); + } + for (final cell in cells.values) { + if (cell.length == 1) { + addNode(cell.first, dot: true); + } else { + markers.add(_clusterMarker(cell)); + } + } + } + + // Selected node always renders individually on top, even when its + // neighbors are clustered or it is filtered out. + if (selectedContact != null && selectedContact.hasLocation) { + markers.add( + _nodeMarker( + selectedContact, + overlapsMode: overlapsMode, + selected: true, + ), + ); + markers.add( + _buildNodeLabelMarker( + point: LatLng(selectedContact.latitude!, selectedContact.longitude!), + label: selectedContact.name, + ), + ); + } + return markers; } + Marker _nodeMarker( + Contact contact, { + bool overlapsMode = false, + bool dot = false, + bool selected = false, + }) { + final age = _ageOf(contact); + final baseColor = overlapsMode + ? MapPalette.batteryLow + : _markerColor(contact); + final stale = age == _NodeAge.stale; + final online = age == _NodeAge.online; + final batteryLow = _isBatteryLow(contact); + final size = selected ? 46.0 : (dot ? 22.0 : 40.0); + return Marker( + point: LatLng(contact.latitude!, contact.longitude!), + width: size, + height: size, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: () => + _isBuildingPathTrace ? _showNodeInfo(context, contact) : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, contact) + : _selectNode(contact), + child: Center( + child: dot && !selected + ? Container( + width: 15, + height: 15, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: baseColor, + border: Border.all( + color: MapPalette.markerOutline, + width: 2, + ), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + ) + : _buildNodeMarkerWidget( + color: baseColor, + icon: _getNodeIcon(contact.type), + selected: selected, + stale: stale, + online: online, + batteryLow: batteryLow, + ), + ), + ), + ); + } + + Marker _clusterMarker(List members) { + final count = members.length; + double lat = 0, lon = 0; + var online = 0; + for (final m in members) { + lat += m.latitude!; + lon += m.longitude!; + if (_ageOf(m) == _NodeAge.online) online++; + } + final center = LatLng(lat / count, lon / count); + final size = count >= 50 + ? 54.0 + : count >= 16 + ? 50.0 + : count >= 6 + ? 46.0 + : 42.0; + return Marker( + point: center, + width: size, + height: size, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _zoomToCluster(members), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MapPalette.cluster, + border: Border.all(color: MapPalette.markerOutline, width: 3), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 8, + offset: Offset(0, 3), + ), + ], + ), + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '$count', + style: MeshTheme.mono( + fontSize: count >= 100 ? 11.5 : 13.5, + fontWeight: FontWeight.w800, + color: Colors.white, + ), + ), + if (online > 0) + Container( + width: 7, + height: 7, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MapPalette.online, + border: Border.all(color: Colors.white, width: 1), + ), + ), + ], + ), + ), + ), + ); + } + + void _zoomToCluster(List members) { + HapticFeedback.selectionClick(); + var minLat = double.infinity, maxLat = -double.infinity; + var minLon = double.infinity, maxLon = -double.infinity; + for (final m in members) { + minLat = min(minLat, m.latitude!); + maxLat = max(maxLat, m.latitude!); + minLon = min(minLon, m.longitude!); + maxLon = max(maxLon, m.longitude!); + } + _mapController.fitCamera( + CameraFit.bounds( + bounds: LatLngBounds(LatLng(minLat, minLon), LatLng(maxLat, maxLon)), + padding: const EdgeInsets.all(72), + maxZoom: 16, + ), + ); + } + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { return Marker( point: point, @@ -1115,18 +1430,26 @@ class _MapScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), + color: MapPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.xs), + border: Border.all(color: MapPalette.border), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 4, + offset: Offset(0, 1), + ), + ], ), alignment: Alignment.center, child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, + style: MeshTheme.mono( + fontSize: 10, + fontWeight: FontWeight.w700, + color: MapPalette.textPrimary, ), ), ), @@ -1139,18 +1462,44 @@ class _MapScreenState extends State { Color _getNodeColor(int type) { switch (type) { case advTypeChat: - return Colors.blue; + return MapPalette.selected; case advTypeRepeater: - return Colors.green; + return MapPalette.repeater; case advTypeRoom: - return Colors.purple; + return MapPalette.router; case advTypeSensor: - return Colors.orange; + return MapPalette.sensor; default: - return Colors.grey; + return MapPalette.offline; } } + Color _markerColor(Contact contact) { + switch (contact.type) { + case advTypeRepeater: + return MapPalette.repeater; + case advTypeRoom: + return MapPalette.router; + case advTypeSensor: + return MapPalette.sensor; + default: + return _ageColor(_ageOf(contact)); + } + } + + bool _isBatteryLow(Contact contact) { + if (contact.type != advTypeRepeater) return false; + final connector = context.read(); + final millivolts = connector.getRepeaterBatteryMillivolts( + contact.publicKeyHex, + ); + if (millivolts == null) return false; + final chemistry = context + .read() + .batteryChemistryForRepeater(contact.publicKeyHex); + return estimateBatteryPercentFromMillivolts(millivolts, chemistry) <= 20; + } + IconData _getNodeIcon(int type) { switch (type) { case advTypeChat: @@ -1166,211 +1515,862 @@ class _MapScreenState extends State { } } - Widget _buildLegend( - List contacts, - List contactsWithLocation, - settings, - int markerCount, - int guessedCount, - ) { - final filteredContacts = _filterContactsBySettings( - contacts, - settings, - noLocations: false, - ); - final filteredContactsAll = _filterContactsBySettings( - contacts, - settings, - noLocations: true, - ); - - final nodeCount = filteredContacts.length; - final nodeCountAll = filteredContactsAll.length; - - return Positioned( - top: 16, - right: 16, - child: Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - setState(() { - _legendExpanded = !_legendExpanded; - }); - }, - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.map_nodesCount( - nodeCount + - (settings.mapShowGuessedLocations - ? guessedCount - : 0), - ), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - Row( - children: [ - Icon( - Icons.location_on, - size: 16, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - Text( - ": $nodeCount", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ), - Row( - children: [ - Icon( - Icons.wrong_location, - size: 16, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - Text( - ": ${nodeCountAll - nodeCount}", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ), - Row( - children: [ - Icon( - Icons.add_outlined, - size: 16, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - Text( - ": $nodeCountAll", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ), - Text( - context.l10n.map_pinsCount(markerCount), - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 12, - ), - ), - ], - ), - const SizedBox(width: 8), - AnimatedRotation( - turns: _legendExpanded ? 0.5 : 0, - duration: const Duration(milliseconds: 200), - child: const Icon(Icons.expand_more, size: 20), - ), - ], - ), - ), + Widget _buildNodeMarkerWidget({ + required Color color, + required IconData icon, + bool selected = false, + bool stale = false, + bool online = false, + bool batteryLow = false, + }) { + final statusColor = batteryLow + ? MapPalette.batteryLow + : online + ? MapPalette.online + : stale + ? MapPalette.offline + : MapPalette.stale; + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Container( + width: selected ? 44 : 36, + height: selected ? 44 : 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: selected ? MapPalette.selected : color, + border: Border.all( + color: MapPalette.markerOutline, + width: selected ? 3 : 2.5, ), - AnimatedCrossFade( - firstChild: const SizedBox.shrink(), - secondChild: Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 6), - _buildLegendItem( - Icons.person, - context.l10n.map_chat, - Colors.blue, - ), - _buildLegendItem( - Icons.router, - context.l10n.map_repeater, - Colors.green, - ), - _buildLegendItem( - Icons.meeting_room, - context.l10n.map_room, - Colors.purple, - ), - _buildLegendItem( - Icons.sensors, - context.l10n.map_sensor, - Colors.orange, - ), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinDm, - Colors.blue, - ), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinPrivate, - Colors.purple, - ), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinPublic, - Colors.orange, - ), - if (settings.mapShowGuessedLocations && guessedCount > 0) - _buildLegendItem( - Icons.not_listed_location, - context.l10n.map_guessedLocation, - Colors.grey, - ), - ], - ), + boxShadow: [ + const BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 8, + offset: Offset(0, 3), ), - crossFadeState: _legendExpanded - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - duration: const Duration(milliseconds: 200), - ), - ], + if (selected) + BoxShadow( + color: MapPalette.selected.withValues(alpha: 0.75), + blurRadius: 14, + spreadRadius: 3, + ), + ], + ), + alignment: Alignment.center, + child: Icon(icon, color: Colors.white, size: selected ? 22 : 19), ), - ), + Positioned( + right: selected ? -1 : -2, + bottom: selected ? 0 : -2, + child: Container( + width: batteryLow ? 16 : (selected ? 13 : 12), + height: batteryLow ? 16 : (selected ? 13 : 12), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: statusColor, + border: Border.all(color: MapPalette.panelDark, width: 2), + ), + alignment: Alignment.center, + child: batteryLow + ? const Icon(Icons.battery_alert, size: 10, color: Colors.white) + : null, + ), + ), + ], ); } Widget _buildLegendItem(IconData icon, String label, Color color) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 1.0), + padding: const EdgeInsets.symmetric(vertical: 1.5), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 16, color: color), + Icon(icon, size: 15, color: color), const SizedBox(width: 8), - Text(label, style: const TextStyle(fontSize: 12)), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: MapPalette.textSecondary, + ), + overflow: TextOverflow.ellipsis, + ), + ), ], ), ); } + Color _ageColor(_NodeAge age) { + switch (age) { + case _NodeAge.online: + return MapPalette.online; + case _NodeAge.recent: + return MapPalette.stale; + case _NodeAge.stale: + return MapPalette.textMuted; + } + } + + String _ageLabel(_NodeAge age) { + switch (age) { + case _NodeAge.online: + return context.l10n.map_online; + case _NodeAge.recent: + return context.l10n.map_recent; + case _NodeAge.stale: + return context.l10n.map_stale; + } + } + + Widget _buildTopOverlay( + BuildContext context, { + required MeshCoreConnector connector, + required AppSettingsService settingsService, + required List allContacts, + required List<_GuessedLocation> guessedLocations, + required int visibleCount, + required int onlineCount, + required int repeaterCount, + required int hiddenCount, + required int pinCount, + }) { + final settings = settingsService.settings; + final hasQuery = _searchQuery.trim().isNotEmpty; + return Positioned( + top: 8, + left: 12, + right: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Material( + color: MapPalette.panelDark, + shape: StadiumBorder( + side: const BorderSide(color: MapPalette.border), + ), + clipBehavior: Clip.antiAlias, + child: TextField( + controller: _searchController, + focusNode: _searchFocus, + decoration: InputDecoration( + hintText: context.l10n.map_searchHint, + hintStyle: const TextStyle( + color: MapPalette.textSecondary, + ), + prefixIcon: const Icon( + Icons.search, + size: 20, + color: MapPalette.textPrimary, + ), + suffixIcon: hasQuery + ? IconButton( + color: MapPalette.textPrimary, + icon: const Icon(Icons.close, size: 18), + onPressed: () { + setState(() { + _searchQuery = ''; + _searchController.clear(); + }); + }, + ) + : null, + filled: false, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 12, + ), + ), + style: const TextStyle( + fontSize: 14, + color: MapPalette.textPrimary, + fontWeight: FontWeight.w600, + ), + cursorColor: MapPalette.selected, + onChanged: (value) { + setState(() => _searchQuery = value); + }, + ), + ), + ), + const SizedBox(width: 8), + Material( + color: MapPalette.panelDark, + shape: StadiumBorder( + side: const BorderSide(color: MapPalette.border), + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => setState(() => _statsExpanded = !_statsExpanded), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 11, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.hub, + size: 15, + color: MapPalette.selected, + ), + const SizedBox(width: 6), + Text( + '$visibleCount', + style: MeshTheme.mono( + fontSize: 13, + fontWeight: FontWeight.w700, + color: MapPalette.textPrimary, + ), + ), + const SizedBox(width: 2), + AnimatedRotation( + turns: _statsExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: const Icon( + Icons.expand_more, + size: 16, + color: MapPalette.textPrimary, + ), + ), + ], + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + LayoutBuilder( + builder: (context, constraints) { + final chips = [ + _mapChip( + label: context.l10n.time_allTime, + selected: _freshness == _Freshness.all, + onTap: () => setState(() => _freshness = _Freshness.all), + ), + _mapChip( + label: context.l10n.map_online, + selected: _freshness == _Freshness.online, + color: MapPalette.online, + onTap: () => setState(() => _freshness = _Freshness.online), + ), + _mapChip( + label: context.l10n.map_recent, + selected: _freshness == _Freshness.recent, + color: MapPalette.stale, + onTap: () => setState(() => _freshness = _Freshness.recent), + ), + _mapChip( + label: context.l10n.map_stale, + selected: _freshness == _Freshness.stale, + color: MapPalette.offline, + onTap: () => setState(() => _freshness = _Freshness.stale), + ), + _mapChip( + label: context.l10n.map_repeaters, + selected: settings.mapShowRepeaters, + color: MapPalette.repeater, + onTap: () => settingsService.setMapShowRepeaters( + !settings.mapShowRepeaters, + ), + ), + _mapChip( + label: context.l10n.map_chatNodes, + selected: settings.mapShowChatNodes, + color: MapPalette.selected, + onTap: () => settingsService.setMapShowChatNodes( + !settings.mapShowChatNodes, + ), + ), + ]; + + if (constraints.maxWidth < 600) { + return Wrap(runSpacing: 6, children: chips); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: chips), + ); + }, + ), + if (hasQuery) + _buildSearchResults(context, allContacts, guessedLocations) + else if (_statsExpanded) + Align( + alignment: Alignment.centerRight, + child: _buildStatsCard( + context, + settings: settings, + visibleCount: visibleCount, + onlineCount: onlineCount, + repeaterCount: repeaterCount, + hiddenCount: hiddenCount, + pinCount: pinCount, + guessedCount: guessedLocations.length, + ), + ), + ], + ), + ); + } + + Widget _mapChip({ + required String label, + required bool selected, + required VoidCallback onTap, + Color? color, + }) { + final accent = color ?? MapPalette.selected; + return Padding( + padding: const EdgeInsets.only(right: 6), + child: Material( + color: selected + ? Color.alphaBlend( + accent.withValues(alpha: 0.34), + MapPalette.panelDark, + ) + : MapPalette.panelDark, + shape: StadiumBorder( + side: BorderSide( + color: selected ? accent : MapPalette.border, + width: selected ? 1.5 : 1, + ), + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () { + HapticFeedback.selectionClick(); + onTap(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (selected) ...[ + const Icon( + Icons.check, + size: 13, + color: MapPalette.textPrimary, + ), + const SizedBox(width: 4), + ], + Text( + label, + style: TextStyle( + fontSize: 12.5, + fontWeight: FontWeight.w600, + color: selected + ? MapPalette.textPrimary + : MapPalette.textSecondary, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSearchResults( + BuildContext context, + List allContacts, + List<_GuessedLocation> guessedLocations, + ) { + final query = _searchQuery.trim().toLowerCase(); + final matches = + allContacts.where((c) => matchesContactQuery(c, query)).toList() + ..sort((a, b) { + if (a.hasLocation != b.hasLocation) { + return a.hasLocation ? -1 : 1; + } + return b.lastSeen.compareTo(a.lastSeen); + }); + final results = matches.take(8).toList(); + return Container( + margin: const EdgeInsets.only(top: 6), + constraints: const BoxConstraints(maxHeight: 300), + decoration: BoxDecoration( + color: MapPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MapPalette.border), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: results.isEmpty + ? Padding( + padding: const EdgeInsets.all(16), + child: Text( + context.l10n.map_noResults, + style: const TextStyle( + color: MapPalette.textSecondary, + fontSize: 13, + ), + ), + ) + : ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: results.length, + separatorBuilder: (_, _) => + const Divider(height: 1, color: MapPalette.border), + itemBuilder: (context, index) { + final c = results[index]; + final color = _getNodeColor(c.type); + return InkWell( + onTap: () => _onSearchResultTap(c, guessedLocations), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + Icon(_getNodeIcon(c.type), size: 18, color: color), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + c.name, + style: const TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w600, + color: MapPalette.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + c.publicKeyHex.substring(0, 12), + style: MeshTheme.mono( + fontSize: 10.5, + color: MapPalette.textSecondary, + ), + ), + ], + ), + ), + if (c.hasLocation) + Icon( + Icons.chevron_right, + size: 18, + color: MapPalette.textSecondary, + ) + else + Text( + context.l10n.map_noGps.toUpperCase(), + style: MeshTheme.accentLabel( + color: MapPalette.textMuted, + fontSize: 8.5, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + void _onSearchResultTap( + Contact contact, + List<_GuessedLocation> guessedLocations, + ) { + if (contact.hasLocation) { + _selectNode(contact); + _mapController.move( + LatLng(contact.latitude!, contact.longitude!), + max(_zoom, 14), + ); + return; + } + _GuessedLocation? guess; + for (final g in guessedLocations) { + if (g.contact.publicKeyHex == contact.publicKeyHex) { + guess = g; + break; + } + } + if (guess != null) { + _selectNode(contact, guessedPosition: guess.position); + _mapController.move(guess.position, max(_zoom, 13)); + } else { + setState(() { + _searchQuery = ''; + _searchController.clear(); + _searchFocus.unfocus(); + }); + _showNodeInfo(context, contact); + } + } + + Widget _buildStatsCard( + BuildContext context, { + required dynamic settings, + required int visibleCount, + required int onlineCount, + required int repeaterCount, + required int hiddenCount, + required int pinCount, + required int guessedCount, + }) { + return Container( + margin: const EdgeInsets.only(top: 6), + width: 230, + padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), + decoration: BoxDecoration( + color: MapPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MapPalette.border), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _statRow(context.l10n.map_visible, visibleCount, MapPalette.selected), + _statRow(context.l10n.map_online, onlineCount, MapPalette.online), + _statRow( + context.l10n.map_repeaters, + repeaterCount, + MapPalette.repeater, + ), + _statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline), + _statRow(context.l10n.map_markers, pinCount, MapPalette.shared), + const Divider(height: 16, color: MapPalette.border), + _buildLegendItem( + Icons.person, + context.l10n.map_chat, + MapPalette.selected, + ), + _buildLegendItem( + Icons.router, + context.l10n.map_repeater, + MapPalette.repeater, + ), + _buildLegendItem( + Icons.meeting_room, + context.l10n.map_room, + MapPalette.router, + ), + _buildLegendItem( + Icons.sensors, + context.l10n.map_sensor, + MapPalette.sensor, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinDm, + MapPalette.shared, + ), + if (settings.mapShowGuessedLocations && guessedCount > 0) + _buildLegendItem( + Icons.not_listed_location, + context.l10n.map_guessedLocation, + MapPalette.textMuted, + ), + ], + ), + ); + } + + Widget _statRow(String label, int value, Color color) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Container( + width: 7, + height: 7, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '$value', + style: MeshTheme.mono( + fontSize: 13, + fontWeight: FontWeight.w700, + color: MapPalette.textPrimary, + ), + ), + ], + ), + ); + } + + Widget _buildSelectedNodeCard( + BuildContext context, + Contact contact, + MeshCoreConnector connector, + ) { + final color = _markerColor(contact); + final age = _ageOf(contact); + final pos = contact.hasLocation + ? LatLng(contact.latitude!, contact.longitude!) + : _selectedGuessPos; + return Positioned( + left: 12, + right: 12, + bottom: 12, + child: TweenAnimationBuilder( + tween: Tween(begin: 1, end: 0), + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + builder: (context, t, child) => Transform.translate( + offset: Offset(0, 32 * t), + child: Opacity(opacity: 1 - t, child: child), + ), + child: MeshCard( + margin: EdgeInsets.zero, + padding: const EdgeInsets.fromLTRB(14, 12, 8, 12), + color: MapPalette.panelDark, + borderColor: MapPalette.border, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AvatarCircle( + name: contact.name, + size: 38, + color: color, + icon: _getNodeIcon(contact.type), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + contact.name, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: MapPalette.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (contact.isFavorite) ...[ + const SizedBox(width: 4), + const Icon( + Icons.star, + size: 14, + color: MapPalette.stale, + ), + ], + ], + ), + const SizedBox(height: 3), + Row( + children: [ + StatusChip( + label: _ageLabel(age), + color: _ageColor(age), + fontSize: 9.5, + pulse: age == _NodeAge.online, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + contact.typeLabel(context.l10n), + style: const TextStyle( + fontSize: 11.5, + color: MapPalette.textSecondary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + if (pos != null) + IconButton( + color: MapPalette.textPrimary, + icon: const Icon(Icons.center_focus_strong, size: 20), + tooltip: context.l10n.map_centerOnNode, + onPressed: () => _mapController.move(pos, max(_zoom, 15)), + ), + IconButton( + color: MapPalette.textPrimary, + icon: const Icon(Icons.close, size: 20), + onPressed: _clearSelection, + ), + ], + ), + const SizedBox(height: 8), + Wrap( + spacing: 14, + runSpacing: 4, + children: [ + _miniMeta( + context.l10n.map_lastSeen, + _formatLastSeen(contact.lastSeen), + ), + _miniMeta( + context.l10n.map_path, + contact.pathLabel(context.l10n), + ), + _miniMeta('ID', contact.publicKeyHex.substring(0, 12)), + if (pos != null) + _miniMeta( + context.l10n.map_location, + '${contact.hasLocation ? '' : '~'}${pos.latitude.toStringAsFixed(5)}, ${pos.longitude.toStringAsFixed(5)}', + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + ..._selectedNodeActions(context, contact, connector), + TextButton( + style: TextButton.styleFrom( + foregroundColor: MapPalette.selected, + ), + onPressed: () => _showNodeInfo( + context, + contact, + guessedPosition: contact.hasLocation + ? null + : _selectedGuessPos, + ), + child: Text(context.l10n.map_details), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _miniMeta(String label, String value) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label.toUpperCase(), + style: MeshTheme.accentLabel( + color: MapPalette.textMuted, + fontSize: 8, + ), + ), + const SizedBox(height: 1), + Text( + value, + style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary), + ), + ], + ); + } + + List _selectedNodeActions( + BuildContext context, + Contact contact, + MeshCoreConnector connector, + ) { + Widget action(String label, IconData icon, VoidCallback onPressed) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FilledButton.icon( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + visualDensity: VisualDensity.compact, + ), + onPressed: onPressed, + icon: Icon(icon, size: 16), + label: Text(label, style: const TextStyle(fontSize: 12.5)), + ), + ); + } + + switch (contact.type) { + case advTypeChat: + return [ + action(context.l10n.contacts_openChat, Icons.chat_bubble_outline, () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } + final unread = connector.getUnreadCountForContactKey( + contact.publicKeyHex, + ); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ChatScreen(contact: contact, initialUnreadCount: unread), + ), + ); + }), + ]; + case advTypeRepeater: + return [ + action(context.l10n.map_manageRepeater, Icons.cell_tower, () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } + _showRepeaterLogin(context, contact); + }), + ]; + case advTypeRoom: + return [ + action(context.l10n.map_joinRoom, Icons.meeting_room, () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } + _showRoomLogin(context, contact); + }), + ]; + default: + return const []; + } + } + List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) { // Build a _SharedMarker per message (history empty), grouped by dedupe key. // Afterwards pick the latest per key and fill its history from older ones. @@ -1468,8 +2468,8 @@ class _MapScreenState extends State { Marker _buildSharedMarker(_SharedMarker marker) { final markerColor = marker.isChannel - ? (marker.isPublicChannel ? Colors.orange : Colors.purple) - : Colors.blue; + ? (marker.isPublicChannel ? MapPalette.cluster : MapPalette.router) + : MapPalette.shared; return Marker( point: marker.position, width: 60, @@ -1487,20 +2487,22 @@ class _MapScreenState extends State { child: Column( children: [ Container( - padding: const EdgeInsets.all(6), + width: 36, + height: 36, decoration: BoxDecoration( - color: markerColor, shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ + color: markerColor, + border: Border.all(color: MapPalette.markerOutline, width: 2.5), + boxShadow: const [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + color: MapPalette.markerShadow, + blurRadius: 8, + offset: Offset(0, 3), ), ], ), - child: const Icon(Icons.flag, color: Colors.white, size: 20), + alignment: Alignment.center, + child: const Icon(Icons.flag, color: Colors.white, size: 19), ), ], ), @@ -1560,9 +2562,8 @@ class _MapScreenState extends State { LatLng? guessedPosition, }) { final connector = context.read(); - showModalBottomSheet( - context: context, - showDragHandle: true, + showMeshSheet( + context, builder: (sheetContext) { final actions = []; if (contact.type == advTypeChat) { @@ -1619,58 +2620,53 @@ class _MapScreenState extends State { ); } return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BottomSheetHeader( + title: contact.name, + subtitle: contact.typeLabel(context.l10n), + trailing: Icon( + _getNodeIcon(contact.type), + color: _getNodeColor(contact.type), + size: 20, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - _getNodeIcon(contact.type), - color: _getNodeColor(contact.type), + _buildInfoRow( + context.l10n.map_path, + contact.pathLabel(context.l10n), ), - const SizedBox(width: 8), - Expanded(child: SelectableText(contact.name)), + if (contact.hasLocation) + _buildInfoRow( + context.l10n.map_location, + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + context.l10n.map_estLocation, + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), + _buildInfoRow( + context.l10n.map_lastSeen, + _formatLastSeen(contact.lastSeen), + ), + _buildInfoRow( + context.l10n.map_publicKey, + contact.publicKeyHex, + ), + const SizedBox(height: 16), + ...actions, ], ), - const SizedBox(height: 8), - _buildInfoRow( - context.l10n.map_type, - contact.typeLabel(context.l10n), - ), - _buildInfoRow( - context.l10n.map_path, - contact.pathLabel(context.l10n), - ), - if (contact.hasLocation) - _buildInfoRow( - context.l10n.map_location, - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ) - else if (guessedPosition != null) - _buildInfoRow( - context.l10n.map_estLocation, - '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', - ), - _buildInfoRow( - context.l10n.map_lastSeen, - _formatLastSeen(contact.lastSeen), - ), - _buildInfoRow( - context.l10n.map_publicKey, - contact.publicKeyHex, - ), - const SizedBox(height: 16), - ...actions, - TextButton( - onPressed: () => Navigator.pop(sheetContext), - child: Text(context.l10n.common_close), - ), - ], - ), + ), + ], ), ), ); @@ -1798,7 +2794,13 @@ class _MapScreenState extends State { ), ), const SizedBox(height: 2), - SelectableText(value, style: const TextStyle(fontSize: 14)), + SelectableText( + value, + style: MeshTheme.mono( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurface, + ), + ), ], ), ); @@ -2057,7 +3059,9 @@ class _MapScreenState extends State { return ListTile( leading: Icon( isPublic ? Icons.public : Icons.tag, - color: isPublic ? Colors.orange : Colors.blue, + color: isPublic + ? MapPalette.cluster + : MapPalette.repeater, ), title: Text(label), onTap: () async { @@ -2115,164 +3119,205 @@ class _MapScreenState extends State { return result ?? false; } - void _showFilterDialog( + void _showFilterSheet( BuildContext context, AppSettingsService settingsService, ) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(context.l10n.map_filterNodes), - contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), - content: SingleChildScrollView( - child: Consumer( + showMeshSheet( + context, + builder: (sheetContext) => StatefulBuilder( + builder: (sheetContext, setSheetState) { + return Consumer( builder: (consumerContext, service, child) { final settings = service.settings; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.map_nodeTypes, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - CheckboxListTile( - title: Text(context.l10n.map_chatNodes), - value: settings.mapShowChatNodes, - onChanged: (value) { - service.setMapShowChatNodes(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), - CheckboxListTile( - title: Text(context.l10n.map_repeaters), - value: settings.mapShowRepeaters, - onChanged: (value) { - service.setMapShowRepeaters(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), - CheckboxListTile( - title: Text(context.l10n.map_otherNodes), - value: settings.mapShowOtherNodes, - onChanged: (value) { - service.setMapShowOtherNodes(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), - CheckboxListTile( - title: Text(context.l10n.map_showGuessedLocations), - value: settings.mapShowGuessedLocations, - onChanged: (value) { - service.setMapShowGuessedLocations(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), - CheckboxListTile( - title: Text(context.l10n.map_showDiscoveryContacts), - value: settings.mapShowDiscoveryContacts, - onChanged: (value) { - service.setMapShowDiscoveryContacts(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), - CheckboxListTile( - title: Text(context.l10n.map_showOverlaps), - value: settings.mapShowOverlaps, - onChanged: (value) { - service.setMapShowOverlaps(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), + final scheme = Theme.of(sheetContext).colorScheme; - const SizedBox(height: 16), - Text( - context.l10n.map_keyPrefix, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + Widget freshnessChip(_Freshness value, String label) { + final selected = _freshness == value; + final accent = switch (value) { + _Freshness.all => MapPalette.selected, + _Freshness.online => MapPalette.online, + _Freshness.recent => MapPalette.stale, + _Freshness.stale => MapPalette.offline, + }; + return FilterChip( + label: Text(label), + selected: selected, + showCheckmark: true, + checkmarkColor: accent, + backgroundColor: scheme.surfaceContainerLow, + selectedColor: Color.alphaBlend( + accent.withValues(alpha: 0.22), + scheme.surfaceContainerHigh, + ), + side: BorderSide( + color: selected ? accent : scheme.outline, + width: selected ? 1.5 : 1, + ), + labelStyle: TextStyle( + color: selected + ? scheme.onSurface + : scheme.onSurfaceVariant, + fontWeight: selected ? FontWeight.w700 : FontWeight.w600, + ), + onSelected: (_) { + setSheetState(() {}); + setState(() => _freshness = value); + }, + ); + } + + return SafeArea( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(sheetContext).height * 0.8, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BottomSheetHeader( + title: sheetContext.l10n.map_filterNodes, + ), + SectionHeader(sheetContext.l10n.map_activity), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 8, + runSpacing: 4, + children: [ + freshnessChip( + _Freshness.all, + sheetContext.l10n.time_allTime, + ), + freshnessChip( + _Freshness.online, + sheetContext.l10n.map_online, + ), + freshnessChip( + _Freshness.recent, + sheetContext.l10n.map_recent, + ), + freshnessChip( + _Freshness.stale, + sheetContext.l10n.map_stale, + ), + ], + ), + ), + SectionHeader( + sheetContext.l10n.map_lastSeenTime, + trailing: Text( + _getTimeFilterLabel(settings.mapTimeFilterHours), + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Slider( + value: _hoursToSliderValue( + settings.mapTimeFilterHours, + ), + min: 0, + max: 100, + divisions: 100, + onChanged: (value) { + final hours = _sliderValueToHours(value); + service.setMapTimeFilterHours(hours); + }, + ), + ), + SectionHeader(sheetContext.l10n.map_nodeTypes), + SwitchListTile( + title: Text(sheetContext.l10n.map_chatNodes), + value: settings.mapShowChatNodes, + dense: true, + onChanged: (value) => + service.setMapShowChatNodes(value), + ), + SwitchListTile( + title: Text(sheetContext.l10n.map_repeaters), + value: settings.mapShowRepeaters, + dense: true, + onChanged: (value) => + service.setMapShowRepeaters(value), + ), + SwitchListTile( + title: Text(sheetContext.l10n.map_otherNodes), + value: settings.mapShowOtherNodes, + dense: true, + onChanged: (value) => + service.setMapShowOtherNodes(value), + ), + SectionHeader(sheetContext.l10n.map_markers), + SwitchListTile( + title: Text(sheetContext.l10n.map_showSharedMarkers), + value: settings.mapShowMarkers, + dense: true, + onChanged: (value) => + service.setMapShowMarkers(value), + ), + SwitchListTile( + title: Text( + sheetContext.l10n.map_showGuessedLocations, + ), + value: settings.mapShowGuessedLocations, + dense: true, + onChanged: (value) => + service.setMapShowGuessedLocations(value), + ), + SwitchListTile( + title: Text( + sheetContext.l10n.map_showDiscoveryContacts, + ), + value: settings.mapShowDiscoveryContacts, + dense: true, + onChanged: (value) => + service.setMapShowDiscoveryContacts(value), + ), + SwitchListTile( + title: Text(sheetContext.l10n.map_showOverlaps), + value: settings.mapShowOverlaps, + dense: true, + onChanged: (value) => + service.setMapShowOverlaps(value), + ), + SectionHeader(sheetContext.l10n.map_keyPrefix), + SwitchListTile( + title: Text(sheetContext.l10n.map_filterByKeyPrefix), + value: settings.mapKeyPrefixEnabled, + dense: true, + onChanged: (value) => + service.setMapKeyPrefixEnabled(value), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 20), + child: TextFormField( + initialValue: settings.mapKeyPrefix, + enabled: settings.mapKeyPrefixEnabled, + decoration: InputDecoration( + labelText: sheetContext.l10n.map_publicKeyPrefix, + hintText: + sheetContext.l10n.map_publicKeyPrefixHint, + isDense: true, + ), + style: MeshTheme.mono(fontSize: 13), + onChanged: (value) => + service.setMapKeyPrefix(value), + ), + ), + ], ), ), - const SizedBox(height: 8), - CheckboxListTile( - title: Text(context.l10n.map_filterByKeyPrefix), - value: settings.mapKeyPrefixEnabled, - onChanged: (value) { - service.setMapKeyPrefixEnabled(value ?? false); - }, - contentPadding: EdgeInsets.zero, - ), - TextFormField( - initialValue: settings.mapKeyPrefix, - enabled: settings.mapKeyPrefixEnabled, - decoration: InputDecoration( - labelText: context.l10n.map_publicKeyPrefix, - hintText: context.l10n.map_publicKeyPrefixHint, - border: const OutlineInputBorder(), - isDense: true, - ), - onChanged: (value) { - service.setMapKeyPrefix(value); - }, - ), - const SizedBox(height: 16), - Text( - context.l10n.map_markers, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - CheckboxListTile( - title: Text(context.l10n.map_showSharedMarkers), - value: settings.mapShowMarkers, - onChanged: (value) { - service.setMapShowMarkers(value ?? true); - }, - contentPadding: EdgeInsets.zero, - ), - const SizedBox(height: 16), - Text( - context.l10n.map_lastSeenTime, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - _getTimeFilterLabel(settings.mapTimeFilterHours), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - Slider( - value: _hoursToSliderValue(settings.mapTimeFilterHours), - min: 0, - max: 100, - divisions: 100, - onChanged: (value) { - final hours = _sliderValueToHours(value); - service.setMapTimeFilterHours(hours); - }, - ), - ], + ), ); }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(context.l10n.common_close), - ), - ], + ); + }, ), ); } @@ -2385,110 +3430,129 @@ class _MapScreenState extends State { top: 16, left: 16, right: 16, - child: Card( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.contacts_pathTrace, - style: TextStyle(fontWeight: FontWeight.bold), - ), - if (_pathTrace.isEmpty) const SizedBox(height: 8), - if (_pathTrace.isEmpty) - Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), - const SizedBox(height: 6), - if (_pathTrace.isNotEmpty) + child: DecoratedBox( + decoration: BoxDecoration( + color: MapPalette.panelDark, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MapPalette.border), + boxShadow: const [ + BoxShadow( + color: MapPalette.markerShadow, + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(MeshRadii.md), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Text( - "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, + l10n.contacts_pathTrace, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (_pathTrace.isEmpty) const SizedBox(height: 8), + if (_pathTrace.isEmpty) + Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), + const SizedBox(height: 6), + if (_pathTrace.isNotEmpty) + Text( + "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", + style: MeshTheme.mono( + fontSize: 12, + color: MapPalette.textSecondary, + ), + ), + SelectableText( + _pathTrace + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(','), + style: MeshTheme.mono( + fontSize: 18, + fontWeight: FontWeight.w700, + color: MapPalette.selected, ), ), - SelectableText( - _pathTrace - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(','), - style: TextStyle(fontSize: 18), - ), - // const SizedBox(height: 6), - Wrap( - alignment: WrapAlignment.center, - spacing: 1, - runSpacing: 1, - children: [ - if (_pathTrace.isNotEmpty) - IconButton( - onPressed: () { - final hashW = context - .read() - .pathHashByteWidth; - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PathTraceMapScreen( - title: l10n.contacts_pathTrace, - path: Uint8List.fromList(_pathTrace), - pathHashByteWidth: hashW, - pathContacts: _pathTraceContacts, + // const SizedBox(height: 6), + Wrap( + alignment: WrapAlignment.center, + spacing: 1, + runSpacing: 1, + children: [ + if (_pathTrace.isNotEmpty) + IconButton( + onPressed: () { + final hashW = context + .read() + .pathHashByteWidth; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: l10n.contacts_pathTrace, + path: Uint8List.fromList(_pathTrace), + pathHashByteWidth: hashW, + pathContacts: _pathTraceContacts, + ), ), - ), - ); - setState(() { - _isBuildingPathTrace = false; - }); - }, - tooltip: l10n.map_runTrace, - icon: const Icon(Icons.arrow_forward_outlined), - ), - if (_pathTrace.isNotEmpty) - IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PathTraceMapScreen( - title: l10n.contacts_pathTrace, - path: Uint8List.fromList(_pathTrace), - flipPathAround: true, + ); + setState(() { + _isBuildingPathTrace = false; + }); + }, + tooltip: l10n.map_runTrace, + icon: const Icon(Icons.arrow_forward_outlined), + ), + if (_pathTrace.isNotEmpty) + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: l10n.contacts_pathTrace, + path: Uint8List.fromList(_pathTrace), + flipPathAround: true, + ), ), - ), - ); - setState(() { - _isBuildingPathTrace = false; - }); - }, - tooltip: l10n.map_runTraceWithReturnPath, - icon: const Icon(Icons.replay), - ), - if (_pathTrace.isNotEmpty) - IconButton( - onPressed: _removePath, - tooltip: l10n.map_removeLast, - icon: const Icon(Icons.undo), - ), - if (_pathTrace.isEmpty) - IconButton( - onPressed: () { - setState(() { - _isBuildingPathTrace = false; - _pathTrace.clear(); - _points.clear(); - _polylines.clear(); - }); - showDismissibleSnackBar( - context, - content: Text(l10n.map_pathTraceCancelled), - ); - }, - tooltip: l10n.common_cancel, - icon: const Icon(Icons.close), - ), - ], - ), - ], + ); + setState(() { + _isBuildingPathTrace = false; + }); + }, + tooltip: l10n.map_runTraceWithReturnPath, + icon: const Icon(Icons.replay), + ), + if (_pathTrace.isNotEmpty) + IconButton( + onPressed: _removePath, + tooltip: l10n.map_removeLast, + icon: const Icon(Icons.undo), + ), + if (_pathTrace.isEmpty) + IconButton( + onPressed: () { + setState(() { + _isBuildingPathTrace = false; + _pathTrace.clear(); + _points.clear(); + _polylines.clear(); + }); + showDismissibleSnackBar( + context, + content: Text(l10n.map_pathTraceCancelled), + ); + }, + tooltip: l10n.common_cancel, + icon: const Icon(Icons.close), + ), + ], + ), + ], + ), ), ), ), @@ -2496,6 +3560,10 @@ class _MapScreenState extends State { } } +enum _NodeAge { online, recent, stale } + +enum _Freshness { all, online, recent, stale } + class _GuessedLocation { final Contact contact; final LatLng position; diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index c34af7f1..d043fbfe 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -9,9 +9,10 @@ import '../models/path_selection.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/empty_state.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/routing_sheet.dart'; -import '../widgets/snr_indicator.dart'; import '../helpers/snack_bar_builder.dart'; class NeighborsScreen extends StatefulWidget { @@ -321,7 +322,7 @@ class _NeighborsScreenState extends State { child: RefreshIndicator( onRefresh: _loadNeighbors, child: ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), children: [ if (!_isLoaded && !_hasData && @@ -330,9 +331,7 @@ class _NeighborsScreenState extends State { if (_isLoaded || _hasData && !(_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) - _buildNeighborsInfoCard( - "${l10n.repeater_neighbors} - $_neighborCount", - ), + _buildNeighborsList(connector), ], ), ), @@ -340,81 +339,100 @@ class _NeighborsScreenState extends State { ); } - Widget _buildNeighborsInfoCard(String title) { - final connector = Provider.of(context, listen: false); - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + Widget _buildNeighborsList(MeshCoreConnector connector) { + final l10n = context.l10n; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader( + '${l10n.repeater_neighbors} — $_neighborCount', + padding: const EdgeInsets.fromLTRB(4, 8, 4, 10), + ), + for (var i = 0; i < _parsedNeighbors!.length; i++) + ListEntrance( + index: i, + child: _buildNeighborRow( + _parsedNeighbors![i], + connector.currentSf, + ), + ), + ], + ); + } + + Widget _buildNeighborRow(Map data, int? spreadingFactor) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + final Contact? contact = data['contact'] as Contact?; + final double snr = data['snr'] as double; + final int lastHeardSeconds = data['lastHeard'] as int; + + final name = contact != null + ? contact.name + : l10n.neighbors_unknownContact( + '<${pubKeyToHex(data['publicKey'] as Uint8List)}>', + ); + + final snrColor = MeshTheme.snrColor(snr, blocked: false); + final heardLabel = l10n.neighbors_heardAgo( + fmtDuration(lastHeardSeconds + 0.0), + ); + + return MeshCard( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + margin: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + AvatarCircle( + name: name, + size: 40, + color: contact != null ? MeshPalette.warn : scheme.onSurfaceVariant, + icon: contact != null ? Icons.cell_tower : Icons.device_unknown, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), Text( - title, + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ), + const SizedBox(height: 2), + Text( + heardLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), - const Divider(), - for (final entry in _parsedNeighbors!.asMap().entries) - _buildInfoRow( - entry.value['contact'] != null - ? entry.value['contact'].name - : context.l10n.neighbors_unknownContact( - "<${pubKeyToHex(entry.value['publicKey'])}>", - ), - context.l10n.neighbors_heardAgo( - fmtDuration(entry.value['lastHeard'] + 0.0), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + SignalBars(snr: snr, height: 16), + const SizedBox(height: 4), + Text( + '${snr.toStringAsFixed(1)} dB', + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w600, + color: snrColor, ), - entry.value['snr'], - connector.currentSf, ), - ], - ), - ), - ); - } - - Widget _buildInfoRow( - String label, - String value, - double snr, - int? spreadingFactor, - ) { - final snrUi = snrUiFromSNR(snr, spreadingFactor); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 3), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - label, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text(value), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(snrUi.icon, color: snrUi.color, size: 18.0), - Text( - snrUi.text, - style: TextStyle(fontSize: 10, color: snrUi.color), - ), - ], - ), - ), + ], ), ], ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 99bc4deb..d955b30b 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -10,31 +10,21 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/l10n/l10n.dart'; import 'package:meshcore_open/models/app_settings.dart'; import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/models/display_path.dart'; +import 'package:meshcore_open/models/path_history.dart'; +import 'package:meshcore_open/models/path_playback.dart'; import 'package:meshcore_open/services/app_settings_service.dart'; import 'package:meshcore_open/services/map_tile_cache_service.dart'; +import 'package:meshcore_open/services/path_history_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; +import 'package:meshcore_open/widgets/path_map_ui.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; +import 'package:meshcore_open/widgets/themed_map_tile_layer.dart'; import 'package:provider/provider.dart'; +import '../theme/mesh_theme.dart'; -double getPathDistanceMeters(List points) { - if (points.length <= 1) return 0.0; - - double distanceMeters = 0.0; - final distanceCalculator = Distance(); - - for (int i = 0; i < points.length - 1; i++) { - distanceMeters += distanceCalculator(points[i], points[i + 1]); - } - - return distanceMeters; -} - -String formatDistance(double distanceMeters, {required bool isImperial}) { - if (isImperial) { - return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)'; - } - return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)'; -} +export 'package:meshcore_open/widgets/path_map_ui.dart' + show formatDistance, getPathDistanceMeters; class PathTraceData { final Uint8List pathData; @@ -74,7 +64,8 @@ class PathTraceMapScreen extends StatefulWidget { State createState() => _PathTraceMapScreenState(); } -class _PathTraceMapScreenState extends State { +class _PathTraceMapScreenState extends State + with SingleTickerProviderStateMixin { static const double _labelZoomThreshold = 8.5; static const double _mapMinZoom = 2.0; static const double _mapMaxZoom = 18.0; @@ -107,6 +98,18 @@ class _PathTraceMapScreenState extends State { // endpoint inference so it matches the path that was actually traced. Uint8List _tracedPath = Uint8List(0); + // Packet-flow animation + multi-path view state. + late final PathPlaybackController _playback; + PathHistoryService? _pathHistory; + PathViewMode _viewMode = PathViewMode.single; + List _displayPaths = []; + List _primaryOutboundHops = []; + String _selectedPathId = 'primary'; + final Set _hiddenPathIds = {}; + bool _panelCollapsed = false; + bool _animationEnabled = true; + bool _followPacket = false; + String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) @@ -116,18 +119,51 @@ class _PathTraceMapScreenState extends State { @override void initState() { super.initState(); + _playback = PathPlaybackController(this); + _playback.addListener(_followPacketCamera); + _pathHistory = context.read(); + _pathHistory!.addListener(_onPathHistoryChanged); _setupFrameListener(); _doPathTrace(); } @override void dispose() { + _pathHistory?.removeListener(_onPathHistoryChanged); + _playback.dispose(); _mapController.dispose(); _frameSubscription?.cancel(); _timeoutTimer?.cancel(); super.dispose(); } + void _onPathHistoryChanged() { + if (!mounted || !_hasData) return; + setState(() { + _rebuildDisplayPaths(context.read()); + }); + } + + /// Keeps the camera centered on the packet while the follow lock is on. + void _followPacketCamera() { + if (!_followPacket || + !_animationEnabled || + !_playback.started || + !_playback.hasPath || + !mounted || + !_hasData) { + return; + } + _mapController.move(_playback.position, _mapController.camera.zoom); + } + + void _toggleFollowPacket() { + setState(() { + _followPacket = !_followPacket; + }); + _followPacketCamera(); + } + bool _isDesktopPlatform(TargetPlatform platform) { return platform == TargetPlatform.linux || platform == TargetPlatform.windows || @@ -164,27 +200,34 @@ class _PathTraceMapScreenState extends State { return Positioned( top: 16, left: 16, - child: Card( - elevation: 4, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.add), - tooltip: context.l10n.map_zoomIn, - onPressed: () => _zoomMapBy(1), - ), - IconButton( - icon: const Icon(Icons.remove), - tooltip: context.l10n.map_zoomOut, - onPressed: () => _zoomMapBy(-1), - ), - IconButton( - icon: const Icon(Icons.my_location), - tooltip: context.l10n.map_centerMap, - onPressed: _resetMapView, - ), - ], + child: DecoratedBox( + decoration: BoxDecoration( + color: MeshPalette.bg1.withValues(alpha: 0.90), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MeshPalette.line2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(MeshRadii.md), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: context.l10n.map_zoomIn, + onPressed: () => _zoomMapBy(1), + ), + IconButton( + icon: const Icon(Icons.remove), + tooltip: context.l10n.map_zoomOut, + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + icon: const Icon(Icons.my_location), + tooltip: context.l10n.map_centerMap, + onPressed: _resetMapView, + ), + ], + ), ), ), ); @@ -249,6 +292,7 @@ class _PathTraceMapScreenState extends State { } Future _doPathTrace() async { + _playback.stop(); if (mounted) { setState(() { _isLoading = true; @@ -542,6 +586,8 @@ class _PathTraceMapScreenState extends State { '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', ); _pathDistanceMeters = getPathDistanceMeters(_points); + _primaryOutboundHops = _outboundHops(pathData); + _rebuildDisplayPaths(connector); }); } catch (e) { appLogger.error( @@ -557,6 +603,252 @@ class _PathTraceMapScreenState extends State { } } + /// Outbound hop bytes of the traced path, mirroring the round-trip + /// dedup logic used when building [_points]. + List _outboundHops(Uint8List pathData) { + final hops = []; + int hopLast = 0; + int hopLastLast = 0; + for (final hop in pathData) { + if (hop == hopLastLast && widget.flipPathAround) break; + hops.add(hop); + hopLastLast = hopLast; + hopLast = hop; + } + return hops; + } + + Contact? _contactForHop(int hop, MeshCoreConnector connector) { + final traced = _traceData?.pathContacts[hop]; + if (traced != null) return traced; + for (final c in connector.allContactsUnfiltered) { + if (c.type != advTypeChat && + c.publicKey.isNotEmpty && + c.publicKey[0] == hop) { + return c; + } + } + return null; + } + + LatLng? _inferredPositionForHop(int hop, MeshCoreConnector connector) { + final cached = _inferredHopPositions[hop]; + if (cached != null) return cached; + final peers = connector.contacts + .where((c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop) + .toList(); + if (peers.isEmpty) return null; + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length; + final pos = LatLng(lat, lon); + _inferredHopPositions[hop] = pos; + return pos; + } + + /// Rebuilds the renderable paths: the traced path as primary plus up to + /// four distinct alternates from the target contact's path history. + void _rebuildDisplayPaths(MeshCoreConnector connector) { + final paths = []; + final primary = _buildDisplayPath( + id: 'primary', + label: context.l10n.pathMap_primary, + color: kPrimaryPathColor, + isPrimary: true, + hops: _primaryOutboundHops, + connector: connector, + ); + if (primary != null) paths.add(primary); + + final target = widget.targetContact; + final history = _pathHistory; + if (target != null && history != null) { + final seen = {_primaryOutboundHops.join(',')}; + var altIndex = 0; + for (final record in history.getRecentPaths(target.publicKeyHex)) { + if (record.pathBytes.isEmpty) continue; + if (!seen.add(record.pathBytes.join(','))) continue; + if (altIndex >= kAlternatePathColors.length) break; + final alt = _buildDisplayPath( + id: 'alt-${record.pathBytes.join('-')}', + label: context.l10n.pathMap_alternate(altIndex + 1), + color: kAlternatePathColors[altIndex], + isPrimary: false, + hops: record.pathBytes, + record: record, + connector: connector, + ); + if (alt != null) { + paths.add(alt); + altIndex++; + } + } + } + + _displayPaths = paths; + _hiddenPathIds.removeWhere((id) => !paths.any((p) => p.id == id)); + if (!paths.any((p) => p.id == _selectedPathId)) { + _selectedPathId = paths.isNotEmpty ? paths.first.id : 'primary'; + } + if (paths.length < 2) _viewMode = PathViewMode.single; + _syncPlaybackToSelection(); + } + + DisplayPath? _buildDisplayPath({ + required String id, + required String label, + required Color color, + required bool isPrimary, + required List hops, + required MeshCoreConnector connector, + PathRecord? record, + }) { + final selfLat = connector.selfLatitude; + final selfLon = connector.selfLongitude; + if (selfLat == null || selfLon == null) return null; + + final points = [LatLng(selfLat, selfLon)]; + final labels = [context.l10n.pathTrace_you]; + final confirmed = [true]; + final hopOrdinals = [-1]; + final gapBefore = [false]; + int gpsConfirmedHops = 0; + int unresolvedHops = 0; + bool pendingGap = false; + + for (var i = 0; i < hops.length; i++) { + final hop = hops[i]; + final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + final contact = _contactForHop(hop, connector); + LatLng? pos; + var isGps = false; + if (contact != null && contact.hasLocation) { + pos = LatLng(contact.latitude!, contact.longitude!); + isGps = true; + gpsConfirmedHops++; + } else { + pos = _inferredPositionForHop(hop, connector); + } + if (pos == null) { + unresolvedHops++; + pendingGap = true; + continue; + } + points.add(pos); + labels.add(contact?.name ?? '~$hex'); + confirmed.add(isGps); + hopOrdinals.add(i); + gapBefore.add(pendingGap); + pendingGap = false; + } + + // Append the chat-target endpoint the same way the traced path does. + final target = widget.targetContact; + final targetPos = _targetContactPosition; + final hasTargetEndpoint = + target != null && target.type == advTypeChat && targetPos != null; + if (hasTargetEndpoint) { + points.add(targetPos); + labels.add(target.name); + confirmed.add(!_targetContactIsGuessed); + hopOrdinals.add(hops.length); + gapBefore.add(pendingGap); + pendingGap = false; + } + + if (points.length < 2) return null; + + final segmentEstimated = []; + final rowForSegment = []; + for (var i = 0; i < points.length - 1; i++) { + segmentEstimated.add( + !confirmed[i] || !confirmed[i + 1] || gapBefore[i + 1], + ); + rowForSegment.add(hopOrdinals[i + 1] < 0 ? 0 : hopOrdinals[i + 1]); + } + + return DisplayPath( + id: id, + label: label, + color: color, + isPrimary: isPrimary, + hopBytes: List.from(hops), + points: points, + pointLabels: labels, + pointConfirmed: confirmed, + segmentEstimated: segmentEstimated, + rowForSegment: rowForSegment, + totalTransmissions: hops.length + (hasTargetEndpoint ? 1 : 0), + hasTargetEndpoint: hasTargetEndpoint, + gpsConfirmedHops: gpsConfirmedHops, + unresolvedHops: unresolvedHops, + distanceMeters: getPathDistanceMeters(points), + record: record, + ); + } + + DisplayPath? get _selectedPath { + if (_displayPaths.isEmpty) return null; + return _displayPaths.firstWhere( + (p) => p.id == _selectedPathId, + orElse: () => _displayPaths.first, + ); + } + + List get _visiblePaths { + if (_viewMode == PathViewMode.single) { + final selected = _selectedPath; + return selected != null ? [selected] : const []; + } + return _displayPaths + .where((p) => !_hiddenPathIds.contains(p.id)) + .toList(); + } + + /// Updates the playback path, but only when the selected path's geometry + /// actually changed, so unrelated path-history updates don't reset a + /// running animation. + void _syncPlaybackToSelection() { + final points = _selectedPath?.points ?? const []; + if (points.length == _playback.points.length) { + var same = true; + for (var i = 0; i < points.length; i++) { + if (points[i] != _playback.points[i]) { + same = false; + break; + } + } + if (same) return; + } + _playback.setPath(points); + } + + void _selectPath(DisplayPath path) { + setState(() { + _selectedPathId = path.id; + _hiddenPathIds.remove(path.id); + _syncPlaybackToSelection(); + }); + } + + void _togglePathVisibility(DisplayPath path) { + setState(() { + if (!_hiddenPathIds.remove(path.id)) { + _hiddenPathIds.add(path.id); + if (path.id == _selectedPathId) { + final visible = _displayPaths.where( + (p) => !_hiddenPathIds.contains(p.id), + ); + if (visible.isNotEmpty) { + _selectedPathId = visible.first.id; + _syncPlaybackToSelection(); + } + } + } + }); + } + @override Widget build(BuildContext context) { return Consumer( @@ -564,23 +856,12 @@ class _PathTraceMapScreenState extends State { final settings = context.watch().settings; final isImperial = settings.unitSystem == UnitSystem.imperial; final tileCache = context.read(); + final scheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - widget.title, - style: const TextStyle(fontSize: 24), - ), - ), - ], - ), - centerTitle: false, + title: Text(widget.title), + centerTitle: true, actions: [ IconButton( icon: _isLoading @@ -604,10 +885,14 @@ class _PathTraceMapScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (_isLoading) const CircularProgressIndicator(), + if (_isLoading) + CircularProgressIndicator(color: MeshPalette.blue), const SizedBox(height: 16), if (!_isLoading && _failed2Loaded) - Text(context.l10n.pathTrace_notAvailable), + Text( + context.l10n.pathTrace_notAvailable, + style: TextStyle(color: scheme.onSurfaceVariant), + ), ], ), ), @@ -615,25 +900,33 @@ class _PathTraceMapScreenState extends State { _buildMapPathTrace(context, tileCache, _targetContact), if (_hasData && _isDesktopPlatform(defaultTargetPlatform)) _buildDesktopMapControls(), + if (_hasData && _displayPaths.length > 1) + PathViewModeToggle( + mode: _viewMode, + onChanged: (mode) => setState(() => _viewMode = mode), + ), if (_points.isEmpty && !_hasData && !_isLoading && !_failed2Loaded) Center( - child: Card( - color: Theme.of( - context, - ).colorScheme.surface.withValues(alpha: 0.9), + child: DecoratedBox( + decoration: BoxDecoration( + color: scheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: scheme.outlineVariant), + ), child: Padding( - padding: EdgeInsets.all(12), + padding: const EdgeInsets.all(12), child: Text( context.l10n.channelPath_noRepeaterLocations, + style: TextStyle(color: scheme.onSurfaceVariant), ), ), ), ), if (_hasData) - _buildLegendCard(context, _traceData!, isImperial), + _buildBottomPanel(context, _traceData!, isImperial), ], ), ), @@ -676,28 +969,33 @@ class _PathTraceMapScreenState extends State { child: Container( width: 35, height: 35, - padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: hasGps - ? Colors.green - : Colors.orange.withValues(alpha: 0.75), shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), + color: hasGps + ? MeshPalette.signal.withValues(alpha: 0.18) + : MeshPalette.warn.withValues(alpha: 0.18), + border: Border.all( + color: hasGps + ? MeshPalette.signal.withValues(alpha: 0.7) + : MeshPalette.warn.withValues(alpha: 0.7), + width: 1.5, + ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + color: hasGps + ? MeshPalette.signal.withValues(alpha: 0.3) + : MeshPalette.warn.withValues(alpha: 0.3), + blurRadius: 5, ), ], ), alignment: Alignment.center, child: Text( hasGps ? label : '~$label', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + style: MeshTheme.mono( + fontSize: 10, + fontWeight: FontWeight.w700, + color: hasGps ? MeshPalette.signal : MeshPalette.warn, ), ), ), @@ -716,6 +1014,17 @@ class _PathTraceMapScreenState extends State { hopLast = hop; } + _addEndpointMarkers(markers, showLabels: showLabels, target: target); + + return markers; + } + + /// Self and target endpoint markers, shared by single and combined views. + void _addEndpointMarkers( + List markers, { + required bool showLabels, + required Contact? target, + }) { final selfLat = context.read().selfLatitude; final selfLon = context.read().selfLongitude; if (selfLat != null && selfLon != null) { @@ -729,26 +1038,27 @@ class _PathTraceMapScreenState extends State { child: Container( width: 35, height: 35, - padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.blue, shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), + color: MeshPalette.blue.withValues(alpha: 0.18), + border: Border.all( + color: MeshPalette.blue.withValues(alpha: 0.7), + width: 2, + ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + color: MeshPalette.blue.withValues(alpha: 0.35), + blurRadius: 6, ), ], ), alignment: Alignment.center, child: Text( context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + style: MeshTheme.mono( + fontSize: 9, + fontWeight: FontWeight.w700, + color: MeshPalette.blue, ), ), ), @@ -779,23 +1089,32 @@ class _PathTraceMapScreenState extends State { child: Container( width: 35, height: 35, - padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: isGuessed - ? Colors.purple.withValues(alpha: 0.55) - : Colors.red, shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), + color: isGuessed + ? MeshPalette.magenta.withValues(alpha: 0.18) + : MeshPalette.alert.withValues(alpha: 0.18), + border: Border.all( + color: isGuessed + ? MeshPalette.magenta.withValues(alpha: 0.7) + : MeshPalette.alert.withValues(alpha: 0.7), + width: 1.5, + ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + color: isGuessed + ? MeshPalette.magenta.withValues(alpha: 0.3) + : MeshPalette.alert.withValues(alpha: 0.3), + blurRadius: 5, ), ], ), alignment: Alignment.center, - child: const Icon(Icons.person, color: Colors.white, size: 18), + child: Icon( + Icons.person, + color: isGuessed ? MeshPalette.magenta : MeshPalette.alert, + size: 18, + ), ), ), ), @@ -809,10 +1128,133 @@ class _PathTraceMapScreenState extends State { ); } } + } + + /// Markers for the union of hops across all visible paths, with a badge on + /// repeaters used by more than one path. + List _buildCombinedHopMarkers({ + required bool showLabels, + required Contact? target, + }) { + final connector = context.read(); + final markers = []; + + // Hop byte -> paths that use it, in display order. + final hopPaths = >{}; + for (final path in _visiblePaths) { + for (final hop in path.hopBytes) { + final list = hopPaths.putIfAbsent(hop, () => []); + if (!list.contains(path)) list.add(path); + } + } + + for (final entry in hopPaths.entries) { + final hop = entry.key; + final paths = entry.value; + final contact = _contactForHop(hop, connector); + final hasGps = contact != null && contact.hasLocation; + final point = hasGps + ? LatLng(contact.latitude!, contact.longitude!) + : _inferredPositionForHop(hop, connector); + if (point == null) continue; + final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + final baseColor = hasGps ? MeshPalette.signal : MeshPalette.warn; + final shared = paths.length > 1; + + markers.add( + Marker( + point: point, + width: 48, + height: 48, + child: GestureDetector( + onTap: () => _showSharedNodeSheet(hop, contact, paths), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 35, + height: 35, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: baseColor.withValues(alpha: 0.18), + border: Border.all( + color: baseColor.withValues(alpha: 0.7), + width: shared ? 2.5 : 1.5, + ), + boxShadow: [ + BoxShadow( + color: baseColor.withValues(alpha: 0.3), + blurRadius: 5, + ), + ], + ), + alignment: Alignment.center, + child: Text( + hasGps ? label : '~$label', + style: MeshTheme.mono( + fontSize: 10, + fontWeight: FontWeight.w700, + color: baseColor, + ), + ), + ), + if (shared) + Positioned( + top: 0, + right: 0, + child: Container( + width: 17, + height: 17, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MeshPalette.bg1, + border: Border.all(color: MeshPalette.line3), + ), + alignment: Alignment.center, + child: Text( + '${paths.length}', + style: MeshTheme.mono( + fontSize: 9, + fontWeight: FontWeight.w700, + color: MeshPalette.ink, + ), + ), + ), + ), + ], + ), + ), + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: point, + label: contact?.name ?? '~$label', + ), + ); + } + } + + _addEndpointMarkers(markers, showLabels: showLabels, target: target); return markers; } + void _showSharedNodeSheet( + int hop, + Contact? contact, + List paths, + ) { + final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + showSharedNodeSheet( + context, + title: '$hex: ${contact?.name ?? context.l10n.channelPath_unknownRepeater}', + paths: paths, + onSelect: _selectPath, + ); + } + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { return Marker( point: point, @@ -827,18 +1269,19 @@ class _PathTraceMapScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), + color: MeshPalette.bg.withValues(alpha: 0.82), + borderRadius: BorderRadius.circular(MeshRadii.xs), + border: Border.all(color: MeshPalette.line, width: 0.5), ), alignment: Alignment.center, child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, + style: MeshTheme.mono( + fontSize: 10, fontWeight: FontWeight.w500, + color: MeshPalette.ink2, ), ), ), @@ -941,8 +1384,15 @@ class _PathTraceMapScreenState extends State { minZoom: _mapMinZoom, maxZoom: _mapMaxZoom, onPositionChanged: (camera, hasGesture) { + if (!mounted) return; + // A manual pan/zoom releases the follow lock. + if (hasGesture && _followPacket) { + setState(() { + _followPacket = false; + }); + } final shouldShow = camera.zoom >= _labelZoomThreshold; - if (shouldShow != _showNodeLabels && mounted) { + if (shouldShow != _showNodeLabels) { setState(() { _showNodeLabels = shouldShow; }); @@ -950,14 +1400,23 @@ class _PathTraceMapScreenState extends State { }, ), children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: MapTileCacheService.userAgentPackageName, - maxZoom: 19, + ThemedMapTileLayer(tileCache: tileCache), + AnimatedBuilder( + animation: _playback, + builder: (context, _) { + final lines = _buildDisplayPolylines(); + if (lines.isEmpty) return const SizedBox.shrink(); + return PolylineLayer(polylines: lines); + }, ), - if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines), - if (_traceData!.pathData.isNotEmpty) + if (_viewMode == PathViewMode.combined) + MarkerLayer( + markers: _buildCombinedHopMarkers( + showLabels: _showNodeLabels, + target: target, + ), + ) + else if (_traceData!.pathData.isNotEmpty) MarkerLayer( markers: _buildHopMarkers( _traceData!.pathData, @@ -965,25 +1424,70 @@ class _PathTraceMapScreenState extends State { target: target, ), ), + AnimatedBuilder( + animation: _playback, + builder: (context, _) { + final markers = _buildPacketMarkers(); + if (markers.isEmpty) return const SizedBox.shrink(); + return MarkerLayer(markers: markers); + }, + ), ], ); } - Widget _colorDot(Color color) => Container( - width: 10, - height: 10, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ); + /// Polylines for the visible paths. While the packet animation is running, + /// the selected path's base line is dimmed and the traversed portion plus + /// the active segment are redrawn brightly by the playback overlay. + List _buildDisplayPolylines() { + final visible = _visiblePaths; + if (_displayPaths.isEmpty) return List.of(_polylines); + if (visible.isEmpty) return const []; - Widget _buildLegendCard( + final selected = _selectedPath; + final animating = + _animationEnabled && _playback.started && _playback.hasPath; + + final lines = buildMultiPathPolylines( + visible: visible, + selected: selected, + combined: _viewMode == PathViewMode.combined, + animating: animating, + ); + if (animating && selected != null) { + lines.addAll(buildPacketTrailPolylines(_playback, selected.color)); + } + return lines; + } + + List _buildPacketMarkers() { + final selected = _selectedPath; + if (!_animationEnabled || selected == null) return const []; + return buildPacketMarkers(_playback, selected.color); + } + + Widget _buildBottomPanel( BuildContext context, PathTraceData pathTraceData, bool isImperial, ) { final l10n = context.l10n; - final maxHeight = MediaQuery.of(context).size.height * 0.35; - final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0); - final cardHeight = max(96.0, min(maxHeight, estimatedHeight)); + final selected = _selectedPath; + final combined = _viewMode == PathViewMode.combined; + final maxHeight = + MediaQuery.of(context).size.height * (combined ? 0.45 : 0.35); + + double cardHeight; + if (_panelCollapsed) { + cardHeight = 128; + } else { + final summaryHeight = combined ? 34.0 + _displayPaths.length * 36.0 : 0; + final hopRows = combined + ? (selected?.totalTransmissions ?? 0) + : pathTraceData.pathData.length + 1; + final estimatedHeight = 132.0 + summaryHeight + hopRows * 56.0; + cardHeight = max(176.0, min(maxHeight, estimatedHeight)); + } return Positioned( left: 16, @@ -991,107 +1495,276 @@ class _PathTraceMapScreenState extends State { bottom: 16, child: SizedBox( height: cardHeight, - child: Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - Row( - children: [ - _colorDot(Colors.green), - const SizedBox(width: 4), - Text( - l10n.pathTrace_legendGpsConfirmed, - style: const TextStyle(fontSize: 11), - ), - const SizedBox(width: 12), - _colorDot(Colors.orange), - const SizedBox(width: 4), - Text( - l10n.pathTrace_legendInferred, - style: const TextStyle(fontSize: 11), - ), - ], - ), - ], - ), - ), - const Divider(height: 1), - Expanded( - child: pathTraceData.pathData.isEmpty - ? Center( - child: Text(l10n.channelPath_noHopDetailsAvailable), - ) - : Scrollbar( - child: ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: pathTraceData.pathData.length + 1, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final snrUi = snrUiFromSNR( - index < pathTraceData.snrData.length - ? pathTraceData.snrData[index] - : null, - context.read().currentSf, - ); - return Column( - children: [ - ListTile( - leading: - index >= pathTraceData.snrData.length / 2 - ? Icon(Icons.call_received) - : Icon(Icons.call_made), - title: Text( - formatDirectionText(pathTraceData, index), - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - formatDirectionSubText( - pathTraceData, - index, - ), - style: const TextStyle(fontSize: 14), - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - snrUi.icon, - color: snrUi.color, - size: 18.0, - ), - Text( - snrUi.text, - style: TextStyle( - fontSize: 10, - color: snrUi.color, - ), - ), - ], - ), - onTap: () { - // Handle item tap - }, - ), - ], - ); - }, + child: DecoratedBox( + decoration: BoxDecoration( + color: MeshPalette.bg1.withValues(alpha: 0.95), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all(color: MeshPalette.line2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(MeshRadii.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 4, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${l10n.channelPath_repeaterHops} ${formatDistance(selected?.distanceMeters ?? _pathDistanceMeters, isImperial: isImperial)}', + style: MeshTheme.mono( + fontWeight: FontWeight.w600, + fontSize: 13, + color: MeshPalette.ink, + ), + ), + const SizedBox(height: 4), + PathMiniLegend(combined: combined), + ], ), ), - ), - ], + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + _panelCollapsed + ? Icons.expand_less + : Icons.expand_more, + size: 20, + ), + tooltip: _panelCollapsed + ? l10n.pathMap_expandPanel + : l10n.pathMap_collapsePanel, + onPressed: () => setState( + () => _panelCollapsed = !_panelCollapsed, + ), + ), + ], + ), + ), + PathAnimationControls( + playback: _playback, + selected: selected, + animationEnabled: _animationEnabled, + onToggleAnimation: () => setState(() { + _animationEnabled = !_animationEnabled; + if (!_animationEnabled) _playback.stop(); + }), + followEnabled: _followPacket, + onToggleFollow: _toggleFollowPacket, + ), + if (!_panelCollapsed) ...[ + if (selected != null && selected.unresolvedHops > 0) + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), + child: Text( + l10n.pathMap_partialAnimation(selected.unresolvedHops), + style: TextStyle( + fontSize: 10.5, + color: MeshPalette.warn, + ), + ), + ), + if (combined) + PathSummaryList( + paths: _displayPaths, + selectedId: _selectedPathId, + hiddenIds: _hiddenPathIds, + isImperial: isImperial, + onSelect: _selectPath, + onToggleVisibility: _togglePathVisibility, + onShowAll: () => setState(_hiddenPathIds.clear), + ), + const Divider(height: 1), + Expanded(child: _buildHopList(pathTraceData, selected)), + ], + ], + ), ), ), ), ); } + + Widget _buildHopList(PathTraceData pathTraceData, DisplayPath? selected) { + final useSnrList = + _viewMode == PathViewMode.single && (selected?.isPrimary ?? true); + return ValueListenableBuilder( + valueListenable: _playback.activeSegment, + builder: (context, activeSegment, _) { + int highlightRow = -1; + if (_animationEnabled && + selected != null && + activeSegment >= 0 && + activeSegment < selected.rowForSegment.length) { + highlightRow = selected.rowForSegment[activeSegment]; + } + if (useSnrList) { + return _buildSnrHopList(pathTraceData, highlightRow); + } + if (selected == null) { + return Center( + child: Text(context.l10n.channelPath_noHopDetailsAvailable), + ); + } + return _buildGenericHopList(selected, pathTraceData, highlightRow); + }, + ); + } + + Widget _buildSnrHopList(PathTraceData pathTraceData, int highlightRow) { + final l10n = context.l10n; + if (pathTraceData.pathData.isEmpty) { + return Center(child: Text(l10n.channelPath_noHopDetailsAvailable)); + } + return Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: pathTraceData.pathData.length + 1, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final snrUi = snrUiFromSNR( + index < pathTraceData.snrData.length + ? pathTraceData.snrData[index] + : null, + context.read().currentSf, + ); + return ListTile( + tileColor: index == highlightRow + ? kPrimaryPathColor.withValues(alpha: 0.14) + : null, + leading: index >= pathTraceData.snrData.length / 2 + ? Icon(Icons.call_received) + : Icon(Icons.call_made), + title: Text( + formatDirectionText(pathTraceData, index), + style: MeshTheme.mono(fontSize: 13, color: MeshPalette.ink), + ), + subtitle: Text( + formatDirectionSubText(pathTraceData, index), + style: MeshTheme.mono(fontSize: 12, color: MeshPalette.ink3), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(snrUi.icon, color: snrUi.color, size: 18.0), + Text( + snrUi.text, + style: MeshTheme.mono(fontSize: 10, color: snrUi.color), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildGenericHopList( + DisplayPath path, + PathTraceData pathTraceData, + int highlightRow, + ) { + final connector = context.read(); + final l10n = context.l10n; + + final hopUseCount = {}; + if (_viewMode == PathViewMode.combined) { + for (final p in _visiblePaths) { + for (final hop in p.hopBytes.toSet()) { + hopUseCount.update(hop, (v) => v + 1, ifAbsent: () => 1); + } + } + } + + return Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: path.totalTransmissions, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + String title; + String subtitle; + Widget? trailing; + if (index < path.hopBytes.length) { + final hop = path.hopBytes[index]; + final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + final contact = _contactForHop(hop, connector); + title = contact != null + ? '$hex: ${contact.name}' + : '$hex: ${l10n.channelPath_unknownRepeater}'; + final hasGps = contact != null && contact.hasLocation; + final inferred = + !hasGps && _inferredPositionForHop(hop, connector) != null; + final status = hasGps + ? l10n.pathTrace_legendGpsConfirmed + : inferred + ? l10n.pathTrace_legendInferred + : l10n.pathMap_noLocation; + final sharedCount = hopUseCount[hop] ?? 0; + subtitle = sharedCount > 1 + ? '$status · ${l10n.pathMap_sharedNodeCount(sharedCount)}' + : status; + if (path.isPrimary && index < pathTraceData.snrData.length) { + final snrUi = snrUiFromSNR( + pathTraceData.snrData[index], + connector.currentSf, + ); + trailing = Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(snrUi.icon, color: snrUi.color, size: 18.0), + Text( + snrUi.text, + style: MeshTheme.mono(fontSize: 10, color: snrUi.color), + ), + ], + ); + } + } else { + title = widget.targetContact?.name ?? ''; + subtitle = _targetContactIsGuessed + ? l10n.pathTrace_legendInferred + : l10n.pathTrace_legendGpsConfirmed; + } + return ListTile( + dense: true, + tileColor: index == highlightRow + ? path.color.withValues(alpha: 0.14) + : null, + leading: Container( + width: 26, + height: 26, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: path.color, width: 1.5), + ), + alignment: Alignment.center, + child: Text( + '${index + 1}', + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w700, + color: path.color, + ), + ), + ), + title: Text( + title, + style: MeshTheme.mono(fontSize: 13, color: MeshPalette.ink), + ), + subtitle: Text( + subtitle, + style: MeshTheme.mono(fontSize: 11, color: MeshPalette.ink3), + ), + trailing: trailing, + ); + }, + ), + ); + } } diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 80269061..5ed1b4eb 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -1,11 +1,12 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/debug_frame_viewer.dart'; import '../services/repeater_command_service.dart'; import '../widgets/routing_sheet.dart'; @@ -34,7 +35,6 @@ class _RepeaterCliScreenState extends State { StreamSubscription? _frameSubscription; RepeaterCommandService? _commandService; - // Common commands for quick access late final List> _quickCommands = [ {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'getName', 'command': 'get name'}, @@ -67,12 +67,8 @@ class _RepeaterCliScreenState extends State { void _setupMessageListener() { final connector = Provider.of(context, listen: false); - - // Listen for incoming text messages from the repeater _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; - - // Check if it's a text message response if (frame[0] == respCodeContactMsgRecv || frame[0] == respCodeContactMsgRecvV3) { _handleTextMessageResponse(frame); @@ -102,12 +98,7 @@ class _RepeaterCliScreenState extends State { final parsed = parseContactMessageText(frame); if (parsed == null) return; if (!_matchesRepeaterPrefix(parsed.senderPrefix)) return; - - // Notify command service of response (for retry handling) _commandService?.handleResponse(widget.repeater, parsed.text); - - // Note: The command service will handle the response via the Future - // We don't need to add it to history here anymore as _sendCommand will do it } bool _matchesRepeaterPrefix(Uint8List prefix) { @@ -131,7 +122,6 @@ class _RepeaterCliScreenState extends State { }); }); - // Show debug info if requested if (showDebug && mounted) { final frame = buildSendCliCommandFrame( widget.repeater.publicKey, @@ -144,7 +134,6 @@ class _RepeaterCliScreenState extends State { ); } - // Send CLI command to repeater with retry try { if (_commandService != null) { final connector = Provider.of( @@ -157,7 +146,6 @@ class _RepeaterCliScreenState extends State { command, retries: 1, ); - if (mounted) { setState(() { _commandHistory.add({ @@ -184,7 +172,6 @@ class _RepeaterCliScreenState extends State { _historyIndex = -1; _commandFocusNode.requestFocus(); - // Auto-scroll to bottom Future.delayed(const Duration(milliseconds: 100), () { if (_scrollController.hasClients) { _scrollController.animateTo( @@ -239,36 +226,46 @@ class _RepeaterCliScreenState extends State { }); } + String _quickCommandLabel(String key) { + final l10n = context.l10n; + switch (key) { + case 'getName': + return l10n.repeater_cliQuickGetName; + case 'getRadio': + return l10n.repeater_cliQuickGetRadio; + case 'getTx': + return l10n.repeater_cliQuickGetTx; + case 'neighbors': + return l10n.repeater_cliQuickNeighbors; + case 'version': + return l10n.repeater_cliQuickVersion; + case 'advertise': + return l10n.repeater_cliQuickAdvertise; + case 'clock': + return l10n.repeater_cliQuickClock; + case 'clock sync': + return l10n.repeater_cliQuickClockSync; + case 'discovery': + return l10n.repeater_cliQuickDiscovery; + default: + return key; + } + } + @override Widget build(BuildContext context) { final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; final connector = context.watch(); final repeater = _resolveRepeater(connector); final isFloodMode = repeater.pathOverride == -1; return Scaffold( + backgroundColor: MeshPalette.bg, appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.repeater_cliTitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - repeater.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - centerTitle: false, + backgroundColor: MeshPalette.bg1, + title: Text(l10n.repeater_cliTitle), + centerTitle: true, actions: [ IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), @@ -317,93 +314,181 @@ class _RepeaterCliScreenState extends State { ), body: Column( children: [ - _buildQuickCommandsBar(), - const Divider(height: 1), + // Quick commands bar + Container( + color: MeshPalette.bg1, + padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _quickCommands.map((cmd) { + final label = _quickCommandLabel(cmd['labelKey']!); + return Padding( + padding: const EdgeInsets.only(right: 6), + child: ActionChip( + label: Text( + label, + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w600, + color: MeshPalette.blue, + ), + ), + backgroundColor: MeshPalette.blueBg, + side: const BorderSide(color: MeshPalette.blueLine), + visualDensity: VisualDensity.compact, + onPressed: () => _useQuickCommand(cmd['command']!), + ), + ); + }).toList(), + ), + ), + ), + Divider(height: 1, color: MeshPalette.line), + + // Output area Expanded( child: _commandHistory.isEmpty ? _buildEmptyState() : _buildCommandHistory(), ), - const Divider(height: 1), - _buildCommandInput(), + + Divider(height: 1, color: MeshPalette.line), + + // Command input + Container( + color: MeshPalette.bg1, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SafeArea( + child: Row( + children: [ + IconButton( + icon: Icon( + Icons.arrow_upward, + size: 18, + color: scheme.onSurfaceVariant, + ), + tooltip: l10n.repeater_previousCommand, + onPressed: () => _navigateHistory(true), + visualDensity: VisualDensity.compact, + ), + IconButton( + icon: Icon( + Icons.arrow_downward, + size: 18, + color: scheme.onSurfaceVariant, + ), + tooltip: l10n.repeater_nextCommand, + onPressed: () => _navigateHistory(false), + visualDensity: VisualDensity.compact, + ), + const SizedBox(width: 4), + Expanded( + child: TextField( + controller: _commandController, + focusNode: _commandFocusNode, + style: MeshTheme.mono( + fontSize: 13, + color: MeshPalette.ink, + ), + decoration: InputDecoration( + hintText: context.l10n.repeater_enterCommandHint, + hintStyle: MeshTheme.mono( + fontSize: 13, + color: MeshPalette.ink4, + ), + prefixText: '> ', + prefixStyle: MeshTheme.mono( + fontSize: 13, + color: MeshPalette.blue, + fontWeight: FontWeight.w700, + ), + filled: true, + fillColor: MeshPalette.bg2, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + borderSide: const BorderSide( + color: MeshPalette.line2, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + borderSide: const BorderSide( + color: MeshPalette.line2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + borderSide: const BorderSide( + color: MeshPalette.blue, + width: 1.5, + ), + ), + ), + textInputAction: TextInputAction.send, + onSubmitted: (_) => _sendCommand(), + ), + ), + const SizedBox(width: 6), + Material( + color: MeshPalette.blue.withValues(alpha: 0.15), + shape: const CircleBorder( + side: BorderSide(color: MeshPalette.blueLine), + ), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + HapticFeedback.lightImpact(); + _sendCommand(); + }, + child: const Padding( + padding: EdgeInsets.all(10), + child: Icon( + Icons.send, + size: 18, + color: MeshPalette.blue, + ), + ), + ), + ), + ], + ), + ), + ), ], ), ); } - Widget _buildQuickCommandsBar() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: _quickCommands.map((cmd) { - final label = _quickCommandLabel(cmd['labelKey']!); - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ActionChip( - label: Text(label), - onPressed: () => _useQuickCommand(cmd['command']!), - avatar: const Icon(Icons.play_arrow, size: 16), - ), - ); - }).toList(), - ), - ), - ); - } - - String _quickCommandLabel(String key) { - final l10n = context.l10n; - switch (key) { - case 'getName': - return l10n.repeater_cliQuickGetName; - case 'getRadio': - return l10n.repeater_cliQuickGetRadio; - case 'getTx': - return l10n.repeater_cliQuickGetTx; - case 'neighbors': - return l10n.repeater_cliQuickNeighbors; - case 'version': - return l10n.repeater_cliQuickVersion; - case 'advertise': - return l10n.repeater_cliQuickAdvertise; - case 'clock': - return l10n.repeater_cliQuickClock; - case 'clock sync': - return l10n.repeater_cliQuickClockSync; - case 'discovery': - return l10n.repeater_cliQuickDiscovery; - default: - return key; - } - } - Widget _buildEmptyState() { final l10n = context.l10n; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.terminal, - size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 48, + color: MeshPalette.ink4, ), - const SizedBox(height: 16), + const SizedBox(height: 12), Text( l10n.repeater_noCommandsSent, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + style: MeshTheme.mono( + fontSize: 13, + color: MeshPalette.ink3, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( l10n.repeater_typeCommandOrUseQuick, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, + style: const TextStyle( + fontSize: 12, + color: MeshPalette.ink4, ), ), ], @@ -414,49 +499,41 @@ class _RepeaterCliScreenState extends State { Widget _buildCommandHistory() { return ListView.builder( controller: _scrollController, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: _commandHistory.length, itemBuilder: (context, index) { final entry = _commandHistory[index]; final isCommand = entry['type'] == 'command'; return Padding( - padding: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.only(bottom: 2), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: isCommand - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - isCommand ? Icons.chevron_right : Icons.arrow_back, - size: 16, - color: isCommand - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.onSecondaryContainer, + // Gutter prefix + SizedBox( + width: 20, + child: Text( + isCommand ? '>' : ' ', + style: MeshTheme.mono( + fontSize: 12, + fontWeight: FontWeight.w700, + color: isCommand + ? MeshPalette.blue + : MeshPalette.ink3, + ), ), ), - const SizedBox(width: 12), + const SizedBox(width: 6), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - entry['text']!, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 13, - color: isCommand - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - ], + child: SelectableText( + entry['text']!, + style: MeshTheme.mono( + fontSize: 12.5, + color: isCommand + ? MeshPalette.blue + : MeshPalette.ink, + ), ), ), ], @@ -466,54 +543,6 @@ class _RepeaterCliScreenState extends State { ); } - Widget _buildCommandInput() { - final l10n = context.l10n; - return Container( - padding: const EdgeInsets.all(12), - color: Theme.of(context).colorScheme.surface, - child: SafeArea( - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_upward, size: 20), - tooltip: l10n.repeater_previousCommand, - onPressed: () => _navigateHistory(true), - ), - IconButton( - icon: const Icon(Icons.arrow_downward, size: 20), - tooltip: l10n.repeater_nextCommand, - onPressed: () => _navigateHistory(false), - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: _commandController, - focusNode: _commandFocusNode, - decoration: InputDecoration( - hintText: l10n.repeater_enterCommandHint, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - prefixText: '> ', - ), - style: const TextStyle(fontFamily: 'monospace'), - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendCommand(), - ), - ), - const SizedBox(width: 8), - IconButton.filled( - icon: const Icon(Icons.send), - onPressed: _sendCommand, - ), - ], - ), - ), - ); - } - void _applyHelpCommand(String command) { _commandController.text = command; _commandController.selection = TextSelection.fromPosition( @@ -530,522 +559,156 @@ class _RepeaterCliScreenState extends State { void _showCommandHelp(BuildContext context) { final l10n = context.l10n; final generalCommands = [ - _CommandHelpEntry( - command: 'advert', - description: l10n.repeater_cliHelpAdvert, - ), - _CommandHelpEntry( - command: 'reboot', - description: l10n.repeater_cliHelpReboot, - ), - _CommandHelpEntry( - command: 'clock', - description: l10n.repeater_cliHelpClock, - ), - _CommandHelpEntry( - command: 'password {new-password}', - description: l10n.repeater_cliHelpPassword, - ), - _CommandHelpEntry( - command: 'ver', - description: l10n.repeater_cliHelpVersion, - ), - _CommandHelpEntry( - command: 'clear stats', - description: l10n.repeater_cliHelpClearStats, - ), - _CommandHelpEntry( - command: 'poweroff', - description: l10n.repeater_cliHelpPowerOff, - ), - _CommandHelpEntry( - command: 'shutdown', - description: l10n.repeater_cliHelpPowerOff, - ), - _CommandHelpEntry( - command: 'clkreboot', - description: l10n.repeater_cliHelpClkReboot, - ), - _CommandHelpEntry( - command: 'advert.zerohop', - description: l10n.repeater_cliHelpAdvertZeroHop, - ), - _CommandHelpEntry( - command: 'start ota', - description: l10n.repeater_cliHelpStartOta, - ), - _CommandHelpEntry( - command: 'time {epoch-seconds}', - description: l10n.repeater_cliHelpTime, - ), - _CommandHelpEntry( - command: 'board', - description: l10n.repeater_cliHelpBoard, - ), - _CommandHelpEntry( - command: 'discover.neighbors', - description: l10n.repeater_cliHelpDiscoverNeighbors, - ), - _CommandHelpEntry( - command: 'powersaving', - description: l10n.repeater_cliHelpPowersaving, - ), - _CommandHelpEntry( - command: 'powersaving {on|off}', - description: l10n.repeater_cliHelpPowersavingOnOff, - ), - _CommandHelpEntry( - command: 'erase', - description: l10n.repeater_cliHelpErase, - ), - _CommandHelpEntry( - command: 'stats-packets', - description: l10n.repeater_cliHelpStatsPackets, - ), - _CommandHelpEntry( - command: 'stats-radio', - description: l10n.repeater_cliHelpStatsRadio, - ), - _CommandHelpEntry( - command: 'stats-core', - description: l10n.repeater_cliHelpStatsCore, - ), + _CommandHelpEntry(command: 'advert', description: l10n.repeater_cliHelpAdvert), + _CommandHelpEntry(command: 'reboot', description: l10n.repeater_cliHelpReboot), + _CommandHelpEntry(command: 'clock', description: l10n.repeater_cliHelpClock), + _CommandHelpEntry(command: 'password {new-password}', description: l10n.repeater_cliHelpPassword), + _CommandHelpEntry(command: 'ver', description: l10n.repeater_cliHelpVersion), + _CommandHelpEntry(command: 'clear stats', description: l10n.repeater_cliHelpClearStats), + _CommandHelpEntry(command: 'poweroff', description: l10n.repeater_cliHelpPowerOff), + _CommandHelpEntry(command: 'shutdown', description: l10n.repeater_cliHelpPowerOff), + _CommandHelpEntry(command: 'clkreboot', description: l10n.repeater_cliHelpClkReboot), + _CommandHelpEntry(command: 'advert.zerohop', description: l10n.repeater_cliHelpAdvertZeroHop), + _CommandHelpEntry(command: 'start ota', description: l10n.repeater_cliHelpStartOta), + _CommandHelpEntry(command: 'time {epoch-seconds}', description: l10n.repeater_cliHelpTime), + _CommandHelpEntry(command: 'board', description: l10n.repeater_cliHelpBoard), + _CommandHelpEntry(command: 'discover.neighbors', description: l10n.repeater_cliHelpDiscoverNeighbors), + _CommandHelpEntry(command: 'powersaving', description: l10n.repeater_cliHelpPowersaving), + _CommandHelpEntry(command: 'powersaving {on|off}', description: l10n.repeater_cliHelpPowersavingOnOff), + _CommandHelpEntry(command: 'erase', description: l10n.repeater_cliHelpErase), + _CommandHelpEntry(command: 'stats-packets', description: l10n.repeater_cliHelpStatsPackets), + _CommandHelpEntry(command: 'stats-radio', description: l10n.repeater_cliHelpStatsRadio), + _CommandHelpEntry(command: 'stats-core', description: l10n.repeater_cliHelpStatsCore), ]; final settingsCommands = [ - _CommandHelpEntry( - command: 'set af {air-time-factor}', - description: l10n.repeater_cliHelpSetAf, - ), - _CommandHelpEntry( - command: 'set tx {tx-power-dbm}', - description: l10n.repeater_cliHelpSetTx, - ), - _CommandHelpEntry( - command: 'set repeat {on|off}', - description: l10n.repeater_cliHelpSetRepeat, - ), - _CommandHelpEntry( - command: 'set allow.read.only {on|off}', - description: l10n.repeater_cliHelpSetAllowReadOnly, - ), - _CommandHelpEntry( - command: 'set flood.max {max-hops}', - description: l10n.repeater_cliHelpSetFloodMax, - ), - _CommandHelpEntry( - command: 'set int.thresh {db}', - description: l10n.repeater_cliHelpSetIntThresh, - ), - _CommandHelpEntry( - command: 'set agc.reset.interval {seconds}', - description: l10n.repeater_cliHelpSetAgcResetInterval, - ), - _CommandHelpEntry( - command: 'set multi.acks {0|1}', - description: l10n.repeater_cliHelpSetMultiAcks, - ), - _CommandHelpEntry( - command: 'set advert.interval {minutes}', - description: l10n.repeater_cliHelpSetAdvertInterval, - ), - _CommandHelpEntry( - command: 'set flood.advert.interval {hours}', - description: l10n.repeater_cliHelpSetFloodAdvertInterval, - ), - _CommandHelpEntry( - command: 'set guest.password {guess-password}', - description: l10n.repeater_cliHelpSetGuestPassword, - ), - _CommandHelpEntry( - command: 'set name {name}', - description: l10n.repeater_cliHelpSetName, - ), - _CommandHelpEntry( - command: 'set lat {latitude}', - description: l10n.repeater_cliHelpSetLat, - ), - _CommandHelpEntry( - command: 'set lon {longitude}', - description: l10n.repeater_cliHelpSetLon, - ), - _CommandHelpEntry( - command: 'set radio {freq},{bw},{sf},{cr}', - description: l10n.repeater_cliHelpSetRadio, - ), - _CommandHelpEntry( - command: 'set rxdelay {base}', - description: l10n.repeater_cliHelpSetRxDelay, - ), - _CommandHelpEntry( - command: 'set txdelay {factor}', - description: l10n.repeater_cliHelpSetTxDelay, - ), - _CommandHelpEntry( - command: 'set direct.txdelay {factor}', - description: l10n.repeater_cliHelpSetDirectTxDelay, - ), - _CommandHelpEntry( - command: 'set bridge.enabled {on|off}', - description: l10n.repeater_cliHelpSetBridgeEnabled, - ), - _CommandHelpEntry( - command: 'set bridge.delay {0-10000}', - description: l10n.repeater_cliHelpSetBridgeDelay, - ), - _CommandHelpEntry( - command: 'set bridge.source {rx|tx}', - description: l10n.repeater_cliHelpSetBridgeSource, - ), - _CommandHelpEntry( - command: 'set bridge.baud {speed}', - description: l10n.repeater_cliHelpSetBridgeBaud, - ), - _CommandHelpEntry( - command: 'set bridge.secret {shared-secret}', - description: l10n.repeater_cliHelpSetBridgeSecret, - ), - _CommandHelpEntry( - command: 'set adc.multiplier {factor}', - description: l10n.repeater_cliHelpSetAdcMultiplier, - ), - _CommandHelpEntry( - command: 'tempradio {freq},{bw},{sf},{cr},{minutes}', - description: l10n.repeater_cliHelpTempRadio, - ), - _CommandHelpEntry( - command: 'setperm {pubkey-hex} {permissions}', - description: l10n.repeater_cliHelpSetPerm, - ), - _CommandHelpEntry( - command: 'set dutycycle {1-100}', - description: l10n.repeater_cliHelpSetDutyCycle, - ), - _CommandHelpEntry( - command: 'set prv.key {hex}', - description: l10n.repeater_cliHelpSetPrvKey, - ), - _CommandHelpEntry( - command: 'set radio.rxgain {on|off}', - description: l10n.repeater_cliHelpSetRadioRxGain, - ), - _CommandHelpEntry( - command: 'set owner.info {text}', - description: l10n.repeater_cliHelpSetOwnerInfo, - ), - _CommandHelpEntry( - command: 'set path.hash.mode {0|1|2}', - description: l10n.repeater_cliHelpSetPathHashMode, - ), - _CommandHelpEntry( - command: 'set loop.detect {off|minimal|moderate|strict}', - description: l10n.repeater_cliHelpSetLoopDetect, - ), - _CommandHelpEntry( - command: 'set freq {mhz}', - description: l10n.repeater_cliHelpSetFreq, - ), - _CommandHelpEntry( - command: 'set bridge.channel {1-14}', - description: l10n.repeater_cliHelpSetBridgeChannel, - ), + _CommandHelpEntry(command: 'set af {air-time-factor}', description: l10n.repeater_cliHelpSetAf), + _CommandHelpEntry(command: 'set tx {tx-power-dbm}', description: l10n.repeater_cliHelpSetTx), + _CommandHelpEntry(command: 'set repeat {on|off}', description: l10n.repeater_cliHelpSetRepeat), + _CommandHelpEntry(command: 'set allow.read.only {on|off}', description: l10n.repeater_cliHelpSetAllowReadOnly), + _CommandHelpEntry(command: 'set flood.max {max-hops}', description: l10n.repeater_cliHelpSetFloodMax), + _CommandHelpEntry(command: 'set int.thresh {db}', description: l10n.repeater_cliHelpSetIntThresh), + _CommandHelpEntry(command: 'set agc.reset.interval {seconds}', description: l10n.repeater_cliHelpSetAgcResetInterval), + _CommandHelpEntry(command: 'set multi.acks {0|1}', description: l10n.repeater_cliHelpSetMultiAcks), + _CommandHelpEntry(command: 'set advert.interval {minutes}', description: l10n.repeater_cliHelpSetAdvertInterval), + _CommandHelpEntry(command: 'set flood.advert.interval {hours}', description: l10n.repeater_cliHelpSetFloodAdvertInterval), + _CommandHelpEntry(command: 'set guest.password {guess-password}', description: l10n.repeater_cliHelpSetGuestPassword), + _CommandHelpEntry(command: 'set name {name}', description: l10n.repeater_cliHelpSetName), + _CommandHelpEntry(command: 'set lat {latitude}', description: l10n.repeater_cliHelpSetLat), + _CommandHelpEntry(command: 'set lon {longitude}', description: l10n.repeater_cliHelpSetLon), + _CommandHelpEntry(command: 'set radio {freq},{bw},{sf},{cr}', description: l10n.repeater_cliHelpSetRadio), + _CommandHelpEntry(command: 'set rxdelay {base}', description: l10n.repeater_cliHelpSetRxDelay), + _CommandHelpEntry(command: 'set txdelay {factor}', description: l10n.repeater_cliHelpSetTxDelay), + _CommandHelpEntry(command: 'set direct.txdelay {factor}', description: l10n.repeater_cliHelpSetDirectTxDelay), + _CommandHelpEntry(command: 'set bridge.enabled {on|off}', description: l10n.repeater_cliHelpSetBridgeEnabled), + _CommandHelpEntry(command: 'set bridge.delay {0-10000}', description: l10n.repeater_cliHelpSetBridgeDelay), + _CommandHelpEntry(command: 'set bridge.source {rx|tx}', description: l10n.repeater_cliHelpSetBridgeSource), + _CommandHelpEntry(command: 'set bridge.baud {speed}', description: l10n.repeater_cliHelpSetBridgeBaud), + _CommandHelpEntry(command: 'set bridge.secret {shared-secret}', description: l10n.repeater_cliHelpSetBridgeSecret), + _CommandHelpEntry(command: 'set adc.multiplier {factor}', description: l10n.repeater_cliHelpSetAdcMultiplier), + _CommandHelpEntry(command: 'tempradio {freq},{bw},{sf},{cr},{minutes}', description: l10n.repeater_cliHelpTempRadio), + _CommandHelpEntry(command: 'setperm {pubkey-hex} {permissions}', description: l10n.repeater_cliHelpSetPerm), + _CommandHelpEntry(command: 'set dutycycle {1-100}', description: l10n.repeater_cliHelpSetDutyCycle), + _CommandHelpEntry(command: 'set prv.key {hex}', description: l10n.repeater_cliHelpSetPrvKey), + _CommandHelpEntry(command: 'set radio.rxgain {on|off}', description: l10n.repeater_cliHelpSetRadioRxGain), + _CommandHelpEntry(command: 'set owner.info {text}', description: l10n.repeater_cliHelpSetOwnerInfo), + _CommandHelpEntry(command: 'set path.hash.mode {0|1|2}', description: l10n.repeater_cliHelpSetPathHashMode), + _CommandHelpEntry(command: 'set loop.detect {off|minimal|moderate|strict}', description: l10n.repeater_cliHelpSetLoopDetect), + _CommandHelpEntry(command: 'set freq {mhz}', description: l10n.repeater_cliHelpSetFreq), + _CommandHelpEntry(command: 'set bridge.channel {1-14}', description: l10n.repeater_cliHelpSetBridgeChannel), ]; final bridgeCommands = [ - _CommandHelpEntry( - command: 'get bridge.type', - description: l10n.repeater_cliHelpGetBridgeType, - ), + _CommandHelpEntry(command: 'get bridge.type', description: l10n.repeater_cliHelpGetBridgeType), ]; final loggingCommands = [ - _CommandHelpEntry( - command: 'log start', - description: l10n.repeater_cliHelpLogStart, - ), - _CommandHelpEntry( - command: 'log stop', - description: l10n.repeater_cliHelpLogStop, - ), - _CommandHelpEntry( - command: 'log erase', - description: l10n.repeater_cliHelpLogErase, - ), + _CommandHelpEntry(command: 'log start', description: l10n.repeater_cliHelpLogStart), + _CommandHelpEntry(command: 'log stop', description: l10n.repeater_cliHelpLogStop), + _CommandHelpEntry(command: 'log erase', description: l10n.repeater_cliHelpLogErase), ]; final neighborCommands = [ - _CommandHelpEntry( - command: 'neighbors', - description: l10n.repeater_cliHelpNeighbors, - ), - _CommandHelpEntry( - command: 'neighbor.remove {pubkey-prefix}', - description: l10n.repeater_cliHelpNeighborRemove, - ), + _CommandHelpEntry(command: 'neighbors', description: l10n.repeater_cliHelpNeighbors), + _CommandHelpEntry(command: 'neighbor.remove {pubkey-prefix}', description: l10n.repeater_cliHelpNeighborRemove), ]; final regionCommands = [ - _CommandHelpEntry( - command: 'region', - description: l10n.repeater_cliHelpRegion, - ), - _CommandHelpEntry( - command: 'region load', - description: l10n.repeater_cliHelpRegionLoad, - ), - _CommandHelpEntry( - command: 'region get {* | name-prefix}', - description: l10n.repeater_cliHelpRegionGet, - ), - _CommandHelpEntry( - command: 'region put {name} {* | parent-name-prefix}', - description: l10n.repeater_cliHelpRegionPut, - ), - _CommandHelpEntry( - command: 'region remove {name}', - description: l10n.repeater_cliHelpRegionRemove, - ), - _CommandHelpEntry( - command: 'region allowf {* | name-prefix}', - description: l10n.repeater_cliHelpRegionAllowf, - ), - _CommandHelpEntry( - command: 'region denyf {* | name-prefix}', - description: l10n.repeater_cliHelpRegionDenyf, - ), - _CommandHelpEntry( - command: 'region home', - description: l10n.repeater_cliHelpRegionHome, - ), - _CommandHelpEntry( - command: 'region home {* | name-prefix}', - description: l10n.repeater_cliHelpRegionHomeSet, - ), - _CommandHelpEntry( - command: 'region save', - description: l10n.repeater_cliHelpRegionSave, - ), - _CommandHelpEntry( - command: 'region default', - description: l10n.repeater_cliHelpRegionDefault, - ), - _CommandHelpEntry( - command: 'region default {* | name-prefix | }', - description: l10n.repeater_cliHelpRegionDefaultSet, - ), - _CommandHelpEntry( - command: 'region list allowed', - description: l10n.repeater_cliHelpRegionListAllowed, - ), - _CommandHelpEntry( - command: 'region list denied', - description: l10n.repeater_cliHelpRegionListDenied, - ), + _CommandHelpEntry(command: 'region', description: l10n.repeater_cliHelpRegion), + _CommandHelpEntry(command: 'region load', description: l10n.repeater_cliHelpRegionLoad), + _CommandHelpEntry(command: 'region get {* | name-prefix}', description: l10n.repeater_cliHelpRegionGet), + _CommandHelpEntry(command: 'region put {name} {* | parent-name-prefix}', description: l10n.repeater_cliHelpRegionPut), + _CommandHelpEntry(command: 'region remove {name}', description: l10n.repeater_cliHelpRegionRemove), + _CommandHelpEntry(command: 'region allowf {* | name-prefix}', description: l10n.repeater_cliHelpRegionAllowf), + _CommandHelpEntry(command: 'region denyf {* | name-prefix}', description: l10n.repeater_cliHelpRegionDenyf), + _CommandHelpEntry(command: 'region home', description: l10n.repeater_cliHelpRegionHome), + _CommandHelpEntry(command: 'region home {* | name-prefix}', description: l10n.repeater_cliHelpRegionHomeSet), + _CommandHelpEntry(command: 'region save', description: l10n.repeater_cliHelpRegionSave), + _CommandHelpEntry(command: 'region default', description: l10n.repeater_cliHelpRegionDefault), + _CommandHelpEntry(command: 'region default {* | name-prefix | }', description: l10n.repeater_cliHelpRegionDefaultSet), + _CommandHelpEntry(command: 'region list allowed', description: l10n.repeater_cliHelpRegionListAllowed), + _CommandHelpEntry(command: 'region list denied', description: l10n.repeater_cliHelpRegionListDenied), ]; final getCommands = [ - _CommandHelpEntry( - command: 'get name', - description: l10n.repeater_cliHelpGetName, - ), - _CommandHelpEntry( - command: 'get role', - description: l10n.repeater_cliHelpGetRole, - ), - _CommandHelpEntry( - command: 'get public.key', - description: l10n.repeater_cliHelpGetPublicKey, - ), - _CommandHelpEntry( - command: 'get prv.key', - description: l10n.repeater_cliHelpGetPrvKey, - ), - _CommandHelpEntry( - command: 'get repeat', - description: l10n.repeater_cliHelpGetRepeat, - ), - _CommandHelpEntry( - command: 'get tx', - description: l10n.repeater_cliHelpGetTx, - ), - _CommandHelpEntry( - command: 'get freq', - description: l10n.repeater_cliHelpGetFreq, - ), - _CommandHelpEntry( - command: 'get radio', - description: l10n.repeater_cliHelpGetRadio, - ), - _CommandHelpEntry( - command: 'get radio.rxgain', - description: l10n.repeater_cliHelpGetRadioRxGain, - ), - _CommandHelpEntry( - command: 'get af', - description: l10n.repeater_cliHelpGetAf, - ), - _CommandHelpEntry( - command: 'get dutycycle', - description: l10n.repeater_cliHelpGetDutyCycle, - ), - _CommandHelpEntry( - command: 'get int.thresh', - description: l10n.repeater_cliHelpGetIntThresh, - ), - _CommandHelpEntry( - command: 'get agc.reset.interval', - description: l10n.repeater_cliHelpGetAgcResetInterval, - ), - _CommandHelpEntry( - command: 'get multi.acks', - description: l10n.repeater_cliHelpGetMultiAcks, - ), - _CommandHelpEntry( - command: 'get allow.read.only', - description: l10n.repeater_cliHelpGetAllowReadOnly, - ), - _CommandHelpEntry( - command: 'get advert.interval', - description: l10n.repeater_cliHelpGetAdvertInterval, - ), - _CommandHelpEntry( - command: 'get flood.advert.interval', - description: l10n.repeater_cliHelpGetFloodAdvertInterval, - ), - _CommandHelpEntry( - command: 'get guest.password', - description: l10n.repeater_cliHelpGetGuestPassword, - ), - _CommandHelpEntry( - command: 'get lat', - description: l10n.repeater_cliHelpGetLat, - ), - _CommandHelpEntry( - command: 'get lon', - description: l10n.repeater_cliHelpGetLon, - ), - _CommandHelpEntry( - command: 'get rxdelay', - description: l10n.repeater_cliHelpGetRxDelay, - ), - _CommandHelpEntry( - command: 'get txdelay', - description: l10n.repeater_cliHelpGetTxDelay, - ), - _CommandHelpEntry( - command: 'get direct.txdelay', - description: l10n.repeater_cliHelpGetDirectTxDelay, - ), - _CommandHelpEntry( - command: 'get flood.max', - description: l10n.repeater_cliHelpGetFloodMax, - ), - _CommandHelpEntry( - command: 'get owner.info', - description: l10n.repeater_cliHelpGetOwnerInfo, - ), - _CommandHelpEntry( - command: 'get path.hash.mode', - description: l10n.repeater_cliHelpGetPathHashMode, - ), - _CommandHelpEntry( - command: 'get loop.detect', - description: l10n.repeater_cliHelpGetLoopDetect, - ), - _CommandHelpEntry( - command: 'get acl', - description: l10n.repeater_cliHelpGetAcl, - ), - _CommandHelpEntry( - command: 'get bridge.enabled', - description: l10n.repeater_cliHelpGetBridgeEnabled, - ), - _CommandHelpEntry( - command: 'get bridge.delay', - description: l10n.repeater_cliHelpGetBridgeDelay, - ), - _CommandHelpEntry( - command: 'get bridge.source', - description: l10n.repeater_cliHelpGetBridgeSource, - ), - _CommandHelpEntry( - command: 'get bridge.baud', - description: l10n.repeater_cliHelpGetBridgeBaud, - ), - _CommandHelpEntry( - command: 'get bridge.channel', - description: l10n.repeater_cliHelpGetBridgeChannel, - ), - _CommandHelpEntry( - command: 'get bridge.secret', - description: l10n.repeater_cliHelpGetBridgeSecret, - ), - _CommandHelpEntry( - command: 'get bootloader.ver', - description: l10n.repeater_cliHelpGetBootloaderVer, - ), - _CommandHelpEntry( - command: 'get adc.multiplier', - description: l10n.repeater_cliHelpGetAdcMultiplier, - ), + _CommandHelpEntry(command: 'get name', description: l10n.repeater_cliHelpGetName), + _CommandHelpEntry(command: 'get role', description: l10n.repeater_cliHelpGetRole), + _CommandHelpEntry(command: 'get public.key', description: l10n.repeater_cliHelpGetPublicKey), + _CommandHelpEntry(command: 'get prv.key', description: l10n.repeater_cliHelpGetPrvKey), + _CommandHelpEntry(command: 'get repeat', description: l10n.repeater_cliHelpGetRepeat), + _CommandHelpEntry(command: 'get tx', description: l10n.repeater_cliHelpGetTx), + _CommandHelpEntry(command: 'get freq', description: l10n.repeater_cliHelpGetFreq), + _CommandHelpEntry(command: 'get radio', description: l10n.repeater_cliHelpGetRadio), + _CommandHelpEntry(command: 'get radio.rxgain', description: l10n.repeater_cliHelpGetRadioRxGain), + _CommandHelpEntry(command: 'get af', description: l10n.repeater_cliHelpGetAf), + _CommandHelpEntry(command: 'get dutycycle', description: l10n.repeater_cliHelpGetDutyCycle), + _CommandHelpEntry(command: 'get int.thresh', description: l10n.repeater_cliHelpGetIntThresh), + _CommandHelpEntry(command: 'get agc.reset.interval', description: l10n.repeater_cliHelpGetAgcResetInterval), + _CommandHelpEntry(command: 'get multi.acks', description: l10n.repeater_cliHelpGetMultiAcks), + _CommandHelpEntry(command: 'get allow.read.only', description: l10n.repeater_cliHelpGetAllowReadOnly), + _CommandHelpEntry(command: 'get advert.interval', description: l10n.repeater_cliHelpGetAdvertInterval), + _CommandHelpEntry(command: 'get flood.advert.interval', description: l10n.repeater_cliHelpGetFloodAdvertInterval), + _CommandHelpEntry(command: 'get guest.password', description: l10n.repeater_cliHelpGetGuestPassword), + _CommandHelpEntry(command: 'get lat', description: l10n.repeater_cliHelpGetLat), + _CommandHelpEntry(command: 'get lon', description: l10n.repeater_cliHelpGetLon), + _CommandHelpEntry(command: 'get rxdelay', description: l10n.repeater_cliHelpGetRxDelay), + _CommandHelpEntry(command: 'get txdelay', description: l10n.repeater_cliHelpGetTxDelay), + _CommandHelpEntry(command: 'get direct.txdelay', description: l10n.repeater_cliHelpGetDirectTxDelay), + _CommandHelpEntry(command: 'get flood.max', description: l10n.repeater_cliHelpGetFloodMax), + _CommandHelpEntry(command: 'get owner.info', description: l10n.repeater_cliHelpGetOwnerInfo), + _CommandHelpEntry(command: 'get path.hash.mode', description: l10n.repeater_cliHelpGetPathHashMode), + _CommandHelpEntry(command: 'get loop.detect', description: l10n.repeater_cliHelpGetLoopDetect), + _CommandHelpEntry(command: 'get acl', description: l10n.repeater_cliHelpGetAcl), + _CommandHelpEntry(command: 'get bridge.enabled', description: l10n.repeater_cliHelpGetBridgeEnabled), + _CommandHelpEntry(command: 'get bridge.delay', description: l10n.repeater_cliHelpGetBridgeDelay), + _CommandHelpEntry(command: 'get bridge.source', description: l10n.repeater_cliHelpGetBridgeSource), + _CommandHelpEntry(command: 'get bridge.baud', description: l10n.repeater_cliHelpGetBridgeBaud), + _CommandHelpEntry(command: 'get bridge.channel', description: l10n.repeater_cliHelpGetBridgeChannel), + _CommandHelpEntry(command: 'get bridge.secret', description: l10n.repeater_cliHelpGetBridgeSecret), + _CommandHelpEntry(command: 'get bootloader.ver', description: l10n.repeater_cliHelpGetBootloaderVer), + _CommandHelpEntry(command: 'get adc.multiplier', description: l10n.repeater_cliHelpGetAdcMultiplier), ]; final powerMgmtCommands = [ - _CommandHelpEntry( - command: 'get pwrmgt.support', - description: l10n.repeater_cliHelpGetPwrMgtSupport, - ), - _CommandHelpEntry( - command: 'get pwrmgt.source', - description: l10n.repeater_cliHelpGetPwrMgtSource, - ), - _CommandHelpEntry( - command: 'get pwrmgt.bootreason', - description: l10n.repeater_cliHelpGetPwrMgtBootReason, - ), - _CommandHelpEntry( - command: 'get pwrmgt.bootmv', - description: l10n.repeater_cliHelpGetPwrMgtBootMv, - ), + _CommandHelpEntry(command: 'get pwrmgt.support', description: l10n.repeater_cliHelpGetPwrMgtSupport), + _CommandHelpEntry(command: 'get pwrmgt.source', description: l10n.repeater_cliHelpGetPwrMgtSource), + _CommandHelpEntry(command: 'get pwrmgt.bootreason', description: l10n.repeater_cliHelpGetPwrMgtBootReason), + _CommandHelpEntry(command: 'get pwrmgt.bootmv', description: l10n.repeater_cliHelpGetPwrMgtBootMv), ]; final sensorCommands = [ - _CommandHelpEntry( - command: 'sensor get {key}', - description: l10n.repeater_cliHelpSensorGet, - ), - _CommandHelpEntry( - command: 'sensor set {key} {value}', - description: l10n.repeater_cliHelpSensorSet, - ), - _CommandHelpEntry( - command: 'sensor list [start]', - description: l10n.repeater_cliHelpSensorList, - ), + _CommandHelpEntry(command: 'sensor get {key}', description: l10n.repeater_cliHelpSensorGet), + _CommandHelpEntry(command: 'sensor set {key} {value}', description: l10n.repeater_cliHelpSensorSet), + _CommandHelpEntry(command: 'sensor list [start]', description: l10n.repeater_cliHelpSensorList), ]; final gpsCommands = [ _CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps), - _CommandHelpEntry( - command: 'gps {on|off}', - description: l10n.repeater_cliHelpGpsOnOff, - ), - _CommandHelpEntry( - command: 'gps sync', - description: l10n.repeater_cliHelpGpsSync, - ), - _CommandHelpEntry( - command: 'gps setloc', - description: l10n.repeater_cliHelpGpsSetLoc, - ), - _CommandHelpEntry( - command: 'gps advert', - description: l10n.repeater_cliHelpGpsAdvert, - ), - _CommandHelpEntry( - command: 'gps advert {none|share|prefs}', - description: l10n.repeater_cliHelpGpsAdvertSet, - ), + _CommandHelpEntry(command: 'gps {on|off}', description: l10n.repeater_cliHelpGpsOnOff), + _CommandHelpEntry(command: 'gps sync', description: l10n.repeater_cliHelpGpsSync), + _CommandHelpEntry(command: 'gps setloc', description: l10n.repeater_cliHelpGpsSetLoc), + _CommandHelpEntry(command: 'gps advert', description: l10n.repeater_cliHelpGpsAdvert), + _CommandHelpEntry(command: 'gps advert {none|share|prefs}', description: l10n.repeater_cliHelpGpsAdvertSet), ]; showDialog( @@ -1057,64 +720,27 @@ class _RepeaterCliScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.repeater_commandsListNote, - style: const TextStyle(fontSize: 13), - ), + Text(l10n.repeater_commandsListNote, style: const TextStyle(fontSize: 13)), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_general, - generalCommands, - ), + _buildHelpSection(context, l10n.repeater_general, generalCommands), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_getCategory, - getCommands, - ), + _buildHelpSection(context, l10n.repeater_getCategory, getCommands), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_settingsCategory, - settingsCommands, - ), + _buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_powerMgmt, - powerMgmtCommands, - ), + _buildHelpSection(context, l10n.repeater_powerMgmt, powerMgmtCommands), const SizedBox(height: 16), _buildHelpSection(context, l10n.repeater_sensors, sensorCommands), const SizedBox(height: 16), _buildHelpSection(context, l10n.repeater_bridge, bridgeCommands), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_logging, - loggingCommands, - ), + _buildHelpSection(context, l10n.repeater_logging, loggingCommands), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_neighborsRepeaterOnly, - neighborCommands, - ), + _buildHelpSection(context, l10n.repeater_neighborsRepeaterOnly, neighborCommands), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_regionManagementRepeaterOnly, - regionCommands, - note: l10n.repeater_regionNote, - ), + _buildHelpSection(context, l10n.repeater_regionManagementRepeaterOnly, regionCommands, note: l10n.repeater_regionNote), const SizedBox(height: 16), - _buildHelpSection( - context, - l10n.repeater_gpsManagement, - gpsCommands, - note: l10n.repeater_gpsNote, - ), + _buildHelpSection(context, l10n.repeater_gpsManagement, gpsCommands, note: l10n.repeater_gpsNote), ], ), ), @@ -1134,16 +760,14 @@ class _RepeaterCliScreenState extends State { List<_CommandHelpEntry> commands, { String? note, }) { + final scheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), + Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), if (note != null) ...[ - const SizedBox(height: 6), - Text(note, style: const TextStyle(fontSize: 12)), + const SizedBox(height: 4), + Text(note, style: TextStyle(fontSize: 11, color: scheme.onSurfaceVariant)), ], const SizedBox(height: 8), ...commands.map((entry) => _buildHelpCommandCard(context, entry)), @@ -1152,39 +776,35 @@ class _RepeaterCliScreenState extends State { } Widget _buildHelpCommandCard(BuildContext context, _CommandHelpEntry entry) { - final colorScheme = Theme.of(context).colorScheme; + final scheme = Theme.of(context).colorScheme; return Card( elevation: 0, - margin: const EdgeInsets.only(bottom: 8), - color: colorScheme.surfaceContainerHighest, + margin: const EdgeInsets.only(bottom: 6), + color: scheme.surfaceContainerHighest, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(MeshRadii.sm), + side: BorderSide(color: scheme.outlineVariant), ), child: InkWell( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(MeshRadii.sm), onTap: () => _applyHelpCommand(entry.command), child: Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entry.command, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 13, - fontWeight: FontWeight.bold, - color: colorScheme.onSurfaceVariant, + style: MeshTheme.mono( + fontSize: 12, + fontWeight: FontWeight.w600, + color: MeshPalette.blue, ), ), - const SizedBox(height: 6), + const SizedBox(height: 4), Text( entry.description, - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), + style: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant), ), ], ), @@ -1197,6 +817,5 @@ class _RepeaterCliScreenState extends State { class _CommandHelpEntry { final String command; final String description; - const _CommandHelpEntry({required this.command, required this.description}); } diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 2aebcf03..bbdec602 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../l10n/contact_localization.dart'; import '../services/app_settings_service.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import 'repeater_status_screen.dart'; import 'repeater_cli_screen.dart'; import 'repeater_settings_screen.dart'; @@ -26,189 +29,157 @@ class RepeaterHubScreen extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; final settingsService = context.watch(); final chemistry = settingsService.batteryChemistryForRepeater( repeater.publicKeyHex, ); + return Scaffold( appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (isAdmin) - Text( - repeater.type == advTypeRepeater - ? l10n.repeater_management - : l10n.room_management, - ), - if (!isAdmin) - Text( - repeater.type == advTypeRepeater - ? l10n.repeater_guest - : l10n.room_guest, - ), - Text( - repeater.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - ), - ), - ], + title: Text( + repeater.type == advTypeRepeater + ? (isAdmin ? l10n.repeater_management : l10n.repeater_guest) + : (isAdmin ? l10n.room_management : l10n.room_guest), ), - centerTitle: false, + centerTitle: true, ), body: SafeArea( top: false, child: ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only(bottom: 24), children: [ - // Repeater info card - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( + // ── Identity card ───────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 4), + child: MeshCard( + margin: EdgeInsets.zero, + padding: const EdgeInsets.all(20), + child: Row( children: [ - CircleAvatar( - radius: 40, - backgroundColor: Theme.of( - context, - ).colorScheme.tertiaryContainer, - child: Icon( - Icons.cell_tower, - size: 40, - color: Theme.of( - context, - ).colorScheme.onTertiaryContainer, - ), + AvatarCircle( + name: repeater.name, + size: 52, + color: MeshPalette.warn, + icon: Icons.cell_tower, ), - const SizedBox(height: 16), - Text( - repeater.name, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - repeater.shortPubKeyHex, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), - Text( - repeater.pathLabel(context.l10n), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - if (repeater.hasLocation) ...[ - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.center, + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.location_on, - size: 14, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 4), Text( - '${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', - style: TextStyle( - fontSize: 12, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + repeater.name, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w700), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + repeater.shortPubKeyHex, + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, ), ), + const SizedBox(height: 4), + Text( + repeater.pathLabel(l10n), + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: scheme.onSurfaceVariant), + ), + if (repeater.hasLocation) ...[ + const SizedBox(height: 2), + Row( + children: [ + Icon( + Icons.location_on, + size: 12, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 3), + Expanded( + child: Text( + '${repeater.latitude?.toStringAsFixed(4)}, ' + '${repeater.longitude?.toStringAsFixed(4)}', + style: MeshTheme.mono( + fontSize: 10, + color: scheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], ], ), - ], + ), + StatusChip( + label: isAdmin ? 'ADMIN' : 'GUEST', + color: isAdmin ? MeshPalette.blue : scheme.onSurfaceVariant, + ), ], ), ), ), - const SizedBox(height: 24), - if (isAdmin) - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.battery_full), - const SizedBox(width: 10), - Expanded( - child: Text( - l10n.appSettings_batteryChemistry, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: chemistry, - isExpanded: true, - decoration: const InputDecoration( - border: UnderlineInputBorder(), - isDense: true, - ), - onChanged: (value) { - if (value == null) return; - settingsService.setBatteryChemistryForRepeater( - repeater.publicKeyHex, - value, - ); - }, - items: [ - DropdownMenuItem( - value: 'nmc', - child: Text(l10n.appSettings_batteryNmc), - ), - DropdownMenuItem( - value: 'lifepo4', - child: Text(l10n.appSettings_batteryLifepo4), - ), - DropdownMenuItem( - value: 'lipo', - child: Text(l10n.appSettings_batteryLipo), - ), - ], - ), - ], + + // ── Battery chemistry (admin only) ───────────────────────────── + if (isAdmin) ...[ + SectionHeader(l10n.appSettings_batteryChemistry), + MeshCard( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.fromLTRB(14, 10, 14, 14), + child: DropdownButtonFormField( + initialValue: chemistry, + isExpanded: true, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.battery_full, size: 18), + labelText: l10n.appSettings_batteryChemistry, ), + onChanged: (value) { + if (value == null) return; + settingsService.setBatteryChemistryForRepeater( + repeater.publicKeyHex, + value, + ); + }, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(l10n.appSettings_batteryNmc), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + ], ), ), - const SizedBox(height: 24), - Text( - isAdmin - ? l10n.repeater_managementTools - : l10n.repeater_guestTools, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ], + + // ── Tools ────────────────────────────────────────────────────── + SectionHeader( + isAdmin ? l10n.repeater_managementTools : l10n.repeater_guestTools, ), - const SizedBox(height: 16), - // Status button - _buildManagementCard( - context, + + _HubActionTile( + index: 0, icon: Icons.analytics, title: l10n.repeater_status, subtitle: l10n.repeater_statusSubtitle, - color: Theme.of(context).colorScheme.primary, + accentColor: MeshPalette.blue, onTap: () { + HapticFeedback.selectionClick(); Navigator.push( context, MaterialPageRoute( @@ -220,15 +191,15 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - const SizedBox(height: 16), - // Telemetry button - _buildManagementCard( - context, + + _HubActionTile( + index: 1, icon: Icons.bar_chart_sharp, title: l10n.repeater_telemetry, subtitle: l10n.repeater_telemetrySubtitle, - color: Theme.of(context).colorScheme.secondary, + accentColor: MeshPalette.magenta, onTap: () { + HapticFeedback.selectionClick(); Navigator.push( context, MaterialPageRoute( @@ -237,16 +208,34 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - if (isAdmin) const SizedBox(height: 12), - // CLI button - if (isAdmin) - _buildManagementCard( - context, + + _HubActionTile( + index: 2, + icon: Icons.group, + title: l10n.repeater_neighbors, + subtitle: l10n.repeater_neighborsSubtitle, + accentColor: MeshPalette.signal, + onTap: () { + HapticFeedback.selectionClick(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + NeighborsScreen(repeater: repeater, password: password), + ), + ); + }, + ), + + if (isAdmin) ...[ + _HubActionTile( + index: 3, icon: Icons.terminal, title: l10n.repeater_cli, subtitle: l10n.repeater_cliSubtitle, - color: Theme.of(context).colorScheme.tertiary, + accentColor: MeshPalette.warn, onTap: () { + HapticFeedback.selectionClick(); Navigator.push( context, MaterialPageRoute( @@ -258,34 +247,14 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - const SizedBox(height: 12), - // Neighbors button - _buildManagementCard( - context, - icon: Icons.group, - title: l10n.repeater_neighbors, - subtitle: l10n.repeater_neighborsSubtitle, - color: Theme.of(context).colorScheme.tertiary, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - NeighborsScreen(repeater: repeater, password: password), - ), - ); - }, - ), - if (isAdmin) const SizedBox(height: 12), - // Settings button - if (isAdmin) - _buildManagementCard( - context, + _HubActionTile( + index: 4, icon: Icons.settings, title: l10n.repeater_settings, subtitle: l10n.repeater_settingsSubtitle, - color: Theme.of(context).colorScheme.error, + accentColor: MeshPalette.alert, onTap: () { + HapticFeedback.selectionClick(); Navigator.push( context, MaterialPageRoute( @@ -297,66 +266,82 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), + ], ], ), ), ); } +} - Widget _buildManagementCard( - BuildContext context, { - required IconData icon, - required String title, - required String subtitle, - required Color color, - required VoidCallback onTap, - }) { - return Card( - elevation: 2, - child: InkWell( +class _HubActionTile extends StatelessWidget { + final int index; + final IconData icon; + final String title; + final String subtitle; + final Color accentColor; + final VoidCallback onTap; + + const _HubActionTile({ + required this.index, + required this.icon, + required this.title, + required this.subtitle, + required this.accentColor, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return ListEntrance( + index: index, + child: MeshCard( onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon(icon, color: color, size: 32), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - subtitle, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(MeshRadii.md), + border: Border.all( + color: accentColor.withValues(alpha: 0.3), ), ), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.onSurfaceVariant, + alignment: Alignment.center, + child: Icon(icon, size: 22, color: accentColor), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: TextStyle( + fontSize: 12.5, + color: scheme.onSurfaceVariant, + ), + ), + ], ), - ], - ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 20, + ), + ], ), ), ); diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index e04cfb70..30675e58 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -8,6 +8,8 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; import '../services/storage_service.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/routing_sheet.dart'; import '../helpers/snack_bar_builder.dart'; @@ -1003,39 +1005,6 @@ class _RepeaterSettingsScreenState extends State { } } - Widget _buildSectionHeader({ - required IconData icon, - required String title, - String? tooltip, - bool isRefreshing = false, - VoidCallback? onRefresh, - }) { - return Row( - children: [ - Icon(icon, color: Theme.of(context).textTheme.headlineSmall?.color), - const SizedBox(width: 8), - Text( - title, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - if (onRefresh != null) ...[ - const Spacer(), - IconButton( - icon: isRefreshing - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - onPressed: isRefreshing ? null : onRefresh, - tooltip: tooltip, - ), - ], - ], - ); - } - Widget _buildInlineRefreshButton({ required bool isRefreshing, required VoidCallback onRefresh, @@ -1067,21 +1036,8 @@ class _RepeaterSettingsScreenState extends State { return Scaffold( appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.repeater_settingsTitle), - Text( - repeater.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - centerTitle: false, + title: Text(l10n.repeater_settingsTitle), + centerTitle: true, actions: [ IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), @@ -1102,27 +1058,17 @@ class _RepeaterSettingsScreenState extends State { child: _isLoading && _nameController.text.isEmpty ? const Center(child: CircularProgressIndicator()) : ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only(bottom: 32), children: [ _buildBasicSettingsCard(), - const SizedBox(height: 16), _buildRadioSettingsCard(), - const SizedBox(height: 16), _buildLocationSettingsCard(), - const SizedBox(height: 16), _buildFeatureTogglesCard(), - const SizedBox(height: 16), _buildNetworkHealthCard(), - const SizedBox(height: 16), _buildAdvertisementSettingsCard(), - const SizedBox(height: 16), _buildOwnerInfoCard(), - const SizedBox(height: 16), _buildActionsCard(), - const SizedBox(height: 16), _buildAdvancedCard(), - const SizedBox(height: 32), - const Divider(), const SizedBox(height: 16), _buildDangerZoneCard(), ], @@ -1133,363 +1079,350 @@ class _RepeaterSettingsScreenState extends State { Widget _buildBasicSettingsCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader( - icon: Icons.settings, - title: l10n.repeater_basicSettings, - tooltip: l10n.repeater_refreshBasicSettings, - isRefreshing: _refreshingBasic, - onRefresh: _refreshBasicSettings, - ), - const Divider(), - TextField( - controller: _nameController, - decoration: InputDecoration( - labelText: l10n.repeater_repeaterName, - helperText: l10n.repeater_repeaterNameHelper, - border: const OutlineInputBorder(), + final refreshButton = _refreshingBasic + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : IconButton( + icon: const Icon(Icons.refresh, size: 18), + onPressed: _refreshBasicSettings, + tooltip: l10n.repeater_refreshBasicSettings, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_basicSettings, trailing: refreshButton), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: l10n.repeater_repeaterName, + helperText: l10n.repeater_repeaterNameHelper, + ), + onChanged: (_) => _markChanged(_SettingField.name), ), - onChanged: (_) => _markChanged(_SettingField.name), - ), - const SizedBox(height: 16), - TextField( - controller: _passwordController, - decoration: InputDecoration( - labelText: l10n.repeater_adminPassword, - helperText: l10n.repeater_adminPasswordHelper, - border: const OutlineInputBorder(), + const SizedBox(height: 12), + TextField( + controller: _passwordController, + decoration: InputDecoration( + labelText: l10n.repeater_adminPassword, + helperText: l10n.repeater_adminPasswordHelper, + ), + obscureText: true, + onChanged: (_) => _flagHasChanges(), ), - obscureText: true, - onChanged: (_) => _flagHasChanges(), - ), - const SizedBox(height: 16), - TextField( - controller: _guestPasswordController, - decoration: InputDecoration( - labelText: l10n.repeater_guestPassword, - helperText: l10n.repeater_guestPasswordHelper, - border: const OutlineInputBorder(), + const SizedBox(height: 12), + TextField( + controller: _guestPasswordController, + decoration: InputDecoration( + labelText: l10n.repeater_guestPassword, + helperText: l10n.repeater_guestPasswordHelper, + ), + obscureText: true, + onChanged: (_) => _flagHasChanges(), ), - obscureText: true, - onChanged: (_) => _flagHasChanges(), - ), - ], + ], + ), ), - ), + ], ); } Widget _buildRadioSettingsCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader( - icon: Icons.radio, - title: l10n.repeater_radioSettings, - tooltip: l10n.repeater_refreshRadioSettings, - isRefreshing: _refreshingRadio, - onRefresh: _refreshRadioSettings, - ), - const Divider(), - TextField( - controller: _freqController, - decoration: InputDecoration( - labelText: l10n.repeater_frequencyMhz, - helperText: l10n.repeater_frequencyHelper, - border: const OutlineInputBorder(), - suffixText: 'MHz', + final refreshButton = _refreshingRadio + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : IconButton( + icon: const Icon(Icons.refresh, size: 18), + onPressed: _refreshRadioSettings, + tooltip: l10n.repeater_refreshRadioSettings, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_radioSettings, trailing: refreshButton), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _freqController, + decoration: InputDecoration( + labelText: l10n.repeater_frequencyMhz, + helperText: l10n.repeater_frequencyHelper, + suffixText: 'MHz', + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + onChanged: (_) => _markChanged(_SettingField.radio), ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - onChanged: (_) => _markChanged(_SettingField.radio), - ), - const SizedBox(height: 16), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - controller: _txPowerController, - decoration: InputDecoration( - labelText: l10n.repeater_txPower, - helperText: l10n.repeater_txPowerHelper, - border: const OutlineInputBorder(), - suffixText: 'dBm', + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _txPowerController, + decoration: InputDecoration( + labelText: l10n.repeater_txPower, + helperText: l10n.repeater_txPowerHelper, + suffixText: 'dBm', + ), + keyboardType: TextInputType.number, + onChanged: (_) => _markChanged(_SettingField.txPower), ), - keyboardType: TextInputType.number, - onChanged: (_) => _markChanged(_SettingField.txPower), ), + _buildInlineRefreshButton( + isRefreshing: _refreshingTxPower, + onRefresh: _refreshTxPower, + tooltip: l10n.repeater_refreshTxPower, + ), + ], + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: _bandwidth, + decoration: InputDecoration( + labelText: l10n.repeater_bandwidth, ), - const SizedBox(width: 8), - _buildInlineRefreshButton( - isRefreshing: _refreshingTxPower, - onRefresh: _refreshTxPower, - tooltip: l10n.repeater_refreshTxPower, + items: _bandwidthOptions.map((bw) { + return DropdownMenuItem( + value: bw, + child: Text(_formatBandwidthLabel(bw)), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _bandwidth = value; + }); + _markChanged(_SettingField.radio); + } + }, + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: _spreadingFactor, + decoration: InputDecoration( + labelText: l10n.repeater_spreadingFactor, ), - ], - ), - const SizedBox(height: 16), - DropdownButtonFormField( - initialValue: _bandwidth, - decoration: InputDecoration( - labelText: l10n.repeater_bandwidth, - border: const OutlineInputBorder(), + items: _spreadingFactorOptions.map((sf) { + return DropdownMenuItem(value: sf, child: Text('SF$sf')); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _spreadingFactor = value; + }); + _markChanged(_SettingField.radio); + } + }, ), - items: _bandwidthOptions.map((bw) { - return DropdownMenuItem( - value: bw, - child: Text(_formatBandwidthLabel(bw)), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - _bandwidth = value; - }); - _markChanged(_SettingField.radio); - } - }, - ), - const SizedBox(height: 16), - DropdownButtonFormField( - initialValue: _spreadingFactor, - decoration: InputDecoration( - labelText: l10n.repeater_spreadingFactor, - border: const OutlineInputBorder(), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: _codingRate, + decoration: InputDecoration( + labelText: l10n.repeater_codingRate, + ), + items: _codingRateOptions.map((cr) { + return DropdownMenuItem(value: cr, child: Text('4/$cr')); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _codingRate = value; + }); + _markChanged(_SettingField.radio); + } + }, ), - items: _spreadingFactorOptions.map((sf) { - return DropdownMenuItem(value: sf, child: Text('SF$sf')); - }).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - _spreadingFactor = value; - }); - _markChanged(_SettingField.radio); - } - }, - ), - const SizedBox(height: 16), - DropdownButtonFormField( - initialValue: _codingRate, - decoration: InputDecoration( - labelText: l10n.repeater_codingRate, - border: const OutlineInputBorder(), + const SizedBox(height: 4), + _buildFeatureToggleRow( + title: l10n.repeater_rxGain, + subtitle: l10n.repeater_rxGainHelper, + value: _rxGainBoosted, + isRefreshing: _refreshingRxGain, + onChanged: (v) { + setState(() => _rxGainBoosted = v); + _markChanged(_SettingField.rxGain); + }, + onRefresh: _refreshRxGain, + refreshTooltip: l10n.repeater_refreshRxGain, ), - items: _codingRateOptions.map((cr) { - return DropdownMenuItem(value: cr, child: Text('4/$cr')); - }).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - _codingRate = value; - }); - _markChanged(_SettingField.radio); - } - }, - ), - const SizedBox(height: 8), - _buildFeatureToggleRow( - title: l10n.repeater_rxGain, - subtitle: l10n.repeater_rxGainHelper, - value: _rxGainBoosted, - isRefreshing: _refreshingRxGain, - onChanged: (v) { - setState(() => _rxGainBoosted = v); - _markChanged(_SettingField.rxGain); - }, - onRefresh: _refreshRxGain, - refreshTooltip: l10n.repeater_refreshRxGain, - ), - ], + ], + ), ), - ), + ], ); } Widget _buildLocationSettingsCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader( - icon: Icons.location_on, - title: l10n.repeater_locationSettings, - ), - const Divider(), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - controller: _latController, - decoration: InputDecoration( - labelText: l10n.repeater_latitude, - helperText: l10n.repeater_latitudeHelper, - errorText: _latInvalid - ? l10n.settings_locationInvalid - : null, - border: const OutlineInputBorder(), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_locationSettings), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _latController, + decoration: InputDecoration( + labelText: l10n.repeater_latitude, + helperText: l10n.repeater_latitudeHelper, + errorText: _latInvalid + ? l10n.settings_locationInvalid + : null, + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + onChanged: (value) { + _markChanged(_SettingField.lat); + final invalid = !_isValidCoordinate(value, 90); + if (invalid != _latInvalid) { + setState(() => _latInvalid = invalid); + } + }, ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - onChanged: (value) { - _markChanged(_SettingField.lat); - final invalid = !_isValidCoordinate(value, 90); - if (invalid != _latInvalid) { - setState(() => _latInvalid = invalid); - } - }, ), - ), - const SizedBox(width: 8), - _buildInlineRefreshButton( - isRefreshing: _refreshingLat, - onRefresh: _refreshLat, - tooltip: l10n.repeater_latitude, - ), - ], - ), - const SizedBox(height: 16), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - controller: _lonController, - decoration: InputDecoration( - labelText: l10n.repeater_longitude, - helperText: l10n.repeater_longitudeHelper, - errorText: _lonInvalid - ? l10n.settings_locationInvalid - : null, - border: const OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - onChanged: (value) { - _markChanged(_SettingField.lon); - final invalid = !_isValidCoordinate(value, 180); - if (invalid != _lonInvalid) { - setState(() => _lonInvalid = invalid); - } - }, + _buildInlineRefreshButton( + isRefreshing: _refreshingLat, + onRefresh: _refreshLat, + tooltip: l10n.repeater_latitude, ), - ), - const SizedBox(width: 8), - _buildInlineRefreshButton( - isRefreshing: _refreshingLon, - onRefresh: _refreshLon, - tooltip: l10n.repeater_longitude, - ), - ], - ), - ], + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _lonController, + decoration: InputDecoration( + labelText: l10n.repeater_longitude, + helperText: l10n.repeater_longitudeHelper, + errorText: _lonInvalid + ? l10n.settings_locationInvalid + : null, + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + onChanged: (value) { + _markChanged(_SettingField.lon); + final invalid = !_isValidCoordinate(value, 180); + if (invalid != _lonInvalid) { + setState(() => _lonInvalid = invalid); + } + }, + ), + ), + _buildInlineRefreshButton( + isRefreshing: _refreshingLon, + onRefresh: _refreshLon, + tooltip: l10n.repeater_longitude, + ), + ], + ), + ], + ), ), - ), + ], ); } Widget _buildFeatureTogglesCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.toggle_on, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_features, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - _buildFeatureToggleRow( - title: l10n.repeater_packetForwarding, - subtitle: l10n.repeater_packetForwardingSubtitle, - value: _repeatEnabled, - isRefreshing: _refreshingRepeat, - onChanged: (value) { - setState(() { - _repeatEnabled = value; - }); - _markChanged(_SettingField.repeat); - }, - onRefresh: _refreshRepeat, - refreshTooltip: l10n.repeater_refreshPacketForwarding, - ), - _buildFeatureToggleRow( - title: l10n.repeater_guestAccess, - subtitle: l10n.repeater_guestAccessSubtitle, - value: _allowReadOnly, - isRefreshing: _refreshingAllowReadOnly, - onChanged: (value) { - setState(() { - _allowReadOnly = value; - }); - _markChanged(_SettingField.allowReadOnly); - }, - onRefresh: _refreshAllowReadOnly, - refreshTooltip: l10n.repeater_refreshGuestAccess, - ), - _buildFeatureToggleRow( - title: l10n.repeater_multiAcks, - subtitle: l10n.repeater_multiAcksSubtitle, - value: _multiAcks, - isRefreshing: _refreshingMultiAcks, - onChanged: (v) { - setState(() => _multiAcks = v); - _markChanged(_SettingField.multiAcks); - }, - onRefresh: _refreshMultiAcks, - refreshTooltip: l10n.repeater_refreshMultiAcks, - ), - SwitchListTile( - title: Text(l10n.repeater_clockSyncAfterLogin), - subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle), - value: _autoClockSyncAfterLogin, - onChanged: (value) async { - setState(() { - _autoClockSyncAfterLogin = value; - }); - await _storage.setRepeaterAutoClockSyncAfterLoginEnabled( - widget.repeater.publicKeyHex, - value, - ); - }, - contentPadding: EdgeInsets.zero, - ), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_features), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFeatureToggleRow( + title: l10n.repeater_packetForwarding, + subtitle: l10n.repeater_packetForwardingSubtitle, + value: _repeatEnabled, + isRefreshing: _refreshingRepeat, + onChanged: (value) { + setState(() { + _repeatEnabled = value; + }); + _markChanged(_SettingField.repeat); + }, + onRefresh: _refreshRepeat, + refreshTooltip: l10n.repeater_refreshPacketForwarding, + ), + _buildFeatureToggleRow( + title: l10n.repeater_guestAccess, + subtitle: l10n.repeater_guestAccessSubtitle, + value: _allowReadOnly, + isRefreshing: _refreshingAllowReadOnly, + onChanged: (value) { + setState(() { + _allowReadOnly = value; + }); + _markChanged(_SettingField.allowReadOnly); + }, + onRefresh: _refreshAllowReadOnly, + refreshTooltip: l10n.repeater_refreshGuestAccess, + ), + _buildFeatureToggleRow( + title: l10n.repeater_multiAcks, + subtitle: l10n.repeater_multiAcksSubtitle, + value: _multiAcks, + isRefreshing: _refreshingMultiAcks, + onChanged: (v) { + setState(() => _multiAcks = v); + _markChanged(_SettingField.multiAcks); + }, + onRefresh: _refreshMultiAcks, + refreshTooltip: l10n.repeater_refreshMultiAcks, + ), + SwitchListTile( + title: Text(l10n.repeater_clockSyncAfterLogin), + subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle), + value: _autoClockSyncAfterLogin, + onChanged: (value) async { + setState(() { + _autoClockSyncAfterLogin = value; + }); + await _storage.setRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + value, + ); + }, + contentPadding: EdgeInsets.zero, + ), + ], + ), ), - ), + ], ); } @@ -1531,388 +1464,370 @@ class _RepeaterSettingsScreenState extends State { Widget _buildAdvertisementSettingsCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader( - icon: Icons.broadcast_on_personal, - title: l10n.repeater_advertisementSettings, - ), - const Divider(), - Row( - children: [ - Expanded( - child: ListTile( - title: Text(l10n.repeater_localAdvertInterval), - subtitle: Text( - l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_advertisementSettings), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: ListTile( + title: Text(l10n.repeater_localAdvertInterval), + subtitle: Text( + l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + ), + trailing: Switch( + value: _advertEnable, + onChanged: (value) { + setState(() { + _advertInterval = value ? 60 : 0; + _advertEnable = value; + }); + _markChanged(_SettingField.advertInterval); + }, + ), + contentPadding: EdgeInsets.zero, ), - trailing: Switch( - value: _advertEnable, - onChanged: (value) { + ), + IconButton( + icon: _refreshingAdvertInterval + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 20), + onPressed: _refreshingAdvertInterval + ? null + : _refreshAdvertInterval, + tooltip: l10n.repeater_localAdvertInterval, + visualDensity: VisualDensity.compact, + ), + ], + ), + Slider( + value: _advertInterval == 0 + ? 60.toDouble() + : _advertInterval.toDouble(), + min: 60, + max: 240, + divisions: 18, + label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + onChanged: _advertEnable + ? (value) { setState(() { - _advertInterval = value ? 60 : 0; - _advertEnable = value; + _advertInterval = value.toInt(); }); _markChanged(_SettingField.advertInterval); - }, - ), - contentPadding: EdgeInsets.zero, - ), - ), - IconButton( - icon: _refreshingAdvertInterval - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh, size: 20), - onPressed: _refreshingAdvertInterval - ? null - : _refreshAdvertInterval, - tooltip: l10n.repeater_localAdvertInterval, - visualDensity: VisualDensity.compact, - ), - ], - ), - Slider( - value: _advertInterval == 0 - ? 60.toDouble() - : _advertInterval.toDouble(), - min: 60, - max: 240, - divisions: 18, - label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval), - onChanged: _advertEnable - ? (value) { - setState(() { - _advertInterval = value.toInt(); - }); - _markChanged(_SettingField.advertInterval); - } - : null, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: ListTile( - title: Text(l10n.repeater_floodAdvertInterval), - subtitle: Text( - l10n.repeater_floodAdvertIntervalHours( - _floodAdvertInterval, + } + : null, + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ListTile( + title: Text(l10n.repeater_floodAdvertInterval), + subtitle: Text( + l10n.repeater_floodAdvertIntervalHours( + _floodAdvertInterval, + ), ), + trailing: Switch( + value: _floodAdvertEnable, + onChanged: (value) { + setState(() { + _floodAdvertInterval = value ? 3 : 0; + _floodAdvertEnable = value; + }); + _markChanged(_SettingField.floodAdvertInterval); + }, + ), + contentPadding: EdgeInsets.zero, ), - trailing: Switch( - value: _floodAdvertEnable, - onChanged: (value) { + ), + IconButton( + icon: _refreshingFloodAdvertInterval + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 20), + onPressed: _refreshingFloodAdvertInterval + ? null + : _refreshFloodAdvertInterval, + tooltip: l10n.repeater_floodAdvertInterval, + visualDensity: VisualDensity.compact, + ), + ], + ), + Slider( + value: _floodAdvertInterval == 0 + ? 3.toDouble() + : _floodAdvertInterval.toDouble(), + min: 3, + max: 168, + divisions: 165, + label: l10n.repeater_floodAdvertIntervalHours( + _floodAdvertInterval, + ), + onChanged: _floodAdvertEnable + ? (value) { setState(() { - _floodAdvertInterval = value ? 3 : 0; - _floodAdvertEnable = value; + _floodAdvertInterval = value.toInt(); }); _markChanged(_SettingField.floodAdvertInterval); - }, - ), - contentPadding: EdgeInsets.zero, - ), - ), - IconButton( - icon: _refreshingFloodAdvertInterval - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh, size: 20), - onPressed: _refreshingFloodAdvertInterval - ? null - : _refreshFloodAdvertInterval, - tooltip: l10n.repeater_floodAdvertInterval, - visualDensity: VisualDensity.compact, - ), - ], - ), - Slider( - value: _floodAdvertInterval == 0 - ? 3.toDouble() - : _floodAdvertInterval.toDouble(), - min: 3, - max: 168, - divisions: 165, - label: l10n.repeater_floodAdvertIntervalHours( - _floodAdvertInterval, + } + : null, ), - onChanged: _floodAdvertEnable - ? (value) { - setState(() { - _floodAdvertInterval = value.toInt(); - }); - _markChanged(_SettingField.floodAdvertInterval); - } - : null, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: ListTile( - title: Text(l10n.repeater_floodMax), - subtitle: Text(l10n.repeater_floodMaxHelper), - trailing: Text( - '$_floodMax', - style: const TextStyle(fontWeight: FontWeight.bold), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ListTile( + title: Text(l10n.repeater_floodMax), + subtitle: Text(l10n.repeater_floodMaxHelper), + trailing: Text( + '$_floodMax', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: EdgeInsets.zero, ), - contentPadding: EdgeInsets.zero, ), - ), - IconButton( - icon: _refreshingFloodMax - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh, size: 20), - onPressed: _refreshingFloodMax ? null : _refreshFloodMax, - tooltip: l10n.repeater_floodMax, - visualDensity: VisualDensity.compact, - ), - ], - ), - Slider( - value: _floodMax.toDouble(), - min: 0, - max: 64, - divisions: 64, - label: '$_floodMax', - onChanged: (v) { - setState(() => _floodMax = v.toInt()); - _markChanged(_SettingField.floodMax); - }, - ), - ], + IconButton( + icon: _refreshingFloodMax + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 20), + onPressed: _refreshingFloodMax ? null : _refreshFloodMax, + tooltip: l10n.repeater_floodMax, + visualDensity: VisualDensity.compact, + ), + ], + ), + Slider( + value: _floodMax.toDouble(), + min: 0, + max: 64, + divisions: 64, + label: '$_floodMax', + onChanged: (v) { + setState(() => _floodMax = v.toInt()); + _markChanged(_SettingField.floodMax); + }, + ), + ], + ), ), - ), + ], ); } Widget _buildNetworkHealthCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader( - icon: Icons.health_and_safety, - title: l10n.repeater_networkHealth, - ), - const Divider(), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: DropdownButtonFormField( - initialValue: _loopDetect, - decoration: InputDecoration( - labelText: l10n.repeater_loopDetect, - helperText: l10n.repeater_loopDetectHelper, - helperMaxLines: 3, - border: const OutlineInputBorder(), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_networkHealth), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: DropdownButtonFormField( + initialValue: _loopDetect, + decoration: InputDecoration( + labelText: l10n.repeater_loopDetect, + helperText: l10n.repeater_loopDetectHelper, + helperMaxLines: 3, + ), + items: [ + DropdownMenuItem( + value: 'off', + child: Text(l10n.repeater_loopDetectOff), + ), + DropdownMenuItem( + value: 'minimal', + child: Text(l10n.repeater_loopDetectMinimal), + ), + DropdownMenuItem( + value: 'moderate', + child: Text(l10n.repeater_loopDetectModerate), + ), + DropdownMenuItem( + value: 'strict', + child: Text(l10n.repeater_loopDetectStrict), + ), + ], + onChanged: (v) { + if (v != null) { + setState(() => _loopDetect = v); + _markChanged(_SettingField.loopDetect); + } + }, ), - items: [ - DropdownMenuItem( - value: 'off', - child: Text(l10n.repeater_loopDetectOff), - ), - DropdownMenuItem( - value: 'minimal', - child: Text(l10n.repeater_loopDetectMinimal), - ), - DropdownMenuItem( - value: 'moderate', - child: Text(l10n.repeater_loopDetectModerate), - ), - DropdownMenuItem( - value: 'strict', - child: Text(l10n.repeater_loopDetectStrict), - ), - ], - onChanged: (v) { - if (v != null) { - setState(() => _loopDetect = v); - _markChanged(_SettingField.loopDetect); - } - }, ), - ), - const SizedBox(width: 8), - _buildInlineRefreshButton( - isRefreshing: _refreshingLoopDetect, - onRefresh: _refreshLoopDetect, - tooltip: l10n.repeater_loopDetect, - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: ListTile( - title: Text(l10n.repeater_dutyCycle), - subtitle: Text(l10n.repeater_dutyCycleHelper), - trailing: Text( - l10n.repeater_dutyCyclePercent(_dutyCycle), - style: const TextStyle(fontWeight: FontWeight.bold), + _buildInlineRefreshButton( + isRefreshing: _refreshingLoopDetect, + onRefresh: _refreshLoopDetect, + tooltip: l10n.repeater_loopDetect, + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ListTile( + title: Text(l10n.repeater_dutyCycle), + subtitle: Text(l10n.repeater_dutyCycleHelper), + trailing: Text( + l10n.repeater_dutyCyclePercent(_dutyCycle), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: EdgeInsets.zero, ), - contentPadding: EdgeInsets.zero, ), - ), - IconButton( - icon: _refreshingDutyCycle - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh, size: 20), - onPressed: _refreshingDutyCycle ? null : _refreshDutyCycle, - tooltip: l10n.repeater_dutyCycle, - visualDensity: VisualDensity.compact, - ), - ], - ), - Slider( - value: _dutyCycle.toDouble(), - min: 1, - max: 100, - divisions: 99, - label: l10n.repeater_dutyCyclePercent(_dutyCycle), - onChanged: (v) { - setState(() => _dutyCycle = v.toInt()); - _markChanged(_SettingField.dutyCycle); - }, - ), - ], + IconButton( + icon: _refreshingDutyCycle + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 20), + onPressed: _refreshingDutyCycle ? null : _refreshDutyCycle, + tooltip: l10n.repeater_dutyCycle, + visualDensity: VisualDensity.compact, + ), + ], + ), + Slider( + value: _dutyCycle.toDouble(), + min: 1, + max: 100, + divisions: 99, + label: l10n.repeater_dutyCyclePercent(_dutyCycle), + onChanged: (v) { + setState(() => _dutyCycle = v.toInt()); + _markChanged(_SettingField.dutyCycle); + }, + ), + ], + ), ), - ), + ], ); } Widget _buildOwnerInfoCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader( - icon: Icons.person_outline, - title: l10n.repeater_ownerInfo, - tooltip: l10n.repeater_refreshOwnerInfo, - isRefreshing: _refreshingOwnerInfo, - onRefresh: _refreshOwnerInfo, + final refreshButton = _refreshingOwnerInfo + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : IconButton( + icon: const Icon(Icons.refresh, size: 18), + onPressed: _refreshOwnerInfo, + tooltip: l10n.repeater_refreshOwnerInfo, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_ownerInfo, trailing: refreshButton), + MeshCard( + child: TextField( + controller: _ownerInfoController, + decoration: InputDecoration( + labelText: l10n.repeater_ownerInfo, + helperText: l10n.repeater_ownerInfoHelper, + helperMaxLines: 3, ), - const Divider(), - TextField( - controller: _ownerInfoController, - decoration: InputDecoration( - labelText: l10n.repeater_ownerInfo, - helperText: l10n.repeater_ownerInfoHelper, - helperMaxLines: 3, - border: const OutlineInputBorder(), - ), - maxLines: 4, - minLines: 2, - onChanged: (_) => _markChanged(_SettingField.ownerInfo), - ), - ], + maxLines: 4, + minLines: 2, + onChanged: (_) => _markChanged(_SettingField.ownerInfo), + ), ), - ), + ], ); } Widget _buildActionsCard() { final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.flash_on, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_actionsTitle, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - ListTile( - leading: const Icon(Icons.podcasts), - title: Text(l10n.repeater_sendAdvert), - subtitle: Text(l10n.repeater_sendAdvertSubtitle), - enabled: !_runningAction, - onTap: _runningAction - ? null - : () => _runAction('advert', l10n.repeater_sendAdvert), - contentPadding: EdgeInsets.zero, - ), - ListTile( - leading: const Icon(Icons.cell_tower), - title: Text(l10n.repeater_sendAdvertZeroHop), - subtitle: Text(l10n.repeater_sendAdvertZeroHopSubtitle), - enabled: !_runningAction, - onTap: _runningAction - ? null - : () => _runAction( - 'advert.zerohop', - l10n.repeater_sendAdvertZeroHop, - ), - contentPadding: EdgeInsets.zero, - ), - ListTile( - leading: const Icon(Icons.access_time), - title: Text(l10n.repeater_clockSync), - subtitle: Text(l10n.repeater_clockSyncSubtitle), - enabled: !_runningAction, - onTap: _runningAction - ? null - : () => _runAction('clock sync', l10n.repeater_clockSync), - contentPadding: EdgeInsets.zero, - ), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(l10n.repeater_actionsTitle), + MeshCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: const Icon(Icons.podcasts), + title: Text(l10n.repeater_sendAdvert), + subtitle: Text(l10n.repeater_sendAdvertSubtitle), + enabled: !_runningAction, + onTap: _runningAction + ? null + : () => _runAction('advert', l10n.repeater_sendAdvert), + contentPadding: EdgeInsets.zero, + ), + ListTile( + leading: const Icon(Icons.cell_tower), + title: Text(l10n.repeater_sendAdvertZeroHop), + subtitle: Text(l10n.repeater_sendAdvertZeroHopSubtitle), + enabled: !_runningAction, + onTap: _runningAction + ? null + : () => _runAction( + 'advert.zerohop', + l10n.repeater_sendAdvertZeroHop, + ), + contentPadding: EdgeInsets.zero, + ), + ListTile( + leading: const Icon(Icons.access_time), + title: Text(l10n.repeater_clockSync), + subtitle: Text(l10n.repeater_clockSyncSubtitle), + enabled: !_runningAction, + onTap: _runningAction + ? null + : () => _runAction('clock sync', l10n.repeater_clockSync), + contentPadding: EdgeInsets.zero, + ), + ], + ), ), - ), + ], ); } Widget _buildAdvancedCard() { final l10n = context.l10n; - return Card( + return MeshCard( child: ExpansionTile( leading: const Icon(Icons.tune), title: Text( l10n.repeater_advancedSettings, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ), subtitle: Text(l10n.repeater_advancedSettingsSubtitle), - childrenPadding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + childrenPadding: const EdgeInsets.fromLTRB(0, 8, 0, 4), children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -1924,7 +1839,6 @@ class _RepeaterSettingsScreenState extends State { labelText: l10n.repeater_pathHashMode, helperText: l10n.repeater_pathHashModeHelper, helperMaxLines: 5, - border: const OutlineInputBorder(), ), items: const [ DropdownMenuItem(value: 0, child: Text('0')), @@ -1939,7 +1853,6 @@ class _RepeaterSettingsScreenState extends State { }, ), ), - const SizedBox(width: 8), _buildInlineRefreshButton( isRefreshing: _refreshingPathHashMode, onRefresh: _refreshPathHashMode, @@ -1947,7 +1860,7 @@ class _RepeaterSettingsScreenState extends State { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1958,7 +1871,6 @@ class _RepeaterSettingsScreenState extends State { labelText: l10n.repeater_txDelay, helperText: l10n.repeater_txDelayHelper, helperMaxLines: 3, - border: const OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions( decimal: true, @@ -1966,7 +1878,6 @@ class _RepeaterSettingsScreenState extends State { onChanged: (_) => _markChanged(_SettingField.txDelay), ), ), - const SizedBox(width: 8), _buildInlineRefreshButton( isRefreshing: _refreshingTxDelay, onRefresh: _refreshTxDelay, @@ -1974,7 +1885,7 @@ class _RepeaterSettingsScreenState extends State { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1985,7 +1896,6 @@ class _RepeaterSettingsScreenState extends State { labelText: l10n.repeater_directTxDelay, helperText: l10n.repeater_directTxDelayHelper, helperMaxLines: 3, - border: const OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions( decimal: true, @@ -1993,7 +1903,6 @@ class _RepeaterSettingsScreenState extends State { onChanged: (_) => _markChanged(_SettingField.directTxDelay), ), ), - const SizedBox(width: 8), _buildInlineRefreshButton( isRefreshing: _refreshingDirectTxDelay, onRefresh: _refreshDirectTxDelay, @@ -2001,7 +1910,7 @@ class _RepeaterSettingsScreenState extends State { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -2012,13 +1921,11 @@ class _RepeaterSettingsScreenState extends State { labelText: l10n.repeater_intThresh, helperText: l10n.repeater_intThreshHelper, helperMaxLines: 3, - border: const OutlineInputBorder(), ), keyboardType: TextInputType.number, onChanged: (_) => _markChanged(_SettingField.intThresh), ), ), - const SizedBox(width: 8), _buildInlineRefreshButton( isRefreshing: _refreshingIntThresh, onRefresh: _refreshIntThresh, @@ -2026,7 +1933,7 @@ class _RepeaterSettingsScreenState extends State { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), Row( children: [ Expanded( @@ -2077,85 +1984,72 @@ class _RepeaterSettingsScreenState extends State { Widget _buildDangerZoneCard() { final l10n = context.l10n; - final colorScheme = Theme.of(context).colorScheme; - return Card( - color: colorScheme.errorContainer, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.warning, color: colorScheme.onErrorContainer), - const SizedBox(width: 8), - Text( - l10n.repeater_dangerZone, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: colorScheme.onErrorContainer, - ), - ), - ], - ), - const Divider(), - ListTile( - leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer), - title: Text( - l10n.repeater_rebootRepeater, - style: TextStyle(color: colorScheme.onErrorContainer), - ), - subtitle: Text( - l10n.repeater_rebootRepeaterSubtitle, - style: TextStyle( - color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + return MeshCard( + color: MeshPalette.alertBg, + borderColor: MeshPalette.alertLine, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.warning, color: MeshPalette.alert), + const SizedBox(width: 8), + Text( + l10n.repeater_dangerZone, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: MeshPalette.alert, ), ), - onTap: () => _confirmAction( - l10n.repeater_rebootRepeater, - l10n.repeater_rebootRepeaterConfirm, - () => _sendDangerCommand('reboot'), + ], + ), + const Divider(height: 20, color: MeshPalette.alertLine), + ListTile( + leading: const Icon(Icons.refresh, color: MeshPalette.alert), + title: Text( + l10n.repeater_rebootRepeater, + style: const TextStyle(color: MeshPalette.alert), + ), + subtitle: Text( + l10n.repeater_rebootRepeaterSubtitle, + style: const TextStyle( + color: MeshPalette.warnDim, ), ), - // Regenerate identity key - hidden until fully implemented - // ListTile( - // leading: Icon(Icons.vpn_key, color: colorScheme.onErrorContainer), - // title: Text('Regenerate Identity Key', style: TextStyle(color: colorScheme.onErrorContainer)), - // subtitle: Text( - // 'Generate new public/private key pair', - // style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), - // ), - // onTap: () => _confirmAction( - // 'Regenerate Identity', - // 'This will generate a new identity for the repeater. Continue?', - // () => _sendDangerCommand('regen key'), - // ), - // ), - ListTile( - leading: Icon( - Icons.delete_forever, - color: colorScheme.onErrorContainer, - ), - title: Text( - l10n.repeater_eraseFileSystem, - style: TextStyle(color: colorScheme.onErrorContainer), - ), - subtitle: Text( - l10n.repeater_eraseFileSystemSubtitle, - style: TextStyle( - color: colorScheme.onErrorContainer.withValues(alpha: 0.8), - ), - ), - onTap: () => _confirmAction( - l10n.repeater_eraseFileSystem, - l10n.repeater_eraseFileSystemConfirm, - () => _sendDangerCommand('erase'), - isDestructive: true, + onTap: () => _confirmAction( + l10n.repeater_rebootRepeater, + l10n.repeater_rebootRepeaterConfirm, + () => _sendDangerCommand('reboot'), + ), + contentPadding: EdgeInsets.zero, + ), + // Regenerate identity key - hidden until fully implemented + ListTile( + leading: const Icon( + Icons.delete_forever, + color: MeshPalette.alert, + ), + title: Text( + l10n.repeater_eraseFileSystem, + style: const TextStyle(color: MeshPalette.alert), + ), + subtitle: Text( + l10n.repeater_eraseFileSystemSubtitle, + style: const TextStyle( + color: MeshPalette.warnDim, ), ), - ], - ), + onTap: () => _confirmAction( + l10n.repeater_eraseFileSystem, + l10n.repeater_eraseFileSystemConfirm, + () => _sendDangerCommand('erase'), + isDestructive: true, + ), + contentPadding: EdgeInsets.zero, + ), + ], ), ); } diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index f121605d..080d5b7f 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -10,7 +11,9 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; +import '../theme/mesh_theme.dart'; import '../utils/battery_utils.dart'; +import '../widgets/mesh_ui.dart'; import '../widgets/routing_sheet.dart'; import '../helpers/snack_bar_builder.dart'; @@ -64,8 +67,6 @@ class _RepeaterStatusScreenState extends State { final connector = Provider.of(context, listen: false); _commandService = RepeaterCommandService(connector); _setupMessageListener(); - // Defer until after the first frame so any notifyListeners() triggered - // during preparePathForContactSend doesn't fire mid-build. WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _loadStatus(); }); @@ -81,12 +82,8 @@ class _RepeaterStatusScreenState extends State { void _setupMessageListener() { final connector = Provider.of(context, listen: false); - - // Listen for incoming text messages from the repeater _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; - - // Check if it's a text message response if (frame[0] == pushCodeStatusResponse) { _handleStatusResponse(frame); } else if (frame[0] == respCodeContactMsgRecv || @@ -118,11 +115,7 @@ class _RepeaterStatusScreenState extends State { final parsed = parseContactMessageText(frame); if (parsed == null) return; if (!_matchesRepeaterPrefix(parsed.senderPrefix)) return; - - // Notify command service of response (for retry handling) _commandService?.handleResponse(widget.repeater, parsed.text); - - // Parse status responses _parseStatusResponse(parsed.text); _recordStatusResult(true); } @@ -131,7 +124,6 @@ class _RepeaterStatusScreenState extends State { if (frame.length < 8) return; final prefix = frame.sublist(2, 8); if (!_matchesRepeaterPrefix(prefix)) return; - if (frame.length < _statusResponseBytes) return; final data = ByteData.sublistView( @@ -254,14 +246,9 @@ class _RepeaterStatusScreenState extends State { _dupFlood = _asInt(data['dup_flood']); _dupDirect = _asInt(data['dup_direct']); } - } catch (_) { - // Ignore parse failures for non-JSON responses. - } - } - - if (mounted) { - setState(() {}); + } catch (_) {} } + if (mounted) setState(() {}); } Future _loadStatus() async { @@ -302,9 +289,7 @@ class _RepeaterStatusScreenState extends State { var messageBytes = frame.length >= _statusResponseBytes ? frame.length : _statusResponseBytes; - if (messageBytes < maxFrameSize) { - messageBytes = maxFrameSize; - } + if (messageBytes < maxFrameSize) messageBytes = maxFrameSize; final timeoutMs = connector.calculateTimeout( pathLength: pathLengthValue, messageBytes: messageBytes, @@ -312,9 +297,7 @@ class _RepeaterStatusScreenState extends State { _statusTimeout?.cancel(); _statusTimeout = Timer(Duration(milliseconds: timeoutMs), () { if (!mounted) return; - setState(() { - _isLoading = false; - }); + setState(() => _isLoading = false); showDismissibleSnackBar( context, content: Text(context.l10n.repeater_statusRequestTimeout), @@ -324,10 +307,7 @@ class _RepeaterStatusScreenState extends State { }); } catch (e) { if (mounted) { - setState(() { - _isLoading = false; - }); - + setState(() => _isLoading = false); showDismissibleSnackBar( context, content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())), @@ -347,214 +327,6 @@ class _RepeaterStatusScreenState extends State { _pendingStatusSelection = null; } - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - final connector = context.watch(); - final repeater = _resolveRepeater(connector); - final isFloodMode = repeater.pathOverride == -1; - - return Scaffold( - appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.repeater_statusTitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - repeater.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - centerTitle: false, - actions: [ - IconButton( - icon: Icon(isFloodMode ? Icons.waves : Icons.route), - tooltip: l10n.repeater_routingMode, - onPressed: () => - ContactRoutingSheet.show(context, contact: repeater), - ), - IconButton( - icon: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - onPressed: _isLoading ? null : _loadStatus, - tooltip: l10n.repeater_refresh, - ), - ], - ), - body: SafeArea( - top: false, - child: RefreshIndicator( - onRefresh: _loadStatus, - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildSystemInfoCard(), - const SizedBox(height: 16), - _buildRadioStatsCard(), - const SizedBox(height: 16), - _buildPacketStatsCard(), - ], - ), - ), - ), - ); - } - - Widget _buildSystemInfoCard() { - final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_systemInformation, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - _buildInfoRow(l10n.repeater_battery, _batteryText()), - _buildInfoRow(l10n.repeater_clockAtLogin, _clockText()), - _buildInfoRow(l10n.repeater_uptime, _formatDuration(_uptimeSecs)), - _buildInfoRow(l10n.repeater_queueLength, _formatValue(_queueLen)), - _buildInfoRow(l10n.repeater_debugFlags, _formatValue(_debugFlags)), - ], - ), - ), - ); - } - - Widget _buildRadioStatsCard() { - final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.radio, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_radioStatistics, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - _buildInfoRow( - l10n.repeater_lastRssi, - _formatValue(_lastRssi, suffix: ' dB'), - ), - _buildInfoRow(l10n.repeater_lastSnr, _formatSnr(_lastSnr)), - _buildInfoRow( - l10n.repeater_noiseFloor, - _formatValue(_noiseFloor, suffix: ' dB'), - ), - _buildInfoRow(l10n.repeater_txAirtime, _formatDuration(_txAirSecs)), - _buildInfoRow(l10n.repeater_rxAirtime, _formatDuration(_rxAirSecs)), - ], - ), - ), - ); - } - - Widget _buildPacketStatsCard() { - final l10n = context.l10n; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.analytics, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - l10n.repeater_packetStatistics, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - _buildInfoRow(l10n.repeater_sent, _packetTxText()), - _buildInfoRow(l10n.repeater_received, _packetRxText()), - _buildInfoRow(l10n.repeater_duplicates, _duplicateText()), - _buildInfoRow(l10n.repeater_chanUtil, _chanUtilText()), - ], - ), - ), - ); - } - - Widget _buildInfoRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - label, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(width: 8), - Text( - value, - style: const TextStyle(fontWeight: FontWeight.w400), - textAlign: TextAlign.end, - ), - ], - ), - ); - } - int? _asInt(dynamic value) { if (value == null) return null; if (value is int) return value; @@ -661,4 +433,218 @@ class _RepeaterStatusScreenState extends State { if (snr == null) return '—'; return snr.toStringAsFixed(2); } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final connector = context.watch(); + final repeater = _resolveRepeater(connector); + final isFloodMode = repeater.pathOverride == -1; + + return Scaffold( + appBar: AppBar( + title: Text(l10n.repeater_statusTitle), + centerTitle: true, + actions: [ + IconButton( + icon: Icon(isFloodMode ? Icons.waves : Icons.route), + tooltip: l10n.repeater_routingMode, + onPressed: () => + ContactRoutingSheet.show(context, contact: repeater), + ), + IconButton( + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + onPressed: _isLoading ? null : _loadStatus, + tooltip: l10n.repeater_refresh, + ), + ], + ), + body: SafeArea( + top: false, + child: RefreshIndicator( + onRefresh: _loadStatus, + child: _isLoading && _batteryMv == null + ? const Center(child: CircularProgressIndicator()) + : _buildBody(l10n, repeater.name), + ), + ), + ); + } + + Widget _buildBody(dynamic l10n, String name) { + final scheme = Theme.of(context).colorScheme; + return ListView( + padding: const EdgeInsets.only(bottom: 24), + children: [ + // ── System ───────────────────────────────────────────────────────── + SectionHeader(l10n.repeater_systemInformation), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildStatGrid([ + _StatItem( + icon: Icons.battery_std, + label: l10n.repeater_battery, + value: _batteryText(), + color: _batteryColor(), + ), + _StatItem( + icon: Icons.timer_outlined, + label: l10n.repeater_uptime, + value: _formatDuration(_uptimeSecs), + color: MeshPalette.blue, + ), + _StatItem( + icon: Icons.schedule, + label: l10n.repeater_clockAtLogin, + value: _clockText(), + color: scheme.onSurfaceVariant, + ), + _StatItem( + icon: Icons.inbox, + label: l10n.repeater_queueLength, + value: _formatValue(_queueLen), + color: scheme.onSurfaceVariant, + ), + _StatItem( + icon: Icons.bug_report_outlined, + label: l10n.repeater_debugFlags, + value: _formatValue(_debugFlags), + color: _debugFlags != null && _debugFlags! > 0 + ? MeshPalette.warn + : scheme.onSurfaceVariant, + ), + ]), + ), + + // ── Radio ────────────────────────────────────────────────────────── + SectionHeader(l10n.repeater_radioStatistics), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildStatGrid([ + _StatItem( + icon: Icons.signal_cellular_alt, + label: l10n.repeater_lastRssi, + value: _formatValue(_lastRssi, suffix: ' dB'), + color: MeshPalette.blue, + ), + _StatItem( + icon: Icons.waves, + label: l10n.repeater_lastSnr, + value: _formatSnr(_lastSnr), + color: MeshTheme.snrColor(_lastSnr, blocked: false), + ), + _StatItem( + icon: Icons.noise_control_off, + label: l10n.repeater_noiseFloor, + value: _formatValue(_noiseFloor, suffix: ' dB'), + color: scheme.onSurfaceVariant, + ), + _StatItem( + icon: Icons.upload, + label: l10n.repeater_txAirtime, + value: _formatDuration(_txAirSecs), + color: MeshPalette.warn, + ), + _StatItem( + icon: Icons.download, + label: l10n.repeater_rxAirtime, + value: _formatDuration(_rxAirSecs), + color: MeshPalette.signal, + ), + ]), + ), + + // ── Packets ──────────────────────────────────────────────────────── + SectionHeader(l10n.repeater_packetStatistics), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildStatGrid([ + _StatItem( + icon: Icons.send, + label: l10n.repeater_sent, + value: _packetTxText(), + color: MeshPalette.blue, + ), + _StatItem( + icon: Icons.call_received, + label: l10n.repeater_received, + value: _packetRxText(), + color: MeshPalette.signal, + ), + _StatItem( + icon: Icons.content_copy, + label: l10n.repeater_duplicates, + value: _duplicateText(), + color: scheme.onSurfaceVariant, + ), + _StatItem( + icon: Icons.percent, + label: l10n.repeater_chanUtil, + value: _chanUtilText(), + color: _chanUtil != null && _chanUtil! > 80 + ? MeshPalette.alert + : _chanUtil != null && _chanUtil! > 50 + ? MeshPalette.warn + : MeshPalette.signal, + ), + ]), + ), + const SizedBox(height: 8), + ], + ); + } + + Color _batteryColor() { + final connector = context.watch(); + final batteryMv = + connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? + _batteryMv; + if (batteryMv == null) return Theme.of(context).colorScheme.onSurfaceVariant; + final percent = estimateBatteryPercentFromMillivolts( + batteryMv, + _batteryChemistry(), + ); + if (percent < 20) return MeshPalette.alert; + if (percent < 40) return MeshPalette.warn; + return MeshPalette.signal; + } + + Widget _buildStatGrid(List<_StatItem> items) { + return GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 2.2, + children: items + .map((item) => StatTile( + icon: item.icon, + label: item.label, + value: item.value, + color: item.color, + )) + .toList(), + ); + } +} + +class _StatItem { + final IconData icon; + final String label; + final String value; + final Color color; + + const _StatItem({ + required this.icon, + required this.label, + required this.value, + required this.color, + }); } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 1fd2c53c..212ab819 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../utils/platform_info.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:provider/provider.dart'; @@ -7,10 +8,12 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../services/linux_ble_error_classifier.dart'; +import '../theme/mesh_theme.dart'; import '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; import '../widgets/empty_state.dart'; +import '../widgets/mesh_ui.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'tcp_screen.dart'; @@ -136,12 +139,21 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { return Column( children: [ - // Bluetooth off warning - if (_bluetoothState == BluetoothAdapterState.off) - _bluetoothOffWarning(context), + // Bluetooth off warning — slides in/out with AnimatedSize + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: _bluetoothState == BluetoothAdapterState.off + ? _BluetoothOffBanner( + onEnable: PlatformInfo.isAndroid + ? () => FlutterBluePlus.turnOn() + : null, + ) + : const SizedBox.shrink(), + ), - // Status bar - _buildStatusBar(context, connector), + // Connection status header + _ConnectionStatusHeader(connector: connector), // Device list Expanded(child: _buildDeviceList(context, connector)), @@ -158,14 +170,31 @@ class _ScannerScreenState extends State { return FloatingActionButton.extended( heroTag: 'scanner_ble_action', - onPressed: isBluetoothOff ? null : () => _toggleScan(connector), - icon: isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.bluetooth_searching), + onPressed: isBluetoothOff + ? null + : () { + HapticFeedback.lightImpact(); + _toggleScan(connector); + }, + icon: AnimatedSwitcher( + duration: const Duration(milliseconds: 220), + transitionBuilder: (child, anim) => + ScaleTransition(scale: anim, child: child), + child: isScanning + ? SizedBox( + key: const ValueKey('scanning'), + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.onPrimary, + ), + ) + : const Icon( + Icons.bluetooth_searching, + key: ValueKey('idle'), + ), + ), label: Text( isScanning ? context.l10n.scanner_stop @@ -189,51 +218,6 @@ class _ScannerScreenState extends State { } } - Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { - String statusText; - Color statusColor; - - final l10n = context.l10n; - switch (connector.state) { - case MeshCoreConnectionState.scanning: - statusText = l10n.scanner_scanning; - statusColor = Colors.blue; - break; - case MeshCoreConnectionState.connecting: - statusText = l10n.scanner_connecting; - statusColor = Colors.orange; - break; - case MeshCoreConnectionState.connected: - statusText = l10n.scanner_connectedTo(connector.deviceDisplayName); - statusColor = Colors.green; - break; - case MeshCoreConnectionState.disconnecting: - statusText = l10n.scanner_disconnecting; - statusColor = Colors.orange; - break; - case MeshCoreConnectionState.disconnected: - statusText = l10n.scanner_notConnected; - statusColor = Colors.grey; - break; - } - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: statusColor.withValues(alpha: 0.1), - child: Row( - children: [ - Icon(Icons.circle, size: 12, color: statusColor), - const SizedBox(width: 8), - Text( - statusText, - style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), - ), - ], - ), - ); - } - Widget _buildDeviceList(BuildContext context, MeshCoreConnector connector) { if (connector.scanResults.isEmpty) { final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; @@ -251,7 +235,10 @@ class _ScannerScreenState extends State { action: (isBluetoothOff || isScanning) ? null : FilledButton.icon( - onPressed: () => _toggleScan(connector), + onPressed: () { + HapticFeedback.lightImpact(); + _toggleScan(connector); + }, icon: const Icon(Icons.bluetooth_searching), label: Text(context.l10n.scanner_scan), ), @@ -259,19 +246,21 @@ class _ScannerScreenState extends State { } final isConnecting = connector.state == MeshCoreConnectionState.connecting; - return ListView.separated( - padding: const EdgeInsets.all(8), + return ListView.builder( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 96), itemCount: connector.scanResults.length, - separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) { final result = connector.scanResults[index]; final deviceId = result.device.remoteId.toString(); - return DeviceTile( - scanResult: result, - isConnecting: isConnecting && _connectingDeviceId == deviceId, - onTap: isConnecting - ? null - : () => _connectToDevice(context, connector, result), + return ListEntrance( + index: index, + child: DeviceTile( + scanResult: result, + isConnecting: isConnecting && _connectingDeviceId == deviceId, + onTap: isConnecting + ? null + : () => _connectToDevice(context, connector, result), + ), ); }, ); @@ -413,47 +402,117 @@ class _ScannerScreenState extends State { ); return pin; } +} - Widget _bluetoothOffWarning(BuildContext context) { - final errorColor = Theme.of(context).colorScheme.error; - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - color: errorColor.withValues(alpha: 0.15), +// ── Private sub-widgets ──────────────────────────────────────────────────── + +/// Bluetooth-off warning banner — styled as an alert MeshCard. +class _BluetoothOffBanner extends StatelessWidget { + final VoidCallback? onEnable; + + const _BluetoothOffBanner({this.onEnable}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return MeshCard( + color: scheme.error.withValues(alpha: 0.08), + borderColor: scheme.error.withValues(alpha: 0.35), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ - Icon(Icons.bluetooth_disabled, size: 24, color: errorColor), - const SizedBox(width: 12), + Icon(Icons.bluetooth_disabled, size: 20, color: scheme.error), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ Text( context.l10n.scanner_bluetoothOff, style: TextStyle( - color: errorColor, + color: scheme.error, fontWeight: FontWeight.w600, - fontSize: 14, + fontSize: 13.5, ), ), - const SizedBox(height: 4), + const SizedBox(height: 2), Text( context.l10n.scanner_bluetoothOffMessage, style: TextStyle( - color: errorColor.withValues(alpha: 0.85), + color: scheme.error.withValues(alpha: 0.8), fontSize: 12, ), ), ], ), ), - if (PlatformInfo.isAndroid) + if (onEnable != null) ...[ + const SizedBox(width: 8), TextButton( - onPressed: () => FlutterBluePlus.turnOn(), + onPressed: onEnable, child: Text(context.l10n.scanner_enableBluetooth), ), + ], ], ), ); } } + +/// Connection status header with AnimatedSwitcher between states. +class _ConnectionStatusHeader extends StatelessWidget { + final MeshCoreConnector connector; + + const _ConnectionStatusHeader({required this.connector}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + + final (String label, Color color, bool pulse) = switch (connector.state) { + MeshCoreConnectionState.scanning => ( + l10n.scanner_scanning, + MeshPalette.blue, + true, + ), + MeshCoreConnectionState.connecting => ( + l10n.scanner_connecting, + MeshPalette.warn, + true, + ), + MeshCoreConnectionState.connected => ( + l10n.scanner_connectedTo(connector.deviceDisplayName), + MeshPalette.signal, + false, + ), + MeshCoreConnectionState.disconnecting => ( + l10n.scanner_disconnecting, + MeshPalette.warn, + true, + ), + MeshCoreConnectionState.disconnected => ( + l10n.scanner_notConnected, + scheme.onSurfaceVariant, + false, + ), + }; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Align( + key: ValueKey(connector.state), + alignment: Alignment.centerLeft, + child: StatusChip( + label: label, + color: color, + pulse: pulse, + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index b6eefb05..3d12016a 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -10,8 +10,10 @@ import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; import '../services/app_debug_log_service.dart'; +import '../theme/mesh_theme.dart'; import '../widgets/app_bar.dart'; import '../helpers/snack_bar_builder.dart'; +import '../widgets/mesh_ui.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -69,6 +71,7 @@ class _SettingsScreenState extends State { indicators: false, subtitle: false, ), + centerTitle: true, bottom: const SyncProgressAppBarBottom(), ), body: SafeArea( @@ -76,21 +79,81 @@ class _SettingsScreenState extends State { child: Consumer( builder: (context, connector, child) { return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(0, 8, 0, 24), children: [ - _buildDeviceInfoCard(context, connector), - const SizedBox(height: 16), - _buildAppSettingsCard(context), - const SizedBox(height: 16), - _buildNodeSettingsCard(context, connector), - const SizedBox(height: 16), - _buildActionsCard(context, connector), - const SizedBox(height: 16), - _buildDebugCard(context), - const SizedBox(height: 16), - _buildExportCard(connector), - const SizedBox(height: 16), - _buildAboutCard(context), + // IDENTITY section + SectionHeader(l10n.settings_deviceInfo), + MeshCard( + padding: EdgeInsets.zero, + child: _buildIdentityCardContent(context, connector), + ), + + // NODE section + SectionHeader(l10n.settings_nodeSettings), + MeshCard( + padding: EdgeInsets.zero, + child: _buildNodeCardContent(context, connector), + ), + + // LOCATION section + SectionHeader(l10n.settings_location), + MeshCard( + padding: EdgeInsets.zero, + child: _buildLocationCardContent(context, connector), + ), + + // APP SETTINGS + SectionHeader(l10n.settings_appSettings), + MeshCard( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const AppSettingsScreen(), + ), + ), + child: _buildNavTileContent( + context, + icon: Icons.settings_outlined, + title: l10n.settings_appSettings, + subtitle: l10n.settings_appSettingsSubtitle, + ), + ), + + // ACTIONS section + SectionHeader(l10n.settings_actions), + MeshCard( + padding: EdgeInsets.zero, + child: _buildActionsCardContent(context, connector), + ), + + // EXPORT section + SectionHeader(l10n.settings_gpxExportRepeaters), + MeshCard( + padding: EdgeInsets.zero, + child: _buildExportCardContent(context, connector), + ), + + // DEBUG section + SectionHeader(l10n.settings_debug), + MeshCard( + padding: EdgeInsets.zero, + child: _buildDebugCardContent(context), + ), + + // ABOUT + SectionHeader(l10n.settings_about), + MeshCard( + onTap: () => _showAbout(context), + child: _buildNavTileContent( + context, + icon: Icons.info_outline, + title: l10n.settings_about, + subtitle: l10n.settings_aboutVersion( + _appVersion.isEmpty ? l10n.common_loading : _appVersion, + ), + showChevron: false, + ), + ), ], ); }, @@ -99,91 +162,217 @@ class _SettingsScreenState extends State { ); } - Widget _buildDeviceInfoCard( + Widget _buildNavTileContent( + BuildContext context, { + required IconData icon, + required String title, + required String subtitle, + bool showChevron = true, + }) { + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return Row( + children: [ + Icon(icon, size: 20, color: scheme.onSurfaceVariant), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + if (showChevron) + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], + ); + } + + Widget _buildIdentityCardContent( BuildContext context, MeshCoreConnector connector, ) { final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; - return Card( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row: device name + status chip + expand toggle + InkWell( + onTap: () { + setState(() { + _deviceInfoExpanded = !_deviceInfoExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + connector.deviceDisplayName, + style: MeshTheme.mono( + fontSize: 16, + fontWeight: FontWeight.w700, + color: scheme.onSurface, + ), + ), + const SizedBox(height: 4), + StatusChip( + label: connector.isConnected + ? l10n.common_connected + : l10n.common_disconnected, + color: connector.isConnected + ? MeshPalette.blue + : scheme.onSurfaceVariant, + pulse: connector.isConnected, + ), + ], + ), + ), + AnimatedRotation( + turns: _deviceInfoExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: Icon( + Icons.expand_more, + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + + // Expandable detail rows + AnimatedSize( + duration: const Duration(milliseconds: 200), + alignment: Alignment.topCenter, + child: _deviceInfoExpanded + ? Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 1), + const SizedBox(height: 10), + _infoRow( + context, + label: l10n.settings_infoId, + value: connector.deviceIdLabel, + ), + _buildBatteryInfoRow(context, connector), + if (connector.selfName != null) + _infoRow( + context, + label: l10n.settings_nodeName, + value: connector.selfName!, + ), + if (connector.selfPublicKey != null) + _infoRow( + context, + label: l10n.settings_infoPublicKey, + value: + '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', + mono: true, + ), + _infoRow( + context, + label: l10n.settings_infoContactsCount, + value: '${connector.contacts.length}', + ), + _infoRow( + context, + label: l10n.settings_infoChannelCount, + value: '${connector.channels.length}', + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ); + } + + Widget _infoRow( + BuildContext context, { + required String label, + required String value, + bool mono = false, + Widget? leading, + Color? valueColor, + VoidCallback? onTap, + }) { + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + final content = Padding( + padding: const EdgeInsets.symmetric(vertical: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - setState(() { - _deviceInfoExpanded = !_deviceInfoExpanded; - }); - }, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: Row( - children: [ - Expanded( - child: Text( - l10n.settings_deviceInfo, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - AnimatedRotation( - turns: _deviceInfoExpanded ? 0.5 : 0, - duration: const Duration(milliseconds: 200), - child: const Icon(Icons.expand_more), - ), - ], + Row( + children: [ + if (leading != null) ...[leading, const SizedBox(width: 6)], + Text( + label, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), ), - ), + ], ), - - AnimatedCrossFade( - firstChild: const SizedBox.shrink(), - secondChild: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow( - l10n.settings_infoName, - connector.deviceDisplayName, + const SizedBox(height: 2), + mono + ? Text( + value, + style: MeshTheme.mono( + fontSize: 13, + fontWeight: FontWeight.w500, + color: valueColor ?? scheme.onSurface, ), - _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), - _buildInfoRow( - l10n.settings_infoStatus, - connector.isConnected - ? l10n.common_connected - : l10n.common_disconnected, + ) + : Text( + value, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: valueColor, ), - _buildBatteryInfoRow(context, connector), - if (connector.selfName != null) - _buildInfoRow(l10n.settings_nodeName, connector.selfName!), - if (connector.selfPublicKey != null) - _buildInfoRow( - l10n.settings_infoPublicKey, - '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', - ), - _buildInfoRow( - l10n.settings_infoContactsCount, - '${connector.contacts.length}', - ), - _buildInfoRow( - l10n.settings_infoChannelCount, - '${connector.channels.length}', - ), - ], - ), - ), - crossFadeState: _deviceInfoExpanded - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - duration: const Duration(milliseconds: 200), - ), + ), ], ), ); + + if (onTap != null) { + return InkWell( + borderRadius: BorderRadius.circular(MeshRadii.xs), + onTap: onTap, + child: content, + ); + } + return content; } Widget _buildBatteryInfoRow( @@ -194,7 +383,6 @@ class _SettingsScreenState extends State { final percent = connector.batteryPercent; final millivolts = connector.batteryMillivolts; - // figure out display value final String displayValue; if (millivolts == null) { displayValue = l10n.common_notAvailable; @@ -226,10 +414,11 @@ class _SettingsScreenState extends State { valueColor = null; } - return _buildInfoRow( - l10n.settings_infoBattery, - displayValue, - leading: Icon(icon, size: 18, color: iconColor), + return _infoRow( + context, + label: l10n.settings_infoBattery, + value: displayValue, + leading: Icon(icon, size: 14, color: iconColor), valueColor: valueColor, onTap: millivolts != null ? () { @@ -241,260 +430,279 @@ class _SettingsScreenState extends State { ); } - Widget _buildAppSettingsCard(BuildContext context) { - final l10n = context.l10n; - return Card( - child: ListTile( - leading: const Icon(Icons.settings_outlined), - title: Text(l10n.settings_appSettings), - subtitle: Text(l10n.settings_appSettingsSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const AppSettingsScreen()), - ); - }, - ), - ); - } - - Widget _buildNodeSettingsCard( + Widget _buildNodeCardContent( BuildContext context, MeshCoreConnector connector, ) { final l10n = context.l10n; - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - l10n.settings_nodeSettings, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - ListTile( - leading: const Icon(Icons.person_outline), - title: Text(l10n.settings_nodeName), - subtitle: Text(connector.selfName ?? l10n.settings_nodeNameNotSet), - trailing: const Icon(Icons.chevron_right), - onTap: () => _editNodeName(context, connector), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.radio), - title: Text(l10n.settings_radioSettings), - subtitle: Text(l10n.settings_radioSettingsSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () => _showRadioSettings(context, connector), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.sensors_outlined), - title: Text(l10n.radioStats_settingsTile), - subtitle: Text(l10n.radioStats_settingsSubtitle), - trailing: const Icon(Icons.chevron_right), - enabled: - connector.isConnected && connector.supportsCompanionRadioStats, - onTap: () => pushCompanionRadioStatsScreen(context), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.location_on_outlined), - title: Text(l10n.settings_location), - subtitle: Text(l10n.settings_locationSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () => _editLocation(context, connector), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.group_add_outlined), - title: Text(l10n.settings_contactSettings), - subtitle: Text(l10n.settings_contactSettingsSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () => _editAutoAddConfig(context, connector), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.visibility_off_outlined), - title: Text(l10n.settings_privacy), - subtitle: Text(l10n.settings_privacySubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () => _privacySettings(context, connector), - ), - ], - ), - ); - } - - Widget _buildActionsCard(BuildContext context, MeshCoreConnector connector) { - final l10n = context.l10n; - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - l10n.settings_actions, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - ListTile( - leading: Icon( - Icons.delete_outline, - color: Theme.of(context).colorScheme.error, - ), - title: Text(l10n.settings_deleteAllPaths), - subtitle: Text( - l10n.settings_deleteAllPathsSubtitle, - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), - onTap: () => _confirmDeleteAllPaths(context, connector), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.sync), - title: Text(l10n.settings_syncTime), - subtitle: Text(l10n.settings_syncTimeSubtitle), - onTap: () => _syncTime(context, connector), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.refresh), - title: Text(l10n.settings_refreshContacts), - subtitle: Text(l10n.settings_refreshContactsSubtitle), - onTap: () => connector.getContacts(), - ), - const Divider(height: 1), - ListTile( - leading: Icon( - Icons.restart_alt, - color: Theme.of(context).colorScheme.tertiary, - ), - title: Text(l10n.settings_rebootDevice), - subtitle: Text(l10n.settings_rebootDeviceSubtitle), - onTap: () => _confirmReboot(context, connector), - ), - ], - ), - ); - } - - Widget _buildAboutCard(BuildContext context) { - final l10n = context.l10n; - return Card( - child: ListTile( - leading: const Icon(Icons.info_outline), - title: Text(l10n.settings_about), - subtitle: Text( - l10n.settings_aboutVersion( - _appVersion.isEmpty ? l10n.common_loading : _appVersion, - ), + return Column( + children: [ + _tappableTile( + context, + icon: Icons.person_outline, + title: l10n.settings_nodeName, + subtitle: connector.selfName ?? l10n.settings_nodeNameNotSet, + onTap: () => _editNodeName(context, connector), ), - onTap: () => _showAbout(context), - ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.radio, + title: l10n.settings_radioSettings, + subtitle: l10n.settings_radioSettingsSubtitle, + onTap: () => _showRadioSettings(context, connector), + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.sensors_outlined, + title: l10n.radioStats_settingsTile, + subtitle: l10n.radioStats_settingsSubtitle, + onTap: connector.isConnected && connector.supportsCompanionRadioStats + ? () => pushCompanionRadioStatsScreen(context) + : null, + ), + ], ); } - Widget _buildDebugCard(BuildContext context) { + Widget _buildLocationCardContent( + BuildContext context, + MeshCoreConnector connector, + ) { final l10n = context.l10n; - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - l10n.settings_debug, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - ListTile( - leading: const Icon(Icons.bluetooth_outlined), - title: Text(l10n.settings_companionDebugLog), - subtitle: Text(l10n.settings_companionDebugLogSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BleDebugLogScreen(), - ), - ); - }, - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.code_outlined), - title: Text(l10n.settings_appDebugLog), - subtitle: Text(l10n.settings_appDebugLogSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AppDebugLogScreen(), - ), - ); - }, - ), - ], - ), + return Column( + children: [ + _tappableTile( + context, + icon: Icons.location_on_outlined, + title: l10n.settings_location, + subtitle: l10n.settings_locationSubtitle, + onTap: () => _editLocation(context, connector), + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.group_add_outlined, + title: l10n.settings_contactSettings, + subtitle: l10n.settings_contactSettingsSubtitle, + onTap: () => _editAutoAddConfig(context, connector), + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.visibility_off_outlined, + title: l10n.settings_privacy, + subtitle: l10n.settings_privacySubtitle, + onTap: () => _privacySettings(context, connector), + ), + ], ); } - Widget _buildInfoRow( - String label, - String value, { - Widget? leading, - Color? valueColor, - VoidCallback? onTap, - }) { - final theme = Theme.of(context); + Widget _buildActionsCardContent( + BuildContext context, + MeshCoreConnector connector, + ) { + final l10n = context.l10n; + return Column( + children: [ + _tappableTile( + context, + icon: Icons.sync, + title: l10n.settings_syncTime, + subtitle: l10n.settings_syncTimeSubtitle, + onTap: () => _syncTime(context, connector), + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.refresh, + title: l10n.settings_refreshContacts, + subtitle: l10n.settings_refreshContactsSubtitle, + onTap: () => connector.getContacts(), + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.restart_alt, + title: l10n.settings_rebootDevice, + subtitle: l10n.settings_rebootDeviceSubtitle, + titleColor: MeshPalette.warn, + iconColor: MeshPalette.warn, + onTap: () => _confirmReboot(context, connector), + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.delete_outline, + title: l10n.settings_deleteAllPaths, + subtitle: l10n.settings_deleteAllPathsSubtitle, + titleColor: MeshPalette.alert, + iconColor: MeshPalette.alert, + onTap: () => _confirmDeleteAllPaths(context, connector), + ), + ], + ); + } - final row = Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (leading != null) ...[leading, const SizedBox(width: 8)], - Expanded( - child: Text( - label, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - ), + Widget _buildExportCardContent( + BuildContext context, + MeshCoreConnector connector, + ) { + final l10n = context.l10n; + return Column( + children: [ + _tappableTile( + context, + icon: Icons.download_outlined, + title: l10n.settings_gpxExportRepeaters, + subtitle: l10n.settings_gpxExportRepeatersSubtitle, + onTap: () async { + final exporter = GpxExport(connector); + exporter.addRepeaters(); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportRepeatersRoom, + 'meshcore_repeaters_', + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); + }, + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.download_outlined, + title: l10n.settings_gpxExportContacts, + subtitle: l10n.settings_gpxExportContactsSubtitle, + onTap: () async { + final exporter = GpxExport(connector); + exporter.addContacts(); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportChat, + 'meshcore_contacts_', + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); + }, + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.download_outlined, + title: l10n.settings_gpxExportAll, + subtitle: l10n.settings_gpxExportAllSubtitle, + onTap: () async { + final exporter = GpxExport(connector); + exporter.addAll(); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportAllContacts, + 'meshcore_all_', + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); + }, + ), + ], + ); + } + + Widget _buildDebugCardContent(BuildContext context) { + final l10n = context.l10n; + return Column( + children: [ + _tappableTile( + context, + icon: Icons.bluetooth_outlined, + title: l10n.settings_companionDebugLog, + subtitle: l10n.settings_companionDebugLogSubtitle, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BleDebugLogScreen(), + ), + ); + }, + ), + const Divider(height: 1, indent: 16), + _tappableTile( + context, + icon: Icons.code_outlined, + title: l10n.settings_appDebugLog, + subtitle: l10n.settings_appDebugLogSubtitle, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AppDebugLogScreen(), + ), + ); + }, + ), + ], + ); + } + + Widget _tappableTile( + BuildContext context, { + required IconData icon, + required String title, + required String subtitle, + VoidCallback? onTap, + Color? titleColor, + Color? iconColor, + }) { + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final effectiveIconColor = iconColor ?? scheme.onSurfaceVariant; + + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(icon, size: 20, color: effectiveIconColor), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: titleColor, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: textTheme.bodySmall?.copyWith( + color: titleColor != null + ? titleColor.withValues(alpha: 0.7) + : scheme.onSurfaceVariant, + ), + ), + ], ), - ], - ), - const SizedBox(height: 4), - Text( - value, - style: theme.textTheme.bodyLarge?.copyWith( - color: valueColor, - fontWeight: FontWeight.w500, ), - ), - ], + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], + ), ), ); - - if (onTap != null) { - return InkWell( - borderRadius: BorderRadius.circular(6), - onTap: onTap, - child: row, - ); - } - - return row; } void _editNodeName(BuildContext context, MeshCoreConnector connector) { @@ -837,70 +1045,6 @@ class _SettingsScreenState extends State { } } - Widget _buildExportCard(MeshCoreConnector connector) { - final l10n = context.l10n; - return Card( - child: Column( - children: [ - ListTile( - leading: const Icon(Icons.download_outlined), - title: Text(l10n.settings_gpxExportRepeaters), - subtitle: Text(l10n.settings_gpxExportRepeatersSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - final exporter = GpxExport(connector); - exporter.addRepeaters(); - _gpxExport( - exporter, - l10n.map_repeater, - l10n.settings_gpxExportRepeatersRoom, - "meshcore_repeaters_", - l10n.settings_gpxExportShareText, - l10n.settings_gpxExportShareSubject, - ); - }, - ), - ListTile( - leading: const Icon(Icons.download_outlined), - title: Text(l10n.settings_gpxExportContacts), - subtitle: Text(l10n.settings_gpxExportContactsSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - final exporter = GpxExport(connector); - exporter.addContacts(); - _gpxExport( - exporter, - l10n.map_repeater, - l10n.settings_gpxExportChat, - "meshcore_contacts_", - l10n.settings_gpxExportShareText, - l10n.settings_gpxExportShareSubject, - ); - }, - ), - ListTile( - leading: const Icon(Icons.download_outlined), - title: Text(l10n.settings_gpxExportAll), - subtitle: Text(l10n.settings_gpxExportAllSubtitle), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - final exporter = GpxExport(connector); - exporter.addAll(); - _gpxExport( - exporter, - l10n.map_repeater, - l10n.settings_gpxExportAllContacts, - "meshcore_all_", - l10n.settings_gpxExportShareText, - l10n.settings_gpxExportShareSubject, - ); - }, - ), - ], - ), - ); - } - void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; bool autoAddChat = false; @@ -933,7 +1077,7 @@ class _SettingsScreenState extends State { setDialogState(() => autoAddChat = value); }, ), - SizedBox(height: 8), + const SizedBox(height: 8), FeatureToggleRow( title: l10n.contactsSettings_autoAddRepeatersTitle, subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle, @@ -942,7 +1086,7 @@ class _SettingsScreenState extends State { setDialogState(() => autoAddRepeater = value); }, ), - SizedBox(height: 8), + const SizedBox(height: 8), FeatureToggleRow( title: l10n.contactsSettings_autoAddRoomServersTitle, subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle, @@ -951,7 +1095,7 @@ class _SettingsScreenState extends State { setDialogState(() => autoAddRoomServer = value); }, ), - SizedBox(height: 8), + const SizedBox(height: 8), FeatureToggleRow( title: l10n.contactsSettings_autoAddSensorsTitle, subtitle: l10n.contactsSettings_autoAddSensorsSubtitle, @@ -960,7 +1104,7 @@ class _SettingsScreenState extends State { setDialogState(() => autoAddSensor = value); }, ), - Divider(height: 4), + const Divider(height: 4), FeatureToggleRow( title: l10n.contactsSettings_overwriteOldestTitle, subtitle: l10n.contactsSettings_overwriteOldestSubtitle, diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index 517b39a6..cbed0277 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -1,13 +1,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; +import '../theme/mesh_theme.dart'; import '../utils/platform_info.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/mesh_ui.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'usb_screen.dart'; @@ -95,15 +98,32 @@ class _TcpScreenState extends State { final isConnecting = connector.state == MeshCoreConnectionState.connecting && connector.activeTransport == MeshCoreTransportType.tcp; - // A running BLE scan must not block TCP connect: connectTcp() stops - // any active scan before connecting, so the only reason to disable - // the button is a TCP connect already in flight. - final isButtonDisabled = isConnecting; - return Column( + // Connect is only available from a fully disconnected state — + // scanning, connecting, or an active session must settle first. + final isButtonDisabled = + connector.state != MeshCoreConnectionState.disconnected; + return ListView( + padding: const EdgeInsets.only(bottom: 32), children: [ - _buildStatusBar(context, connector), - _buildTransportLinks(context), + // Status header Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Align( + key: ValueKey(connector.state), + alignment: Alignment.centerLeft, + child: _buildStatusChip(context, connector), + ), + ), + ), + + // Transport switcher + _buildTransportLinks(context), + + // Connection form + const SectionHeader('TCP / IP'), + MeshCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -113,7 +133,6 @@ class _TcpScreenState extends State { decoration: InputDecoration( labelText: context.l10n.tcpHostLabel, hintText: context.l10n.tcpHostHint, - border: const OutlineInputBorder(), ), enabled: !isConnecting, keyboardType: TextInputType.url, @@ -124,7 +143,6 @@ class _TcpScreenState extends State { decoration: InputDecoration( labelText: context.l10n.tcpPortLabel, hintText: context.l10n.tcpPortHint, - border: const OutlineInputBorder(), ), enabled: !isConnecting, keyboardType: TextInputType.number, @@ -132,7 +150,12 @@ class _TcpScreenState extends State { const SizedBox(height: 16), FilledButton.icon( key: const Key('tcp_connect_button'), - onPressed: isButtonDisabled ? null : _connectTcp, + onPressed: isButtonDisabled + ? null + : () { + HapticFeedback.lightImpact(); + _connectTcp(); + }, icon: isConnecting ? const SizedBox( width: 18, @@ -151,6 +174,39 @@ class _TcpScreenState extends State { ], ), ), + + // Last used endpoint + if (connector.activeTcpEndpoint != null && + connector.isTcpTransportConnected) ...[ + const SectionHeader('CONNECTED TO'), + MeshCard( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + child: Row( + children: [ + Icon( + Icons.lan, + size: 16, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + connector.activeTcpEndpoint!, + style: MeshTheme.mono( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], ], ); }, @@ -159,6 +215,40 @@ class _TcpScreenState extends State { ); } + Widget _buildStatusChip(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + + if (connector.isTcpTransportConnected) { + return StatusChip( + label: l10n.scanner_connectedTo( + connector.activeTcpEndpoint ?? 'TCP', + ), + color: MeshPalette.signal, + ); + } else if (connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.tcp) { + return StatusChip( + label: l10n.tcpStatus_connectingTo( + '${_hostController.text}:${_portController.text}', + ), + color: MeshPalette.warn, + pulse: true, + ); + } else if (connector.state == MeshCoreConnectionState.disconnecting && + connector.activeTransport == MeshCoreTransportType.tcp) { + return StatusChip( + label: l10n.scanner_disconnecting, + color: MeshPalette.warn, + pulse: true, + ); + } else { + return StatusChip( + label: l10n.tcpStatus_notConnected, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ); + } + } + Widget _buildTransportLinks(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -186,52 +276,6 @@ class _TcpScreenState extends State { ); } - Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { - final l10n = context.l10n; - String statusText; - Color statusColor; - - if (connector.isTcpTransportConnected) { - statusText = l10n.scanner_connectedTo( - connector.activeTcpEndpoint ?? 'TCP', - ); - statusColor = Colors.green; - } else if (connector.state == MeshCoreConnectionState.connecting && - connector.activeTransport == MeshCoreTransportType.tcp) { - statusText = l10n.tcpStatus_connectingTo( - '${_hostController.text}:${_portController.text}', - ); - statusColor = Colors.orange; - } else if (connector.state == MeshCoreConnectionState.disconnecting && - connector.activeTransport == MeshCoreTransportType.tcp) { - statusText = l10n.scanner_disconnecting; - statusColor = Colors.orange; - } else { - statusText = l10n.tcpStatus_notConnected; - statusColor = Theme.of(context).colorScheme.onSurfaceVariant; - } - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: statusColor.withValues(alpha: 0.1), - child: Row( - children: [ - Icon(Icons.circle, size: 12, color: statusColor), - const SizedBox(width: 8), - Expanded( - child: Text( - statusText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } - Future _connectTcp() async { if (_connector.state == MeshCoreConnectionState.connecting || _connector.state == MeshCoreConnectionState.connected || diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 4559adac..7087bd2c 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -19,6 +19,8 @@ import '../utils/battery_utils.dart'; import '../helpers/snack_bar_builder.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/telemetry_location_map.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; class TelemetryScreen extends StatefulWidget { final Contact contact; @@ -319,6 +321,7 @@ class _TelemetryScreenState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; final connector = context.watch(); final settings = context.watch().settings; final isImperialUnits = settings.unitSystem == UnitSystem.imperial; @@ -387,7 +390,7 @@ class _TelemetryScreenState extends State { l10n.telemetry_noData, style: TextStyle( fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, ), ), ), @@ -415,34 +418,21 @@ class _TelemetryScreenState extends State { int channel, bool isImperialUnits, ) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - for (final entry in channelData.entries) - _buildTelemetryField(entry, channel, isImperialUnits), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(title, padding: const EdgeInsets.fromLTRB(16, 16, 16, 8)), + MeshCard( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final entry in channelData.entries) + _buildTelemetryField(entry, channel, isImperialUnits), + ], + ), ), - ), + ], ); } @@ -601,30 +591,19 @@ class _TelemetryScreenState extends State { final l10n = context.l10n; final counterText = _autoRefreshCounterText(); - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Icon( - Icons.autorenew, - color: Theme.of(context).textTheme.headlineSmall?.color, - ), - const SizedBox(width: 8), - Text( - l10n.common_autoRefresh, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(), - _buildAutoRefreshNumberField( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader( + l10n.common_autoRefresh, + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + ), + MeshCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildAutoRefreshNumberField( controller: _autoRefreshIntervalController, label: l10n.common_interval, min: _autoRefreshMinIntervalSeconds, @@ -684,6 +663,7 @@ class _TelemetryScreenState extends State { ], ), ), + ], ); } @@ -913,6 +893,7 @@ class _TelemetryScreenState extends State { } Widget _buildInfoRow(String label, String value) { + final scheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( @@ -922,7 +903,8 @@ class _TelemetryScreenState extends State { child: Text( label, style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, + fontSize: 13, fontWeight: FontWeight.w500, ), ), @@ -930,7 +912,10 @@ class _TelemetryScreenState extends State { const SizedBox(width: 8), Text( value, - style: const TextStyle(fontWeight: FontWeight.w400), + style: MeshTheme.mono( + fontSize: 13, + color: scheme.onSurface, + ), textAlign: TextAlign.end, ), ], diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index ec551c6f..5b42767b 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -6,10 +6,13 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; import '../utils/app_logger.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/empty_state.dart'; +import '../widgets/mesh_ui.dart'; import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'tcp_screen.dart'; @@ -97,9 +100,27 @@ class _UsbScreenState extends State { child: Consumer( builder: (context, connector, child) { return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildStatusBar(context, connector), + // Status header + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Align( + key: ValueKey( + '${connector.state}_$_isLoadingPorts', + ), + alignment: Alignment.centerLeft, + child: _buildStatusChip(context, connector), + ), + ), + ), + + // Transport switcher _buildTransportLinks(context), + + // Port list Expanded(child: _buildPortList(context, connector)), ], ); @@ -132,6 +153,52 @@ class _UsbScreenState extends State { ); } + Widget _buildStatusChip(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + + if (_isLoadingPorts) { + return StatusChip( + label: l10n.usbStatus_searching, + color: scheme.primary, + pulse: true, + ); + } else if (connector.isUsbTransportConnected) { + switch (connector.state) { + case MeshCoreConnectionState.connected: + return StatusChip( + label: l10n.scanner_connectedTo( + connector.activeUsbPortDisplayLabel ?? 'USB', + ), + color: MeshPalette.signal, + ); + case MeshCoreConnectionState.disconnecting: + return StatusChip( + label: l10n.scanner_disconnecting, + color: MeshPalette.warn, + pulse: true, + ); + default: + return StatusChip( + label: l10n.usbStatus_notConnected, + color: scheme.onSurfaceVariant, + ); + } + } else if (connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.usb) { + return StatusChip( + label: l10n.usbStatus_connecting, + color: MeshPalette.warn, + pulse: true, + ); + } else { + return StatusChip( + label: l10n.usbStatus_notConnected, + color: scheme.onSurfaceVariant, + ); + } + } + Widget _buildTransportLinks(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -159,105 +226,20 @@ class _UsbScreenState extends State { ); } - Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { - final l10n = context.l10n; - String statusText; - Color statusColor; - - if (_isLoadingPorts) { - statusText = l10n.usbStatus_searching; - statusColor = Theme.of(context).colorScheme.primary; - } else if (connector.isUsbTransportConnected) { - switch (connector.state) { - case MeshCoreConnectionState.connected: - statusText = l10n.scanner_connectedTo( - connector.activeUsbPortDisplayLabel ?? 'USB', - ); - statusColor = Colors.green; - case MeshCoreConnectionState.disconnecting: - statusText = l10n.scanner_disconnecting; - statusColor = Colors.orange; - default: - statusText = l10n.usbStatus_notConnected; - statusColor = Theme.of(context).colorScheme.onSurfaceVariant; - } - } else if (connector.state == MeshCoreConnectionState.connecting && - connector.activeTransport == MeshCoreTransportType.usb) { - statusText = l10n.usbStatus_connecting; - statusColor = Colors.orange; - } else { - statusText = l10n.usbStatus_notConnected; - statusColor = Theme.of(context).colorScheme.onSurfaceVariant; - } - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: statusColor.withValues(alpha: 0.1), - child: Row( - children: [ - Icon(Icons.circle, size: 12, color: statusColor), - const SizedBox(width: 8), - Expanded( - child: Text( - statusText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } - Widget _buildPortList(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; if (_isLoadingPorts) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.usb, - size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - l10n.usbStatus_searching, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), + return EmptyState( + icon: Icons.usb, + title: l10n.usbStatus_searching, ); } if (_ports.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.usb, - size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - l10n.usbScreenEmptyState, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), + return EmptyState( + icon: Icons.usb, + title: l10n.usbScreenEmptyState, ); } @@ -265,10 +247,9 @@ class _UsbScreenState extends State { connector.state == MeshCoreConnectionState.connecting && connector.activeTransport == MeshCoreTransportType.usb; - return ListView.separated( - padding: const EdgeInsets.all(8), + return ListView.builder( + padding: const EdgeInsets.only(bottom: 32), itemCount: _ports.length, - separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) { final port = _ports[index]; final displayName = friendlyUsbPortName(port); @@ -276,15 +257,50 @@ class _UsbScreenState extends State { final showRawName = rawName != displayName && !rawName.startsWith('web:'); - return ListTile( - leading: const Icon(Icons.usb), - title: Text( - displayName, - style: const TextStyle(fontWeight: FontWeight.w500), + return ListEntrance( + index: index, + child: MeshCard( + padding: EdgeInsets.zero, + child: ListTile( + onTap: isConnecting + ? null + : () { + HapticFeedback.selectionClick(); + _connectPort(port); + }, + leading: AvatarCircle( + name: displayName, + size: 40, + icon: Icons.usb, + color: Theme.of(context).colorScheme.primary, + ), + title: Text( + displayName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: showRawName + ? Text( + rawName, + style: MeshTheme.mono( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, + trailing: Icon( + Icons.chevron_right, + size: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), - subtitle: showRawName ? Text(rawName) : null, - trailing: const Icon(Icons.chevron_right), - onTap: isConnecting ? null : () => _connectPort(port), ); }, ); diff --git a/lib/theme/mesh_theme.dart b/lib/theme/mesh_theme.dart index 00b92ec0..fcb15b1b 100644 --- a/lib/theme/mesh_theme.dart +++ b/lib/theme/mesh_theme.dart @@ -1,47 +1,48 @@ +import 'package:flutter/cupertino.dart' show CupertinoPageTransitionsBuilder; import 'package:flutter/material.dart'; -/// MeshCore palette — cool slate dark theme with sky-blue accents. +/// MeshCore palette — high-contrast slate surfaces with sky-blue accents. class MeshPalette { MeshPalette._(); - // Surfaces (cool near-black, slate undertone) - static const bg = Color(0xFF101417); - static const bg1 = Color(0xFF161B1F); - static const bg2 = Color(0xFF1D242A); - static const bg3 = Color(0xFF28313A); - static const bg4 = Color(0xFF344049); + // Surfaces shared with the map overlays and navigation. + static const bg = Color(0xFF0B1220); + static const bg1 = Color(0xFF0F172A); + static const bg2 = Color(0xFF162033); + static const bg3 = Color(0xFF1E293B); + static const bg4 = Color(0xFF334155); // Lines - static const line = Color(0xFF222B31); - static const line2 = Color(0xFF344049); - static const line3 = Color(0xFF485762); + static const line = Color(0xFF1E293B); + static const line2 = Color(0xFF334155); + static const line3 = Color(0xFF475569); // Ink - static const ink = Color(0xFFE9EEF3); - static const ink2 = Color(0xFFB5C0C9); - static const ink3 = Color(0xFF7C8A95); - static const ink4 = Color(0xFF556470); + static const ink = Color(0xFFF8FAFC); + static const ink2 = Color(0xFFCBD5E1); + static const ink3 = Color(0xFF94A3B8); + static const ink4 = Color(0xFF64748B); // Signal-quality green (used only for SNR coloring, not UI chrome) - static const signal = Color(0xFF7BEFA8); - static const signalDim = Color(0xFF4DC580); + static const signal = Color(0xFF22C55E); + static const signalDim = Color(0xFF16A34A); - // Warn (ember) - static const warn = Color(0xFFFFA552); - static const warnDim = Color(0xFFC27E3C); - static const warnBg = Color(0x1CFFA552); - static const warnLine = Color(0x4DFFA552); + // Warn + static const warn = Color(0xFFF59E0B); + static const warnDim = Color(0xFFD97706); + static const warnBg = Color(0x1FF59E0B); + static const warnLine = Color(0x66F59E0B); - // Alert (coral) - static const alert = Color(0xFFFF6A5C); - static const alertBg = Color(0x1CFF6A5C); - static const alertLine = Color(0x52FF6A5C); + // Alert + static const alert = Color(0xFFEF4444); + static const alertBg = Color(0x1FEF4444); + static const alertLine = Color(0x66EF4444); - // Blue (sky) — primary accent - static const blue = Color(0xFF7FCBF5); - static const blueDim = Color(0xFF4A9CC9); - static const blueBg = Color(0x1C7FCBF5); - static const blueLine = Color(0x477FCBF5); + // Blue — primary map/app accent + static const blue = Color(0xFF0EA5E9); + static const blueDim = Color(0xFF0284C7); + static const blueBg = Color(0x290EA5E9); + static const blueLine = Color(0x800EA5E9); // Magenta static const magenta = Color(0xFFDE7FDB); @@ -49,9 +50,9 @@ class MeshPalette { static const magentaLine = Color(0x47DE7FDB); // Me bubble (dusk blue) - static const me = Color(0xFF1B2C3D); - static const meBorder = Color(0xFF2C4A66); - static const meInk = Color(0xFFDCE9F5); + static const me = Color(0xFF0C4A6E); + static const meBorder = Color(0xFF0369A1); + static const meInk = Color(0xFFF0F9FF); // ── Light variant (used when user explicitly picks light theme) static const lightBg = Color(0xFFF4F6F8); @@ -64,6 +65,51 @@ class MeshPalette { static const lightBlue = Color(0xFF2F6EA8); } +/// High-contrast semantic colors for UI rendered over variable map tiles. +class MapPalette { + MapPalette._(); + + static const online = Color(0xFF22C55E); + static const offline = Color(0xFF6B7280); + static const stale = Color(0xFFF59E0B); + static const repeater = Color(0xFF2563EB); + static const router = Color(0xFF7C3AED); + static const batteryLow = Color(0xFFEF4444); + static const cluster = Color(0xFFF97316); + static const selected = Color(0xFF0EA5E9); + static const sensor = Color(0xFF0F766E); + static const shared = Color(0xFF0369A1); + + static const panelLight = Color(0xF0FFFFFF); + static const panelDark = Color(0xF50B1220); + static const textPrimary = Color(0xFFF8FAFC); + static const textSecondary = Color(0xFFCBD5E1); + static const textMuted = Color(0xFF94A3B8); + static const border = Color(0x5264758B); + static const markerOutline = Colors.white; + static const markerShadow = Color(0xB3000000); +} + +/// High-contrast colors for line-of-sight maps and elevation profiles. +class LosPalette { + LosPalette._(); + + static const terrain = Color(0xFFA3E635); + static const beam = Color(0xFF38BDF8); + static const horizon = Color(0xFFFBBF24); + static const blocked = Color(0xFFEF4444); + static const marginal = Color(0xFFF59E0B); + static const clear = Color(0xFF22C55E); + static const selected = Color(0xFF0EA5E9); + static const chartBackground = Color(0xFF0B1220); + static const panelDark = Color(0xF00F172A); + static const panelLight = Color(0xF5FFFFFF); + static const text = Color(0xFFF8FAFC); + static const textMuted = Color(0xFFCBD5E1); + static const border = Color(0x5264758B); + static const shadow = Color(0x99000000); +} + /// Named font stacks — Flutter falls back to system fonts when the named /// family isn't installed, keeping things working without bundled assets. class MeshFonts { @@ -72,6 +118,7 @@ class MeshFonts { static const sans = 'Inter'; static const mono = 'JetBrains Mono'; static const display = 'Instrument Serif'; + static const emoji = 'Noto Color Emoji'; static const List sansFallback = [ 'system-ui', @@ -93,6 +140,11 @@ class MeshFonts { 'Times New Roman', 'serif', ]; + static const List emojiFallback = [ + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Noto Emoji', + ]; } /// Radii used consistently across the app. @@ -113,21 +165,21 @@ class MeshTheme { static ThemeData dark() { const scheme = ColorScheme.dark( primary: MeshPalette.blue, - onPrimary: Color(0xFF0A1A26), - primaryContainer: MeshPalette.blueBg, - onPrimaryContainer: MeshPalette.blue, + onPrimary: Colors.white, + primaryContainer: Color(0xFF075985), + onPrimaryContainer: Colors.white, secondary: MeshPalette.magenta, - onSecondary: Color(0xFF201020), + onSecondary: Colors.white, secondaryContainer: Color(0xFF331A33), - onSecondaryContainer: MeshPalette.magenta, + onSecondaryContainer: Colors.white, tertiary: MeshPalette.warn, - onTertiary: Color(0xFF1F1206), - tertiaryContainer: Color(0xFF3A2710), - onTertiaryContainer: Color(0xFFFFC58A), + onTertiary: Color(0xFF0B1220), + tertiaryContainer: Color(0xFF78350F), + onTertiaryContainer: Colors.white, error: MeshPalette.alert, - onError: Color(0xFF1A0A08), - errorContainer: MeshPalette.alertBg, - onErrorContainer: MeshPalette.alert, + onError: Colors.white, + errorContainer: Color(0xFF7F1D1D), + onErrorContainer: Colors.white, surface: MeshPalette.bg, onSurface: MeshPalette.ink, surfaceContainerLowest: MeshPalette.bg, @@ -334,9 +386,9 @@ class MeshTheme { padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), ), navigationBarTheme: NavigationBarThemeData( - backgroundColor: scheme.surfaceContainerLow, + backgroundColor: scheme.surface, surfaceTintColor: Colors.transparent, - indicatorColor: scheme.primary.withValues(alpha: 0.14), + indicatorColor: scheme.primary, indicatorShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(MeshRadii.md), ), @@ -348,13 +400,13 @@ class MeshTheme { fontSize: 10, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, letterSpacing: 0.1, - color: selected ? scheme.primary : scheme.onSurfaceVariant, + color: selected ? scheme.onPrimary : scheme.onSurfaceVariant, ); }), iconTheme: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return IconThemeData( - color: selected ? scheme.primary : scheme.onSurfaceVariant, + color: selected ? scheme.onPrimary : scheme.onSurfaceVariant, size: 22, ); }), @@ -393,6 +445,106 @@ class MeshTheme { ), iconTheme: IconThemeData(color: scheme.onSurfaceVariant, size: 22), splashFactory: InkSparkle.splashFactory, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeForwardsPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.linux: FadeForwardsPageTransitionsBuilder(), + TargetPlatform.macOS: FadeForwardsPageTransitionsBuilder(), + TargetPlatform.windows: FadeForwardsPageTransitionsBuilder(), + }, + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: SegmentedButton.styleFrom( + selectedBackgroundColor: scheme.primary.withValues(alpha: 0.16), + selectedForegroundColor: scheme.primary, + side: BorderSide(color: scheme.outlineVariant), + textStyle: const TextStyle( + fontFamily: MeshFonts.sans, + fontFamilyFallback: MeshFonts.sansFallback, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + switchTheme: SwitchThemeData( + thumbColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.selected) + ? scheme.onPrimary + : scheme.onSurfaceVariant, + ), + trackColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.selected) + ? scheme.primary + : scheme.surfaceContainerHighest, + ), + trackOutlineColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.selected) + ? Colors.transparent + : scheme.outline, + ), + ), + sliderTheme: SliderThemeData( + activeTrackColor: scheme.primary, + inactiveTrackColor: scheme.surfaceContainerHighest, + thumbColor: scheme.primary, + overlayColor: scheme.primary.withValues(alpha: 0.12), + valueIndicatorColor: scheme.surfaceContainerHighest, + valueIndicatorTextStyle: TextStyle( + fontFamily: MeshFonts.mono, + fontFamilyFallback: MeshFonts.monoFallback, + color: scheme.onSurface, + fontSize: 12, + ), + trackHeight: 3, + ), + tabBarTheme: TabBarThemeData( + labelColor: scheme.primary, + unselectedLabelColor: scheme.onSurfaceVariant, + indicatorColor: scheme.primary, + dividerColor: scheme.outlineVariant, + labelStyle: const TextStyle( + fontFamily: MeshFonts.sans, + fontFamilyFallback: MeshFonts.sansFallback, + fontSize: 13.5, + fontWeight: FontWeight.w700, + ), + unselectedLabelStyle: const TextStyle( + fontFamily: MeshFonts.sans, + fontFamilyFallback: MeshFonts.sansFallback, + fontSize: 13.5, + fontWeight: FontWeight.w500, + ), + ), + progressIndicatorTheme: ProgressIndicatorThemeData( + color: scheme.primary, + linearTrackColor: scheme.surfaceContainerHigh, + circularTrackColor: Colors.transparent, + ), + tooltipTheme: TooltipThemeData( + decoration: BoxDecoration( + color: scheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(MeshRadii.sm), + border: Border.all(color: scheme.outline), + ), + textStyle: TextStyle(color: scheme.onSurface, fontSize: 12), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: scheme.primary, + foregroundColor: scheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(MeshRadii.pill), + ), + textStyle: const TextStyle( + fontFamily: MeshFonts.sans, + fontFamilyFallback: MeshFonts.sansFallback, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), ); } @@ -443,6 +595,16 @@ class MeshTheme { ); } + /// Color-emoji style with platform fallbacks and stable vertical metrics. + static TextStyle emoji({double fontSize = 28}) { + return TextStyle( + fontFamily: MeshFonts.emoji, + fontFamilyFallback: MeshFonts.emojiFallback, + fontSize: fontSize, + height: 1, + ); + } + /// Color-code an SNR value for consistency across the app. static Color snrColor(num? snr, {required bool blocked}) { if (blocked) return MeshPalette.alert; diff --git a/lib/widgets/battery_indicator.dart b/lib/widgets/battery_indicator.dart index ccea59dd..9a74d7d5 100644 --- a/lib/widgets/battery_indicator.dart +++ b/lib/widgets/battery_indicator.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../connector/meshcore_connector.dart'; +import '../theme/mesh_theme.dart'; class BatteryUi { final IconData icon; @@ -10,19 +11,19 @@ class BatteryUi { BatteryUi batteryUiForPercent(int? percent) { if (percent == null) { - return const BatteryUi(Icons.battery_unknown, Colors.grey); + return const BatteryUi(Icons.battery_unknown, null); } final p = percent.clamp(0, 100); return switch (p) { - <= 5 => const BatteryUi(Icons.battery_alert, Colors.redAccent), - <= 15 => const BatteryUi(Icons.battery_0_bar, Colors.redAccent), - <= 30 => const BatteryUi(Icons.battery_1_bar, Colors.orange), - <= 45 => const BatteryUi(Icons.battery_2_bar, Colors.amber), - <= 60 => const BatteryUi(Icons.battery_3_bar, Colors.lightGreen), - <= 80 => const BatteryUi(Icons.battery_5_bar, Colors.green), - _ => const BatteryUi(Icons.battery_full, Colors.green), + <= 5 => const BatteryUi(Icons.battery_alert, MeshPalette.alert), + <= 15 => const BatteryUi(Icons.battery_0_bar, MeshPalette.alert), + <= 30 => const BatteryUi(Icons.battery_1_bar, MeshPalette.warn), + <= 45 => const BatteryUi(Icons.battery_2_bar, MeshPalette.warn), + <= 60 => const BatteryUi(Icons.battery_3_bar, null), + <= 80 => const BatteryUi(Icons.battery_5_bar, null), + _ => const BatteryUi(Icons.battery_full, MeshPalette.signal), }; } @@ -76,9 +77,9 @@ class _BatteryIndicatorState extends State { Flexible( child: Text( displayText, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, + style: MeshTheme.mono( + fontSize: 11, + fontWeight: FontWeight.w600, color: batteryUi.color, ), maxLines: 1, diff --git a/lib/widgets/device_tile.dart b/lib/widgets/device_tile.dart index 9012128c..ef0f5441 100644 --- a/lib/widgets/device_tile.dart +++ b/lib/widgets/device_tile.dart @@ -1,9 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; + import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; +import 'mesh_ui.dart'; import 'signal_ui.dart'; -/// A reusable tile widget for displaying a MeshCore device in a list +/// A MeshCard-based row for displaying a scanned BLE device. +/// Shows an AvatarCircle (router icon, deterministic hue from device name), +/// device name, mono MAC address, mono RSSI dBm, and SignalBars on the right. +/// While connecting, shows a small progress ring instead of signal bars. class DeviceTile extends StatelessWidget { final ScanResult scanResult; final VoidCallback? onTap; @@ -23,27 +30,10 @@ class DeviceTile extends StatelessWidget { final name = device.platformName.isNotEmpty ? device.platformName : scanResult.advertisementData.advName; + final displayName = name.isNotEmpty ? name : context.l10n.common_unknownDevice; + final mac = device.remoteId.toString(); + final scheme = Theme.of(context).colorScheme; - return ListTile( - enabled: onTap != null || isConnecting, - leading: _buildSignalIcon(rssi), - title: Text( - name.isNotEmpty ? name : context.l10n.common_unknownDevice, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text(device.remoteId.toString()), - trailing: isConnecting - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : null, - onTap: onTap, - ); - } - - Widget _buildSignalIcon(int rssi) { final tier = rssi >= -60 ? 0 : rssi >= -70 @@ -55,15 +45,77 @@ class DeviceTile extends StatelessWidget { : 4; final signalUi = signalUiForStrengthTier(tier); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(signalUi.icon, color: signalUi.color), - Text( - '$rssi dBm', - style: TextStyle(fontSize: 10, color: signalUi.color), - ), - ], + return MeshCard( + onTap: onTap == null + ? null + : () { + HapticFeedback.selectionClick(); + onTap!(); + }, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Row( + children: [ + AvatarCircle( + name: displayName, + size: 42, + icon: Icons.router, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + displayName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: scheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), + Text( + mac, + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + if (isConnecting) + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: scheme.primary, + ), + ) + else + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Icon(signalUi.icon, size: 16, color: signalUi.color), + const SizedBox(height: 3), + Text( + '$rssi dBm', + style: MeshTheme.mono( + fontSize: 10, + color: signalUi.color, + ), + ), + ], + ), + ], + ), ); } } diff --git a/lib/widgets/elements_ui.dart b/lib/widgets/elements_ui.dart index 0c462499..55d930db 100644 --- a/lib/widgets/elements_ui.dart +++ b/lib/widgets/elements_ui.dart @@ -29,31 +29,68 @@ class FeatureToggleRow extends StatefulWidget { class _FeatureToggleRow extends State { @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: SwitchListTile( - title: Text(widget.title), - subtitle: Text(widget.subtitle), - value: widget.value, - onChanged: widget.onChanged, - contentPadding: EdgeInsets.zero, + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + widget.subtitle, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), ), - ), - if (widget.hasRefreshing) - IconButton( - icon: widget.isRefreshing - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh, size: 20), - onPressed: widget.isRefreshing ? null : widget.onRefresh, - tooltip: widget.refreshTooltip, - visualDensity: VisualDensity.compact, + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: widget.value, + onChanged: widget.onChanged, + ), + if (widget.hasRefreshing) ...[ + const SizedBox(width: 4), + widget.isRefreshing + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 1.8, + color: scheme.primary, + ), + ) + : IconButton( + icon: const Icon(Icons.refresh, size: 18), + onPressed: widget.onRefresh, + tooltip: widget.refreshTooltip, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), + ], + ], ), - ], + ], + ), ); } } diff --git a/lib/widgets/emoji_picker.dart b/lib/widgets/emoji_picker.dart index 87fd1c9c..3a8fdf79 100644 --- a/lib/widgets/emoji_picker.dart +++ b/lib/widgets/emoji_picker.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; class EmojiPicker extends StatelessWidget { final Function(String) onEmojiSelected; @@ -257,7 +258,11 @@ class EmojiPicker extends StatelessWidget { ), child: Text( emoji, - style: const TextStyle(fontSize: 28), + style: MeshTheme.emoji(), + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), ), ), ), @@ -298,7 +303,12 @@ class EmojiPicker extends StatelessWidget { child: Center( child: Text( emojis[index], - style: const TextStyle(fontSize: 28), + style: MeshTheme.emoji(), + textHeightBehavior: + const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), ), ), ), diff --git a/lib/widgets/empty_state.dart b/lib/widgets/empty_state.dart index 718c1c44..2f0c9847 100644 --- a/lib/widgets/empty_state.dart +++ b/lib/widgets/empty_state.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; /// A centered empty state display with icon, title, and optional subtitle/action. -class EmptyState extends StatelessWidget { +/// Features a tinted icon circle, fade+slide entrance animation, and clear +/// typography hierarchy using the MeshCore design system. +class EmptyState extends StatefulWidget { final IconData icon; final String title; final String? subtitle; @@ -15,29 +17,97 @@ class EmptyState extends StatelessWidget { this.action, }); + @override + State createState() => _EmptyStateState(); +} + +class _EmptyStateState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 420), + ); + late final CurvedAnimation _curve = CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ); + + @override + void initState() { + super.initState(); + _controller.forward(); + } + + @override + void dispose() { + _curve.dispose(); + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 64, color: onSurfaceVariant.withValues(alpha: 0.6)), - const SizedBox(height: 16), - Text(title, style: TextStyle(fontSize: 16, color: onSurfaceVariant)), - if (subtitle != null) ...[ - const SizedBox(height: 8), - Text( - subtitle!, - style: TextStyle( - fontSize: 14, - color: onSurfaceVariant.withValues(alpha: 0.8), - ), - textAlign: TextAlign.center, + final scheme = Theme.of(context).colorScheme; + return FadeTransition( + opacity: _curve, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.06), + end: Offset.zero, + ).animate(_curve), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: scheme.primary.withValues(alpha: 0.08), + border: Border.all( + color: scheme.primary.withValues(alpha: 0.18), + width: 1.5, + ), + ), + child: Icon( + widget.icon, + size: 36, + color: scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 20), + Text( + widget.title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: scheme.onSurface, + letterSpacing: -0.1, + ), + textAlign: TextAlign.center, + ), + if (widget.subtitle != null) ...[ + const SizedBox(height: 8), + Text( + widget.subtitle!, + style: TextStyle( + fontSize: 13.5, + color: scheme.onSurfaceVariant, + height: 1.45, + ), + textAlign: TextAlign.center, + ), + ], + if (widget.action != null) ...[ + const SizedBox(height: 28), + widget.action!, + ], + ], ), - ], - if (action != null) ...[const SizedBox(height: 24), action!], - ], + ), + ), ), ); } diff --git a/lib/widgets/jump_to_bottom_button.dart b/lib/widgets/jump_to_bottom_button.dart index 3f6d96e2..6eab28a9 100644 --- a/lib/widgets/jump_to_bottom_button.dart +++ b/lib/widgets/jump_to_bottom_button.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; + import '../helpers/chat_scroll_controller.dart'; +import '../theme/mesh_theme.dart'; class JumpToBottomButton extends StatelessWidget { final ChatScrollController scrollController; @@ -8,6 +10,7 @@ class JumpToBottomButton extends StatelessWidget { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; return ValueListenableBuilder( valueListenable: scrollController.showJumpToBottom, builder: (context, show, _) { @@ -15,9 +18,36 @@ class JumpToBottomButton extends StatelessWidget { return Positioned( right: 16, bottom: 16, - child: FloatingActionButton.small( - onPressed: scrollController.jumpToBottom, - child: const Icon(Icons.keyboard_arrow_down), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: scrollController.jumpToBottom, + borderRadius: BorderRadius.circular(MeshRadii.pill), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: scheme.surfaceContainerHigh.withValues(alpha: 0.92), + border: Border.all( + color: scheme.outlineVariant, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.keyboard_arrow_down, + size: 22, + color: scheme.primary, + ), + ), + ), ), ); }, diff --git a/lib/widgets/mesh_ui.dart b/lib/widgets/mesh_ui.dart new file mode 100644 index 00000000..bfbe679f --- /dev/null +++ b/lib/widgets/mesh_ui.dart @@ -0,0 +1,643 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../theme/mesh_theme.dart'; + +/// MeshCore shared design kit. +/// +/// Building blocks used across all screens so the app reads as one product: +/// [SectionHeader], [MeshCard], [StatusChip], [StatTile], [AvatarCircle], +/// [SignalBars], [RouteChip], [PulseDot], [BottomSheetHeader] + +/// [showMeshSheet], [ErrorRetryCard], and [ListEntrance]. + +/// Small-caps mono section label, optionally with a trailing widget. +class SectionHeader extends StatelessWidget { + final String label; + final Widget? trailing; + final EdgeInsetsGeometry padding; + + const SectionHeader( + this.label, { + super.key, + this.trailing, + this.padding = const EdgeInsets.fromLTRB(16, 20, 16, 8), + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Padding( + padding: padding, + child: Row( + children: [ + Expanded( + child: Text( + label.toUpperCase(), + style: MeshTheme.accentLabel(color: scheme.onSurfaceVariant), + overflow: TextOverflow.ellipsis, + ), + ), + ?trailing, + ], + ), + ); + } +} + +/// Bordered surface card with press feedback. The standard container for +/// grouped content and tappable list entries. +class MeshCard extends StatelessWidget { + final Widget child; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry margin; + final Color? color; + final Color? borderColor; + final double radius; + + const MeshCard({ + super.key, + required this.child, + this.onTap, + this.onLongPress, + this.padding = const EdgeInsets.all(14), + this.margin = const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + this.color, + this.borderColor, + this.radius = MeshRadii.md, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final shape = RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + side: BorderSide(color: borderColor ?? scheme.outlineVariant), + ); + return Padding( + padding: margin, + child: Material( + color: color ?? scheme.surfaceContainerLow, + shape: shape, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + onLongPress: onLongPress == null + ? null + : () { + HapticFeedback.selectionClick(); + onLongPress!(); + }, + child: Padding(padding: padding, child: child), + ), + ), + ); + } +} + +/// Tinted pill chip for statuses: a dot or icon plus a short label. +class StatusChip extends StatelessWidget { + final String label; + final Color color; + final IconData? icon; + final bool pulse; + final double fontSize; + + const StatusChip({ + super.key, + required this.label, + required this.color, + this.icon, + this.pulse = false, + this.fontSize = 11.5, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(MeshRadii.pill), + border: Border.all(color: color.withValues(alpha: 0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) + Icon(icon, size: fontSize + 2, color: color) + else + PulseDot(color: color, size: 7, animate: pulse), + const SizedBox(width: 5), + Flexible( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: MeshTheme.mono( + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ), + ], + ), + ); + } +} + +/// Compact metric tile: icon, mono value (+ optional unit), small label. +class StatTile extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final String? unit; + final Color? color; + final VoidCallback? onTap; + + const StatTile({ + super.key, + required this.icon, + required this.label, + required this.value, + this.unit, + this.color, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final accent = color ?? scheme.primary; + return MeshCard( + onTap: onTap, + margin: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon(icon, size: 14, color: accent), + const SizedBox(width: 6), + Expanded( + child: Text( + label.toUpperCase(), + style: MeshTheme.accentLabel( + color: scheme.onSurfaceVariant, + fontSize: 9, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 6), + Text.rich( + TextSpan( + text: value, + style: MeshTheme.mono( + fontSize: 17, + fontWeight: FontWeight.w600, + color: scheme.onSurface, + ), + children: [ + if (unit != null) + TextSpan( + text: ' $unit', + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} + +/// Initials avatar with a deterministic per-name hue, or a fixed [color] +/// for node-type coloring. Optional [icon] replaces initials. +class AvatarCircle extends StatelessWidget { + final String name; + final double size; + final Color? color; + final IconData? icon; + + const AvatarCircle({ + super.key, + required this.name, + this.size = 40, + this.color, + this.icon, + }); + + static const _hues = [ + MeshPalette.blue, + MeshPalette.magenta, + MeshPalette.signal, + MeshPalette.warn, + Color(0xFF8FA8F0), + Color(0xFF6FD9CE), + ]; + + Color _colorFor(String s) { + var h = 0; + for (final c in s.codeUnits) { + h = (h * 31 + c) & 0x7fffffff; + } + return _hues[h % _hues.length]; + } + + @override + Widget build(BuildContext context) { + final accent = color ?? _colorFor(name); + final initials = _initials(name); + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: accent.withValues(alpha: 0.14), + border: Border.all(color: accent.withValues(alpha: 0.4)), + ), + alignment: Alignment.center, + child: icon != null + ? Icon(icon, size: size * 0.5, color: accent) + : Text( + initials, + style: MeshTheme.mono( + fontSize: size * 0.36, + fontWeight: FontWeight.w700, + color: accent, + ), + ), + ); + } + + static String _initials(String name) { + final words = name + .trim() + .split(RegExp(r'\s+')) + .where((w) => w.isNotEmpty) + .toList(); + if (words.isEmpty) return '?'; + if (words.length == 1) { + return words.first.characters.take(2).toString().toUpperCase(); + } + return (words.first.characters.take(1).toString() + + words[1].characters.take(1).toString()) + .toUpperCase(); + } +} + +/// Four-bar signal strength indicator driven by an SNR value (dB), colored +/// with the shared [MeshTheme.snrColor] ramp. +class SignalBars extends StatelessWidget { + final double? snr; + final double height; + + const SignalBars({super.key, required this.snr, this.height = 14}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final color = MeshTheme.snrColor(snr, blocked: false); + final active = snr == null + ? 0 + : snr! > 0 + ? 4 + : snr! > -5 + ? 3 + : snr! > -12 + ? 2 + : 1; + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate(4, (i) { + final on = i < active; + return Container( + width: 3, + height: height * (0.4 + i * 0.2), + margin: const EdgeInsets.only(right: 2), + decoration: BoxDecoration( + color: on ? color : scheme.outlineVariant, + borderRadius: BorderRadius.circular(1), + ), + ); + }), + ); + } +} + +/// Chip describing how a message was routed: direct (with hop count) vs flood. +class RouteChip extends StatelessWidget { + final bool isDirect; + final int? hops; + + const RouteChip({super.key, required this.isDirect, this.hops}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final label = isDirect + ? (hops == null || hops == 0 ? 'DIRECT' : '$hops HOP${hops == 1 ? '' : 'S'}') + : 'FLOOD'; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: scheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(MeshRadii.xs), + border: Border.all(color: scheme.outlineVariant), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isDirect ? Icons.trending_flat : Icons.podcasts, + size: 11, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 3), + Text( + label, + style: MeshTheme.accentLabel( + color: scheme.onSurfaceVariant, + fontSize: 8.5, + ), + ), + ], + ), + ); + } +} + +/// Small status dot, optionally with a soft breathing animation. +class PulseDot extends StatefulWidget { + final Color color; + final double size; + final bool animate; + + const PulseDot({ + super.key, + required this.color, + this.size = 8, + this.animate = false, + }); + + @override + State createState() => _PulseDotState(); +} + +class _PulseDotState extends State + with SingleTickerProviderStateMixin { + // Created eagerly: a lazy `late final` initializer would run on first + // access — which can be dispose(), where ticker creation throws. + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + ); + if (widget.animate) _controller.repeat(reverse: true); + } + + @override + void didUpdateWidget(PulseDot old) { + super.didUpdateWidget(old); + if (widget.animate && !_controller.isAnimating) { + _controller.repeat(reverse: true); + } else if (!widget.animate && _controller.isAnimating) { + _controller.stop(); + _controller.value = 0; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: widget.animate + ? Tween(begin: 0.35, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ) + : const AlwaysStoppedAnimation(1.0), + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.color, + boxShadow: [ + BoxShadow( + color: widget.color.withValues(alpha: 0.45), + blurRadius: widget.size * 0.7, + ), + ], + ), + ), + ); + } +} + +/// Standard modal sheet header: drag handle, title, optional subtitle and +/// trailing action, and a close button. +class BottomSheetHeader extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? trailing; + + const BottomSheetHeader({ + super.key, + required this.title, + this.subtitle, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 8, 4), + child: Column( + children: [ + Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: scheme.outline, + borderRadius: BorderRadius.circular(2), + ), + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: -0.2, + ), + ), + if (subtitle != null) + Text( + subtitle!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ?trailing, + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => Navigator.of(context).maybePop(), + ), + ], + ), + ], + ), + ); + } +} + +/// Shows a modal bottom sheet with the app-standard shape, scroll behavior +/// and safe-area handling. Pair the content with [BottomSheetHeader]. +Future showMeshSheet( + BuildContext context, { + required WidgetBuilder builder, + bool isScrollControlled = true, +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + useSafeArea: true, + showDragHandle: false, + builder: (context) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: builder(context), + ), + ); +} + +/// Inline error surface with an optional retry action. +class ErrorRetryCard extends StatelessWidget { + final String message; + final VoidCallback? onRetry; + final String? retryLabel; + + const ErrorRetryCard({ + super.key, + required this.message, + this.onRetry, + this.retryLabel, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return MeshCard( + color: scheme.error.withValues(alpha: 0.08), + borderColor: scheme.error.withValues(alpha: 0.35), + child: Row( + children: [ + Icon(Icons.error_outline, color: scheme.error, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + message, + style: TextStyle(color: scheme.error, fontSize: 13), + ), + ), + if (onRetry != null) + TextButton( + onPressed: onRetry, + child: Text(retryLabel ?? 'Retry'), + ), + ], + ), + ); + } +} + +/// Staggered fade + slide entrance for list items. Wrap each item and pass +/// its [index]; animation only plays once per widget lifecycle. +class ListEntrance extends StatefulWidget { + final int index; + final Widget child; + + const ListEntrance({super.key, required this.index, required this.child}); + + @override + State createState() => _ListEntranceState(); +} + +class _ListEntranceState extends State + with SingleTickerProviderStateMixin { + // Created eagerly: a lazy `late final` initializer would run on first + // access — which can be dispose(), where ticker creation throws. + late final AnimationController _controller; + late final CurvedAnimation _curve; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 280), + ); + _curve = CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ); + final delay = Duration(milliseconds: 24 * widget.index.clamp(0, 12)); + Future.delayed(delay, () { + if (mounted) _controller.forward(); + }); + } + + @override + void dispose() { + _curve.dispose(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _curve, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.04), + end: Offset.zero, + ).animate(_curve), + child: widget.child, + ), + ); + } +} diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart index cf9cd7d6..1d2bbd45 100644 --- a/lib/widgets/message_status_icon.dart +++ b/lib/widgets/message_status_icon.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; class MessageStatusIcon extends StatefulWidget { final bool isAcked; @@ -71,7 +72,11 @@ class _MessageStatusIconState extends State if (widget.isFailed) { return Semantics( label: l10n.messageStatus_failed, - child: Icon(Icons.cancel, size: size, color: colorScheme.error), + child: Icon( + Icons.cancel, + size: size, + color: colorScheme.error, + ), ); } @@ -92,7 +97,10 @@ class _MessageStatusIconState extends State : widget.isAcked ? l10n.messageStatus_delivered : l10n.messageStatus_sent; - final Color color = delivered ? colorScheme.tertiary : baseColor; + // Use palette colors: tertiary (warn/amber) for acked/repeated, base for sent. + final Color color = delivered + ? MeshPalette.signal.withValues(alpha: 0.9) + : baseColor; return Semantics( label: label, diff --git a/lib/widgets/path_map_ui.dart b/lib/widgets/path_map_ui.dart new file mode 100644 index 00000000..cfd3672e --- /dev/null +++ b/lib/widgets/path_map_ui.dart @@ -0,0 +1,659 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +import '../l10n/l10n.dart'; +import '../models/display_path.dart'; +import '../models/path_playback.dart'; +import '../theme/mesh_theme.dart'; + +/// Shared UI for the path map screens (live path trace and received-message +/// path map): packet-flow animation overlays, single/combined view toggle, +/// playback controls, and the multi-path summary/legend. + +enum PathViewMode { single, combined } + +const Color kPrimaryPathColor = Colors.blueAccent; +const List kAlternatePathColors = [ + Color(0xFF8B5CF6), // purple + MeshPalette.signal, // green + MeshPalette.warn, // amber + MeshPalette.magenta, +]; + +double getPathDistanceMeters(List points) { + if (points.length <= 1) return 0.0; + + double distanceMeters = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < points.length - 1; i++) { + distanceMeters += distanceCalculator(points[i], points[i + 1]); + } + + return distanceMeters; +} + +String formatDistance(double distanceMeters, {required bool isImperial}) { + if (isImperial) { + return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)'; + } + return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)'; +} + +String formatLastObserved(BuildContext context, DateTime timestamp) { + final l10n = context.l10n; + final diff = DateTime.now().difference(timestamp); + if (diff.isNegative || diff.inMinutes < 5) return l10n.contacts_lastSeenNow; + if (diff.inMinutes < 60) return l10n.contacts_lastSeenMinsAgo(diff.inMinutes); + if (diff.inHours < 24) { + return diff.inHours == 1 + ? l10n.contacts_lastSeenHourAgo + : l10n.contacts_lastSeenHoursAgo(diff.inHours); + } + return diff.inDays == 1 + ? l10n.contacts_lastSeenDayAgo + : l10n.contacts_lastSeenDaysAgo(diff.inDays); +} + +/// Polylines for the visible paths: shared-segment halos (combined view), +/// dashed runs for estimated segments, dimming for unfocused paths and for +/// the selected path while its packet animation is running. +List buildMultiPathPolylines({ + required List visible, + required DisplayPath? selected, + required bool combined, + required bool animating, +}) { + final lines = []; + + if (combined && visible.length > 1) { + final counts = {}; + for (final path in visible) { + for (var i = 0; i < path.points.length - 1; i++) { + counts.update( + _segmentKey(path.points[i], path.points[i + 1]), + (v) => v + 1, + ifAbsent: () => 1, + ); + } + } + final drawn = {}; + for (final path in visible) { + for (var i = 0; i < path.points.length - 1; i++) { + final key = _segmentKey(path.points[i], path.points[i + 1]); + if ((counts[key] ?? 0) < 2 || !drawn.add(key)) continue; + lines.add( + Polyline( + points: [path.points[i], path.points[i + 1]], + strokeWidth: 11, + color: Colors.white.withValues(alpha: 0.22), + ), + ); + } + } + } + + void addPath(DisplayPath path, {required bool isSelected}) { + final dimmedByFocus = combined && !isSelected; + final alpha = dimmedByFocus ? 0.38 : (isSelected && animating ? 0.30 : 1.0); + final width = isSelected ? 5.0 : 3.0; + var i = 0; + while (i < path.segmentEstimated.length) { + final dashed = path.segmentEstimated[i]; + var j = i; + while (j < path.segmentEstimated.length && + path.segmentEstimated[j] == dashed) { + j++; + } + lines.add( + Polyline( + points: path.points.sublist(i, j + 1), + strokeWidth: width, + color: path.color.withValues(alpha: alpha), + pattern: dashed + ? StrokePattern.dashed(segments: const [10, 7]) + : const StrokePattern.solid(), + ), + ); + i = j; + } + } + + for (final path in visible) { + if (path.id != selected?.id) addPath(path, isSelected: false); + } + if (selected != null && visible.any((p) => p.id == selected.id)) { + addPath(selected, isSelected: true); + } + + return lines; +} + +String _segmentKey(LatLng a, LatLng b) { + final ka = + '${a.latitude.toStringAsFixed(6)},${a.longitude.toStringAsFixed(6)}'; + final kb = + '${b.latitude.toStringAsFixed(6)},${b.longitude.toStringAsFixed(6)}'; + return ka.compareTo(kb) <= 0 ? '$ka|$kb' : '$kb|$ka'; +} + +/// Bright traversed portion plus the glow on the active segment. +List buildPacketTrailPolylines( + PathPlaybackController playback, + Color color, +) { + if (!playback.started || !playback.hasPath) return const []; + final seg = playback.currentSegment; + final traversed = [ + ...playback.points.take(seg + 1), + playback.position, + ]; + return [ + Polyline( + points: [playback.points[seg], playback.position], + strokeWidth: 8, + color: Colors.white.withValues(alpha: 0.45), + ), + Polyline(points: traversed, strokeWidth: 5, color: color), + ]; +} + +/// The moving packet dot and the pulse ring at the hop it just reached. +List buildPacketMarkers( + PathPlaybackController playback, + Color color, +) { + if (!playback.started || !playback.hasPath) return const []; + final markers = []; + + final dwell = playback.dwellProgress; + if (dwell != null) { + final reached = playback.points[playback.reachedPointIndex]; + markers.add( + Marker( + point: reached, + width: 56, + height: 56, + child: IgnorePointer( + child: Center( + child: Container( + width: 24 + 28 * dwell, + height: 24 + 28 * dwell, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: color.withValues(alpha: 1.0 - dwell), + width: 3, + ), + ), + ), + ), + ), + ), + ); + } + + markers.add( + Marker( + point: playback.position, + width: 24, + height: 24, + child: IgnorePointer( + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.7), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + ), + ), + ), + ); + + return markers; +} + +/// Bottom sheet listing the paths that pass through a shared node. +void showSharedNodeSheet( + BuildContext context, { + required String title, + required List paths, + required ValueChanged onSelect, +}) { + final l10n = context.l10n; + showModalBottomSheet( + context: context, + builder: (sheetContext) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: Text( + title, + style: MeshTheme.mono( + fontSize: 14, + fontWeight: FontWeight.w700, + color: MeshPalette.ink, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + l10n.pathMap_sharedNodeCount(paths.length), + style: TextStyle(fontSize: 12, color: MeshPalette.ink3), + ), + ), + const SizedBox(height: 8), + for (final path in paths) + ListTile( + dense: true, + leading: _colorDot(path.color), + title: Text( + path.label, + style: MeshTheme.mono(fontSize: 13, color: MeshPalette.ink), + ), + trailing: Text( + l10n.pathMap_hopCount(path.totalTransmissions), + style: MeshTheme.mono(fontSize: 11, color: MeshPalette.ink3), + ), + onTap: () { + Navigator.pop(sheetContext); + onSelect(path); + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ); +} + +Widget _colorDot(Color color) => Container( + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), +); + +/// Floating Single/Combined toggle for the top of a path map Stack. +class PathViewModeToggle extends StatelessWidget { + final PathViewMode mode; + final ValueChanged onChanged; + + const PathViewModeToggle({ + super.key, + required this.mode, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Positioned( + top: 12, + left: 0, + right: 0, + child: Center( + child: DecoratedBox( + decoration: BoxDecoration( + color: MeshPalette.bg1.withValues(alpha: 0.92), + borderRadius: BorderRadius.circular(MeshRadii.pill), + ), + child: SegmentedButton( + style: const ButtonStyle( + visualDensity: VisualDensity(horizontal: -3, vertical: -3), + ), + showSelectedIcon: false, + segments: [ + ButtonSegment( + value: PathViewMode.single, + label: Text(l10n.pathMap_viewSingle), + ), + ButtonSegment( + value: PathViewMode.combined, + label: Text(l10n.pathMap_viewCombined), + ), + ], + selected: {mode}, + onSelectionChanged: (selection) => onChanged(selection.first), + ), + ), + ), + ); + } +} + +/// Compact playback control row: animation toggle, step/play/replay buttons, +/// follow-packet lock, speed chip, and the live "Hop x of y · from → to" +/// label. +class PathAnimationControls extends StatelessWidget { + final PathPlaybackController playback; + final DisplayPath? selected; + final bool animationEnabled; + final VoidCallback onToggleAnimation; + final bool followEnabled; + final VoidCallback onToggleFollow; + + const PathAnimationControls({ + super.key, + required this.playback, + required this.selected, + required this.animationEnabled, + required this.onToggleAnimation, + required this.followEnabled, + required this.onToggleFollow, + }); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: playback, + builder: (context, _) { + final l10n = context.l10n; + final enabled = animationEnabled && playback.hasPath; + final path = selected; + String? hopLabel; + if (animationEnabled && + playback.started && + playback.hasPath && + path != null) { + final seg = playback.currentSegment; + final row = seg < path.rowForSegment.length + ? path.rowForSegment[seg] + : 0; + final from = path.pointLabels[seg]; + final to = path.pointLabels[seg + 1]; + hopLabel = + '${l10n.pathMap_hopOf(row + 1, path.totalTransmissions)} · $from → $to'; + } + + Widget controlButton({ + required IconData icon, + required String tooltip, + VoidCallback? onPressed, + Color? color, + }) => IconButton( + icon: Icon(icon, size: 20, color: color), + tooltip: tooltip, + onPressed: onPressed, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 34, minHeight: 34), + ); + + return Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 12, 2), + child: Row( + children: [ + controlButton( + icon: Icons.animation, + tooltip: animationEnabled + ? l10n.pathMap_animationOff + : l10n.pathMap_animationOn, + color: animationEnabled ? MeshPalette.blue : MeshPalette.ink4, + onPressed: onToggleAnimation, + ), + controlButton( + icon: Icons.skip_previous, + tooltip: l10n.pathMap_stepBack, + onPressed: enabled && playback.started + ? playback.stepBack + : null, + ), + controlButton( + icon: playback.playing ? Icons.pause : Icons.play_arrow, + tooltip: playback.playing ? l10n.pathMap_pause : l10n.pathMap_play, + onPressed: enabled ? playback.togglePlay : null, + ), + controlButton( + icon: Icons.skip_next, + tooltip: l10n.pathMap_stepForward, + onPressed: enabled ? playback.stepForward : null, + ), + controlButton( + icon: Icons.replay, + tooltip: l10n.pathMap_replay, + onPressed: enabled ? playback.replay : null, + ), + controlButton( + icon: followEnabled ? Icons.lock : Icons.lock_open, + tooltip: followEnabled + ? l10n.pathMap_unfollowPacket + : l10n.pathMap_followPacket, + color: followEnabled ? MeshPalette.blue : null, + onPressed: enabled ? onToggleFollow : null, + ), + TextButton( + onPressed: enabled ? playback.cycleSpeed : null, + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.symmetric(horizontal: 6), + minimumSize: const Size(36, 30), + ), + child: Text( + playback.speed == 0.5 ? '0.5×' : '${playback.speed.toInt()}×', + style: MeshTheme.mono(fontSize: 12), + ), + ), + Expanded( + child: Text( + hopLabel ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: MeshTheme.mono( + fontSize: 10.5, + color: MeshPalette.ink2, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +/// Marker/line style legend swatches. +class PathMiniLegend extends StatelessWidget { + final bool combined; + final bool showInferred; + + const PathMiniLegend({ + super.key, + required this.combined, + this.showInferred = true, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + Widget item(Widget swatch, String text) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + swatch, + const SizedBox(width: 4), + Text(text, style: TextStyle(fontSize: 11, color: MeshPalette.ink3)), + ], + ); + Widget dashSample() => Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < 3; i++) + Container( + width: 5, + height: 3, + margin: const EdgeInsets.only(right: 2), + color: MeshPalette.ink3, + ), + ], + ); + return Wrap( + spacing: 12, + runSpacing: 2, + children: [ + item(_colorDot(MeshPalette.signal), l10n.pathTrace_legendGpsConfirmed), + if (showInferred) + item(_colorDot(MeshPalette.warn), l10n.pathTrace_legendInferred), + if (combined) ...[ + item( + Container( + width: 14, + height: 6, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(3), + ), + ), + l10n.pathMap_legendShared, + ), + item(dashSample(), l10n.pathMap_legendEstimated), + ], + ], + ); + } +} + +/// "Observed paths: N" header plus one selectable row per path with hop +/// count, distance, GPS-confirmed count, last-observed time, and an eye +/// toggle for visibility. +class PathSummaryList extends StatelessWidget { + final List paths; + final String selectedId; + final Set hiddenIds; + final bool isImperial; + final ValueChanged onSelect; + final ValueChanged onToggleVisibility; + final VoidCallback onShowAll; + + const PathSummaryList({ + super.key, + required this.paths, + required this.selectedId, + required this.hiddenIds, + required this.isImperial, + required this.onSelect, + required this.onToggleVisibility, + required this.onShowAll, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 2, 12, 0), + child: Row( + children: [ + Text( + l10n.pathMap_observedPaths(paths.length), + style: MeshTheme.accentLabel(color: MeshPalette.ink3), + ), + const Spacer(), + if (hiddenIds.isNotEmpty) + TextButton( + onPressed: onShowAll, + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.symmetric(horizontal: 8), + minimumSize: const Size(0, 26), + ), + child: Text( + l10n.pathMap_showAllPaths, + style: const TextStyle(fontSize: 11), + ), + ), + ], + ), + ), + for (final path in paths) _buildRow(context, path), + const SizedBox(height: 4), + ], + ); + } + + Widget _buildRow(BuildContext context, DisplayPath path) { + final l10n = context.l10n; + final isSelected = path.id == selectedId; + final hidden = hiddenIds.contains(path.id); + final timestamp = path.record?.timestamp; + final parts = [ + '${l10n.pathMap_hopCount(path.totalTransmissions)} ${formatDistance(path.distanceMeters, isImperial: isImperial)}', + l10n.pathMap_gpsCount(path.gpsConfirmedHops, path.hopBytes.length), + if (timestamp != null) formatLastObserved(context, timestamp), + ]; + + return InkWell( + onTap: () => onSelect(path), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isSelected ? MeshPalette.bg3 : Colors.transparent, + borderRadius: BorderRadius.circular(MeshRadii.sm), + ), + child: Row( + children: [ + Opacity( + opacity: hidden ? 0.45 : 1, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _colorDot(path.color), + const SizedBox(width: 8), + Text( + path.label, + style: MeshTheme.mono( + fontSize: 12, + fontWeight: isSelected + ? FontWeight.w700 + : FontWeight.w500, + color: MeshPalette.ink, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Opacity( + opacity: hidden ? 0.45 : 1, + child: Text( + parts.join(' · '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: MeshTheme.mono(fontSize: 10.5, color: MeshPalette.ink3), + ), + ), + ), + IconButton( + icon: Icon( + hidden ? Icons.visibility_off : Icons.visibility, + size: 16, + color: hidden ? MeshPalette.ink4 : MeshPalette.ink3, + ), + tooltip: hidden ? l10n.pathMap_showPath : l10n.pathMap_hidePath, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 30, minHeight: 30), + onPressed: () => onToggleVisibility(path), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index bcb4781f..90b90b40 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -2,12 +2,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; class QuickSwitchBar extends StatelessWidget { final int selectedIndex; final ValueChanged onDestinationSelected; final int contactsUnreadCount; final int channelsUnreadCount; + final bool highContrast; const QuickSwitchBar({ super.key, @@ -15,6 +17,7 @@ class QuickSwitchBar extends StatelessWidget { required this.onDestinationSelected, this.contactsUnreadCount = 0, this.channelsUnreadCount = 0, + this.highContrast = false, }); @override @@ -22,6 +25,14 @@ class QuickSwitchBar extends StatelessWidget { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final labelStyle = theme.textTheme.labelMedium ?? const TextStyle(); + final background = highContrast ? MapPalette.panelDark : Colors.transparent; + final selectedColor = highContrast + ? MapPalette.textPrimary + : colorScheme.onPrimary; + final unselectedColor = highContrast + ? MapPalette.textSecondary + : colorScheme.onSurfaceVariant; + final indicator = highContrast ? MapPalette.selected : colorScheme.primary; return SizedBox( width: double.infinity, @@ -31,9 +42,11 @@ class QuickSwitchBar extends StatelessWidget { filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14), child: DecoratedBox( decoration: BoxDecoration( - color: Colors.transparent, + color: background, border: Border.all( - color: colorScheme.outlineVariant.withValues(alpha: 0.4), + color: highContrast + ? MapPalette.border + : colorScheme.outlineVariant.withValues(alpha: 0.4), ), ), child: NavigationBarTheme( @@ -41,22 +54,18 @@ class QuickSwitchBar extends StatelessWidget { backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, shadowColor: Colors.transparent, - indicatorColor: colorScheme.primaryContainer, + indicatorColor: indicator, labelTextStyle: WidgetStateProperty.resolveWith((states) { final isSelected = states.contains(WidgetState.selected); return labelStyle.copyWith( fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, + color: isSelected ? selectedColor : unselectedColor, ); }), iconTheme: WidgetStateProperty.resolveWith((states) { final isSelected = states.contains(WidgetState.selected); return IconThemeData( - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, + color: isSelected ? selectedColor : unselectedColor, ); }), ), diff --git a/lib/widgets/radio_stats_entry.dart b/lib/widgets/radio_stats_entry.dart index d5fbc670..aab7c4de 100644 --- a/lib/widgets/radio_stats_entry.dart +++ b/lib/widgets/radio_stats_entry.dart @@ -7,6 +7,9 @@ import 'package:meshcore_open/l10n/l10n.dart'; import 'package:meshcore_open/screens/companion_radio_stats_screen.dart'; import 'package:provider/provider.dart'; +import '../theme/mesh_theme.dart'; +import 'mesh_ui.dart'; + void pushCompanionRadioStatsScreen(BuildContext context) { Navigator.push( context, @@ -140,12 +143,12 @@ class AirActivityDotState extends State { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; final on = widget.active && _blink; - return Icon( - Icons.circle, - size: 12, - color: on ? scheme.primary : scheme.outline, + final scheme = Theme.of(context).colorScheme; + return PulseDot( + color: on ? MeshPalette.blue : scheme.outline, + size: 11, + animate: false, ); } } diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 5b45037d..207bbe5d 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -9,6 +9,8 @@ import '../l10n/contact_localization.dart'; import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import '../utils/app_logger.dart'; import 'routing_sheet.dart'; @@ -269,26 +271,40 @@ class _RepeaterLoginDialogState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; final connector = context.watch(); final repeater = _resolveRepeater(connector); final isFloodMode = repeater.pathOverride == -1; return AlertDialog( title: Row( children: [ - Icon(Icons.cell_tower, color: Theme.of(context).colorScheme.tertiary), - const SizedBox(width: 8), + AvatarCircle( + name: repeater.name, + size: 40, + color: MeshPalette.warn, + icon: Icons.cell_tower, + ), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.login_repeaterLogin), + Text( + l10n.login_repeaterLogin, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), Text( repeater.name, style: TextStyle( - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), @@ -319,14 +335,14 @@ class _RepeaterLoginDialogState extends State { Icon( Icons.error, size: 18, - color: Theme.of(context).colorScheme.error, + color: scheme.error, ), const SizedBox(width: 8), Expanded( child: Text( _loginError!, style: TextStyle( - color: Theme.of(context).colorScheme.error, + color: scheme.error, fontSize: 13, ), ), @@ -341,7 +357,6 @@ class _RepeaterLoginDialogState extends State { decoration: InputDecoration( labelText: l10n.login_password, hintText: l10n.login_enterPassword, - border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( icon: Icon( @@ -390,9 +405,9 @@ class _RepeaterLoginDialogState extends State { children: [ Text( l10n.login_routing, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, + style: MeshTheme.accentLabel( + color: scheme.onSurfaceVariant, + fontSize: 11, ), ), const Spacer(), @@ -421,7 +436,7 @@ class _RepeaterLoginDialogState extends State { Icons.auto_mode, size: 20, color: !isFloodMode - ? Theme.of(context).primaryColor + ? scheme.primary : null, ), const SizedBox(width: 8), @@ -444,7 +459,7 @@ class _RepeaterLoginDialogState extends State { Icons.waves, size: 20, color: isFloodMode - ? Theme.of(context).primaryColor + ? scheme.primary : null, ), const SizedBox(width: 8), @@ -468,7 +483,7 @@ class _RepeaterLoginDialogState extends State { repeater.pathLabel(context.l10n), style: TextStyle( fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -502,7 +517,7 @@ class _RepeaterLoginDialogState extends State { height: 16, child: CircularProgressIndicator( strokeWidth: 2, - color: Theme.of(context).colorScheme.onPrimary, + color: scheme.onPrimary, ), ), const SizedBox(width: 12), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index a6475cfb..34a3f994 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -10,6 +10,8 @@ import '../l10n/contact_localization.dart'; import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../theme/mesh_theme.dart'; +import '../widgets/mesh_ui.dart'; import '../utils/app_logger.dart'; import '../helpers/snack_bar_builder.dart'; import 'routing_sheet.dart'; @@ -226,26 +228,40 @@ class _RoomLoginDialogState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; final connector = context.watch(); final repeater = _resolveRepeater(connector); final isFloodMode = repeater.pathOverride == -1; return AlertDialog( title: Row( children: [ - Icon(Icons.group, color: Theme.of(context).colorScheme.secondary), - const SizedBox(width: 8), + AvatarCircle( + name: repeater.name, + size: 40, + color: MeshPalette.magenta, + icon: Icons.group, + ), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.login_roomLogin), + Text( + l10n.login_roomLogin, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), Text( repeater.name, style: TextStyle( - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), @@ -275,7 +291,6 @@ class _RoomLoginDialogState extends State { decoration: InputDecoration( labelText: l10n.login_password, hintText: l10n.login_enterPassword, - border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( icon: Icon( @@ -319,9 +334,9 @@ class _RoomLoginDialogState extends State { children: [ Text( l10n.login_routing, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, + style: MeshTheme.accentLabel( + color: scheme.onSurfaceVariant, + fontSize: 11, ), ), const Spacer(), @@ -350,7 +365,7 @@ class _RoomLoginDialogState extends State { Icons.auto_mode, size: 20, color: !isFloodMode - ? Theme.of(context).primaryColor + ? scheme.primary : null, ), const SizedBox(width: 8), @@ -373,7 +388,7 @@ class _RoomLoginDialogState extends State { Icons.waves, size: 20, color: isFloodMode - ? Theme.of(context).primaryColor + ? scheme.primary : null, ), const SizedBox(width: 8), @@ -397,7 +412,7 @@ class _RoomLoginDialogState extends State { repeater.pathLabel(context.l10n), style: TextStyle( fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: scheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -431,7 +446,7 @@ class _RoomLoginDialogState extends State { height: 16, child: CircularProgressIndicator( strokeWidth: 2, - color: Theme.of(context).colorScheme.onPrimary, + color: scheme.onPrimary, ), ), const SizedBox(width: 12), diff --git a/lib/widgets/signal_ui.dart b/lib/widgets/signal_ui.dart index e0e05111..0edcb9af 100644 --- a/lib/widgets/signal_ui.dart +++ b/lib/widgets/signal_ui.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../theme/mesh_theme.dart'; class SignalUi { final IconData icon; @@ -12,27 +13,27 @@ SignalUi signalUiForStrengthTier(int tier) { case 0: return const SignalUi( icon: Icons.signal_cellular_4_bar, - color: Colors.green, + color: MeshPalette.signal, ); case 1: return const SignalUi( icon: Icons.signal_cellular_alt, - color: Colors.lightGreen, + color: MeshPalette.signalDim, ); case 2: return const SignalUi( icon: Icons.signal_cellular_alt_2_bar, - color: Colors.amber, + color: MeshPalette.warn, ); case 3: return const SignalUi( icon: Icons.signal_cellular_alt_1_bar, - color: Colors.orange, + color: MeshPalette.warnDim, ); default: return const SignalUi( icon: Icons.signal_cellular_alt_1_bar, - color: Colors.red, + color: MeshPalette.alert, ); } } diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 21efffd1..f57776de 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -5,6 +5,8 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../theme/mesh_theme.dart'; +import 'mesh_ui.dart'; import 'signal_ui.dart'; Contact? _getRepeaterPrefixMatchNearLocation( @@ -218,10 +220,6 @@ class _SNRIndicatorState extends State { separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final repeater = directBestRepeaters[index]; - final snrUi = snrUiFromSNR( - repeater.snr, - widget.connector.currentSf, - ); final allContacts = widget.connector.allContacts; final selfLat = widget.connector.selfLatitude; @@ -242,22 +240,47 @@ class _SNRIndicatorState extends State { ); final name = contact?.name; + final hex = repeater.pubkeyFirstByte + .toRadixString(16) + .padLeft(2, '0'); + final snrColor = MeshTheme.snrColor( + repeater.snr, + blocked: false, + ); - return Column( - children: [ - ListTile( - leading: Icon(snrUi.icon, color: snrUi.color), - title: Text( - name ?? - repeater.pubkeyFirstByte - .toRadixString(16) - .padLeft(2, '0'), + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + child: Row( + children: [ + AvatarCircle( + name: name ?? hex, + size: 36, + color: snrColor, ), - subtitle: Text( - 'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}', + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name ?? hex, + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '${repeater.snr.toStringAsFixed(1)} dB • ${_formatLastUpdated(repeater.lastUpdated)}', + style: MeshTheme.mono( + fontSize: 11, + color: snrColor, + ), + ), + ], + ), ), - ), - ], + ], + ), ); }, ), diff --git a/lib/widgets/telemetry_location_map.dart b/lib/widgets/telemetry_location_map.dart index a73fb640..f8983c04 100644 --- a/lib/widgets/telemetry_location_map.dart +++ b/lib/widgets/telemetry_location_map.dart @@ -11,6 +11,7 @@ import '../models/app_settings.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; +import 'themed_map_tile_layer.dart'; class TelemetryLocationMap extends StatefulWidget { final double latitude; @@ -114,13 +115,7 @@ class _TelemetryLocationMapState extends State { ), ), children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), + ThemedMapTileLayer(tileCache: tileCache), MarkerLayer( markers: [ ...contacts.map(_buildContactMarker), diff --git a/lib/widgets/themed_map_tile_layer.dart b/lib/widgets/themed_map_tile_layer.dart new file mode 100644 index 00000000..c6d2eaad --- /dev/null +++ b/lib/widgets/themed_map_tile_layer.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import '../services/map_tile_cache_service.dart'; + +/// Shared cached map tiles with an automatic dark-mode treatment. +/// +/// The dark style transforms the existing OpenStreetMap raster tiles, so light +/// and dark maps share the same offline cache and network requests. +class ThemedMapTileLayer extends StatelessWidget { + final MapTileCacheService tileCache; + final double opacity; + + const ThemedMapTileLayer({ + super.key, + required this.tileCache, + this.opacity = 1, + }); + + static const ColorFilter _darkMapFilter = ColorFilter.matrix([ + -0.0850, + -0.2861, + -0.0289, + 0, + 120, + -0.0957, + -0.3218, + -0.0325, + 0, + 140, + -0.1169, + -0.3934, + -0.0397, + 0, + 170, + 0, + 0, + 0, + 1, + 0, + ]); + + @override + Widget build(BuildContext context) { + Widget layer = TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ); + + if (Theme.of(context).brightness == Brightness.dark) { + layer = ColorFiltered(colorFilter: _darkMapFilter, child: layer); + } + if (opacity < 1) { + layer = Opacity(opacity: opacity, child: layer); + } + return layer; + } +} diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 424cb6f5..1cf5c1d6 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../theme/mesh_theme.dart'; + class UnreadBadge extends StatelessWidget { final int count; @@ -9,17 +11,18 @@ class UnreadBadge extends StatelessWidget { Widget build(BuildContext context) { final display = count > 9999 ? '9999+' : count.toString(); return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( - color: Colors.redAccent, - borderRadius: BorderRadius.circular(10), + color: MeshPalette.blue.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(MeshRadii.pill), + border: Border.all(color: MeshPalette.blue.withValues(alpha: 0.45)), ), child: Text( display, - style: const TextStyle( - color: Colors.white, + style: MeshTheme.mono( fontSize: 11, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, + color: MeshPalette.blue, ), ), ); diff --git a/lib/widgets/unread_divider.dart b/lib/widgets/unread_divider.dart index f9ebd1d0..d238b1a1 100644 --- a/lib/widgets/unread_divider.dart +++ b/lib/widgets/unread_divider.dart @@ -1,30 +1,49 @@ import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import '../theme/mesh_theme.dart'; class UnreadDivider extends StatelessWidget { const UnreadDivider({super.key}); @override Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.primary; + final scheme = Theme.of(context).colorScheme; + final color = scheme.primary; return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 10), child: Row( children: [ - Expanded(child: Divider(color: color)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), + Expanded( + child: Container( + height: 1, + color: color.withValues(alpha: 0.25), + ), + ), + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(MeshRadii.pill), + border: Border.all(color: color.withValues(alpha: 0.35)), + ), child: Text( context.l10n.chat_newMessages, - style: TextStyle( + style: MeshTheme.mono( + fontSize: 10.5, + fontWeight: FontWeight.w600, color: color, - fontSize: 12, - fontWeight: FontWeight.w500, ), ), ), - Expanded(child: Divider(color: color)), + const SizedBox(width: 10), + Expanded( + child: Container( + height: 1, + color: color.withValues(alpha: 0.25), + ), + ), ], ), ); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 93e46829..379e36fa 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/untranslated.json b/untranslated.json index 9e26dfee..c1e57a13 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,698 @@ -{} \ No newline at end of file +{ + "bg": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "de": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "es": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "fr": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "hu": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "it": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "ja": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "ko": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "nl": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "pl": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "pt": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "ru": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "sk": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "sl": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "sv": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "uk": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ], + + "zh": [ + "map_searchHint", + "map_activity", + "map_online", + "map_recent", + "map_stale", + "map_visible", + "map_hidden", + "map_centerOnNode", + "map_details", + "map_noGps", + "map_noResults", + "pathMap_viewSingle", + "pathMap_viewCombined", + "pathMap_play", + "pathMap_pause", + "pathMap_replay", + "pathMap_stepBack", + "pathMap_stepForward", + "pathMap_animationOn", + "pathMap_animationOff", + "pathMap_hopOf", + "pathMap_observedPaths", + "pathMap_primary", + "pathMap_alternate", + "pathMap_hopCount", + "pathMap_gpsCount", + "pathMap_legendShared", + "pathMap_legendEstimated", + "pathMap_sharedNodeCount", + "pathMap_partialAnimation", + "pathMap_showAllPaths", + "pathMap_hidePath", + "pathMap_showPath", + "pathMap_collapsePanel", + "pathMap_expandPanel", + "pathMap_noLocation", + "pathMap_followPacket", + "pathMap_unfollowPacket" + ] +} diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 533a1712..f02857f4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows - jni ) set(PLUGIN_BUNDLED_LIBRARIES) From 3707acb12458908da925787d0dec1856e3b3fcaa Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 12 Jun 2026 22:55:41 -0700 Subject: [PATCH 09/16] Refactor code structure and remove redundant sections for improved readability and maintainability --- BUGS_FOUND.md | 362 ++++++++++++++++++ lib/connector/meshcore_connector.dart | 34 +- lib/connector/meshcore_connector_usb.dart | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_bg.dart | 4 + lib/l10n/app_localizations_de.dart | 4 + lib/l10n/app_localizations_en.dart | 4 + lib/l10n/app_localizations_es.dart | 4 + lib/l10n/app_localizations_fr.dart | 4 + lib/l10n/app_localizations_hu.dart | 4 + lib/l10n/app_localizations_it.dart | 4 + lib/l10n/app_localizations_ja.dart | 4 + lib/l10n/app_localizations_ko.dart | 4 + lib/l10n/app_localizations_nl.dart | 4 + lib/l10n/app_localizations_pl.dart | 4 + lib/l10n/app_localizations_pt.dart | 4 + lib/l10n/app_localizations_ru.dart | 4 + lib/l10n/app_localizations_sk.dart | 4 + lib/l10n/app_localizations_sl.dart | 4 + lib/l10n/app_localizations_sv.dart | 4 + lib/l10n/app_localizations_uk.dart | 4 + lib/l10n/app_localizations_zh.dart | 4 + lib/models/app_settings.dart | 8 +- lib/screens/app_settings_screen.dart | 2 +- lib/screens/chat_screen.dart | 34 +- lib/screens/map_screen.dart | 398 ++++++++++++++++++-- lib/screens/scanner_screen.dart | 9 + lib/screens/settings_screen.dart | 28 +- lib/screens/usb_screen.dart | 14 + lib/services/notification_service.dart | 44 ++- lib/services/usb_serial_service_native.dart | 3 + lib/services/usb_serial_service_web.dart | 59 ++- untranslated.json | 17 + 34 files changed, 1008 insertions(+), 84 deletions(-) create mode 100644 BUGS_FOUND.md diff --git a/BUGS_FOUND.md b/BUGS_FOUND.md new file mode 100644 index 00000000..7553b192 --- /dev/null +++ b/BUGS_FOUND.md @@ -0,0 +1,362 @@ +# MeshCore Open — Bugs Found (Web build, manual QA) + +Session: 2026-06-12 · Build served at `http://localhost:42751/` (Flutter web, debug/DDC) · Browser: Chrome + +Each entry: **Severity** · where · what · repro · expected. + +--- + +## Status +Fixes for BUG-1..4 applied on 2026-06-12 (analyze clean). **BUG-2/3 VERIFIED FIXED live:** USB device `VID:239A PID:8029` now connects on web — console shows `Open: vendorId=0x239a uartBridge=false (DTR left default)` → `Got SELF_INFO` → `connectUsb: complete`, no "device has been lost". Continuing to test the now-reachable connected screens. See "Fixes applied" at the bottom. + +## Open bugs + +### BUG-1 · Medium · USB connect — raw browser exception leaked to UI on picker cancel +- **Where:** "Connect over USB" screen (`usb_screen.dart`), tap **Select a USB device** → Web Serial port picker → dismiss/cancel without choosing a port. +- **What:** A red error snackbar shows the raw browser API string: `NotFoundError: Failed to execute 'requestPort' on 'Serial': No port selected by the user.` +- **Why it's a bug:** (1) Cancelling the port picker is a normal user action, not an error — it should be silent (or a neutral "No device selected" message), not a red error toast. (2) Even for real failures, leaking a raw JS `DOMException` string is poor UX. The catch handler should map known cases (user cancellation) and present friendly copy. +- **Expected:** Cancelling shows nothing or an info-level message; the UI never prints raw exception text. +- **Root cause (confirmed in source):** `usb_screen.dart:396 _friendlyErrorMessage()` maps `PlatformException`/`StateError`/etc., but the web cancel path throws a JS `DOMException` (`NotFoundError`) that matches none of the branches, so it falls through to `return error.toString();` (line 441). A friendly `l10n.usbErrorNoDeviceSelected` string already exists (line 428) but is only matched for a native `StateError` containing `'No USB serial device selected'`, not the web DOMException. Fix: detect the web no-port-selected case and route it to the existing friendly string (or suppress entirely). + +### BUG-2 · HIGH · Web USB connect fails with misleading "Timed out waiting for SELF_INFO" — read-pump error is dropped by a subscription race +- **Where:** `usb_serial_service_web.dart` + `meshcore_connector.dart connectUsb()`. Repro: on web (Chrome), connect to a USB serial device (observed with `Web Serial Device VID:239A PID:8029`, an Adafruit/nRF52840 board). +- **Observed (console, happens every attempt):** + 1. Port opens OK: `USB serial opened port=Web Serial Device (VID:239A PID:8029)` + 2. **Immediately:** `_pumpReads error: NetworkError: The device has been lost.` → `_pumpReads: ended` — the read stream dies the instant it opens. + 3. Connect logic ignores that and proceeds: `requesting device info…`, writes TX frames, `ChannelSync Starting sync for 40 channels`. + 4. Nothing can ever be read back → SELF_INFO + ChannelSync retry/timeout for ~7s. + 5. Ends with `USB connection error: Bad state: Timed out waiting for SELF_INFO during connect` → disconnect. User sees a generic timeout error, not the real cause. +- **Root cause (confirmed in source):** a **subscription-timing race on a broadcast stream**: + - `_frameController` is `StreamController.broadcast()` (`usb_serial_service_web.dart:24-25`). Broadcast streams **do not buffer** — events emitted with no listener attached are silently discarded. + - `_usbManager.connect()` starts `_pumpReads()` fire-and-forget (`usb_serial_service_web.dart:114`). On this device `_pumpReads` errors instantly and calls `_addFrameError()` (line 387/393). + - But `connectUsb()` attaches its error listener **only after** `await Future.delayed(200ms)` (`meshcore_connector.dart:1609`), then `frameStream.listen(onError: → disconnect(), onDone: → disconnect())` (lines 1610-1620). The read-pump error fired during that 200ms gap, so the `onError → disconnect` fail-fast path **never runs**. + - With the safety net disarmed, connect falls through to `_waitForSelfInfo` (3s) + retry (3s) and throws the misleading `Timed out waiting for SELF_INFO during connect` (line 1647). +- **Expected:** A read-stream failure during connect should abort immediately with the real cause ("USB device disconnected / lost"), not a 7-second generic SELF_INFO timeout. +- **Fix directions:** attach the `frameStream` listener (or otherwise observe transport health) *before* the read pump can emit — i.e. before/at port-open, not 200ms later; OR latch the last transport error in the service and have `connectUsb` check it; OR make connect race `_waitForSelfInfo` against a transport-error future. Remove/justify the unconditional 200ms delay that opens the race window. + +### BUG-3 · Likely device-level root cause for BUG-2 · DTR assertion may reset/lose nRF52840 (Adafruit, VID 0x239A) boards +- **Where:** `usb_serial_service_web.dart:285-295 _openPort()`. +- **What:** Immediately after `open()`, the code asserts `setSignals({dataTerminalReady:true, requestToSend:false})` with the comment *"Prevent ESP32 USB-CDC reset"*. That logic is tuned for ESP32. On Adafruit nRF52840 boards (VID `0x239A`, as seen here, PID `0x8029`), toggling DTR is associated with the bootloader/reset line and can cause the device to re-enumerate/reset — which plausibly produces the immediate `NetworkError: The device has been lost.` seen in BUG-2. +- **Status:** Strong hypothesis, not yet isolated (would need to test connecting with the `setSignals` call removed/varied for this board). Flagging because the device class that fails (nRF52840/Adafruit) is exactly the one where DTR semantics differ from ESP32. +- **Expected:** USB serial open should work for nRF52840-class MeshCore boards on web, or DTR handling should be conditional per device/VID. + +### BUG-4 · Medium · BLE "Scan" on web (no Bluetooth adapter / unsupported) gives zero feedback +- **Where:** Scanner screen (`scanner_screen.dart`). Repro: on web with no BLE module (or any web build, since `flutter_blue_plus` doesn't support web), tap **Scan**. +- **What:** Nothing happens — no spinner, no "scanning" state, no error toast, no "Bluetooth unavailable on web" message. The button just sits there and status stays "Not connected". (Confirmed by user: "its not connecting on chrome and my computer doesn't have a ble module".) +- **Expected:** Either disable/hide BLE scan on web (the app already gates non-Chrome via `ChromeRequiredScreen`; Chrome+web still can't use `flutter_blue_plus`), or show a clear "Bluetooth isn't available in the browser — use USB or TCP" message. Silent no-op leaves the user stuck. + +### BUG-5 · Low/Medium · Notifications never work until manually enabled; every incoming message logs an error +- **Where:** `notification_service.dart` — `show()` calls at lines 201/248/306/394/593 are made without ensuring notification permission was granted. +- **What (observed in console while connected):** repeated `Failed to show channel notification: Bad state: FlutterLocalNotifications.show(): You must request notifications permissions first` and the same for advert/message notifications. Each incoming channel message / advert triggers one. +- **Why:** permission is only ever requested from `app_settings_screen.dart:239` (when the user interacts with that setting). If the user never visits it, `requestPermissions()` is never called, so `show()` throws on web (and would on Android 13+ with denied permission). The errors are swallowed by `try/catch` (no crash), but notifications silently never fire and the log fills with errors. +- **Expected:** request notification permission during init / on first connect (or check `areNotificationsEnabled()` and skip `show()` when not granted) instead of calling `show()` unconditionally and relying on a caught exception per message. + +### OBS-1 · RESOLVED (not a bug) · USB-on-web connection dropped after ~4 min (`device has been lost`) +- **What:** At 9:25:13 the read pump errored `NetworkError: The device has been lost.` → `USB transport error` → clean auto-disconnect back to the scanner. +- **Cause:** confirmed by user — they physically dropped/disconnected the radio. So this was a real physical disconnect, NOT a Web Serial stability issue. Positive finding: the app (with the BUG-2 fix) handled an abrupt physical disconnect cleanly and returned to the scanner. + +### BUG-6 · Medium · Battery Chemistry setting permanently disabled on USB (and TCP) connections +- **Where:** App Settings → BATTERY → Battery Chemistry. `app_settings_screen.dart:610-611` gates the control on `connector.deviceId != null`; `meshcore_connector.dart` only ever assigns `_deviceId` in the **BLE** connect path (`_deviceId = device.remoteId.toString()`, line 1839, right after `_activeTransport = bluetooth`). +- **What:** When connected over USB (verified) — and by the same logic TCP — `connector.deviceId` is `null`, so the Battery Chemistry dropdown shows the subtitle "Connect to a device to choose" and is disabled, even though the device is fully connected (header shows `088EDAA0 · Connected`, radio stats live). +- **Why it matters:** USB/TCP users can never set per-device battery chemistry, so the battery percentage indicator uses the wrong voltage curve for their pack. Also user-confusing ("connect first" while connected). +- **Note:** The `088EDAA0` shown throughout the UI is a node/public-key-derived identifier, distinct from `_deviceId` (the BLE remoteId). The battery setting keys off the BLE-only `_deviceId`. +- **Fix direction:** populate a stable per-device identifier in the USB/TCP connect paths (e.g. the node public-key prefix already used for storage scoping), or key battery chemistry off that same node identity rather than the BLE remoteId. + +### BUG-7 · Low/Medium · Node Name can be saved empty (no validation) +- **Where:** Settings → Node Name dialog, `settings_screen.dart:708-744 _editNodeName()`. +- **What:** Clearing the field leaves the **Save** button enabled; the handler (line 728-740) calls `connector.setNodeName(controller.text)` directly with no `trim()`/empty check. An empty or whitespace-only name can be written to the device, leaving the node nameless on the mesh (others see a blank contact). Reproduced: cleared field → "0/31", Save still active. (Did not actually save.) +- **Compare:** the Radio Settings dialog correctly disables Save on invalid input — Node Name should do the same. +- **Expected:** disable Save (or show an error) when the trimmed name is empty. + +### OBS-2 · Watch · Auto-reconnect to cached Web Serial port fails after a physical drop (stale handle) +- **What:** After physical disconnects, the app auto-retries the cached port (`web:port:1`) and fails with `NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port` (seen at 9:26:44, 9:34:23, 9:34:53, ~30s apart). It does eventually recover (header shows Connected again), but the cached `SerialPort` handle is often stale right after an OS-level drop. +- **Note:** the native USB service explicitly documents this class of problem and avoids caching the serial handle (`_freshSerial()` comment, `usb_serial_service_native.dart:47-50`). The web service caches the port object (`_authorizedPortsByKey`), so reopen-after-drop is more fragile. Partly environmental here (user was physically re-plugging), so logged as an observation, not a confirmed bug. Worth confirming the web reconnect path discards/re-requests the port on `open()` failure rather than retry-looping on a dead handle. + +--- + +## Notes / non-bugs +- **Radio Settings validation works:** out-of-range frequency (`9999`) shows "Invalid frequency (300-2500 MHz)" and disables Save. Good. +- **Graceful disconnect verified:** when the USB transport drops, the app auto-navigates back to the scanner and shows "Not connected" — matches the intended "handle disconnection gracefully" behavior. +- **Channel sync starts before handshake completes:** `ChannelSync Starting sync for 40 channels` fires during connect before `SELF_INFO` is confirmed (console 9:07:54). Likely harmless given BUG-2 masks it, but worth confirming the initial-sync pipeline shouldn't wait for SELF_INFO first. +- First load on `localhost:39107` rendered a blank white page and the URL later resolved to `localhost:42751`. This appears to be a dev-server startup/port artifact (the Flutter view never mounted on the first port), not an app bug. Flagging only in case the port hop is intentional behavior worth confirming. + +--- + +## Fixes applied (2026-06-12) + +- **BUG-3 (device-lost root cause):** `usb_serial_service_web.dart _openPort()` now only asserts `setSignals(DTR=true)` for known USB-UART bridge VIDs (`_uartBridgeVendorIds`: CP210x `0x10C4`, CH340 `0x1A86`, FTDI `0x0403`, PL2303 `0x067B`). Native-USB-CDC boards (nRF52840/Adafruit `0x239A`, etc.) are left untouched, since toggling DTR re-enumerates them. Added a debug log line reporting the detected vendorId and whether DTR was asserted. (The native service already documented this exact NRF52/DTR behavior — confirms the hypothesis.) +- **BUG-2 (dropped read-pump error / misleading timeout):** + - `usb_serial_service_web.dart _pumpReads()` now flips `_status = disconnected` and latches `_lastError` when the read loop dies unexpectedly, instead of leaving the service reporting "connected". + - Added `lastError` getter to both web + native `UsbSerialService` and to `MeshCoreUsbManager`. + - `meshcore_connector.dart connectUsb()` now checks `_usbManager.isConnected` right after the 200ms settle delay (before waiting on SELF_INFO) and throws a clear `USB device disconnected during connect: ` using the latched error — failing in ~200ms with the real reason instead of ~7s with a generic SELF_INFO timeout. +- **BUG-1 (raw exception on picker cancel):** `usb_screen.dart` added `_isUserCancelledPortPicker()`; `_showError()` now returns silently for picker cancellation (matches the web `requestPort`/"No port selected" DOMException and the native StateError) instead of showing a red toast with raw text. +- **BUG-4 (silent BLE scan on web):** `scanner_screen.dart _toggleScan()` now short-circuits on web and shows `scanner_bluetoothWebUnsupported` ("Bluetooth isn't available in the browser. Connect over USB instead.") instead of silently no-opping. New l10n key added to `app_en.arb` and regenerated for all locales (English fallback; pending auto-translation). + +## Fixes applied — round 2 (2026-06-12) + +- **BUG-5 (notifications never fire / error spam):** `notification_service.dart` added `_ensureCanNotify()` — caches whether the platform can actually post notifications and is now the gate on all four `show()` entry points (message/advert/channel/batch-summary). Returns false on web (the plugin has no web backend, so `show()` always threw) and honors `areNotificationsEnabled()` on Android 13+. `requestPermissions()` now refreshes the cache so enabling from settings takes effect immediately. The `cancel()` paths still use `_ensureInitialized()` (unchanged). Net: no more per-message error spam; notifications post when actually permitted. +- **BUG-6 (battery chemistry disabled on USB/TCP):** added `MeshCoreConnector.batteryDeviceKey` — returns the BLE remoteId when present (preserves existing BLE-keyed settings) and falls back to the node public key (`selfPublicKeyHex`) on USB/TCP. Both the internal `_batteryChemistryForDevice()` and the App Settings UI (`app_settings_screen.dart`) now key off `batteryDeviceKey` instead of the BLE-only `connector.deviceId`. Battery chemistry is now selectable on USB/TCP, and the battery-% curve is correct for those connections. +- **BUG-7 (empty node name savable):** `settings_screen.dart _editNodeName()` Save button is now wrapped in a `ListenableBuilder` on the text controller and is disabled (`onPressed: null`) when the trimmed name is empty; the handler saves the trimmed value. Mirrors the Radio Settings dialog's validation behavior. + +All four files analyze clean. Pending live re-verification after hot restart (BUG-5: no notification errors in console; BUG-6: Battery Chemistry enabled while on USB). + +--- + +## Suggestions / further work (2026-06-12) + +### Follow-ons directly implied by the fixes +1. **Audit other per-device features for the same BLE-only gap (BUG-6 was probably not alone).** `_deviceId` is set only in the BLE connect path; anything keyed off `connector.deviceId` or `_device?.remoteId` is silently BLE-only on USB/TCP. Grep those usages and verify each works on USB/TCP, or migrate them to `batteryDeviceKey` / `selfPublicKeyHex`. +2. **Battery-chemistry key inconsistency (tradeoff in my BUG-6 fix).** I kept BLE = remoteId and USB/TCP = public key to avoid wiping saved BLE settings. Consequence: the *same physical radio* gets two different chemistry settings depending on transport. Cleaner long-term: key everything off `selfPublicKeyHex` (the canonical per-radio identity used for all other scoped storage) with a one-time migration of existing BLE-keyed values. +3. **Web reconnect should discard a stale port handle (OBS-2).** After a physical drop, auto-reconnect retry-loops on the cached `SerialPort` with `Failed to execute 'open'` before recovering. The native service deliberately avoids caching the handle (`_freshSerial()` comment). On `open()` failure the web service should drop the cached port from `_authorizedPortsByKey` and re-request (or prompt) instead of retrying a dead handle. +4. **DTR allowlist may need expansion (caveat on my BUG-3 fix).** I now assert DTR only for known UART-bridge VIDs (CP210x/CH340/FTDI/PL2303). A future board with an unlisted bridge chip won't get DTR and could reset on open. Consider a per-board config or a user-visible "hold DTR" toggle as a fallback as more hardware is tested. +5. **On web, the notification toggles are ON but inert** (same spirit as BUG-4). Now that `_ensureCanNotify()` correctly skips web, the four NOTIFICATIONS switches in App Settings still read as enabled while doing nothing in-browser. Grey them out / annotate "not available in browser" on web. Also `requestPermissions()` still returns `true` on web (fallback `return true`), which is misleading to its callers. + +### Robustness / architecture +6. **The connect handshake rides on a `broadcast()` stream + an unconditional 200ms delay** — the exact combo that caused BUG-2. My fix (status flip + liveness check) closes the connect race, but any consumer that subscribes late can still miss early frames/errors. Consider readiness signaling instead of the magic delay, and/or buffering the first frames during connect. + +### UX polish observed +7. **Two "Scan" buttons on the scanner empty state** (center button + bottom-right FAB) — redundant. +8. **Verify destructive actions confirm before acting.** I intentionally did NOT trigger "Clear Chat", "Delete All Paths", "Reboot Device", or "Manage Repeater" (not my device). Worth confirming each has a confirmation dialog. +9. **Telemetry screen** had no obvious "request once" affordance — only autorefresh (interval 20 / qty 10 / Enable). A manual one-shot refresh would help. + +### Coverage gaps (NOT tested this session — unverified) +- Repeater management (hub / CLI / settings / status), Line-of-Sight, Path-Trace map, Community QR scanner. +- Discovery & Neighbors screens, Map cache screen, Debug log screens. +- TCP transport (no TCP device available), USB on native (Android/desktop), on-device translation (LLM). +- Contacts search/sort/filter, channel add/reorder, GPX export. + +--- + +## Styling observations (2026-06-12) +Caveat: only viewed **dark mode** in a **wide desktop browser** (~1298px). Light mode and narrow/mobile widths not assessed. + +**The color system is good — credit where due.** `mesh_theme.dart` is a coherent, semantic palette ("high-contrast slate surfaces with sky-blue accents"): slate surface ramp (`0B1220`→`334155`), sky-blue accent (`0EA5E9`), signal-green reserved for SNR only (`22C55E`), amber warn, red alert, magenta node type, a full ink ramp, and a separate light ramp. Not a generic default-Material look. (Note: the CLAUDE.md claim that MeshPalette/MeshTheme is "not currently wired" is **stale** — `main.dart:204-205` wires `MeshTheme.light()/.dark()`.) + +**Issues are layout/responsiveness, not color:** +1. **No max-width constraint on web/desktop (biggest one).** No `ConstrainedBox`/`maxWidth` in `main.dart`; the app is mobile-first and stretches edge-to-edge on a wide window — chat input spans the full ~1300px, list rows are very wide, chat bubbles hug the far edges, settings rows stretch across. Recommend a centered max-width content column (phone-like, ~480–600px) or responsive breakpoints for the web build. +2. **App-bar title alignment is inconsistent.** Main tab screens (Channels/Contacts/Map) use left-aligned titles with a device-id subtitle; detail screens use `centerTitle: true` (23 files) or `AdaptiveAppBarTitle`. Reads slightly inconsistent screen-to-screen — worth one deliberate rule. +3. **Tall-viewport chat spacing.** Bottom-anchored chat leaves a large empty area at the top on a tall window (related to #1 — no height/width framing for big viewports). +4. **Minor:** map node-detail sheet stacked over the older bottom info bar (z-order overlap); the two redundant Scan buttons are also a visual duplication; verify disabled-control contrast is legible (the pre-fix battery dropdown was quite dim). +5. **Unverified:** light theme polish (light ramp exists in the palette but wasn't viewed). + +--- + +# Retry & Path-Selection Analysis (2026-06-12, code review vs firmware) + +Static review of the ACK/retry/path-selection pipeline (`message_retry_service.dart`, `path_history_service.dart`, `timeout_prediction_service.dart`, connector wiring) cross-checked against the MeshCore C++ firmware at `/mnt/Gaming/meshcore/MeshCore`. Verified against actual firmware source, not just docs. + +**What's correct (verified):** +- **ACK-hash algorithm matches the firmware exactly.** Client `computeExpectedAckHash` (`message_retry_service.dart:104-136`) = `SHA256(timestamp[4 LE] ‖ (attempt&3) ‖ text ‖ sender_pubkey[32])`, first 4 bytes as LE uint32. Firmware `BaseChatMesh.cpp:413-419` builds `temp[0..3]=timestamp`, `temp[4]=attempt&3`, `temp[5..]=text` (null *not* hashed — length passed is `5+text_len`), hashed with `self_id.pub_key`. Byte order and endianness round-trip correctly (`RESP_CODE_SENT`/`PUSH_CODE_SEND_CONFIRMED` both LE uint32, `meshcore_connector.dart:5265-5409`). +- **Retry is entirely the client's job** — firmware `sendMessage` sends once, no retry loop. Correct division of responsibility. +- **Per-contact in-flight serialization** (`_sendQueue`, `message_retry_service.dart:174-208`) is the right idea — see RETRY-1 for why it's not *sufficient*. + +## Open findings + +### RETRY-1 · Medium · Per-contact in-flight cap doesn't protect the firmware's *global* 8-entry ACK table +- **Where:** `message_retry_service.dart:174-183` (one in-flight message *per contact*) vs firmware `examples/companion_radio/MyMesh.h:248-250` + `MyMesh.cpp:1100-1103`. +- **What:** The firmware's `expected_ack_table` is a **single global circular buffer of 8 entries** with a global `next_ack_idx`, shared across *all* contacts. The client only guarantees ≤1 in-flight *per contact*, so messaging **9+ contacts concurrently** (e.g. several active conversations whose retries overlap, or a broadcast-style burst) puts >8 entries in flight. The firmware then overwrites the oldest slot (`next_ack_idx = (next_ack_idx+1) % 8`) with no rejection — the evicted message's ACK expectation is silently dropped. +- **Consequence:** the evicted send never produces a `PUSH_CODE_SEND_CONFIRMED`, so the client times out and retries a message the radio already sent (wasted airtime), and can mark as **failed** a message that actually delivered. +- **The code comment is misleading:** `message_retry_service.dart:173-174` says serialization exists "to avoid overflowing the firmware's 8-entry expected_ack_table" — but a per-contact cap does not bound the global table. +- **Fix:** enforce a **global** concurrent-in-flight cap (≤8, ideally ~6 for headroom) across all contacts, not just per-contact; or track `next_ack_idx` pressure and back-pressure new sends. + +### RETRY-2 · Medium · Firmware's reported `est_timeout` is silently discarded +- **Where:** `meshcore_connector.dart calculateTimeout()` (4379-4408); `message_retry_service.dart:405-415`. +- **What:** `RESP_CODE_SENT` carries the device's own `est_timeout` (firmware `MyMesh.cpp:1106-1110`, computed from real `getEstAirtimeFor(...)`). It's parsed into `timeoutMs` and passed to `updateMessageFromSent` as the default `actualTimeout`. But `config.calculateTimeout` is **always** set, so the device value is immediately overwritten on every message — and when the ML model has no data, `calculateTimeout` returns `physicsMax` (line 4407), **never the device-provided value**. +- **Why it's a bug:** the docstring at `message_retry_service.dart:405` ("prefer ML prediction, then device-provided, then physics fallback") describes a tier that doesn't exist — device-provided is never used. The physics fallback re-derives the firmware's flood/direct formulas, but `_estimateAirtimeMs` falls back to a hard-coded **50 ms** when radio params (freq/bw/sf/cr) aren't yet known (`meshcore_connector.dart:4341-...`), which can diverge sharply from the device's actual airtime estimate (e.g. SF12). The device already did this math correctly with the real airtime. +- **Fix:** use the device `timeoutMs` as the fallback when ML is unavailable, and as the clamp ceiling when ML is present — it's strictly better information than a 50 ms guess. + +### RETRY-3 · Medium · Premature ML timeouts can delete good routes (timeout floor too low + aggressive weight decay) +- **Where:** `meshcore_connector.dart _physicsMinTimeout` (≈4360-4375); `message_retry_service.dart:491-538`; `path_history_service.dart:131-141`. +- **What:** For direct paths the timeout floor is `airtime*(hops+1)` — it omits the firmware's base (`SEND_TIMEOUT_BASE_MILLIS=500`) and per-hop processing terms (`6*airtime+250` per hop). So an ML prediction clamps as low as that floor, well under the firmware's own `500 + (6*airtime+250)*hops`. When the timer fires before an ACK could realistically return, `_handleTimeout` records a **false failure** → `recordPathResult(success:false)` decrements `routeWeight` by 0.5. A fresh path starts at weight 1.0, so **two** false timeouts drive it to ≤0 and `removePathRecord` deletes it (`path_history_service.dart:136-140`). Net: an overly tight timeout estimator actively erodes the learned route table — the opposite of what the ML system is for. +- **Fix:** raise the direct-path min floor to include the firmware base + per-hop terms (don't let ML clamp below a physically plausible RTT); and require more evidence (e.g. a higher failure count, or a confirmed `MSG_SEND` with no ACK over multiple attempts) before deleting a route rather than after two timer fires. + +### RETRY-4 · Low · `handleAckReceived` fallback computes a bogus `attemptIndex` +- **Where:** `message_retry_service.dart:613-626` (fallback branch), uses `expectedHashes.indexOf(expectedHash)` as the attempt index. +- **What:** `_expectedAckHashes[messageId]` is de-duplicated on insert (line 401), and because both firmware and client mask `attempt & 0x03`, attempts 0/4/8 (and 1/5/9, …) hash to the **same** value. So the list index is neither the real attempt number nor stable. The derived `matchedAttemptIndex` is only used to pick which attempt's `PathSelection` gets credited for the delivery — so the impact is limited to path-attribution accuracy on the fallback path (the primary `_ackHashToMessageId` mapping carries the correct snapshot). Low severity, but the value is simply wrong. +- **Fix:** store the attempt index alongside each expected hash (or just reuse the snapshot in `_ackHashToMessageId`) instead of inferring it from a list position. + +### RETRY-5 · Low · ML model feeds flood as `pathLength = -1`, polluting the linear coefficient +- **Where:** `timeout_prediction_service.dart:59-98` (observation), `161-200` (training). +- **What:** Flood deliveries are recorded with `pathLength: -1` **and** `isFlood: true`, then both are fed as features into a single global OLS regressor. A linear term that sees hop counts of `-1, 0, 1, 2, 3, …` with a discontinuity at the flood case distorts the `pathLength` coefficient; the `isFlood` dummy only partially compensates (it shifts the intercept, not the slope). Direct-path timeout predictions are therefore biased by flood observations and vice-versa. +- **Fix:** train separate flood vs direct models, or set `pathLength = 0` (or a dedicated flood feature only) when `isFlood`, so the hop-count slope is learned from direct paths alone. + +### RETRY-6 · Low / verify · ACK hash is computed over SMAZ-transformed text — the sent frame must use byte-identical text +- **Where:** `message_retry_service.dart:328-351` — hash uses `prepareContactOutboundText(contact, text)` (SMAZ-encoded), but `config.sendMessage(contact, message.text, …)` is handed the **raw** text; correctness depends on `_sendMessageDirect`/`buildSendTextMsgFrame` applying the *exact same* SMAZ transform downstream. +- **Why it matters:** if those two paths ever diverge, or if a message exceeds the firmware's `MAX_TEXT_LEN = 160` and gets truncated device-side, the receiver hashes different bytes than the client predicted → the ACK never matches → the message retries to `failed` even though it was delivered. This is a silent, hard-to-diagnose failure mode. +- **Fix:** compute the hash from the *same* byte buffer that is actually framed and sent (single source of truth), and add a unit test that asserts hash-over-frame-text == expected ACK hash, including a >160-byte/SMAZ case. + +## Observations (heuristics, not correctness bugs) +- **Flood path attribution credits a path the send didn't use.** On a successful *flood* delivery, `_recordPathResult` (`meshcore_connector.dart:1309-1354`) boosts the weight of `contact.path` (the device's *current* path) even though the message was flooded, not routed over that path. It's a reasonable "the ACK probably came back this way" heuristic, but (a) it inflates weight on an unexercised route, and (b) it reads `contact.path` which the path-return packet triggered by this very ACK may have just mutated (hence the `unawaited(getContactByKey(...))` re-fetch — a race the code papers over). Worth documenting as a heuristic and considering attributing only when the device path is confirmed fresh. +- **`calculateDefaultTimeout` (`message_retry_service.dart:713-719`)** appears to be legacy/unused by the active retry path (which always goes through `config.calculateTimeout`). If dead, remove it to avoid confusion about which timeout logic is authoritative. + +## How the system could be improved +1. **Single global in-flight budget tied to the firmware's table size.** Replace the per-contact gate with a global semaphore of N (≤8) outstanding ACKs, with a small reserve. This directly models the firmware constraint and eliminates RETRY-1. +2. **Make the device timeout authoritative, ML advisory.** Use the firmware's `est_timeout` as the baseline (it has the real airtime), and let the ML model only *widen* it when history shows this contact/path is consistently slower. Never let ML clamp below a physically plausible RTT (fixes RETRY-2 + RETRY-3). +3. **Decouple "timed out" from "route failed."** A timeout is weak evidence (could be transient congestion). Track per-path *consecutive* failures and only decay/delete a route after repeated, independent failures — or after the firmware reports a genuine send failure. Add hysteresis so one slow ACK doesn't erase a known-good route. +4. **Separate latency models per route class** (flood vs direct, and ideally per hop-count bucket), or add the airtime estimate itself as a feature so the model learns a multiplier rather than re-deriving physics. Fixes RETRY-5 and makes predictions interpretable. +5. **One source of truth for outbound bytes.** Frame the message once, compute the ACK hash from those exact bytes, and reject/queue anything that would exceed `MAX_TEXT_LEN` *before* sending (rather than discovering it via never-arriving ACKs). Fixes RETRY-6. +6. **Confidence-based path selection.** `_scorePathRecord` weights reliability 0.45 / latency 0.25 / freshness 0.1 / routeWeight 0.2 with hard-coded constants. Consider surfacing these as tunables (some already are via `appSettings`) and using a Wilson lower-bound on the success ratio instead of the current `(s+1)/(n+2)` Laplace estimate, so a 1/1 path isn't ranked above a 20/22 path purely on the smoothed mean. +7. **Bound the late-ACK grace + 15-min mapping cleanup explicitly.** `handleAckReceived` cleans `_ackHashToMessageId` older than 15 min on every ACK (`message_retry_service.dart:589-599`); combined with the 30 s post-failure grace timer and `attempt&3` hash reuse, it's worth a test that a delivered-after-failed message resolves exactly once and never double-advances the send queue. + + +### Possible ML issues + + 1. High: training/prediction mismatch for secondsSinceLastRx. + _lastRxTime is reset when the ACK frame arrives, then the observation is recorded, making this feature effectively zero. Prediction measures it before + sending. + lib/connector/meshcore_connector.dart:3867 + + 3. Medium: incorrect message-size input. + Retry prediction and training use message.text.length, not UTF-8/transformed payload bytes. Unicode and compressed messages therefore receive incorrect + airtime inputs. + lib/services/message_retry_service.dart:405 + lib/services/message_retry_service.dart:669 + + 4. Medium: stale per-contact statistics. + Old observations are removed from the 100-item model window, but never removed from _contactStats. The global model adapts while contact averages remain + lifetime averages. + lib/services/timeout_prediction_service.dart:76 + + 5. Medium: model is underconstrained. + Ordinary linear regression starts with only 10 samples and up to four features, without outlier rejection or validation. Physics clamping limits damage, + but unstable predictions are still likely. + + 6. Low: pending observations may not persist. + dispose() cancels the delayed save without flushing it. + lib/services/timeout_prediction_service.dart:216 +--- + +# ACK & Message-Delivery Analysis (2026-06-12, code review vs firmware) + +Focused pass on ACK matching and message delivery (sending + receiving), cross-checked against the MeshCore firmware at `/mnt/Gaming/meshcore/MeshCore`. Companion of the earlier "Retry & Path-Selection" section — this one is about whether messages reliably reach `delivered`/`failed` and aren't lost or double-shown. + +**Verified correct (recording these to close earlier open questions):** +- **ACK-hash text consistency holds.** `prepareContactOutboundText` (SMAZ / Cyr2Lat) transforms the text, and the *same* transformed bytes are used both for the wire frame (`buildSendTextMsgFrame`, `meshcore_connector.dart:~1113`) and for the expected-ack hash (`message_retry_service.dart:328-338`). The firmware hashes the raw received text bytes (SMAZ is opaque to it), so both sides agree. This resolves the RETRY-6 "verify" concern (the >160-byte truncation guard is still worth adding). +- **Connect-time queue drain exists.** `_startPostChannelInitialQueuedMessageSync` (`meshcore_connector.dart:3835`) plus the `respCodeEndOfContacts` handler (`:3936-3941`) proactively call `syncQueuedMessages(force:true)` after the SELF_INFO→channels→contacts pipeline, so messages that arrived while disconnected are pulled on reconnect — not solely dependent on a live `PUSH_CODE_MSG_WAITING` tickle. +- **Firmware de-dups inbound packets.** `SimpleMeshTables::hasSeen` keeps a 160-entry packet-hash seen-table (`src/helpers/SimpleMeshTables.h:34-53`), so the same on-air packet arriving via multiple flood paths is dropped, not delivered twice. +- **Channel sends do reach `sent`.** `_handleOk` promotes the pending channel message on the firmware's generic OK (`meshcore_connector.dart:5370`), and the echo-heard path promotes it again (`:5990`). (So the "stuck pending on success" worry is unfounded — but see DELIVERY-1 for the error path.) + +## Open findings + +### DELIVERY-1 · Medium · A rejected (or lost-response) channel send is stuck "pending" forever +- **Where:** `_handleErrorFrame` (`meshcore_connector.dart:4007-4024`); `ChannelMessageStatus` has only `{pending, sent, failed}` and nothing ever sets `failed` for a channel message. +- **What:** When the firmware answers a `cmdSendChannelTxtMsg` with an error frame (e.g. `ERR_CODE_NOT_FOUND` for an unknown/unconfigured channel — firmware `MyMesh.cpp:1131-1135` writes an err frame on failure), the handler removes the message from `_pendingChannelSentQueue` (line 4023) **but never marks the message `failed`**. The same happens if the OK/ERR response is simply lost. Result: the bubble shows "pending/sending" indefinitely with no error, and there is no retry or timeout to resolve it. +- **Contrast:** direct messages have a full timeout→retry→`failed` path; channel messages have *no* failure path at all. +- **Fix:** in `_handleErrorFrame`, set the matching channel message to `failed` (mirror `_markPendingChannelMessageSentById`). Optionally add a short watchdog so a channel send with no OK/ERR within N seconds flips `pending`→`failed`. + +### DELIVERY-2 · Medium · Firmware offline queue (16) silently drops *contact* messages on overflow +- **Where:** firmware `examples/companion_radio/MyMesh.cpp:219-255` (`addToOfflineQueue`), `MyMesh.h:240` (`OFFLINE_QUEUE_SIZE 16`). +- **What:** Inbound messages are buffered in a 16-slot queue and tickled to the app via `PUSH_CODE_MSG_WAITING`. On overflow the firmware evicts the oldest **channel** message to make room; if all 16 queued entries are **contact/direct** messages, the new message is **silently dropped** — no error, no notification to the app. +- **Consequence:** a burst of direct messages while the app is backgrounded/slow/disconnected can lose messages with zero indication. The client drains in a loop on tickle and on connect (good), but it cannot recover a message the firmware already discarded. +- **Fix (client side):** drain as fast as possible — the current one-at-a-time request/response loop (`_requestNextQueuedMessage`) round-trips per message; consider keeping the pump saturated. There's no app-side way to detect the drop, so also worth raising upstream (a queue-overflow counter in firmware stats would let the app warn the user). + +### DELIVERY-3 · Low/Medium · Inbound contact-message de-dup can drop a *legitimate* message +- **Where:** `_handleIncomingMessage` de-dup (`meshcore_connector.dart:~4759-4772`): skips an incoming message if a message with the same `(timestampSeconds, text)` exists in the last 10 messages from that sender. +- **What:** Timestamps are second-resolution, so two genuinely distinct messages with identical text within the same second (e.g. sending "ok" twice) collide and the second is silently dropped. The firmware already de-dups true on-air duplicates at the packet-hash layer (see "verified correct"), so this app-layer heuristic is largely redundant *and* introduces a real (if uncommon) data-loss path; it also only looks back 10 messages, so a re-delivered queued message after a longer gap can slip through as a "new" duplicate. +- **Fix:** lean on the firmware's packet identity instead of `(timestamp,text)`, or narrow the heuristic (e.g. only treat as duplicate within a very short window AND when flagged as a flood repeat), so identical-text messages aren't lost. + +### DELIVERY-4 · Low · ACK match is 4-byte-hash-only, no sender identity, against a *global* 8-slot table +- **Where:** firmware `processAck` (`MyMesh.cpp:411-427`) — `memcmp(data, &expected_ack_table[i].ack, 4)` over 8 slots, clears the slot to 0 on first match. +- **What:** matching uses only the first 4 hash bytes with no check that the ACK came from the intended recipient, against the same global 8-entry circular table called out in RETRY-1. So (a) >8 concurrent in-flight sends evict a slot → that message never gets `PUSH_CODE_SEND_CONFIRMED` → client retries/false-fails; (b) a ~1/2^32-per-concurrent-send chance of a cross-message false confirm. Duplicate ACKs are handled safely (slot cleared after first match; the random 6th ACK byte doesn't affect the 4-byte compare). Low severity on its own; the real lever is RETRY-1's recommended **global** in-flight cap (≤8). + +### DELIVERY-5 · Info · Round-trip time is measured from queueing, not transmission +- **Where:** firmware `trip_time = getMillis() - expected_ack_table[i].msg_sent` (`MyMesh.cpp:417`), where `msg_sent` is stamped when the message is enqueued (`:1100`). +- **What:** the `tripTimeMs` the app receives (and feeds the ML timeout model) includes the firmware's TX-queue wait when the radio is busy, so observations are inflated under load. Compounds the ML-input issues already logged (RETRY-5 / the ML notes above). Not a delivery bug, but it skews timeout prediction. + +### DELIVERY-6 · Low / verify · Command/CLI sends use `expected_ack = 0` — must not ride the ACK retry path +- **Where:** firmware sets `expected_ack = 0` for `TXT_TYPE_CLI_DATA` and the recipient sends no ACK (`MyMesh.cpp:1088-1110`, `BaseChatMesh.cpp:244-252`); `RESP_CODE_SENT` then carries a zero hash. +- **What:** if any client path routes a CLI/repeater command through `MessageRetryService` (which computes a *non-zero* expected hash and waits for `PUSH_CODE_SEND_CONFIRMED`), it would never match → spurious retries and a false "failed". Need to confirm the repeater-CLI/command path is separate from the text-message ACK machinery (the connector does call `_recordPathResult` for "repeater command results" — worth tracing that it isn't also arming an ACK wait). +- **Fix/verify:** ensure command sends bypass the expected-ack/retry tracking entirely, or special-case `expected_ack == 0` in `updateMessageFromSent` as "no ACK expected → mark sent, don't wait". + +--- + +# Live Log Analysis — "JC Room Server" session (2026-06-12) + +Real device log (BLE/USB, auto route rotation ON) sending to a multi-hop room server. This is a near-perfect live reproduction of RETRY-2, RETRY-3, and RETRY-5. The ACK *matching* is correct (every `RESP_CODE_SENT` matches its message); what's broken is **timeout estimation and path rotation** — the app declares failures and retries messages that are actually still in flight. + +### Evidence by symptom + +1. **Flood timeout = 151 seconds (RETRY-5, live).** + `raw prediction=100638ms for pathLength=-1 → ML timeout 150959ms`. Feeding `pathLength=-1` into the linear model makes the flood case extrapolate off a cliff; the `.clamp(physicsMin, physicsMax)` didn't help because the flood `physicsMax` (`500 + 16·airtime`) was also huge. A message that falls back to flood effectively hangs for ~2.5 minutes. + - Also seen: `raw prediction=-3467ms for pathLength=1, messageBytes=172` — a **negative** predicted time. Discarded by the `<=0` guard, but proves the OLS model is unstable/underconstrained (matches the ML notes). + +2. **Premature timeouts on a working route (RETRY-3, live).** + `"Its going well"` was **delivered in 16037ms**, but attempt 0/1 used timeouts of **14420ms / 10240ms** — shorter than the real round-trip — so they fired `Timeout → retrying` before the ACK could return. The `g:` message `delivered ... in 9418ms` arrived *after* a retry had already been scheduled. Messages that are genuinely in flight are being declared failed. + +3. **Device `est_timeout` ignored (RETRY-2, live).** + Every send gets a clean `RESP_CODE_SENT` (which carries the firmware's own `est_timeout`), but the app overrides it with the runaway ML value. The device's estimate would have been far saner than both the 10s (too short) and 151s (too long) ML outputs. + +4. **Path rotation never converges.** + Single message, per-retry path: `pathLength 3 → 2 → 1 → 2 → -1(flood)`; timeout target swings `25s → 19s → 13s → 19s → 151s`. Routes that just delivered (`g:` over 2 hops) are abandoned on the next message. On a 2-5 hop room server on a congested channel, the thrash never settles. + +5. **Retries congest the channel.** + `Radio quiet for 3037ms`, `Post-inbound backoff: waiting 14808ms` — channel is busy. Backoffs (`1/2/4/8s`) are far shorter than the 19-34s timeouts, so retries stack onto a multi-hop path and reduce delivery odds. + +6. **`attempt & 3` hash reuse visible.** `"I have some really c..."` attempt 0 and attempt 4 both expect `b8bfa902`; `"JC you around?"` reuses `b2074391`. Attempts 0/4 are indistinguishable on the wire (RETRY-4 / masking). + +7. **Room login runs a separate, untracked ACK path (relates to DELIVERY-6).** `No pending message found for ACK hash: b0023dab` ×5 during `[RoomLogin]` — login sends with an expected ACK that `MessageRetryService` never registered, so every login confirm logs as unmatched. Login has its own 26s timeout/retry and eventually succeeds. + +### Highest-impact fix (from this log) +Fix the timeout side: **stop feeding flood as `pathLength=-1`, hard-cap the ML output (≈30-45s), and fall back to the device's `est_timeout` instead of the runaway ML value.** That alone kills the 151s hang and the premature retries. Contained to `timeout_prediction_service.dart` + `calculateTimeout` in `meshcore_connector.dart`. + +--- + +# Design Proposal — Replace the timeout regressor; split timeout from loss (2026-06-12) + +Outcome of analyzing the JC Room Server log: the OLS `LinearRegressor` in `timeout_prediction_service.dart` is the wrong tool, and it's conflating two separate questions. This sketches the replacement before any code changes. + +## The core insight (why this isn't TCP, and what that implies) + +This is slow, **lossy** multi-hop radio: a message can be lost on a forward hop (never reaches the recipient) or delivered-but-the-ACK-return-is-lost. Two consequences drive the whole design: + +1. **On a lossy link, a longer timeout recovers nothing — only a retry does.** The timeout is your *time-to-retry*. Loss is the common outcome to a distant (5-hop) node, so inflating the timeout (e.g. the observed 151 s flood value) just maximizes idle time before the one action that helps. The right timeout is **the tightest value that still covers a realistically *successful* round-trip** (~P90–95 of success RTT), then give up and retry. +2. **The latency model is already blind to loss.** `recordObservation` is only called from `handleAckReceived` — i.e. only on success. Lost messages produce no sample. So the regressor estimates "time of a *successful* round-trip" (same data an EWMA would use), just with extrapolation/negative/explosion failure modes on top. Modeling loss for real would require censored-data/survival analysis, which is far heavier than needed. + +**Conclusion:** stop using one fragile latency regressor to answer both "how long to wait" and "is this route any good." Split them. + +## Part A — Timeout: physics-anchored, capped, adaptive RTT (replaces the OLS model) + +Drop `ml_algo` / `ml_dataframe`. Keep the existing `predictTimeout` / `recordObservation` interface so `calculateTimeout` and the connector wiring are untouched; swap the internals. + +Per **route class** = (hop count, flood-vs-direct); shared across contacts since data is sparse. + +``` +// baseline from physics (deterministic, generalizes to unseen hop/byte combos +// from sample #1). Use the LoRa airtime formula already in the connector, or the +// device's est_timeout from RESP_CODE_SENT (RETRY-2). +base = physicsEstimate(hops, bytes, sf, bw, cr) + +// learn only a BOUNDED multiplicative overhead from real successes (congestion, +// per-hop processing). EWMA, clamped so it can never explode or go negative. +on success(sample, hops, bytes): + ratio = sample / physicsEstimate(hops, bytes, ...) + f[class] = (1-α)·f[class] + α·clamp(ratio, 0.5, 4.0) // α≈0.125 + v[class] = (1-β)·v[class] + β·|f[class] - ratio| // β≈0.25 (deviation) + +predictTimeout(hops, bytes): + est = base · f[class] + return clamp(est + 4·v[class]·base, physicsMin, HARD_MAX) // HARD_MAX ≈ 30–45 s +``` + +Properties this fixes, by construction: +- **No 151 s / no negatives** — output is `physics × bounded factor + bounded variance`, hard-capped. +- **No flood discontinuity** — flood is its own class with its own factor, never `pathLength = -1` on a shared axis (kills RETRY-5). +- **No cold start** — seed `f=1`, `v=0`; the physics/device baseline is sensible from message #1; observations only refine the factor (removes the "need 10 samples" gate). +- **Variance is the timeout's whole point** — `est + k·deviation` is a calibrated high-percentile wait, so genuinely-in-flight messages aren't cut off early (kills RETRY-3), while staying tight enough that lost messages retry promptly. +- **Drops two dependencies**, ~15 lines, debuggable. + +Also remove the dead/broken `secSinceRx` feature (recorded ≈0; see ML notes) and feed transformed/UTF-8 payload bytes, not `text.length`. + +## Part B — Loss: route reliability drives retry / reroute / flood (data already exists) + +The "is this path even working" question belongs to `PathHistoryService`, which already tracks `successCount` / `failureCount` / `routeWeight` per path. Treat that as a Bernoulli delivery-probability estimate (Wilson or Beta lower bound on success rate) and use it for the *decision*, not the timeout: + +- **High-confidence working route** → keep using it; on a single timeout, retry the *same* path once (the ACK return may just have been lost) before changing anything. +- **Degrading route** (success rate dropping) → switch to the next-best ranked path rather than hammering a dying one. +- **No confident route / repeated failures** → flood to rediscover, then pin the rediscovered path (don't immediately rotate off it — the log shows good routes being abandoned the next message). + +This replaces the current "rotate the path on every retry" churn (which made the timeout target swing 25 s→19 s→13 s→19 s→151 s and never converged) with: **pin a working route; only re-route/flood when reliability says the route is actually bad.** + +## Sequencing +1. Part A first (self-contained, biggest visible win — kills the 151 s hang and premature retries; removes ml_algo). Same public interface, so low blast radius. +2. Then Part B (path-decision policy), which also subsumes much of the separate "location-change brittleness" work — a route that stops delivering after you move is just a route whose reliability dropped. diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 579d8257..1bff354a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -328,6 +328,20 @@ class MeshCoreConnector extends ChangeNotifier { String? get deviceId => _deviceId; String get deviceIdLabel => _deviceId ?? 'Unknown'; + /// Stable per-radio key for transport-agnostic per-device settings such as + /// battery chemistry. On BLE this is the existing remoteId (so previously + /// saved settings are preserved); on USB/TCP — where there is no BLE + /// remoteId — it falls back to the node's public key, which identifies the + /// same physical radio across transports. Null until a device identity is + /// known. + String? get batteryDeviceKey { + if (_deviceId != null) return _deviceId; + if (_selfPublicKey != null && _selfPublicKey!.isNotEmpty) { + return selfPublicKeyHex; + } + return null; + } + MeshCoreTransportType get activeTransport => _activeTransport; String? get activeUsbPort => _usbManager.activePortKey; String? get activeUsbPortDisplayLabel => _usbManager.activePortDisplayLabel; @@ -493,7 +507,7 @@ class MeshCoreConnector extends ChangeNotifier { } String _batteryChemistryForDevice() { - final deviceId = _device?.remoteId.toString(); + final deviceId = batteryDeviceKey; if (deviceId == null || _appSettingsService == null) return 'nmc'; return _appSettingsService!.batteryChemistryForDevice(deviceId); } @@ -1607,6 +1621,20 @@ class MeshCoreConnector extends ChangeNotifier { await stopScan(); } await Future.delayed(const Duration(milliseconds: 200)); + + // The read pump can fail the instant the port opens (e.g. a device that + // re-enumerates on open). That error is emitted on a broadcast stream + // before the listener below attaches, so it would otherwise be lost and + // the connect would stall until the SELF_INFO timeout. Check transport + // liveness directly and abort fast with the real cause. + if (!_usbManager.isConnected) { + final cause = _usbManager.lastError; + throw StateError( + 'USB device disconnected during connect' + '${cause == null ? '' : ': $cause'}', + ); + } + _usbFrameSubscription = _usbManager.frameStream.listen( _handleFrame, onError: (error, stackTrace) { @@ -5737,7 +5765,9 @@ class MeshCoreConnector extends ChangeNotifier { ) { if (!isRoomServer) return null; if (!msg.isOutgoing) { - final senderContact = _contacts.cast().firstWhere( + // Saved contacts first, then discovery-only nodes, so reaction matching + // resolves the author's name even when they haven't been saved. + final senderContact = allContactsUnfiltered.cast().firstWhere( (c) => c != null && _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index 56718bc2..7e44798a 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -19,6 +19,7 @@ class MeshCoreUsbManager { String? get activePortKey => _activePortKey; String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey; bool get isConnected => _service.isConnected; + Object? get lastError => _service.lastError; Stream get frameStream => _service.frameStream; // --- Configuration --- diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 37308ba5..705caecb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -143,6 +143,7 @@ "scanner_chromeRequired": "Chrome Browser Required", "scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.", "scanner_enableBluetooth": "Enable Bluetooth", + "scanner_bluetoothWebUnsupported": "Bluetooth isn't available in the browser. Connect over USB instead.", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", "settings_title": "Settings", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8b513be5..cd19b2a7 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -694,6 +694,12 @@ abstract class AppLocalizations { /// **'Enable Bluetooth'** String get scanner_enableBluetooth; + /// No description provided for @scanner_bluetoothWebUnsupported. + /// + /// In en, this message translates to: + /// **'Bluetooth isn\'t available in the browser. Connect over USB instead.'** + String get scanner_bluetoothWebUnsupported; + /// No description provided for @device_quickSwitch. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 48d0ea2a..69e165ec 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -318,6 +318,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_enableBluetooth => 'Активирайте Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Бързо превключване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 10cc0baf..0b6e5833 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -321,6 +321,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_enableBluetooth => 'Bluetooth aktivieren'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Schnelles Umschalten'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f851b914..39cf3727 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -315,6 +315,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanner_enableBluetooth => 'Enable Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Quick switch'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c6e8ad2f..ef47ff3f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -319,6 +319,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_enableBluetooth => 'Habilitar Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Cambiar rápidamente'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3fd7839d..46f8cf2f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -321,6 +321,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_enableBluetooth => 'Activer le Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Basculement rapide'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 0a09c402..01c06a91 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -318,6 +318,10 @@ class AppLocalizationsHu extends AppLocalizations { @override String get scanner_enableBluetooth => 'Engedje be a Bluetooth funkciót'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Gyors váltás'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b748de9e..de44f72f 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -321,6 +321,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanner_enableBluetooth => 'Abilita il Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Passa velocemente'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 150fef5f..0e6f5922 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -308,6 +308,10 @@ class AppLocalizationsJa extends AppLocalizations { @override String get scanner_enableBluetooth => 'Bluetoothを有効にする'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => '素早い切り替え'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 783990cc..302ff925 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -307,6 +307,10 @@ class AppLocalizationsKo extends AppLocalizations { @override String get scanner_enableBluetooth => '블루투스 활성화'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => '빠른 전환'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 087a0204..9a8f1615 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -317,6 +317,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scanner_enableBluetooth => 'Activeer Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Snelle overschakeling'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index ae102624..2ec5378b 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -322,6 +322,10 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scanner_enableBluetooth => 'Włącz Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Szybka zmiana'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 55dccf17..3eae0e3c 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -320,6 +320,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanner_enableBluetooth => 'Ative o Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Mudar rapidamente'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index a66e99d4..b61320c9 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -320,6 +320,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanner_enableBluetooth => 'Включите Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Быстрое переключение'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 349c4c6e..71286d55 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -319,6 +319,10 @@ class AppLocalizationsSk extends AppLocalizations { @override String get scanner_enableBluetooth => 'Povolte Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Rýchle prepínač'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f79b0d2e..8f09c5ce 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -318,6 +318,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_enableBluetooth => 'Omogočite Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Hitro preklop'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8a354354..4a281eef 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -316,6 +316,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get scanner_enableBluetooth => 'Aktivera Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Snabb växling'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 8379a505..6471e563 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -319,6 +319,10 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => 'Швидке перемикання'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d2643bd9..9add4a3e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -305,6 +305,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanner_enableBluetooth => '启用蓝牙'; + @override + String get scanner_bluetoothWebUnsupported => + 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + @override String get device_quickSwitch => '快速切换'; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 6995b6ed..3fd0d05f 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -141,7 +141,7 @@ class AppSettings { this.mapKeyPrefix = '', this.mapShowMarkers = true, this.mapShowGuessedLocations = true, - this.enableMessageTracing = false, + this.enableMessageTracing = true, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, @@ -149,7 +149,7 @@ class AppSettings { this.notifyOnNewMessage = true, this.notifyOnNewChannelMessage = true, this.notifyOnNewAdvert = true, - this.autoRouteRotationEnabled = false, + this.autoRouteRotationEnabled = true, this.maxRouteWeight = 5.0, this.initialRouteWeight = 3.0, this.routeWeightSuccessIncrement = 0.5, @@ -264,7 +264,7 @@ class AppSettings { mapShowMarkers: json['map_show_markers'] as bool? ?? true, mapShowGuessedLocations: json['map_show_guessed_locations'] as bool? ?? true, - enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, + enableMessageTracing: json['enable_message_tracing'] as bool? ?? true, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), @@ -276,7 +276,7 @@ class AppSettings { json['notify_on_new_channel_message'] as bool? ?? true, notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, autoRouteRotationEnabled: - json['auto_route_rotation_enabled'] as bool? ?? false, + json['auto_route_rotation_enabled'] as bool? ?? true, maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0, initialRouteWeight: (json['initial_route_weight'] as num?)?.toDouble() ?? 3.0, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 3d3af8b9..5c8258ad 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -607,7 +607,7 @@ class AppSettingsScreen extends StatelessWidget { AppSettingsService settingsService, MeshCoreConnector connector, ) { - final deviceId = connector.deviceId; + final deviceId = connector.batteryDeviceKey; final isConnected = connector.isConnected && deviceId != null; final selection = isConnected ? settingsService.batteryChemistryForDevice(deviceId) diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 3b0a2441..5bdd0fdc 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -381,12 +381,14 @@ class _ChatScreenState extends State { } final messageIndex = index; Contact contact = _resolveContact(connector); + final bool isRoom = contact.type == advTypeRoom; final message = reversedMessages[messageIndex]; String fourByteHex = ''; - if (contact.type == advTypeRoom) { + Contact? roomAuthor; + if (isRoom) { // Room-server messages carry the original author's 4-byte prefix // separately from message.text; use it only for resolving the name. - contact = _resolveContactFrom4Bytes( + roomAuthor = _resolveContactFrom4Bytes( connector, message.fourByteRoomContactKey.isEmpty ? Uint8List.fromList([0, 0, 0, 0]) @@ -396,6 +398,9 @@ class _ChatScreenState extends State { .map((b) => b.toRadixString(16).padLeft(2, '0')) .join() .toUpperCase(); + // Only adopt the author identity when we actually know them; never + // fall back to the room server's own name as the sender. + if (roomAuthor != null) contact = roomAuthor; } return Builder( @@ -403,11 +408,12 @@ class _ChatScreenState extends State { final textScale = context.select( (service) => service.scale, ); - final resolvedContact = _resolveContact(connector); final bubble = _MessageBubble( message: message, - senderName: resolvedContact.type == advTypeRoom - ? "${contact.name} [$fourByteHex]" + senderName: isRoom + ? (roomAuthor != null + ? "${roomAuthor.name} [$fourByteHex]" + : "[$fourByteHex]") : contact.name, sourceId: widget.contact.publicKeyHex, textScale: textScale, @@ -755,13 +761,17 @@ class _ChatScreenState extends State { return connector.contacts[_resolveContactIndex]; } - Contact _resolveContactFrom4Bytes( + Contact? _resolveContactFrom4Bytes( MeshCoreConnector connector, Uint8List key4Bytes, ) { - return connector.contacts.firstWhere( - (c) => listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)), - orElse: () => widget.contact, + // Match against saved contacts first, then nodes only seen via discovery — + // a room poster you haven't saved may still be in the discovered list. + return connector.allContactsUnfiltered.cast().firstWhere( + (c) => + c != null && + listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)), + orElse: () => null, ); } @@ -1049,7 +1059,11 @@ class _ChatScreenState extends State { if (message.isOutgoing) { senderName = connector.selfName ?? context.l10n.chat_me; } else if (_resolveContact(connector).type == advTypeRoom) { - senderName = "${contact.name} [$fourByteHex]"; + // An unresolved author leaves `contact` as the room server itself; show + // only the prefix rather than mislabeling the post with the room's name. + senderName = contact.type == advTypeRoom + ? "[$fourByteHex]" + : "${contact.name} [$fourByteHex]"; } else { senderName = _resolveContact(connector).name; } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f14a1820..3f57dfac 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -94,6 +93,11 @@ class _MapScreenState extends State { String _searchQuery = ''; List<_GuessedLocation> _cachedGuessedLocations = []; String _guessedLocationsCacheKey = ''; + int? _sharedMarkersCacheSignature; + Locale? _sharedMarkersCacheLocale; + List<_SharedMarker> _cachedSharedMarkers = const []; + _NodeMarkersCacheKey? _nodeMarkersCacheKey; + List _cachedNodeMarkers = const []; @override void dispose() { @@ -283,11 +287,23 @@ class _MapScreenState extends State { @override Widget build(BuildContext context) { - return Consumer3( - builder: (context, connector, settingsService, pathHistory, child) { + return Builder( + builder: (context) { + final connectorSnapshot = context + .select( + _MapConnectorSnapshot.fromConnector, + ); + final connector = connectorSnapshot.connector; + final settings = context.select( + (service) => service.settings, + ); + final pathHistoryVersion = context.select( + (service) => service.version, + ); + final settingsService = context.read(); + final pathHistory = context.read(); final tileCache = context.read(); final isDesktop = _isDesktopPlatform(defaultTargetPlatform); - final settings = settingsService.settings; final allContacts = connector.allContacts; final contacts = settings.mapShowDiscoveryContacts @@ -296,7 +312,10 @@ class _MapScreenState extends State { final highlightPosition = widget.highlightPosition; final sharedMarkers = settings.mapShowMarkers - ? _collectSharedMarkers(connector) + ? _collectSharedMarkers( + connector, + connectorSnapshot.markerSignature, + ) .where( (marker) => !_hiddenMarkerIds.contains(marker.id) && @@ -347,9 +366,17 @@ class _MapScreenState extends State { .where((c) => c.hasLocation) .toList(); + // Guessed markers represent the same node types as known-location + // markers, so apply the node-type filters before estimating positions. + final guessCandidates = _filterContactsBySettings( + filteredByKeyPrefix, + settings, + noLocations: true, + ); + // Compute guessed locations with caching final maxRangeKm = _estimateLoRaRangeKm(connector); - final filteredKeys = filteredByKeyPrefix + final filteredKeys = guessCandidates .map((c) => '${c.publicKeyHex}:${c.path.join("-")}') .join(','); final anchorKeys = allContactsWithLocation @@ -359,12 +386,12 @@ class _MapScreenState extends State { ) .join(','); final cacheKey = - '$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; + '$filteredKeys|$anchorKeys|$pathHistoryVersion:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; if (cacheKey != _guessedLocationsCacheKey) { _guessedLocationsCacheKey = cacheKey; _cachedGuessedLocations = settings.mapShowGuessedLocations ? _computeGuessedLocations( - filteredByKeyPrefix, + guessCandidates, allContactsWithLocation, pathHistory, maxRangeKm, @@ -759,9 +786,21 @@ class _MapScreenState extends State { guessedLocations, showLabels: _showNodeLabels, ), - ..._buildNodeMarkers( + ..._buildNodeMarkersCached( visibleContacts, settings, + connectorSnapshot.contactsSignature, + connectorSnapshot.batterySignature, + _freshness, + settings.mapTimeFilterHours, + settings.mapKeyPrefixEnabled, + settings.mapKeyPrefix, + settings.mapShowDiscoveryContacts, + Object.hashAllUnordered( + settings.batteryChemistryByRepeaterId.entries.map( + (entry) => Object.hash(entry.key, entry.value), + ), + ), showLabels: _showNodeLabels, selectedContact: selectedContact, ), @@ -873,6 +912,59 @@ class _MapScreenState extends State { ); } + List _buildNodeMarkersCached( + List contacts, + AppSettings settings, + int contactsSignature, + int batterySignature, + _Freshness freshness, + double timeFilterHours, + bool keyPrefixEnabled, + String keyPrefix, + bool showDiscoveryContacts, + int batteryChemistrySignature, { + required bool showLabels, + Contact? selectedContact, + }) { + final visibleContactsSignature = Object.hashAll( + contacts.map( + (contact) => + Object.hash(_mapContactSignature(contact), _ageOf(contact)), + ), + ); + final key = _NodeMarkersCacheKey( + contactsSignature: contactsSignature, + visibleContactsSignature: visibleContactsSignature, + batterySignature: batterySignature, + freshness: freshness, + timeFilterHours: timeFilterHours, + keyPrefixEnabled: keyPrefixEnabled, + keyPrefix: keyPrefix, + showDiscoveryContacts: showDiscoveryContacts, + batteryChemistrySignature: batteryChemistrySignature, + showLabels: showLabels, + selectedKey: selectedContact?.publicKeyHex, + zoom: _zoom, + overlapsMode: settings.mapShowOverlaps, + showRepeaters: settings.mapShowRepeaters, + showChatNodes: settings.mapShowChatNodes, + showOtherNodes: settings.mapShowOtherNodes, + isBuildingPathTrace: _isBuildingPathTrace, + ); + if (key != _nodeMarkersCacheKey) { + _nodeMarkersCacheKey = key; + _cachedNodeMarkers = List.unmodifiable( + _buildNodeMarkers( + contacts, + settings, + showLabels: showLabels, + selectedContact: selectedContact, + ), + ); + } + return _cachedNodeMarkers; + } + List<_GuessedLocation> _computeGuessedLocations( List allContacts, List withLocation, @@ -1146,17 +1238,18 @@ class _MapScreenState extends State { }) { List filtered = []; bool addContact = false; + for (final contact in contacts) { addContact = false; if (!contact.hasLocation && !noLocations) { continue; } - // Apply node type filters + // Apply node type filters. The overlaps toggle is purely a visual + // highlight (applied in _buildNodeMarkers) and no longer affects which + // nodes are shown. if (contact.type == advTypeRepeater && - (settings.mapShowRepeaters || - _isBuildingPathTrace || - settings.mapShowOverlaps)) { + (settings.mapShowRepeaters || _isBuildingPathTrace)) { addContact = true; } if (contact.type == advTypeChat && @@ -1165,9 +1258,7 @@ class _MapScreenState extends State { } if (contact.type != advTypeChat && contact.type != advTypeRepeater && - (settings.mapShowOtherNodes || - _isBuildingPathTrace || - settings.mapShowOverlaps)) { + (settings.mapShowOtherNodes || _isBuildingPathTrace)) { addContact = true; } @@ -1175,25 +1266,6 @@ class _MapScreenState extends State { addContact = false; } - if (settings.mapShowOverlaps) { - final hasOverlap = contacts - .where( - (c) => - c.publicKeyHex != contact.publicKeyHex && - c.publicKey.first == contact.publicKey.first && - (c.type == advTypeRepeater || c.type == advTypeRoom) && - (contact.type == advTypeRepeater || - contact.type == advTypeRoom), - ) - .firstOrNull; - - if (hasOverlap == null && - settings.mapShowOverlaps && - !_isBuildingPathTrace) { - addContact = false; - } - } - if (addContact) { filtered.add(contact); } @@ -1212,13 +1284,34 @@ class _MapScreenState extends State { final selectedKey = selectedContact?.publicKeyHex; final items = contacts.where((c) => c.publicKeyHex != selectedKey).toList(); + // Key-prefix overlaps are a visual highlight only: flag the repeaters/rooms + // whose first key byte collides with another repeater/room on the map. + final overlapPrefixes = {}; + if (overlapsMode) { + final counts = {}; + for (final contact in contacts) { + if (contact.type == advTypeRepeater || contact.type == advTypeRoom) { + final prefix = contact.publicKey.first; + counts[prefix] = (counts[prefix] ?? 0) + 1; + } + } + counts.forEach((prefix, count) { + if (count > 1) overlapPrefixes.add(prefix); + }); + } + bool isOverlap(Contact contact) => + overlapsMode && + (contact.type == advTypeRepeater || contact.type == advTypeRoom) && + overlapPrefixes.contains(contact.publicKey.first); + void addNode(Contact contact, {bool dot = false}) { - markers.add(_nodeMarker(contact, overlapsMode: overlapsMode, dot: dot)); + final overlap = isOverlap(contact); + markers.add(_nodeMarker(contact, overlapsMode: overlap, dot: dot)); if (showLabels) { markers.add( _buildNodeLabelMarker( point: LatLng(contact.latitude!, contact.longitude!), - label: overlapsMode + label: overlap ? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}" : contact.name, ), @@ -1255,7 +1348,7 @@ class _MapScreenState extends State { markers.add( _nodeMarker( selectedContact, - overlapsMode: overlapsMode, + overlapsMode: isOverlap(selectedContact), selected: true, ), ); @@ -2371,7 +2464,16 @@ class _MapScreenState extends State { } } - List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) { + List<_SharedMarker> _collectSharedMarkers( + MeshCoreConnector connector, + int markerSignature, + ) { + final locale = Localizations.localeOf(context); + if (_sharedMarkersCacheSignature == markerSignature && + _sharedMarkersCacheLocale == locale) { + return _cachedSharedMarkers; + } + // Build a _SharedMarker per message (history empty), grouped by dedupe key. // Afterwards pick the latest per key and fill its history from older ones. final updatesByKey = >{}; @@ -2463,7 +2565,10 @@ class _MapScreenState extends State { }); markers.sort((a, b) => b.timestamp.compareTo(a.timestamp)); - return markers; + _sharedMarkersCacheSignature = markerSignature; + _sharedMarkersCacheLocale = locale; + _cachedSharedMarkers = List.unmodifiable(markers); + return _cachedSharedMarkers; } Marker _buildSharedMarker(_SharedMarker marker) { @@ -3564,6 +3669,219 @@ enum _NodeAge { online, recent, stale } enum _Freshness { all, online, recent, stale } +int _bytesSignature(Iterable? bytes) { + if (bytes == null) return 0; + return Object.hashAll(bytes); +} + +int _mapContactSignature(Contact contact) { + return Object.hash( + contact.publicKeyHex, + contact.name, + contact.type, + contact.flags, + contact.pathLength, + _bytesSignature(contact.path), + contact.pathOverride, + _bytesSignature(contact.pathOverrideBytes), + contact.latitude, + contact.longitude, + contact.lastSeen.millisecondsSinceEpoch, + contact.lastMessageAt.millisecondsSinceEpoch, + contact.isActive, + contact.wasPulled, + ); +} + +class _MapConnectorSnapshot { + final MeshCoreConnector connector; + final int contactsSignature; + final int markerSignature; + final int batterySignature; + final int uiSignature; + + const _MapConnectorSnapshot({ + required this.connector, + required this.contactsSignature, + required this.markerSignature, + required this.batterySignature, + required this.uiSignature, + }); + + factory _MapConnectorSnapshot.fromConnector(MeshCoreConnector connector) { + final allContacts = connector.allContacts; + final contactsSignature = Object.hashAll( + allContacts.map(_mapContactSignature), + ); + final batterySignature = Object.hashAll( + allContacts + .where((contact) => contact.type == advTypeRepeater) + .map( + (contact) => Object.hash( + contact.publicKeyHex, + connector.getRepeaterBatteryMillivolts(contact.publicKeyHex), + ), + ), + ); + + final markerParts = [connector.selfName]; + for (final contact in connector.contacts) { + markerParts.add(contact.publicKeyHex); + markerParts.add(contact.name); + for (final message in connector.getMessages(contact)) { + if (!message.text.trimLeft().startsWith('m:')) continue; + markerParts.add( + Object.hash( + message.messageId, + message.text, + message.timestamp.millisecondsSinceEpoch, + message.isOutgoing, + ), + ); + } + } + for (final channel in connector.channels) { + markerParts.add( + Object.hash( + channel.index, + channel.name, + channel.isPublicChannel, + channel.isEmpty, + ), + ); + for (final message in connector.getChannelMessages(channel)) { + if (!message.text.trimLeft().startsWith('m:')) continue; + markerParts.add( + Object.hash( + message.messageId, + message.text, + message.senderName, + message.timestamp.millisecondsSinceEpoch, + ), + ); + } + } + + return _MapConnectorSnapshot( + connector: connector, + contactsSignature: contactsSignature, + markerSignature: Object.hashAll(markerParts), + batterySignature: batterySignature, + uiSignature: Object.hash( + connector.isConnected, + connector.selfLatitude, + connector.selfLongitude, + connector.currentFreqHz, + connector.currentBwHz, + connector.currentSf, + connector.currentTxPower, + connector.getTotalContactsUnreadCount(), + connector.getTotalChannelsUnreadCount(), + ), + ); + } + + @override + bool operator ==(Object other) { + return other is _MapConnectorSnapshot && + contactsSignature == other.contactsSignature && + markerSignature == other.markerSignature && + batterySignature == other.batterySignature && + uiSignature == other.uiSignature; + } + + @override + int get hashCode => Object.hash( + contactsSignature, + markerSignature, + batterySignature, + uiSignature, + ); +} + +class _NodeMarkersCacheKey { + final int contactsSignature; + final int visibleContactsSignature; + final int batterySignature; + final _Freshness freshness; + final double timeFilterHours; + final bool keyPrefixEnabled; + final String keyPrefix; + final bool showDiscoveryContacts; + final int batteryChemistrySignature; + final bool showLabels; + final String? selectedKey; + final double zoom; + final bool overlapsMode; + final bool showRepeaters; + final bool showChatNodes; + final bool showOtherNodes; + final bool isBuildingPathTrace; + + const _NodeMarkersCacheKey({ + required this.contactsSignature, + required this.visibleContactsSignature, + required this.batterySignature, + required this.freshness, + required this.timeFilterHours, + required this.keyPrefixEnabled, + required this.keyPrefix, + required this.showDiscoveryContacts, + required this.batteryChemistrySignature, + required this.showLabels, + required this.selectedKey, + required this.zoom, + required this.overlapsMode, + required this.showRepeaters, + required this.showChatNodes, + required this.showOtherNodes, + required this.isBuildingPathTrace, + }); + + @override + bool operator ==(Object other) { + return other is _NodeMarkersCacheKey && + contactsSignature == other.contactsSignature && + visibleContactsSignature == other.visibleContactsSignature && + batterySignature == other.batterySignature && + freshness == other.freshness && + timeFilterHours == other.timeFilterHours && + keyPrefixEnabled == other.keyPrefixEnabled && + keyPrefix == other.keyPrefix && + showDiscoveryContacts == other.showDiscoveryContacts && + batteryChemistrySignature == other.batteryChemistrySignature && + showLabels == other.showLabels && + selectedKey == other.selectedKey && + zoom == other.zoom && + overlapsMode == other.overlapsMode && + showRepeaters == other.showRepeaters && + showChatNodes == other.showChatNodes && + showOtherNodes == other.showOtherNodes && + isBuildingPathTrace == other.isBuildingPathTrace; + } + + @override + int get hashCode => Object.hash( + contactsSignature, + visibleContactsSignature, + batterySignature, + freshness, + timeFilterHours, + keyPrefixEnabled, + keyPrefix, + showDiscoveryContacts, + batteryChemistrySignature, + showLabels, + selectedKey, + zoom, + overlapsMode, + showRepeaters, + showChatNodes, + showOtherNodes, + isBuildingPathTrace, + ); +} + class _GuessedLocation { final Contact contact; final LatLng position; diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 212ab819..84be2df6 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -207,6 +207,15 @@ class _ScannerScreenState extends State { } void _toggleScan(MeshCoreConnector connector) { + if (PlatformInfo.isWeb) { + // flutter_blue_plus has no web backend, so a BLE scan silently no-ops in + // the browser. Tell the user instead of leaving them staring at a button. + showDismissibleSnackBar( + context, + content: Text(context.l10n.scanner_bluetoothWebUnsupported), + ); + return; + } if (connector.state == MeshCoreConnectionState.scanning) { connector.stopScan(); } else { diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 3d12016a..81346f5f 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -725,18 +725,26 @@ class _SettingsScreenState extends State { onPressed: () => Navigator.pop(context), child: Text(l10n.common_cancel), ), - TextButton( - onPressed: () async { - Navigator.pop(context); - await connector.setNodeName(controller.text); - await connector.refreshDeviceInfo(); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(l10n.settings_nodeNameUpdated), + ListenableBuilder( + listenable: controller, + builder: (context, _) { + final name = controller.text.trim(); + return TextButton( + onPressed: name.isEmpty + ? null + : () async { + Navigator.pop(context); + await connector.setNodeName(name); + await connector.refreshDeviceInfo(); + if (!context.mounted) return; + showDismissibleSnackBar( + context, + content: Text(l10n.settings_nodeNameUpdated), + ); + }, + child: Text(l10n.common_save), ); }, - child: Text(l10n.common_save), ), ], ), diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 5b42767b..0e40da64 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -386,6 +386,10 @@ class _UsbScreenState extends State { void _showError(Object error) { if (!mounted) return; + // Cancelling the browser's serial port picker is a normal user action, not + // an error — don't show a scary red toast (and never leak the raw + // DOMException text). + if (_isUserCancelledPortPicker(error)) return; showDismissibleSnackBar( context, content: Text(_friendlyErrorMessage(error)), @@ -393,6 +397,16 @@ class _UsbScreenState extends State { ); } + bool _isUserCancelledPortPicker(Object error) { + if (error is StateError && + error.message.contains('No USB serial device selected')) { + return true; + } + final text = error.toString(); + return text.contains('No port selected by the user') || + text.contains("Failed to execute 'requestPort'"); + } + String _friendlyErrorMessage(Object error) { final l10n = context.l10n; diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 36027917..b148c95d 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -114,6 +114,36 @@ class NotificationService { return _isInitialized; } + // Cached "are we allowed to post notifications" result. Null = not yet + // determined. Avoids calling _notifications.show() when it would only throw + // "You must request notifications permissions first" (every web build, and + // Android 13+ before the user grants the permission). + bool? _canNotify; + + Future _ensureCanNotify() async { + if (!await _ensureInitialized()) return false; + final cached = _canNotify; + if (cached != null) return cached; + + // flutter_local_notifications has no web backend, so show() always throws. + // Skip silently instead of logging an error per incoming message. + if (kIsWeb) return _canNotify = false; + + // On Android 13+ notifications require an explicit grant; reflect the real + // OS state so we don't spam failed show() calls when denied. + final androidPlugin = _notifications + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >(); + if (androidPlugin != null) { + final enabled = await androidPlugin.areNotificationsEnabled(); + return _canNotify = enabled ?? false; + } + + // iOS/macOS request permission during initialize(); desktop has no gate. + return _canNotify = true; + } + Future requestPermissions() async { if (!_isInitialized) { await initialize(); @@ -126,7 +156,8 @@ class NotificationService { >(); if (androidPlugin != null) { final granted = await androidPlugin.requestNotificationsPermission(); - return granted ?? false; + _canNotify = granted ?? false; + return _canNotify!; } // iOS permissions are requested during initialization @@ -140,7 +171,8 @@ class NotificationService { badge: true, sound: true, ); - return granted ?? false; + _canNotify = granted ?? false; + return _canNotify!; } return true; @@ -165,7 +197,7 @@ class NotificationService { String? contactId, int? badgeCount, }) async { - if (!await _ensureInitialized()) return; + if (!await _ensureCanNotify()) return; final androidDetails = AndroidNotificationDetails( 'messages', @@ -215,7 +247,7 @@ class NotificationService { required String contactType, String? contactId, }) async { - if (!await _ensureInitialized()) return; + if (!await _ensureCanNotify()) return; const androidDetails = AndroidNotificationDetails( 'adverts', @@ -265,7 +297,7 @@ class NotificationService { int? channelIndex, int? badgeCount, }) async { - if (!await _ensureInitialized()) return; + if (!await _ensureCanNotify()) return; final androidDetails = AndroidNotificationDetails( 'channel_messages', @@ -545,7 +577,7 @@ class NotificationService { } Future _showBatchSummary(List<_PendingNotification> batch) async { - if (!await _ensureInitialized()) return; + if (!await _ensureCanNotify()) return; // Group by type final messages = batch diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 9c8af85e..1976de94 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -33,12 +33,14 @@ class UsbSerialService { String? _connectedPortLabel; FlSerial? _serial; AppDebugLogService? _debugLogService; + Object? _lastError; UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; String? get activePortDisplayLabel => _connectedPortLabel ?? _connectedPortKey; Stream get frameStream => _frameController.stream; + Object? get lastError => _lastError; bool get _useAndroidUsbHost => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; bool get _useDesktopFlSerial => @@ -434,6 +436,7 @@ class UsbSerialService { } void _addFrameError(Object error, [StackTrace? stackTrace]) { + _lastError = error; if (_frameController.isClosed) { return; } diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 5261308d..2429187a 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -15,6 +15,18 @@ class UsbSerialService { static const Map _knownUsbNames = { '2886:1667': 'Seeed Wio Tracker L1', }; + + /// USB-to-UART bridge chips whose hardware auto-reset circuit requires DTR + /// to be held asserted after open (otherwise the MCU resets). Native-USB-CDC + /// boards (nRF52840/Adafruit 0x239A, Espressif native 0x303A, Seeed 0x2886) + /// tie DTR to the bootloader/reset line, so asserting it re-enumerates and + /// drops the device ("The device has been lost"); they must be left alone. + static const Set _uartBridgeVendorIds = { + 0x10C4, // Silicon Labs CP210x + 0x1A86, // QinHeng CH340 / CH9102 + 0x0403, // FTDI + 0x067B, // Prolific PL2303 + }; static final Map _deviceNamesByPortKey = {}; static final Map _baseLabelsByPortKey = {}; static final Map _authorizedPortsByKey = @@ -34,12 +46,14 @@ class UsbSerialService { String _requestPortLabel = 'Choose USB Device'; String _fallbackDeviceName = 'Web Serial Device'; AppDebugLogService? _debugLogService; + Object? _lastError; UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; String? get activePortDisplayLabel => _connectedPortName ?? _connectedPortKey; Stream get frameStream => _frameController.stream; bool get isConnected => _status == UsbSerialStatus.connected; + Object? get lastError => _lastError; JSObject get _navigator => JSObject.fromInteropObject(web.window.navigator); bool get _isSupported => _navigator.has('serial'); @@ -74,6 +88,7 @@ class UsbSerialService { } _status = UsbSerialStatus.connecting; + _lastError = null; _frameDecoder.reset(); try { @@ -282,16 +297,30 @@ class UsbSerialService { ..['flowControl'] = 'none'.toJS; await port.callMethod>('open'.toJS, options).toDart; - // Prevent ESP32 USB-CDC reset: hold DTR=true, RTS=false after open. - try { - final signals = JSObject() - ..['dataTerminalReady'] = true.toJS - ..['requestToSend'] = false.toJS; - await port - .callMethod>('setSignals'.toJS, signals) - .toDart; - } catch (_) { - // setSignals may not be supported on all browsers/devices. + // Only UART-bridge chips (CP210x/CH340/FTDI/PL2303) need DTR held high to + // avoid the auto-reset circuit firing on open. Native-USB-CDC boards + // (e.g. nRF52840/Adafruit) tie DTR to the reset line — toggling it there + // re-enumerates the device and Web Serial reports "The device has been + // lost". Leave their signals untouched. + final vendorId = _portInfo(port)?.usbVendorId; + final isUartBridge = + vendorId != null && _uartBridgeVendorIds.contains(vendorId); + _debugLogService?.info( + 'Open: vendorId=${vendorId == null ? 'unknown' : '0x${vendorId.toRadixString(16)}'} ' + 'uartBridge=$isUartBridge (DTR ${isUartBridge ? 'asserted' : 'left default'})', + tag: 'USB Serial', + ); + if (isUartBridge) { + try { + final signals = JSObject() + ..['dataTerminalReady'] = true.toJS + ..['requestToSend'] = false.toJS; + await port + .callMethod>('setSignals'.toJS, signals) + .toDart; + } catch (_) { + // setSignals may not be supported on all browsers/devices. + } } } @@ -384,13 +413,21 @@ class UsbSerialService { } catch (error, stackTrace) { _debugLogService?.error('_pumpReads error: $error', tag: 'USB Serial'); if (_status == UsbSerialStatus.connected) { + // The transport is dead — reflect that in status immediately so a + // concurrent connect handshake fails fast instead of waiting for a + // SELF_INFO that can never arrive. + _status = UsbSerialStatus.disconnected; + _lastError = error; _addFrameError(error, stackTrace); } } finally { _debugLogService?.info('_pumpReads: ended', tag: 'USB Serial'); _releaseLock(reader); if (_status == UsbSerialStatus.connected && identical(reader, _reader)) { - _addFrameError(StateError('USB serial connection closed')); + _status = UsbSerialStatus.disconnected; + final closedError = StateError('USB serial connection closed'); + _lastError = closedError; + _addFrameError(closedError); } } } diff --git a/untranslated.json b/untranslated.json index c1e57a13..9e0501e8 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,5 +1,6 @@ { "bg": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -41,6 +42,7 @@ ], "de": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -82,6 +84,7 @@ ], "es": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -123,6 +126,7 @@ ], "fr": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -164,6 +168,7 @@ ], "hu": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -205,6 +210,7 @@ ], "it": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -246,6 +252,7 @@ ], "ja": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -287,6 +294,7 @@ ], "ko": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -328,6 +336,7 @@ ], "nl": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -369,6 +378,7 @@ ], "pl": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -410,6 +420,7 @@ ], "pt": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -451,6 +462,7 @@ ], "ru": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -492,6 +504,7 @@ ], "sk": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -533,6 +546,7 @@ ], "sl": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -574,6 +588,7 @@ ], "sv": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -615,6 +630,7 @@ ], "uk": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", @@ -656,6 +672,7 @@ ], "zh": [ + "scanner_bluetoothWebUnsupported", "map_searchHint", "map_activity", "map_online", From 5ea6b17b16b541978cb5b085f9b4a535d0a53bed Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 13 Jun 2026 00:36:45 -0700 Subject: [PATCH 10/16] feat: enhance MeshCoreConnector with improved timeout calculation and path resolution; add PathHopResolver for better contact resolution --- lib/connector/meshcore_connector.dart | 212 ++++++++++++---- lib/helpers/path_hop_resolver.dart | 70 ++++++ lib/screens/channel_message_path_screen.dart | 251 ++++++++----------- lib/screens/discovery_screen.dart | 46 ++-- lib/services/message_retry_service.dart | 58 ++++- lib/services/path_history_service.dart | 4 +- lib/services/timeout_prediction_service.dart | 28 ++- test/helpers/path_hop_resolver_test.dart | 94 +++++++ 8 files changed, 529 insertions(+), 234 deletions(-) create mode 100644 lib/helpers/path_hop_resolver.dart create mode 100644 test/helpers/path_hop_resolver_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1bff354a..ef9a9e24 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -208,6 +208,9 @@ class MeshCoreConnector extends ChangeNotifier { // Intentionally global (not per-contact): tracks overall network activity. // Frequent RX from any source indicates a busy network with more collisions. DateTime _lastRxTime = DateTime.now(); + // Snapshot of _lastRxTime taken before the ACK frame updates it, so that + // onDeliveryObserved records the pre-ACK elapsed time (matching prediction). + DateTime _lastRxBeforeFrame = DateTime.fromMillisecondsSinceEpoch(0); DateTime _lastRadioRxTime = DateTime.fromMillisecondsSinceEpoch(0); DateTime _lastContactMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0); DateTime _lastChannelMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0); @@ -945,11 +948,17 @@ class MeshCoreConnector extends ChangeNotifier { updateMessage: _updateMessage, clearContactPath: clearContactPath, setContactPath: setContactPath, - calculateTimeout: (pathLength, messageBytes, {String? contactKey}) => - calculateTimeout( + calculateTimeout: + ( + pathLength, + messageBytes, { + String? contactKey, + int? deviceTimeoutMs, + }) => calculateTimeout( pathLength: pathLength, messageBytes: messageBytes, contactKey: contactKey, + deviceTimeoutMs: deviceTimeoutMs, ), getSelfPublicKey: () => _selfPublicKey, prepareContactOutboundText: prepareContactOutboundText, @@ -965,7 +974,9 @@ class MeshCoreConnector extends ChangeNotifier { recentSelections: recentSelections, ), onDeliveryObserved: (contactKey, pathLength, messageBytes, tripTimeMs) { - final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; + final secSinceRx = DateTime.now() + .difference(_lastRxBeforeFrame) + .inSeconds; _timeoutPredictionService?.recordObservation( contactKey: contactKey, pathLength: pathLength, @@ -2683,41 +2694,62 @@ class MeshCoreConnector extends ChangeNotifier { Uint8List data, { String? channelSendQueueId, bool expectsGenericAck = false, + bool waitForGenericAck = false, }) async { if (!isConnected) { throw Exception("Not connected to a MeshCore device"); } _bleDebugLogService?.logFrame(data, outgoing: true); - if (_activeTransport == MeshCoreTransportType.usb) { - await _usbManager.write(data); - // Brief pause so the device firmware can process each frame before the - // next arrives. Without this, rapid-fire frames over USB can cause the - // device to miss responses (especially on reconnect). - await Future.delayed(const Duration(milliseconds: 10)); - } else if (_activeTransport == MeshCoreTransportType.tcp) { - await _tcpConnector.write(data); - } else { - if (_rxCharacteristic == null) { - throw Exception("MeshCore RX characteristic not available"); - } - // Prefer write without response when supported; fall back to write with response. - final properties = _rxCharacteristic!.properties; - final canWriteWithoutResponse = properties.writeWithoutResponse; - final canWriteWithResponse = properties.write; - if (!canWriteWithoutResponse && !canWriteWithResponse) { - throw Exception("MeshCore RX characteristic does not support write"); - } - await _rxCharacteristic!.write( - data.toList(), - withoutResponse: canWriteWithoutResponse, - ); - } - _trackPendingGenericAck( + final pendingAck = _trackPendingGenericAck( data, channelSendQueueId: channelSendQueueId, - expectsGenericAck: expectsGenericAck, + expectsGenericAck: expectsGenericAck || waitForGenericAck, + waitForAck: waitForGenericAck, ); + + try { + if (_activeTransport == MeshCoreTransportType.usb) { + await _usbManager.write(data); + // Brief pause so the device firmware can process each frame before the + // next arrives. Without this, rapid-fire frames over USB can cause the + // device to miss responses (especially on reconnect). + await Future.delayed(const Duration(milliseconds: 10)); + } else if (_activeTransport == MeshCoreTransportType.tcp) { + await _tcpConnector.write(data); + } else { + if (_rxCharacteristic == null) { + throw Exception("MeshCore RX characteristic not available"); + } + // Prefer write without response when supported; fall back to write with response. + final properties = _rxCharacteristic!.properties; + final canWriteWithoutResponse = properties.writeWithoutResponse; + final canWriteWithResponse = properties.write; + if (!canWriteWithoutResponse && !canWriteWithResponse) { + throw Exception("MeshCore RX characteristic does not support write"); + } + await _rxCharacteristic!.write( + data.toList(), + withoutResponse: canWriteWithoutResponse, + ); + } + } catch (_) { + if (pendingAck != null) { + _pendingGenericAckQueue.remove(pendingAck); + } + rethrow; + } + + if (pendingAck?.completer != null) { + try { + await pendingAck!.completer!.future.timeout(const Duration(seconds: 5)); + } on TimeoutException { + _pendingGenericAckQueue.remove(pendingAck); + throw TimeoutException( + 'Timed out waiting for firmware acknowledgement', + ); + } + } } Future requestBatteryStatus({bool force = false}) async { @@ -2949,6 +2981,17 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (!isConnected || text.isEmpty) return; + final outboundBytes = utf8.encode( + prepareContactOutboundText(contact, text), + ); + if (outboundBytes.length > maxTextPayloadBytes) { + debugPrint( + 'sendMessage: dropping overlong message ' + '(${outboundBytes.length} > $maxTextPayloadBytes bytes)', + ); + return; + } + // Check if this is a reaction - apply locally with pending status and route through retry service final reactionInfo = ReactionHelper.parseReaction(text); if (reactionInfo != null) { @@ -3419,9 +3462,11 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } - Future importDiscoveredContact(Contact contact) async { - if (!isConnected) return; + Future importDiscoveredContact(Contact contact) async { + if (!isConnected) return false; + // Manual saves must bypass the firmware's auto-add discovery policy. + // CMD_IMPORT_CONTACT replays an advert and may remain discovery-only. await sendFrame( buildUpdateContactPathFrame( contact.publicKey, @@ -3434,6 +3479,7 @@ class MeshCoreConnector extends ChangeNotifier { lon: contact.longitude, lastModified: contact.lastSeen, ), + waitForGenericAck: true, ); // Update the discovered contact to mark it as active (imported) @@ -3459,6 +3505,8 @@ class MeshCoreConnector extends ChangeNotifier { ), ); notifyListeners(); + unawaited(_persistDiscoveredContacts()); + return true; } Future clearContactPath(Contact contact) async { @@ -3864,6 +3912,7 @@ class MeshCoreConnector extends ChangeNotifier { void _handleFrame(List data) { if (data.isEmpty) return; + _lastRxBeforeFrame = _lastRxTime; _lastRxTime = DateTime.now(); final frame = Uint8List.fromList(data); @@ -4016,11 +4065,15 @@ class MeshCoreConnector extends ChangeNotifier { } final failedAck = _pendingGenericAckQueue.removeAt(0); + failedAck.completer?.completeError( + Exception('Firmware rejected command with error code $errCode'), + ); if (failedAck.commandCode != cmdSendChannelTxtMsg || failedAck.channelSendQueueId == null) { return; } _pendingChannelSentQueue.remove(failedAck.channelSendQueueId); + _markPendingChannelMessageFailedById(failedAck.channelSendQueueId!); } void _handlePathUpdated(Uint8List frame) { @@ -4370,16 +4423,28 @@ class MeshCoreConnector extends ChangeNotifier { // Same as max for flood — firmware uses a single formula return 500 + (16 * airtime); } else { - return airtime * (pathLength + 1); + // Include firmware base (500ms) and per-hop processing (6*airtime+250) + // so ML cannot clamp below a physically plausible round-trip. + return 500 + ((airtime * 6 + 250) * pathLength); } } + /// Hard ceiling on any ML-derived or physics-fallback timeout (ms). + /// Prevents the flood formula (500 + 16·airtime at SF12 ≈ 150s) and an + /// unstable OLS model from producing multi-minute waits. + static const int _hardMaxTimeoutMs = 45000; + /// Calculate timeout for a message based on radio settings and path length. /// Returns timeout in milliseconds, considering number of hops. + /// + /// [deviceTimeoutMs] is the firmware's own est_timeout from RESP_CODE_SENT. + /// When ML is absent it is used as the fallback (clamped to physicsMin). + /// When ML is present it is used as an additional ceiling alongside physicsMax. int calculateTimeout({ required int pathLength, int messageBytes = 100, String? contactKey, + int? deviceTimeoutMs, }) { final airtime = _estimateAirtimeMs(messageBytes); final physicsMin = _physicsMinTimeout(pathLength, airtime); @@ -4394,17 +4459,26 @@ class MeshCoreConnector extends ChangeNotifier { secondsSinceLastRx: secSinceRx, ); if (mlTimeout != null) { + // Use device est_timeout as an additional ceiling when available — + // the firmware computed it from real airtime, so it's better than + // a physics guess built on a 50 ms fallback. + final ceiling = deviceTimeoutMs != null && deviceTimeoutMs > physicsMin + ? deviceTimeoutMs.clamp(physicsMin, _hardMaxTimeoutMs) + : physicsMax; if (pathLength < 0) { // Flood: trust ML, only enforce firmware formula as floor if (mlTimeout < physicsMin) { - return physicsMin; + return physicsMin.clamp(0, _hardMaxTimeoutMs); } } - return mlTimeout.clamp(physicsMin, physicsMax); + return mlTimeout.clamp(physicsMin, ceiling).clamp(0, _hardMaxTimeoutMs); } - // No ML data — use firmware formula - return physicsMax; + // No ML data — prefer device est_timeout (it used real airtime), then physics. + if (deviceTimeoutMs != null && deviceTimeoutMs > 0) { + return deviceTimeoutMs.clamp(physicsMin, _hardMaxTimeoutMs); + } + return physicsMax.clamp(0, _hardMaxTimeoutMs); } void _handleContact(Uint8List frame, {bool isContact = true}) { @@ -4760,14 +4834,11 @@ class MeshCoreConnector extends ChangeNotifier { final existing = _conversations[message.senderKeyHex]; final incomingTimestamp = message.timestamp.millisecondsSinceEpoch; if (existing != null && existing.isNotEmpty) { - final startIndex = existing.length > 10 ? existing.length - 10 : 0; - for (int i = existing.length - 1; i >= startIndex; i--) { - final recent = existing[i]; - if (!recent.isOutgoing && - recent.timestamp.millisecondsSinceEpoch == incomingTimestamp && - recent.text == message.text) { - return; - } + final last = existing.last; + if (!last.isOutgoing && + last.timestamp.millisecondsSinceEpoch == incomingTimestamp && + last.text == message.text) { + return; } } } @@ -5351,12 +5422,37 @@ class MeshCoreConnector extends ChangeNotifier { return false; } + void _markPendingChannelMessageFailedById(String messageId) { + for (final entry in _channelMessages.entries) { + final channelMessages = entry.value; + for (int i = channelMessages.length - 1; i >= 0; i--) { + final message = channelMessages[i]; + if (message.messageId != messageId) { + continue; + } + if (!message.isOutgoing || + message.status != ChannelMessageStatus.pending) { + return; + } + channelMessages[i] = message.copyWith( + status: ChannelMessageStatus.failed, + ); + unawaited( + _channelMessageStore.saveChannelMessages(entry.key, channelMessages), + ); + notifyListeners(); + return; + } + } + } + void _handleOk() { if (_pendingGenericAckQueue.isEmpty) { return; } final pendingAck = _pendingGenericAckQueue.removeAt(0); + pendingAck.completer?.complete(); if (pendingAck.commandCode != cmdSendChannelTxtMsg || pendingAck.channelSendQueueId == null) { return; @@ -6188,18 +6284,25 @@ class MeshCoreConnector extends ChangeNotifier { _scheduleReconnect(); } - void _trackPendingGenericAck( + _PendingCommandAck? _trackPendingGenericAck( Uint8List data, { String? channelSendQueueId, required bool expectsGenericAck, + required bool waitForAck, }) { - if (!expectsGenericAck || data.isEmpty) return; - _pendingGenericAckQueue.add( - _PendingCommandAck( - commandCode: data[0], - channelSendQueueId: channelSendQueueId, - ), + if (!expectsGenericAck || data.isEmpty) return null; + final pendingAck = _PendingCommandAck( + commandCode: data[0], + channelSendQueueId: channelSendQueueId, + completer: waitForAck ? Completer() : null, ); + if (pendingAck.completer != null) { + // sendFrame awaits this future after transport I/O; attach an error + // handler immediately in case USB returns an error response first. + unawaited(pendingAck.completer!.future.catchError((_) {})); + } + _pendingGenericAckQueue.add(pendingAck); + return pendingAck; } String _nextReactionSendQueueId() { @@ -6733,6 +6836,11 @@ class _RepeaterAckContext { class _PendingCommandAck { final int commandCode; final String? channelSendQueueId; + final Completer? completer; - _PendingCommandAck({required this.commandCode, this.channelSendQueueId}); + _PendingCommandAck({ + required this.commandCode, + this.channelSendQueueId, + this.completer, + }); } diff --git a/lib/helpers/path_hop_resolver.dart b/lib/helpers/path_hop_resolver.dart new file mode 100644 index 00000000..19f26001 --- /dev/null +++ b/lib/helpers/path_hop_resolver.dart @@ -0,0 +1,70 @@ +import 'package:latlong2/latlong.dart'; + +import '../connector/meshcore_protocol.dart'; +import '../models/contact.dart'; + +class PathHopResolver { + const PathHopResolver._(); + + static List resolve({ + required List pathBytes, + required List contacts, + LatLng? endpoint, + bool resolveFromEnd = false, + }) { + final candidatesByPrefix = >{}; + for (final contact in contacts) { + if (contact.publicKey.isEmpty) continue; + if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { + continue; + } + candidatesByPrefix + .putIfAbsent(contact.publicKey.first, () => []) + .add(contact); + } + for (final candidates in candidatesByPrefix.values) { + candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); + } + + final resolved = List.filled(pathBytes.length, null); + final indexes = resolveFromEnd + ? List.generate(pathBytes.length, (i) => pathBytes.length - 1 - i) + : List.generate(pathBytes.length, (i) => i); + final distance = Distance(); + var previousPosition = endpoint; + + for (final index in indexes) { + final candidates = candidatesByPrefix[pathBytes[index]]; + if (candidates == null || candidates.isEmpty) continue; + + var bestIndex = 0; + if (previousPosition != null && candidates.length > 1) { + double? nearestDistance; + for (var i = 0; i < candidates.length; i++) { + final position = _positionOf(candidates[i]); + if (position == null) continue; + final candidateDistance = distance(previousPosition, position); + if (nearestDistance == null || candidateDistance < nearestDistance) { + nearestDistance = candidateDistance; + bestIndex = i; + } + } + } + + final contact = candidates.removeAt(bestIndex); + resolved[index] = contact; + previousPosition = _positionOf(contact) ?? previousPosition; + } + + return resolved; + } + + static LatLng? _positionOf(Contact contact) { + if (!contact.hasLocation || + contact.latitude == null || + contact.longitude == null) { + return null; + } + return LatLng(contact.latitude!, contact.longitude!); + } +} diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 13bc0c3c..c46c413a 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -8,9 +8,9 @@ import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../helpers/path_hop_resolver.dart'; import '../services/map_tile_cache_service.dart'; import '../services/app_settings_service.dart'; -import '../connector/meshcore_protocol.dart'; import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; import '../models/channel_message.dart'; @@ -46,7 +46,12 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - final hops = _buildPathHops(primaryPath, connector, l10n); + final hops = _buildPathHops( + primaryPath, + connector, + l10n, + resolveFromEnd: !message.isOutgoing, + ); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( primaryPath.length, @@ -808,7 +813,12 @@ class _ChannelMessagePathMapScreenState // Match on the unoriented bytes — observedPaths stores them as // recorded, while selectedPath may be reversed for display. final selectedIndex = _indexForPath(selectedPathTmp, observedPaths); - final hops = _buildPathHops(selectedPath, connector, context.l10n); + final hops = _buildPathHops( + selectedPath, + connector, + context.l10n, + resolveFromEnd: !widget.message.isOutgoing, + ); // Renderable paths for the animation and combined view. final entries = <_ObservedPathEntry>[]; @@ -816,7 +826,12 @@ class _ChannelMessagePathMapScreenState final oriented = _orientPath(observedPaths[i].pathBytes); final pathHops = i == selectedIndex ? hops - : _buildPathHops(oriented, connector, context.l10n); + : _buildPathHops( + oriented, + connector, + context.l10n, + resolveFromEnd: !widget.message.isOutgoing, + ); final display = _buildDisplayPath( index: i, isPrimary: observedPaths[i].isPrimary, @@ -967,8 +982,7 @@ class _ChannelMessagePathMapScreenState lines = buildMultiPathPolylines( visible: visibleDisplays, selected: selectedDisplay, - combined: - effectiveMode == PathViewMode.combined, + combined: effectiveMode == PathViewMode.combined, animating: animating, ); if (animating && selectedDisplay != null) { @@ -1498,17 +1512,14 @@ class _ChannelMessagePathMapScreenState IconButton( visualDensity: VisualDensity.compact, icon: Icon( - _panelCollapsed - ? Icons.expand_less - : Icons.expand_more, + _panelCollapsed ? Icons.expand_less : Icons.expand_more, size: 20, ), tooltip: _panelCollapsed ? l10n.pathMap_expandPanel : l10n.pathMap_collapsePanel, - onPressed: () => setState( - () => _panelCollapsed = !_panelCollapsed, - ), + onPressed: () => + setState(() => _panelCollapsed = !_panelCollapsed), ), ], ), @@ -1559,11 +1570,7 @@ class _ChannelMessagePathMapScreenState ), const Divider(height: 1), Expanded( - child: _buildHopListView( - hops, - selectedDisplay, - hopUseCount, - ), + child: _buildHopListView(hops, selectedDisplay, hopUseCount), ), ], ], @@ -1610,78 +1617,71 @@ class _ChannelMessagePathMapScreenState : isFocused ? MeshPalette.blueBg : Colors.transparent, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - child: Row( - children: [ - Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: MeshPalette.blueDim.withValues( - alpha: 0.3, - ), - shape: BoxShape.circle, - border: Border.all( - color: MeshPalette.blueDim.withValues( - alpha: 0.5, - ), - ), - ), - alignment: Alignment.center, - child: Text( - hop.index.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w700, - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - hop.displayLabel, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, - ), - overflow: TextOverflow.ellipsis, - ), - Text( - [ - hop.hasLocation - ? '${hop.position!.latitude.toStringAsFixed(5)}, ' - '${hop.position!.longitude.toStringAsFixed(5)}' - : context - .l10n - .channelPath_noLocationData, - if (sharedCount > 1) - context.l10n.pathMap_sharedNodeCount( - sharedCount, - ), - ].join(' · '), - style: MeshTheme.mono( - fontSize: 10, - color: MeshPalette.ink3, - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: MeshPalette.blueDim.withValues(alpha: 0.3), + shape: BoxShape.circle, + border: Border.all( + color: MeshPalette.blueDim.withValues(alpha: 0.5), + ), + ), + alignment: Alignment.center, + child: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + hop.displayLabel, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, ), - ); - }, - ); + overflow: TextOverflow.ellipsis, + ), + Text( + [ + hop.hasLocation + ? '${hop.position!.latitude.toStringAsFixed(5)}, ' + '${hop.position!.longitude.toStringAsFixed(5)}' + : context.l10n.channelPath_noLocationData, + if (sharedCount > 1) + context.l10n.pathMap_sharedNodeCount( + sharedCount, + ), + ].join(' · '), + style: MeshTheme.mono( + fontSize: 10, + color: MeshPalette.ink3, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); }, ); } @@ -1743,76 +1743,25 @@ class _ObservedPath { List<_PathHop> _buildPathHops( Uint8List pathBytes, MeshCoreConnector connector, - AppLocalizations l10n, -) { + AppLocalizations l10n, { + bool resolveFromEnd = false, +}) { if (pathBytes.isEmpty) return const []; - final candidatesByPrefix = >{}; - final allContacts = connector.allContacts; - for (final contact in allContacts) { - if (contact.publicKey.isEmpty) continue; - if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { - continue; - } - final prefix = contact.publicKey.first; - candidatesByPrefix.putIfAbsent(prefix, () => []).add(contact); - } - for (final candidates in candidatesByPrefix.values) { - candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); - } - final startPoint = + final endpoint = (connector.selfLatitude != null && connector.selfLongitude != null) ? LatLng(connector.selfLatitude!, connector.selfLongitude!) : null; - var previousPosition = startPoint; - final distance = Distance(); - var lastDistance = 0.0; - var bestDistance = 0.0; + final resolvedContacts = PathHopResolver.resolve( + pathBytes: pathBytes, + contacts: connector.allContacts, + endpoint: endpoint, + resolveFromEnd: resolveFromEnd, + ); + final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { - final searchPoint = i == 0 ? startPoint : previousPosition; - final candidates = candidatesByPrefix[pathBytes[i]]; - Contact? contact; - if (candidates != null && candidates.isNotEmpty) { - var bestIndex = 0; - if (searchPoint != null) { - bestDistance = double.infinity; - for (var j = 0; j < candidates.length; j++) { - final candidate = candidates[j]; - if (!candidate.hasLocation || - candidate.latitude == null || - candidate.longitude == null) { - continue; - } - final currentDistance = distance( - searchPoint, - LatLng(candidate.latitude!, candidate.longitude!), - ); - if (currentDistance < bestDistance) { - bestDistance = currentDistance; - bestIndex = j; - } - } - } - contact = candidates.removeAt(bestIndex); - if (candidates.isEmpty) { - candidatesByPrefix.remove(pathBytes[i]); - } - } - + final contact = resolvedContacts[i]; final resolvedPosition = _resolvePosition(contact); - if (resolvedPosition != null) { - previousPosition = resolvedPosition; - } - // If the best candidate is much farther than the previous hop, it's likely not the correct match. - if (lastDistance + bestDistance > 50000 && - candidates != null && - candidates.isNotEmpty) { - i--; - lastDistance = bestDistance; - continue; - } - lastDistance = bestDistance; - hops.add( _PathHop( index: i + 1, diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 8fb4cf95..415562c3 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -176,18 +176,32 @@ class _DiscoveryScreenState extends State { return ListEntrance( index: index, child: MeshCard( - onTap: () { - connector.importDiscoveredContact(contact); - showDismissibleSnackBar( - context, - content: Text( - context.l10n.discoveredContacts_contactAdded, - ), - action: SnackBarAction( - label: context.l10n.common_undo, - onPressed: () => connector.removeContact(contact), - ), - ); + onTap: () async { + try { + final imported = await connector.importDiscoveredContact(contact); + if (!context.mounted) return; + if (!imported) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), + ); + return; + } + showDismissibleSnackBar( + context, + content: Text(context.l10n.discoveredContacts_contactAdded), + action: SnackBarAction( + label: context.l10n.common_undo, + onPressed: () => connector.removeContact(contact), + ), + ); + } catch (_) { + if (!context.mounted) return; + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), + ); + } }, onLongPress: () => _showContactContextMenu(contact, connector), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), @@ -247,7 +261,9 @@ class _DiscoveryScreenState extends State { Icon( Icons.location_on, size: 13, - color: scheme.onSurfaceVariant.withValues(alpha: 0.55), + color: scheme.onSurfaceVariant.withValues( + alpha: 0.55, + ), ), ], if (contact.rawPacket != null) ...[ @@ -255,7 +271,9 @@ class _DiscoveryScreenState extends State { Icon( Icons.cell_tower, size: 13, - color: scheme.onSurfaceVariant.withValues(alpha: 0.55), + color: scheme.onSurfaceVariant.withValues( + alpha: 0.55, + ), ), ], ], diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index a15b9c7d..56341598 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -39,8 +39,12 @@ class RetryServiceConfig { final void Function(Message) updateMessage; final Function(Contact)? clearContactPath; final Function(Contact, Uint8List, int)? setContactPath; - final int Function(int pathLength, int messageBytes, {String? contactKey})? - calculateTimeout; + final int Function( + int pathLength, + int messageBytes, { + String? contactKey, + int? deviceTimeoutMs, + })? calculateTimeout; final Uint8List? Function()? getSelfPublicKey; final String Function(Contact, String)? prepareContactOutboundText; final AppSettingsService? appSettingsService; @@ -74,6 +78,12 @@ class RetryServiceConfig { class MessageRetryService extends ChangeNotifier { static const int maxAckHistorySize = 100; + + /// Global cap on concurrent in-flight messages across ALL contacts. + /// The firmware's expected_ack_table is a single 8-entry circular buffer + /// shared globally; cap at 6 to leave two slots of headroom. + static const int _maxGlobalInFlight = 6; + int _maxRetries = 5; int get maxRetries => _maxRetries; @@ -170,8 +180,9 @@ class MessageRetryService extends ChangeNotifier { _config?.addMessage(contact.publicKeyHex, message); - // Queue per contact — only one message in-flight at a time to avoid - // overflowing the firmware's 8-entry expected_ack_table. + // Queue per contact — one message in-flight per contact at a time, and + // bounded globally by _maxGlobalInFlight across all contacts so we never + // overflow the firmware's 8-entry global expected_ack_table. final contactKey = contact.publicKeyHex; _sendQueue[contactKey] ??= []; _sendQueue[contactKey]!.add(messageId); @@ -184,6 +195,11 @@ class MessageRetryService extends ChangeNotifier { } void _sendNextForContact(String contactKey) { + // Enforce the global in-flight cap before starting a new send. + // The firmware's expected_ack_table is a single 8-entry circular buffer + // shared across all contacts; exceeding it silently evicts an older slot. + if (_activeMessages.length >= _maxGlobalInFlight) return; + final queue = _sendQueue[contactKey]; if (queue == null) return; @@ -211,7 +227,16 @@ class MessageRetryService extends ChangeNotifier { if (_resolvedMessages.contains(messageId)) return; _resolvedMessages.add(messageId); _activeMessages.remove(messageId); + // Pump this contact's queue first, then any other contacts that are waiting. _sendNextForContact(contactKey); + for (final key in _sendQueue.keys) { + if (key == contactKey) continue; + if (_activeMessages.length >= _maxGlobalInFlight) break; + final queue = _sendQueue[key]; + if (queue != null && queue.isNotEmpty) { + _sendNextForContact(key); + } + } } PathSelection? _selectPathForAttempt(Message message, Contact contact) { @@ -352,6 +377,10 @@ class MessageRetryService extends ChangeNotifier { } bool updateMessageFromSent(int ackHash, int timeoutMs) { + // Firmware sets expected_ack = 0 for CLI/command sends (TXT_TYPE_CLI_DATA). + // No ACK will ever be issued for these, so arming a retry timer is wrong. + if (ackHash == 0) return false; + final config = _config; if (config == null) return false; @@ -404,13 +433,19 @@ class MessageRetryService extends ChangeNotifier { // Calculate timeout: prefer ML prediction, then device-provided, then physics fallback final pathLengthValue = message.pathLength ?? contact.pathLength; + final outboundTextForTimeout = + config.prepareContactOutboundText?.call(contact, message.text) ?? + message.text; + final messageBytesForTimeout = + utf8.encode(outboundTextForTimeout).length; int actualTimeout = timeoutMs; if (config.calculateTimeout != null) { actualTimeout = config.calculateTimeout!( pathLengthValue, - message.text.length, + messageBytesForTimeout, contactKey: contact.publicKeyHex, + deviceTimeoutMs: timeoutMs > 0 ? timeoutMs : null, ); } @@ -617,7 +652,6 @@ class MessageRetryService extends ChangeNotifier { for (final expectedHash in expectedHashes) { if (expectedHash == ackHash) { matchedMessageId = messageId; - matchedAttemptIndex = expectedHashes.indexOf(expectedHash); break; } } @@ -669,10 +703,18 @@ class MessageRetryService extends ChangeNotifier { if (config?.onDeliveryObserved != null && tripTimeMs > 0 && message.pathLength != null) { - config!.onDeliveryObserved!( + final outboundTextForObserved = + config!.prepareContactOutboundText?.call( + contact, + message.text, + ) ?? + message.text; + final messageBytesForObserved = + utf8.encode(outboundTextForObserved).length; + config.onDeliveryObserved!( contact.publicKeyHex, message.pathLength!, - message.text.length, + messageBytesForObserved, tripTimeMs, ); } diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index fc81c565..7a61e3f6 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -134,10 +134,12 @@ class PathHistoryService extends ChangeNotifier { newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight); } else { newWeight = currentWeight - failureDecrement; - if (newWeight <= 0) { + if (newWeight <= 0 && failureCount >= 3) { removePathRecord(contactPubKeyHex, selection.pathBytes); return; } + // Keep the record with a small floor weight until we have enough evidence + newWeight = newWeight.clamp(0.1, maxWeight); } _addPathRecord( diff --git a/lib/services/timeout_prediction_service.dart b/lib/services/timeout_prediction_service.dart index d92ca643..f007e51c 100644 --- a/lib/services/timeout_prediction_service.dart +++ b/lib/services/timeout_prediction_service.dart @@ -63,12 +63,15 @@ class TimeoutPredictionService extends ChangeNotifier { required int tripTimeMs, int secondsSinceLastRx = 0, }) { + final isFlood = pathLength < 0; final observation = DeliveryObservation( contactKey: contactKey, - pathLength: pathLength, + // Clamp to 0 for flood so the hop-count slope is learned from direct paths + // only; isFlood carries the flood signal as a separate feature. + pathLength: isFlood ? 0 : pathLength, messageBytes: messageBytes, secondsSinceLastRx: secondsSinceLastRx, - isFlood: pathLength < 0, + isFlood: isFlood, deliveryMs: tripTimeMs, timestamp: DateTime.now(), ); @@ -76,11 +79,12 @@ class TimeoutPredictionService extends ChangeNotifier { _observations.add(observation); if (_observations.length > maxObservations) { _observations.removeAt(0); + _rebuildContactStats(); + } else { + _contactStats.putIfAbsent(contactKey, () => _ContactStats()); + _contactStats[contactKey]!.add(tripTimeMs.toDouble()); } - _contactStats.putIfAbsent(contactKey, () => _ContactStats()); - _contactStats[contactKey]!.add(tripTimeMs.toDouble()); - _observationsSinceLastTrain++; if (_observationsSinceLastTrain >= _retrainInterval && _observations.length >= minObservations) { @@ -108,11 +112,14 @@ class TimeoutPredictionService extends ChangeNotifier { try { if (_activeFeatures.isEmpty) return null; + final flood = pathLength < 0; final allFeatures = { - 'pathLength': pathLength.toDouble(), + // Clamp to 0 for flood — mirrors recordObservation so training and + // prediction see the same pathLength values; isFlood carries the signal. + 'pathLength': flood ? 0.0 : pathLength.toDouble(), 'messageBytes': messageBytes.toDouble(), 'secSinceRx': secondsSinceLastRx.toDouble(), - 'isFlood': pathLength < 0 ? 1.0 : 0.0, + 'isFlood': flood ? 1.0 : 0.0, }; final row = _activeFeatures.map((f) => allFeatures[f]!).toList(); @@ -164,7 +171,9 @@ class TimeoutPredictionService extends ChangeNotifier { // (ml_algo's OLS produces all-zero coefficients for singular matrices) final allNames = ['pathLength', 'messageBytes', 'secSinceRx', 'isFlood']; final allExtractors = [ - (o) => o.pathLength.toDouble(), + // pathLength is already clamped to >=0 in recordObservation, but guard + // here as well for any observations loaded from older persisted data. + (o) => o.pathLength < 0 ? 0.0 : o.pathLength.toDouble(), (o) => o.messageBytes.toDouble(), (o) => o.secondsSinceLastRx.toDouble(), (o) => o.isFlood ? 1.0 : 0.0, @@ -215,6 +224,9 @@ class TimeoutPredictionService extends ChangeNotifier { @override void dispose() { + if (_persistTimer?.isActive == true) { + _storage?.saveDeliveryObservations(_observations); + } _persistTimer?.cancel(); super.dispose(); } diff --git a/test/helpers/path_hop_resolver_test.dart b/test/helpers/path_hop_resolver_test.dart new file mode 100644 index 00000000..8e903bc6 --- /dev/null +++ b/test/helpers/path_hop_resolver_test.dart @@ -0,0 +1,94 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:meshcore_open/helpers/path_hop_resolver.dart'; +import 'package:meshcore_open/models/contact.dart'; + +Contact _contact({ + required int prefix, + required String name, + required double latitude, + required double longitude, + DateTime? lastSeen, +}) { + return Contact( + publicKey: Uint8List(32)..first = prefix, + name: name, + type: advTypeRepeater, + pathLength: 0, + path: Uint8List(0), + latitude: latitude, + longitude: longitude, + lastSeen: lastSeen ?? DateTime.utc(2026), + ); +} + +void main() { + test('received paths resolve hash conflicts from the receiver backward', () { + final nearReceiver = _contact( + prefix: 0xAA, + name: 'Near receiver', + latitude: 0, + longitude: 0.1, + ); + final nearPreviousHop = _contact( + prefix: 0xBB, + name: 'Near previous hop', + latitude: 0, + longitude: 1.1, + ); + final wrongConflict = _contact( + prefix: 0xBB, + name: 'Near receiver but wrong', + latitude: 0, + longitude: 0.2, + ); + final previousHop = _contact( + prefix: 0xCC, + name: 'Previous hop', + latitude: 0, + longitude: 1, + ); + + final resolved = PathHopResolver.resolve( + pathBytes: const [0xBB, 0xCC, 0xAA], + contacts: [nearReceiver, nearPreviousHop, wrongConflict, previousHop], + endpoint: const LatLng(0, 0), + resolveFromEnd: true, + ); + + expect(resolved.map((contact) => contact?.name), [ + 'Near previous hop', + 'Previous hop', + 'Near receiver', + ]); + }); + + test('falls back to the most recently seen conflict without locations', () { + final older = Contact( + publicKey: Uint8List(32)..first = 0xAA, + name: 'Older', + type: advTypeRepeater, + pathLength: 0, + path: Uint8List(0), + lastSeen: DateTime.utc(2025), + ); + final newer = Contact( + publicKey: Uint8List(32)..first = 0xAA, + name: 'Newer', + type: advTypeRepeater, + pathLength: 0, + path: Uint8List(0), + lastSeen: DateTime.utc(2026), + ); + + final resolved = PathHopResolver.resolve( + pathBytes: const [0xAA], + contacts: [older, newer], + ); + + expect(resolved.single?.name, 'Newer'); + }); +} From 7da4e683847be614984c5544c9bbd692004fb396 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 13 Jun 2026 00:38:00 -0700 Subject: [PATCH 11/16] remove md --- BUGS_FOUND.md | 362 -------------------------------------------------- 1 file changed, 362 deletions(-) delete mode 100644 BUGS_FOUND.md diff --git a/BUGS_FOUND.md b/BUGS_FOUND.md deleted file mode 100644 index 7553b192..00000000 --- a/BUGS_FOUND.md +++ /dev/null @@ -1,362 +0,0 @@ -# MeshCore Open — Bugs Found (Web build, manual QA) - -Session: 2026-06-12 · Build served at `http://localhost:42751/` (Flutter web, debug/DDC) · Browser: Chrome - -Each entry: **Severity** · where · what · repro · expected. - ---- - -## Status -Fixes for BUG-1..4 applied on 2026-06-12 (analyze clean). **BUG-2/3 VERIFIED FIXED live:** USB device `VID:239A PID:8029` now connects on web — console shows `Open: vendorId=0x239a uartBridge=false (DTR left default)` → `Got SELF_INFO` → `connectUsb: complete`, no "device has been lost". Continuing to test the now-reachable connected screens. See "Fixes applied" at the bottom. - -## Open bugs - -### BUG-1 · Medium · USB connect — raw browser exception leaked to UI on picker cancel -- **Where:** "Connect over USB" screen (`usb_screen.dart`), tap **Select a USB device** → Web Serial port picker → dismiss/cancel without choosing a port. -- **What:** A red error snackbar shows the raw browser API string: `NotFoundError: Failed to execute 'requestPort' on 'Serial': No port selected by the user.` -- **Why it's a bug:** (1) Cancelling the port picker is a normal user action, not an error — it should be silent (or a neutral "No device selected" message), not a red error toast. (2) Even for real failures, leaking a raw JS `DOMException` string is poor UX. The catch handler should map known cases (user cancellation) and present friendly copy. -- **Expected:** Cancelling shows nothing or an info-level message; the UI never prints raw exception text. -- **Root cause (confirmed in source):** `usb_screen.dart:396 _friendlyErrorMessage()` maps `PlatformException`/`StateError`/etc., but the web cancel path throws a JS `DOMException` (`NotFoundError`) that matches none of the branches, so it falls through to `return error.toString();` (line 441). A friendly `l10n.usbErrorNoDeviceSelected` string already exists (line 428) but is only matched for a native `StateError` containing `'No USB serial device selected'`, not the web DOMException. Fix: detect the web no-port-selected case and route it to the existing friendly string (or suppress entirely). - -### BUG-2 · HIGH · Web USB connect fails with misleading "Timed out waiting for SELF_INFO" — read-pump error is dropped by a subscription race -- **Where:** `usb_serial_service_web.dart` + `meshcore_connector.dart connectUsb()`. Repro: on web (Chrome), connect to a USB serial device (observed with `Web Serial Device VID:239A PID:8029`, an Adafruit/nRF52840 board). -- **Observed (console, happens every attempt):** - 1. Port opens OK: `USB serial opened port=Web Serial Device (VID:239A PID:8029)` - 2. **Immediately:** `_pumpReads error: NetworkError: The device has been lost.` → `_pumpReads: ended` — the read stream dies the instant it opens. - 3. Connect logic ignores that and proceeds: `requesting device info…`, writes TX frames, `ChannelSync Starting sync for 40 channels`. - 4. Nothing can ever be read back → SELF_INFO + ChannelSync retry/timeout for ~7s. - 5. Ends with `USB connection error: Bad state: Timed out waiting for SELF_INFO during connect` → disconnect. User sees a generic timeout error, not the real cause. -- **Root cause (confirmed in source):** a **subscription-timing race on a broadcast stream**: - - `_frameController` is `StreamController.broadcast()` (`usb_serial_service_web.dart:24-25`). Broadcast streams **do not buffer** — events emitted with no listener attached are silently discarded. - - `_usbManager.connect()` starts `_pumpReads()` fire-and-forget (`usb_serial_service_web.dart:114`). On this device `_pumpReads` errors instantly and calls `_addFrameError()` (line 387/393). - - But `connectUsb()` attaches its error listener **only after** `await Future.delayed(200ms)` (`meshcore_connector.dart:1609`), then `frameStream.listen(onError: → disconnect(), onDone: → disconnect())` (lines 1610-1620). The read-pump error fired during that 200ms gap, so the `onError → disconnect` fail-fast path **never runs**. - - With the safety net disarmed, connect falls through to `_waitForSelfInfo` (3s) + retry (3s) and throws the misleading `Timed out waiting for SELF_INFO during connect` (line 1647). -- **Expected:** A read-stream failure during connect should abort immediately with the real cause ("USB device disconnected / lost"), not a 7-second generic SELF_INFO timeout. -- **Fix directions:** attach the `frameStream` listener (or otherwise observe transport health) *before* the read pump can emit — i.e. before/at port-open, not 200ms later; OR latch the last transport error in the service and have `connectUsb` check it; OR make connect race `_waitForSelfInfo` against a transport-error future. Remove/justify the unconditional 200ms delay that opens the race window. - -### BUG-3 · Likely device-level root cause for BUG-2 · DTR assertion may reset/lose nRF52840 (Adafruit, VID 0x239A) boards -- **Where:** `usb_serial_service_web.dart:285-295 _openPort()`. -- **What:** Immediately after `open()`, the code asserts `setSignals({dataTerminalReady:true, requestToSend:false})` with the comment *"Prevent ESP32 USB-CDC reset"*. That logic is tuned for ESP32. On Adafruit nRF52840 boards (VID `0x239A`, as seen here, PID `0x8029`), toggling DTR is associated with the bootloader/reset line and can cause the device to re-enumerate/reset — which plausibly produces the immediate `NetworkError: The device has been lost.` seen in BUG-2. -- **Status:** Strong hypothesis, not yet isolated (would need to test connecting with the `setSignals` call removed/varied for this board). Flagging because the device class that fails (nRF52840/Adafruit) is exactly the one where DTR semantics differ from ESP32. -- **Expected:** USB serial open should work for nRF52840-class MeshCore boards on web, or DTR handling should be conditional per device/VID. - -### BUG-4 · Medium · BLE "Scan" on web (no Bluetooth adapter / unsupported) gives zero feedback -- **Where:** Scanner screen (`scanner_screen.dart`). Repro: on web with no BLE module (or any web build, since `flutter_blue_plus` doesn't support web), tap **Scan**. -- **What:** Nothing happens — no spinner, no "scanning" state, no error toast, no "Bluetooth unavailable on web" message. The button just sits there and status stays "Not connected". (Confirmed by user: "its not connecting on chrome and my computer doesn't have a ble module".) -- **Expected:** Either disable/hide BLE scan on web (the app already gates non-Chrome via `ChromeRequiredScreen`; Chrome+web still can't use `flutter_blue_plus`), or show a clear "Bluetooth isn't available in the browser — use USB or TCP" message. Silent no-op leaves the user stuck. - -### BUG-5 · Low/Medium · Notifications never work until manually enabled; every incoming message logs an error -- **Where:** `notification_service.dart` — `show()` calls at lines 201/248/306/394/593 are made without ensuring notification permission was granted. -- **What (observed in console while connected):** repeated `Failed to show channel notification: Bad state: FlutterLocalNotifications.show(): You must request notifications permissions first` and the same for advert/message notifications. Each incoming channel message / advert triggers one. -- **Why:** permission is only ever requested from `app_settings_screen.dart:239` (when the user interacts with that setting). If the user never visits it, `requestPermissions()` is never called, so `show()` throws on web (and would on Android 13+ with denied permission). The errors are swallowed by `try/catch` (no crash), but notifications silently never fire and the log fills with errors. -- **Expected:** request notification permission during init / on first connect (or check `areNotificationsEnabled()` and skip `show()` when not granted) instead of calling `show()` unconditionally and relying on a caught exception per message. - -### OBS-1 · RESOLVED (not a bug) · USB-on-web connection dropped after ~4 min (`device has been lost`) -- **What:** At 9:25:13 the read pump errored `NetworkError: The device has been lost.` → `USB transport error` → clean auto-disconnect back to the scanner. -- **Cause:** confirmed by user — they physically dropped/disconnected the radio. So this was a real physical disconnect, NOT a Web Serial stability issue. Positive finding: the app (with the BUG-2 fix) handled an abrupt physical disconnect cleanly and returned to the scanner. - -### BUG-6 · Medium · Battery Chemistry setting permanently disabled on USB (and TCP) connections -- **Where:** App Settings → BATTERY → Battery Chemistry. `app_settings_screen.dart:610-611` gates the control on `connector.deviceId != null`; `meshcore_connector.dart` only ever assigns `_deviceId` in the **BLE** connect path (`_deviceId = device.remoteId.toString()`, line 1839, right after `_activeTransport = bluetooth`). -- **What:** When connected over USB (verified) — and by the same logic TCP — `connector.deviceId` is `null`, so the Battery Chemistry dropdown shows the subtitle "Connect to a device to choose" and is disabled, even though the device is fully connected (header shows `088EDAA0 · Connected`, radio stats live). -- **Why it matters:** USB/TCP users can never set per-device battery chemistry, so the battery percentage indicator uses the wrong voltage curve for their pack. Also user-confusing ("connect first" while connected). -- **Note:** The `088EDAA0` shown throughout the UI is a node/public-key-derived identifier, distinct from `_deviceId` (the BLE remoteId). The battery setting keys off the BLE-only `_deviceId`. -- **Fix direction:** populate a stable per-device identifier in the USB/TCP connect paths (e.g. the node public-key prefix already used for storage scoping), or key battery chemistry off that same node identity rather than the BLE remoteId. - -### BUG-7 · Low/Medium · Node Name can be saved empty (no validation) -- **Where:** Settings → Node Name dialog, `settings_screen.dart:708-744 _editNodeName()`. -- **What:** Clearing the field leaves the **Save** button enabled; the handler (line 728-740) calls `connector.setNodeName(controller.text)` directly with no `trim()`/empty check. An empty or whitespace-only name can be written to the device, leaving the node nameless on the mesh (others see a blank contact). Reproduced: cleared field → "0/31", Save still active. (Did not actually save.) -- **Compare:** the Radio Settings dialog correctly disables Save on invalid input — Node Name should do the same. -- **Expected:** disable Save (or show an error) when the trimmed name is empty. - -### OBS-2 · Watch · Auto-reconnect to cached Web Serial port fails after a physical drop (stale handle) -- **What:** After physical disconnects, the app auto-retries the cached port (`web:port:1`) and fails with `NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port` (seen at 9:26:44, 9:34:23, 9:34:53, ~30s apart). It does eventually recover (header shows Connected again), but the cached `SerialPort` handle is often stale right after an OS-level drop. -- **Note:** the native USB service explicitly documents this class of problem and avoids caching the serial handle (`_freshSerial()` comment, `usb_serial_service_native.dart:47-50`). The web service caches the port object (`_authorizedPortsByKey`), so reopen-after-drop is more fragile. Partly environmental here (user was physically re-plugging), so logged as an observation, not a confirmed bug. Worth confirming the web reconnect path discards/re-requests the port on `open()` failure rather than retry-looping on a dead handle. - ---- - -## Notes / non-bugs -- **Radio Settings validation works:** out-of-range frequency (`9999`) shows "Invalid frequency (300-2500 MHz)" and disables Save. Good. -- **Graceful disconnect verified:** when the USB transport drops, the app auto-navigates back to the scanner and shows "Not connected" — matches the intended "handle disconnection gracefully" behavior. -- **Channel sync starts before handshake completes:** `ChannelSync Starting sync for 40 channels` fires during connect before `SELF_INFO` is confirmed (console 9:07:54). Likely harmless given BUG-2 masks it, but worth confirming the initial-sync pipeline shouldn't wait for SELF_INFO first. -- First load on `localhost:39107` rendered a blank white page and the URL later resolved to `localhost:42751`. This appears to be a dev-server startup/port artifact (the Flutter view never mounted on the first port), not an app bug. Flagging only in case the port hop is intentional behavior worth confirming. - ---- - -## Fixes applied (2026-06-12) - -- **BUG-3 (device-lost root cause):** `usb_serial_service_web.dart _openPort()` now only asserts `setSignals(DTR=true)` for known USB-UART bridge VIDs (`_uartBridgeVendorIds`: CP210x `0x10C4`, CH340 `0x1A86`, FTDI `0x0403`, PL2303 `0x067B`). Native-USB-CDC boards (nRF52840/Adafruit `0x239A`, etc.) are left untouched, since toggling DTR re-enumerates them. Added a debug log line reporting the detected vendorId and whether DTR was asserted. (The native service already documented this exact NRF52/DTR behavior — confirms the hypothesis.) -- **BUG-2 (dropped read-pump error / misleading timeout):** - - `usb_serial_service_web.dart _pumpReads()` now flips `_status = disconnected` and latches `_lastError` when the read loop dies unexpectedly, instead of leaving the service reporting "connected". - - Added `lastError` getter to both web + native `UsbSerialService` and to `MeshCoreUsbManager`. - - `meshcore_connector.dart connectUsb()` now checks `_usbManager.isConnected` right after the 200ms settle delay (before waiting on SELF_INFO) and throws a clear `USB device disconnected during connect: ` using the latched error — failing in ~200ms with the real reason instead of ~7s with a generic SELF_INFO timeout. -- **BUG-1 (raw exception on picker cancel):** `usb_screen.dart` added `_isUserCancelledPortPicker()`; `_showError()` now returns silently for picker cancellation (matches the web `requestPort`/"No port selected" DOMException and the native StateError) instead of showing a red toast with raw text. -- **BUG-4 (silent BLE scan on web):** `scanner_screen.dart _toggleScan()` now short-circuits on web and shows `scanner_bluetoothWebUnsupported` ("Bluetooth isn't available in the browser. Connect over USB instead.") instead of silently no-opping. New l10n key added to `app_en.arb` and regenerated for all locales (English fallback; pending auto-translation). - -## Fixes applied — round 2 (2026-06-12) - -- **BUG-5 (notifications never fire / error spam):** `notification_service.dart` added `_ensureCanNotify()` — caches whether the platform can actually post notifications and is now the gate on all four `show()` entry points (message/advert/channel/batch-summary). Returns false on web (the plugin has no web backend, so `show()` always threw) and honors `areNotificationsEnabled()` on Android 13+. `requestPermissions()` now refreshes the cache so enabling from settings takes effect immediately. The `cancel()` paths still use `_ensureInitialized()` (unchanged). Net: no more per-message error spam; notifications post when actually permitted. -- **BUG-6 (battery chemistry disabled on USB/TCP):** added `MeshCoreConnector.batteryDeviceKey` — returns the BLE remoteId when present (preserves existing BLE-keyed settings) and falls back to the node public key (`selfPublicKeyHex`) on USB/TCP. Both the internal `_batteryChemistryForDevice()` and the App Settings UI (`app_settings_screen.dart`) now key off `batteryDeviceKey` instead of the BLE-only `connector.deviceId`. Battery chemistry is now selectable on USB/TCP, and the battery-% curve is correct for those connections. -- **BUG-7 (empty node name savable):** `settings_screen.dart _editNodeName()` Save button is now wrapped in a `ListenableBuilder` on the text controller and is disabled (`onPressed: null`) when the trimmed name is empty; the handler saves the trimmed value. Mirrors the Radio Settings dialog's validation behavior. - -All four files analyze clean. Pending live re-verification after hot restart (BUG-5: no notification errors in console; BUG-6: Battery Chemistry enabled while on USB). - ---- - -## Suggestions / further work (2026-06-12) - -### Follow-ons directly implied by the fixes -1. **Audit other per-device features for the same BLE-only gap (BUG-6 was probably not alone).** `_deviceId` is set only in the BLE connect path; anything keyed off `connector.deviceId` or `_device?.remoteId` is silently BLE-only on USB/TCP. Grep those usages and verify each works on USB/TCP, or migrate them to `batteryDeviceKey` / `selfPublicKeyHex`. -2. **Battery-chemistry key inconsistency (tradeoff in my BUG-6 fix).** I kept BLE = remoteId and USB/TCP = public key to avoid wiping saved BLE settings. Consequence: the *same physical radio* gets two different chemistry settings depending on transport. Cleaner long-term: key everything off `selfPublicKeyHex` (the canonical per-radio identity used for all other scoped storage) with a one-time migration of existing BLE-keyed values. -3. **Web reconnect should discard a stale port handle (OBS-2).** After a physical drop, auto-reconnect retry-loops on the cached `SerialPort` with `Failed to execute 'open'` before recovering. The native service deliberately avoids caching the handle (`_freshSerial()` comment). On `open()` failure the web service should drop the cached port from `_authorizedPortsByKey` and re-request (or prompt) instead of retrying a dead handle. -4. **DTR allowlist may need expansion (caveat on my BUG-3 fix).** I now assert DTR only for known UART-bridge VIDs (CP210x/CH340/FTDI/PL2303). A future board with an unlisted bridge chip won't get DTR and could reset on open. Consider a per-board config or a user-visible "hold DTR" toggle as a fallback as more hardware is tested. -5. **On web, the notification toggles are ON but inert** (same spirit as BUG-4). Now that `_ensureCanNotify()` correctly skips web, the four NOTIFICATIONS switches in App Settings still read as enabled while doing nothing in-browser. Grey them out / annotate "not available in browser" on web. Also `requestPermissions()` still returns `true` on web (fallback `return true`), which is misleading to its callers. - -### Robustness / architecture -6. **The connect handshake rides on a `broadcast()` stream + an unconditional 200ms delay** — the exact combo that caused BUG-2. My fix (status flip + liveness check) closes the connect race, but any consumer that subscribes late can still miss early frames/errors. Consider readiness signaling instead of the magic delay, and/or buffering the first frames during connect. - -### UX polish observed -7. **Two "Scan" buttons on the scanner empty state** (center button + bottom-right FAB) — redundant. -8. **Verify destructive actions confirm before acting.** I intentionally did NOT trigger "Clear Chat", "Delete All Paths", "Reboot Device", or "Manage Repeater" (not my device). Worth confirming each has a confirmation dialog. -9. **Telemetry screen** had no obvious "request once" affordance — only autorefresh (interval 20 / qty 10 / Enable). A manual one-shot refresh would help. - -### Coverage gaps (NOT tested this session — unverified) -- Repeater management (hub / CLI / settings / status), Line-of-Sight, Path-Trace map, Community QR scanner. -- Discovery & Neighbors screens, Map cache screen, Debug log screens. -- TCP transport (no TCP device available), USB on native (Android/desktop), on-device translation (LLM). -- Contacts search/sort/filter, channel add/reorder, GPX export. - ---- - -## Styling observations (2026-06-12) -Caveat: only viewed **dark mode** in a **wide desktop browser** (~1298px). Light mode and narrow/mobile widths not assessed. - -**The color system is good — credit where due.** `mesh_theme.dart` is a coherent, semantic palette ("high-contrast slate surfaces with sky-blue accents"): slate surface ramp (`0B1220`→`334155`), sky-blue accent (`0EA5E9`), signal-green reserved for SNR only (`22C55E`), amber warn, red alert, magenta node type, a full ink ramp, and a separate light ramp. Not a generic default-Material look. (Note: the CLAUDE.md claim that MeshPalette/MeshTheme is "not currently wired" is **stale** — `main.dart:204-205` wires `MeshTheme.light()/.dark()`.) - -**Issues are layout/responsiveness, not color:** -1. **No max-width constraint on web/desktop (biggest one).** No `ConstrainedBox`/`maxWidth` in `main.dart`; the app is mobile-first and stretches edge-to-edge on a wide window — chat input spans the full ~1300px, list rows are very wide, chat bubbles hug the far edges, settings rows stretch across. Recommend a centered max-width content column (phone-like, ~480–600px) or responsive breakpoints for the web build. -2. **App-bar title alignment is inconsistent.** Main tab screens (Channels/Contacts/Map) use left-aligned titles with a device-id subtitle; detail screens use `centerTitle: true` (23 files) or `AdaptiveAppBarTitle`. Reads slightly inconsistent screen-to-screen — worth one deliberate rule. -3. **Tall-viewport chat spacing.** Bottom-anchored chat leaves a large empty area at the top on a tall window (related to #1 — no height/width framing for big viewports). -4. **Minor:** map node-detail sheet stacked over the older bottom info bar (z-order overlap); the two redundant Scan buttons are also a visual duplication; verify disabled-control contrast is legible (the pre-fix battery dropdown was quite dim). -5. **Unverified:** light theme polish (light ramp exists in the palette but wasn't viewed). - ---- - -# Retry & Path-Selection Analysis (2026-06-12, code review vs firmware) - -Static review of the ACK/retry/path-selection pipeline (`message_retry_service.dart`, `path_history_service.dart`, `timeout_prediction_service.dart`, connector wiring) cross-checked against the MeshCore C++ firmware at `/mnt/Gaming/meshcore/MeshCore`. Verified against actual firmware source, not just docs. - -**What's correct (verified):** -- **ACK-hash algorithm matches the firmware exactly.** Client `computeExpectedAckHash` (`message_retry_service.dart:104-136`) = `SHA256(timestamp[4 LE] ‖ (attempt&3) ‖ text ‖ sender_pubkey[32])`, first 4 bytes as LE uint32. Firmware `BaseChatMesh.cpp:413-419` builds `temp[0..3]=timestamp`, `temp[4]=attempt&3`, `temp[5..]=text` (null *not* hashed — length passed is `5+text_len`), hashed with `self_id.pub_key`. Byte order and endianness round-trip correctly (`RESP_CODE_SENT`/`PUSH_CODE_SEND_CONFIRMED` both LE uint32, `meshcore_connector.dart:5265-5409`). -- **Retry is entirely the client's job** — firmware `sendMessage` sends once, no retry loop. Correct division of responsibility. -- **Per-contact in-flight serialization** (`_sendQueue`, `message_retry_service.dart:174-208`) is the right idea — see RETRY-1 for why it's not *sufficient*. - -## Open findings - -### RETRY-1 · Medium · Per-contact in-flight cap doesn't protect the firmware's *global* 8-entry ACK table -- **Where:** `message_retry_service.dart:174-183` (one in-flight message *per contact*) vs firmware `examples/companion_radio/MyMesh.h:248-250` + `MyMesh.cpp:1100-1103`. -- **What:** The firmware's `expected_ack_table` is a **single global circular buffer of 8 entries** with a global `next_ack_idx`, shared across *all* contacts. The client only guarantees ≤1 in-flight *per contact*, so messaging **9+ contacts concurrently** (e.g. several active conversations whose retries overlap, or a broadcast-style burst) puts >8 entries in flight. The firmware then overwrites the oldest slot (`next_ack_idx = (next_ack_idx+1) % 8`) with no rejection — the evicted message's ACK expectation is silently dropped. -- **Consequence:** the evicted send never produces a `PUSH_CODE_SEND_CONFIRMED`, so the client times out and retries a message the radio already sent (wasted airtime), and can mark as **failed** a message that actually delivered. -- **The code comment is misleading:** `message_retry_service.dart:173-174` says serialization exists "to avoid overflowing the firmware's 8-entry expected_ack_table" — but a per-contact cap does not bound the global table. -- **Fix:** enforce a **global** concurrent-in-flight cap (≤8, ideally ~6 for headroom) across all contacts, not just per-contact; or track `next_ack_idx` pressure and back-pressure new sends. - -### RETRY-2 · Medium · Firmware's reported `est_timeout` is silently discarded -- **Where:** `meshcore_connector.dart calculateTimeout()` (4379-4408); `message_retry_service.dart:405-415`. -- **What:** `RESP_CODE_SENT` carries the device's own `est_timeout` (firmware `MyMesh.cpp:1106-1110`, computed from real `getEstAirtimeFor(...)`). It's parsed into `timeoutMs` and passed to `updateMessageFromSent` as the default `actualTimeout`. But `config.calculateTimeout` is **always** set, so the device value is immediately overwritten on every message — and when the ML model has no data, `calculateTimeout` returns `physicsMax` (line 4407), **never the device-provided value**. -- **Why it's a bug:** the docstring at `message_retry_service.dart:405` ("prefer ML prediction, then device-provided, then physics fallback") describes a tier that doesn't exist — device-provided is never used. The physics fallback re-derives the firmware's flood/direct formulas, but `_estimateAirtimeMs` falls back to a hard-coded **50 ms** when radio params (freq/bw/sf/cr) aren't yet known (`meshcore_connector.dart:4341-...`), which can diverge sharply from the device's actual airtime estimate (e.g. SF12). The device already did this math correctly with the real airtime. -- **Fix:** use the device `timeoutMs` as the fallback when ML is unavailable, and as the clamp ceiling when ML is present — it's strictly better information than a 50 ms guess. - -### RETRY-3 · Medium · Premature ML timeouts can delete good routes (timeout floor too low + aggressive weight decay) -- **Where:** `meshcore_connector.dart _physicsMinTimeout` (≈4360-4375); `message_retry_service.dart:491-538`; `path_history_service.dart:131-141`. -- **What:** For direct paths the timeout floor is `airtime*(hops+1)` — it omits the firmware's base (`SEND_TIMEOUT_BASE_MILLIS=500`) and per-hop processing terms (`6*airtime+250` per hop). So an ML prediction clamps as low as that floor, well under the firmware's own `500 + (6*airtime+250)*hops`. When the timer fires before an ACK could realistically return, `_handleTimeout` records a **false failure** → `recordPathResult(success:false)` decrements `routeWeight` by 0.5. A fresh path starts at weight 1.0, so **two** false timeouts drive it to ≤0 and `removePathRecord` deletes it (`path_history_service.dart:136-140`). Net: an overly tight timeout estimator actively erodes the learned route table — the opposite of what the ML system is for. -- **Fix:** raise the direct-path min floor to include the firmware base + per-hop terms (don't let ML clamp below a physically plausible RTT); and require more evidence (e.g. a higher failure count, or a confirmed `MSG_SEND` with no ACK over multiple attempts) before deleting a route rather than after two timer fires. - -### RETRY-4 · Low · `handleAckReceived` fallback computes a bogus `attemptIndex` -- **Where:** `message_retry_service.dart:613-626` (fallback branch), uses `expectedHashes.indexOf(expectedHash)` as the attempt index. -- **What:** `_expectedAckHashes[messageId]` is de-duplicated on insert (line 401), and because both firmware and client mask `attempt & 0x03`, attempts 0/4/8 (and 1/5/9, …) hash to the **same** value. So the list index is neither the real attempt number nor stable. The derived `matchedAttemptIndex` is only used to pick which attempt's `PathSelection` gets credited for the delivery — so the impact is limited to path-attribution accuracy on the fallback path (the primary `_ackHashToMessageId` mapping carries the correct snapshot). Low severity, but the value is simply wrong. -- **Fix:** store the attempt index alongside each expected hash (or just reuse the snapshot in `_ackHashToMessageId`) instead of inferring it from a list position. - -### RETRY-5 · Low · ML model feeds flood as `pathLength = -1`, polluting the linear coefficient -- **Where:** `timeout_prediction_service.dart:59-98` (observation), `161-200` (training). -- **What:** Flood deliveries are recorded with `pathLength: -1` **and** `isFlood: true`, then both are fed as features into a single global OLS regressor. A linear term that sees hop counts of `-1, 0, 1, 2, 3, …` with a discontinuity at the flood case distorts the `pathLength` coefficient; the `isFlood` dummy only partially compensates (it shifts the intercept, not the slope). Direct-path timeout predictions are therefore biased by flood observations and vice-versa. -- **Fix:** train separate flood vs direct models, or set `pathLength = 0` (or a dedicated flood feature only) when `isFlood`, so the hop-count slope is learned from direct paths alone. - -### RETRY-6 · Low / verify · ACK hash is computed over SMAZ-transformed text — the sent frame must use byte-identical text -- **Where:** `message_retry_service.dart:328-351` — hash uses `prepareContactOutboundText(contact, text)` (SMAZ-encoded), but `config.sendMessage(contact, message.text, …)` is handed the **raw** text; correctness depends on `_sendMessageDirect`/`buildSendTextMsgFrame` applying the *exact same* SMAZ transform downstream. -- **Why it matters:** if those two paths ever diverge, or if a message exceeds the firmware's `MAX_TEXT_LEN = 160` and gets truncated device-side, the receiver hashes different bytes than the client predicted → the ACK never matches → the message retries to `failed` even though it was delivered. This is a silent, hard-to-diagnose failure mode. -- **Fix:** compute the hash from the *same* byte buffer that is actually framed and sent (single source of truth), and add a unit test that asserts hash-over-frame-text == expected ACK hash, including a >160-byte/SMAZ case. - -## Observations (heuristics, not correctness bugs) -- **Flood path attribution credits a path the send didn't use.** On a successful *flood* delivery, `_recordPathResult` (`meshcore_connector.dart:1309-1354`) boosts the weight of `contact.path` (the device's *current* path) even though the message was flooded, not routed over that path. It's a reasonable "the ACK probably came back this way" heuristic, but (a) it inflates weight on an unexercised route, and (b) it reads `contact.path` which the path-return packet triggered by this very ACK may have just mutated (hence the `unawaited(getContactByKey(...))` re-fetch — a race the code papers over). Worth documenting as a heuristic and considering attributing only when the device path is confirmed fresh. -- **`calculateDefaultTimeout` (`message_retry_service.dart:713-719`)** appears to be legacy/unused by the active retry path (which always goes through `config.calculateTimeout`). If dead, remove it to avoid confusion about which timeout logic is authoritative. - -## How the system could be improved -1. **Single global in-flight budget tied to the firmware's table size.** Replace the per-contact gate with a global semaphore of N (≤8) outstanding ACKs, with a small reserve. This directly models the firmware constraint and eliminates RETRY-1. -2. **Make the device timeout authoritative, ML advisory.** Use the firmware's `est_timeout` as the baseline (it has the real airtime), and let the ML model only *widen* it when history shows this contact/path is consistently slower. Never let ML clamp below a physically plausible RTT (fixes RETRY-2 + RETRY-3). -3. **Decouple "timed out" from "route failed."** A timeout is weak evidence (could be transient congestion). Track per-path *consecutive* failures and only decay/delete a route after repeated, independent failures — or after the firmware reports a genuine send failure. Add hysteresis so one slow ACK doesn't erase a known-good route. -4. **Separate latency models per route class** (flood vs direct, and ideally per hop-count bucket), or add the airtime estimate itself as a feature so the model learns a multiplier rather than re-deriving physics. Fixes RETRY-5 and makes predictions interpretable. -5. **One source of truth for outbound bytes.** Frame the message once, compute the ACK hash from those exact bytes, and reject/queue anything that would exceed `MAX_TEXT_LEN` *before* sending (rather than discovering it via never-arriving ACKs). Fixes RETRY-6. -6. **Confidence-based path selection.** `_scorePathRecord` weights reliability 0.45 / latency 0.25 / freshness 0.1 / routeWeight 0.2 with hard-coded constants. Consider surfacing these as tunables (some already are via `appSettings`) and using a Wilson lower-bound on the success ratio instead of the current `(s+1)/(n+2)` Laplace estimate, so a 1/1 path isn't ranked above a 20/22 path purely on the smoothed mean. -7. **Bound the late-ACK grace + 15-min mapping cleanup explicitly.** `handleAckReceived` cleans `_ackHashToMessageId` older than 15 min on every ACK (`message_retry_service.dart:589-599`); combined with the 30 s post-failure grace timer and `attempt&3` hash reuse, it's worth a test that a delivered-after-failed message resolves exactly once and never double-advances the send queue. - - -### Possible ML issues - - 1. High: training/prediction mismatch for secondsSinceLastRx. - _lastRxTime is reset when the ACK frame arrives, then the observation is recorded, making this feature effectively zero. Prediction measures it before - sending. - lib/connector/meshcore_connector.dart:3867 - - 3. Medium: incorrect message-size input. - Retry prediction and training use message.text.length, not UTF-8/transformed payload bytes. Unicode and compressed messages therefore receive incorrect - airtime inputs. - lib/services/message_retry_service.dart:405 - lib/services/message_retry_service.dart:669 - - 4. Medium: stale per-contact statistics. - Old observations are removed from the 100-item model window, but never removed from _contactStats. The global model adapts while contact averages remain - lifetime averages. - lib/services/timeout_prediction_service.dart:76 - - 5. Medium: model is underconstrained. - Ordinary linear regression starts with only 10 samples and up to four features, without outlier rejection or validation. Physics clamping limits damage, - but unstable predictions are still likely. - - 6. Low: pending observations may not persist. - dispose() cancels the delayed save without flushing it. - lib/services/timeout_prediction_service.dart:216 ---- - -# ACK & Message-Delivery Analysis (2026-06-12, code review vs firmware) - -Focused pass on ACK matching and message delivery (sending + receiving), cross-checked against the MeshCore firmware at `/mnt/Gaming/meshcore/MeshCore`. Companion of the earlier "Retry & Path-Selection" section — this one is about whether messages reliably reach `delivered`/`failed` and aren't lost or double-shown. - -**Verified correct (recording these to close earlier open questions):** -- **ACK-hash text consistency holds.** `prepareContactOutboundText` (SMAZ / Cyr2Lat) transforms the text, and the *same* transformed bytes are used both for the wire frame (`buildSendTextMsgFrame`, `meshcore_connector.dart:~1113`) and for the expected-ack hash (`message_retry_service.dart:328-338`). The firmware hashes the raw received text bytes (SMAZ is opaque to it), so both sides agree. This resolves the RETRY-6 "verify" concern (the >160-byte truncation guard is still worth adding). -- **Connect-time queue drain exists.** `_startPostChannelInitialQueuedMessageSync` (`meshcore_connector.dart:3835`) plus the `respCodeEndOfContacts` handler (`:3936-3941`) proactively call `syncQueuedMessages(force:true)` after the SELF_INFO→channels→contacts pipeline, so messages that arrived while disconnected are pulled on reconnect — not solely dependent on a live `PUSH_CODE_MSG_WAITING` tickle. -- **Firmware de-dups inbound packets.** `SimpleMeshTables::hasSeen` keeps a 160-entry packet-hash seen-table (`src/helpers/SimpleMeshTables.h:34-53`), so the same on-air packet arriving via multiple flood paths is dropped, not delivered twice. -- **Channel sends do reach `sent`.** `_handleOk` promotes the pending channel message on the firmware's generic OK (`meshcore_connector.dart:5370`), and the echo-heard path promotes it again (`:5990`). (So the "stuck pending on success" worry is unfounded — but see DELIVERY-1 for the error path.) - -## Open findings - -### DELIVERY-1 · Medium · A rejected (or lost-response) channel send is stuck "pending" forever -- **Where:** `_handleErrorFrame` (`meshcore_connector.dart:4007-4024`); `ChannelMessageStatus` has only `{pending, sent, failed}` and nothing ever sets `failed` for a channel message. -- **What:** When the firmware answers a `cmdSendChannelTxtMsg` with an error frame (e.g. `ERR_CODE_NOT_FOUND` for an unknown/unconfigured channel — firmware `MyMesh.cpp:1131-1135` writes an err frame on failure), the handler removes the message from `_pendingChannelSentQueue` (line 4023) **but never marks the message `failed`**. The same happens if the OK/ERR response is simply lost. Result: the bubble shows "pending/sending" indefinitely with no error, and there is no retry or timeout to resolve it. -- **Contrast:** direct messages have a full timeout→retry→`failed` path; channel messages have *no* failure path at all. -- **Fix:** in `_handleErrorFrame`, set the matching channel message to `failed` (mirror `_markPendingChannelMessageSentById`). Optionally add a short watchdog so a channel send with no OK/ERR within N seconds flips `pending`→`failed`. - -### DELIVERY-2 · Medium · Firmware offline queue (16) silently drops *contact* messages on overflow -- **Where:** firmware `examples/companion_radio/MyMesh.cpp:219-255` (`addToOfflineQueue`), `MyMesh.h:240` (`OFFLINE_QUEUE_SIZE 16`). -- **What:** Inbound messages are buffered in a 16-slot queue and tickled to the app via `PUSH_CODE_MSG_WAITING`. On overflow the firmware evicts the oldest **channel** message to make room; if all 16 queued entries are **contact/direct** messages, the new message is **silently dropped** — no error, no notification to the app. -- **Consequence:** a burst of direct messages while the app is backgrounded/slow/disconnected can lose messages with zero indication. The client drains in a loop on tickle and on connect (good), but it cannot recover a message the firmware already discarded. -- **Fix (client side):** drain as fast as possible — the current one-at-a-time request/response loop (`_requestNextQueuedMessage`) round-trips per message; consider keeping the pump saturated. There's no app-side way to detect the drop, so also worth raising upstream (a queue-overflow counter in firmware stats would let the app warn the user). - -### DELIVERY-3 · Low/Medium · Inbound contact-message de-dup can drop a *legitimate* message -- **Where:** `_handleIncomingMessage` de-dup (`meshcore_connector.dart:~4759-4772`): skips an incoming message if a message with the same `(timestampSeconds, text)` exists in the last 10 messages from that sender. -- **What:** Timestamps are second-resolution, so two genuinely distinct messages with identical text within the same second (e.g. sending "ok" twice) collide and the second is silently dropped. The firmware already de-dups true on-air duplicates at the packet-hash layer (see "verified correct"), so this app-layer heuristic is largely redundant *and* introduces a real (if uncommon) data-loss path; it also only looks back 10 messages, so a re-delivered queued message after a longer gap can slip through as a "new" duplicate. -- **Fix:** lean on the firmware's packet identity instead of `(timestamp,text)`, or narrow the heuristic (e.g. only treat as duplicate within a very short window AND when flagged as a flood repeat), so identical-text messages aren't lost. - -### DELIVERY-4 · Low · ACK match is 4-byte-hash-only, no sender identity, against a *global* 8-slot table -- **Where:** firmware `processAck` (`MyMesh.cpp:411-427`) — `memcmp(data, &expected_ack_table[i].ack, 4)` over 8 slots, clears the slot to 0 on first match. -- **What:** matching uses only the first 4 hash bytes with no check that the ACK came from the intended recipient, against the same global 8-entry circular table called out in RETRY-1. So (a) >8 concurrent in-flight sends evict a slot → that message never gets `PUSH_CODE_SEND_CONFIRMED` → client retries/false-fails; (b) a ~1/2^32-per-concurrent-send chance of a cross-message false confirm. Duplicate ACKs are handled safely (slot cleared after first match; the random 6th ACK byte doesn't affect the 4-byte compare). Low severity on its own; the real lever is RETRY-1's recommended **global** in-flight cap (≤8). - -### DELIVERY-5 · Info · Round-trip time is measured from queueing, not transmission -- **Where:** firmware `trip_time = getMillis() - expected_ack_table[i].msg_sent` (`MyMesh.cpp:417`), where `msg_sent` is stamped when the message is enqueued (`:1100`). -- **What:** the `tripTimeMs` the app receives (and feeds the ML timeout model) includes the firmware's TX-queue wait when the radio is busy, so observations are inflated under load. Compounds the ML-input issues already logged (RETRY-5 / the ML notes above). Not a delivery bug, but it skews timeout prediction. - -### DELIVERY-6 · Low / verify · Command/CLI sends use `expected_ack = 0` — must not ride the ACK retry path -- **Where:** firmware sets `expected_ack = 0` for `TXT_TYPE_CLI_DATA` and the recipient sends no ACK (`MyMesh.cpp:1088-1110`, `BaseChatMesh.cpp:244-252`); `RESP_CODE_SENT` then carries a zero hash. -- **What:** if any client path routes a CLI/repeater command through `MessageRetryService` (which computes a *non-zero* expected hash and waits for `PUSH_CODE_SEND_CONFIRMED`), it would never match → spurious retries and a false "failed". Need to confirm the repeater-CLI/command path is separate from the text-message ACK machinery (the connector does call `_recordPathResult` for "repeater command results" — worth tracing that it isn't also arming an ACK wait). -- **Fix/verify:** ensure command sends bypass the expected-ack/retry tracking entirely, or special-case `expected_ack == 0` in `updateMessageFromSent` as "no ACK expected → mark sent, don't wait". - ---- - -# Live Log Analysis — "JC Room Server" session (2026-06-12) - -Real device log (BLE/USB, auto route rotation ON) sending to a multi-hop room server. This is a near-perfect live reproduction of RETRY-2, RETRY-3, and RETRY-5. The ACK *matching* is correct (every `RESP_CODE_SENT` matches its message); what's broken is **timeout estimation and path rotation** — the app declares failures and retries messages that are actually still in flight. - -### Evidence by symptom - -1. **Flood timeout = 151 seconds (RETRY-5, live).** - `raw prediction=100638ms for pathLength=-1 → ML timeout 150959ms`. Feeding `pathLength=-1` into the linear model makes the flood case extrapolate off a cliff; the `.clamp(physicsMin, physicsMax)` didn't help because the flood `physicsMax` (`500 + 16·airtime`) was also huge. A message that falls back to flood effectively hangs for ~2.5 minutes. - - Also seen: `raw prediction=-3467ms for pathLength=1, messageBytes=172` — a **negative** predicted time. Discarded by the `<=0` guard, but proves the OLS model is unstable/underconstrained (matches the ML notes). - -2. **Premature timeouts on a working route (RETRY-3, live).** - `"Its going well"` was **delivered in 16037ms**, but attempt 0/1 used timeouts of **14420ms / 10240ms** — shorter than the real round-trip — so they fired `Timeout → retrying` before the ACK could return. The `g:` message `delivered ... in 9418ms` arrived *after* a retry had already been scheduled. Messages that are genuinely in flight are being declared failed. - -3. **Device `est_timeout` ignored (RETRY-2, live).** - Every send gets a clean `RESP_CODE_SENT` (which carries the firmware's own `est_timeout`), but the app overrides it with the runaway ML value. The device's estimate would have been far saner than both the 10s (too short) and 151s (too long) ML outputs. - -4. **Path rotation never converges.** - Single message, per-retry path: `pathLength 3 → 2 → 1 → 2 → -1(flood)`; timeout target swings `25s → 19s → 13s → 19s → 151s`. Routes that just delivered (`g:` over 2 hops) are abandoned on the next message. On a 2-5 hop room server on a congested channel, the thrash never settles. - -5. **Retries congest the channel.** - `Radio quiet for 3037ms`, `Post-inbound backoff: waiting 14808ms` — channel is busy. Backoffs (`1/2/4/8s`) are far shorter than the 19-34s timeouts, so retries stack onto a multi-hop path and reduce delivery odds. - -6. **`attempt & 3` hash reuse visible.** `"I have some really c..."` attempt 0 and attempt 4 both expect `b8bfa902`; `"JC you around?"` reuses `b2074391`. Attempts 0/4 are indistinguishable on the wire (RETRY-4 / masking). - -7. **Room login runs a separate, untracked ACK path (relates to DELIVERY-6).** `No pending message found for ACK hash: b0023dab` ×5 during `[RoomLogin]` — login sends with an expected ACK that `MessageRetryService` never registered, so every login confirm logs as unmatched. Login has its own 26s timeout/retry and eventually succeeds. - -### Highest-impact fix (from this log) -Fix the timeout side: **stop feeding flood as `pathLength=-1`, hard-cap the ML output (≈30-45s), and fall back to the device's `est_timeout` instead of the runaway ML value.** That alone kills the 151s hang and the premature retries. Contained to `timeout_prediction_service.dart` + `calculateTimeout` in `meshcore_connector.dart`. - ---- - -# Design Proposal — Replace the timeout regressor; split timeout from loss (2026-06-12) - -Outcome of analyzing the JC Room Server log: the OLS `LinearRegressor` in `timeout_prediction_service.dart` is the wrong tool, and it's conflating two separate questions. This sketches the replacement before any code changes. - -## The core insight (why this isn't TCP, and what that implies) - -This is slow, **lossy** multi-hop radio: a message can be lost on a forward hop (never reaches the recipient) or delivered-but-the-ACK-return-is-lost. Two consequences drive the whole design: - -1. **On a lossy link, a longer timeout recovers nothing — only a retry does.** The timeout is your *time-to-retry*. Loss is the common outcome to a distant (5-hop) node, so inflating the timeout (e.g. the observed 151 s flood value) just maximizes idle time before the one action that helps. The right timeout is **the tightest value that still covers a realistically *successful* round-trip** (~P90–95 of success RTT), then give up and retry. -2. **The latency model is already blind to loss.** `recordObservation` is only called from `handleAckReceived` — i.e. only on success. Lost messages produce no sample. So the regressor estimates "time of a *successful* round-trip" (same data an EWMA would use), just with extrapolation/negative/explosion failure modes on top. Modeling loss for real would require censored-data/survival analysis, which is far heavier than needed. - -**Conclusion:** stop using one fragile latency regressor to answer both "how long to wait" and "is this route any good." Split them. - -## Part A — Timeout: physics-anchored, capped, adaptive RTT (replaces the OLS model) - -Drop `ml_algo` / `ml_dataframe`. Keep the existing `predictTimeout` / `recordObservation` interface so `calculateTimeout` and the connector wiring are untouched; swap the internals. - -Per **route class** = (hop count, flood-vs-direct); shared across contacts since data is sparse. - -``` -// baseline from physics (deterministic, generalizes to unseen hop/byte combos -// from sample #1). Use the LoRa airtime formula already in the connector, or the -// device's est_timeout from RESP_CODE_SENT (RETRY-2). -base = physicsEstimate(hops, bytes, sf, bw, cr) - -// learn only a BOUNDED multiplicative overhead from real successes (congestion, -// per-hop processing). EWMA, clamped so it can never explode or go negative. -on success(sample, hops, bytes): - ratio = sample / physicsEstimate(hops, bytes, ...) - f[class] = (1-α)·f[class] + α·clamp(ratio, 0.5, 4.0) // α≈0.125 - v[class] = (1-β)·v[class] + β·|f[class] - ratio| // β≈0.25 (deviation) - -predictTimeout(hops, bytes): - est = base · f[class] - return clamp(est + 4·v[class]·base, physicsMin, HARD_MAX) // HARD_MAX ≈ 30–45 s -``` - -Properties this fixes, by construction: -- **No 151 s / no negatives** — output is `physics × bounded factor + bounded variance`, hard-capped. -- **No flood discontinuity** — flood is its own class with its own factor, never `pathLength = -1` on a shared axis (kills RETRY-5). -- **No cold start** — seed `f=1`, `v=0`; the physics/device baseline is sensible from message #1; observations only refine the factor (removes the "need 10 samples" gate). -- **Variance is the timeout's whole point** — `est + k·deviation` is a calibrated high-percentile wait, so genuinely-in-flight messages aren't cut off early (kills RETRY-3), while staying tight enough that lost messages retry promptly. -- **Drops two dependencies**, ~15 lines, debuggable. - -Also remove the dead/broken `secSinceRx` feature (recorded ≈0; see ML notes) and feed transformed/UTF-8 payload bytes, not `text.length`. - -## Part B — Loss: route reliability drives retry / reroute / flood (data already exists) - -The "is this path even working" question belongs to `PathHistoryService`, which already tracks `successCount` / `failureCount` / `routeWeight` per path. Treat that as a Bernoulli delivery-probability estimate (Wilson or Beta lower bound on success rate) and use it for the *decision*, not the timeout: - -- **High-confidence working route** → keep using it; on a single timeout, retry the *same* path once (the ACK return may just have been lost) before changing anything. -- **Degrading route** (success rate dropping) → switch to the next-best ranked path rather than hammering a dying one. -- **No confident route / repeated failures** → flood to rediscover, then pin the rediscovered path (don't immediately rotate off it — the log shows good routes being abandoned the next message). - -This replaces the current "rotate the path on every retry" churn (which made the timeout target swing 25 s→19 s→13 s→19 s→151 s and never converged) with: **pin a working route; only re-route/flood when reliability says the route is actually bad.** - -## Sequencing -1. Part A first (self-contained, biggest visible win — kills the 151 s hang and premature retries; removes ml_algo). Same public interface, so low blast radius. -2. Then Part B (path-decision policy), which also subsumes much of the separate "location-change brittleness" work — a route that stops delivering after you move is just a route whose reliability dropped. From becfbedc99b8e64bf415007af9c176c392833f7e Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 13 Jun 2026 00:39:13 -0700 Subject: [PATCH 12/16] format files --- lib/screens/app_settings_screen.dart | 307 ++++---- lib/screens/channels_screen.dart | 104 +-- lib/screens/community_qr_scanner_screen.dart | 263 ++++--- lib/screens/companion_radio_stats_screen.dart | 5 +- lib/screens/contacts_screen.dart | 7 +- lib/screens/neighbors_screen.dart | 5 +- lib/screens/path_trace_map.dart | 12 +- lib/screens/repeater_cli_screen.dart | 699 ++++++++++++++---- lib/screens/repeater_hub_screen.dart | 26 +- lib/screens/repeater_settings_screen.dart | 25 +- lib/screens/repeater_status_screen.dart | 17 +- lib/screens/scanner_screen.dart | 46 +- lib/screens/settings_screen.dart | 12 +- lib/screens/tcp_screen.dart | 4 +- lib/screens/telemetry_screen.dart | 119 ++- lib/screens/usb_screen.dart | 14 +- lib/services/message_retry_service.dart | 16 +- lib/widgets/device_tile.dart | 15 +- lib/widgets/elements_ui.dart | 5 +- lib/widgets/jump_to_bottom_button.dart | 5 +- lib/widgets/mesh_ui.dart | 18 +- lib/widgets/message_status_icon.dart | 6 +- lib/widgets/path_map_ui.dart | 14 +- lib/widgets/repeater_login_dialog.dart | 19 +- lib/widgets/room_login_dialog.dart | 8 +- lib/widgets/unread_divider.dart | 10 +- 26 files changed, 1040 insertions(+), 741 deletions(-) diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 5c8258ad..e54687c9 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -30,87 +30,103 @@ class AppSettingsScreen extends StatelessWidget { ), body: SafeArea( top: false, - child: Consumer3( - builder: ( - context, - settingsService, - connector, - translationService, - child, - ) { - return ListView( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 24), - children: [ - // APPEARANCE - SectionHeader(context.l10n.appSettings_appearance), - MeshCard( - padding: EdgeInsets.zero, - child: _buildAppearanceContent(context, settingsService), - ), - - // NOTIFICATIONS - SectionHeader(context.l10n.appSettings_notifications), - MeshCard( - padding: EdgeInsets.zero, - child: _buildNotificationsContent(context, settingsService), - ), - - // MESSAGING - SectionHeader(context.l10n.appSettings_messaging), - MeshCard( - padding: EdgeInsets.zero, - child: _buildMessagingContent(context, settingsService), - ), - - // BATTERY - SectionHeader(context.l10n.appSettings_battery), - MeshCard( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), - child: _buildBatteryContent( + child: + Consumer3< + AppSettingsService, + MeshCoreConnector, + TranslationService + >( + builder: + ( context, settingsService, connector, - ), - ), + translationService, + child, + ) { + return ListView( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 24), + children: [ + // APPEARANCE + SectionHeader(context.l10n.appSettings_appearance), + MeshCard( + padding: EdgeInsets.zero, + child: _buildAppearanceContent( + context, + settingsService, + ), + ), - // MAP - SectionHeader(context.l10n.appSettings_mapDisplay), - MeshCard( - padding: EdgeInsets.zero, - child: _buildMapContent(context, settingsService), - ), + // NOTIFICATIONS + SectionHeader(context.l10n.appSettings_notifications), + MeshCard( + padding: EdgeInsets.zero, + child: _buildNotificationsContent( + context, + settingsService, + ), + ), - // TRANSLATION (non-web only) - if (!kIsWeb) ...[ - SectionHeader(context.l10n.translation_title), - MeshCard( - padding: EdgeInsets.zero, - child: _buildTranslationContent( - context, - settingsService, - translationService, - ), - ), - ], + // MESSAGING + SectionHeader(context.l10n.appSettings_messaging), + MeshCard( + padding: EdgeInsets.zero, + child: _buildMessagingContent( + context, + settingsService, + ), + ), - // CYR2LAT - SectionHeader(context.l10n.channels_cyr2latSettingsHeading), - MeshCard( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), - child: _buildCyr2LatContent(context, settingsService), - ), + // BATTERY + SectionHeader(context.l10n.appSettings_battery), + MeshCard( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), + child: _buildBatteryContent( + context, + settingsService, + connector, + ), + ), - // DEBUG - SectionHeader(context.l10n.appSettings_debugCard), - MeshCard( - padding: EdgeInsets.zero, - child: _buildDebugContent(context, settingsService), - ), - ], - ); - }, - ), + // MAP + SectionHeader(context.l10n.appSettings_mapDisplay), + MeshCard( + padding: EdgeInsets.zero, + child: _buildMapContent(context, settingsService), + ), + + // TRANSLATION (non-web only) + if (!kIsWeb) ...[ + SectionHeader(context.l10n.translation_title), + MeshCard( + padding: EdgeInsets.zero, + child: _buildTranslationContent( + context, + settingsService, + translationService, + ), + ), + ], + + // CYR2LAT + SectionHeader( + context.l10n.channels_cyr2latSettingsHeading, + ), + MeshCard( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), + child: _buildCyr2LatContent(context, settingsService), + ), + + // DEBUG + SectionHeader(context.l10n.appSettings_debugCard), + MeshCard( + padding: EdgeInsets.zero, + child: _buildDebugContent(context, settingsService), + ), + ], + ); + }, + ), ), ); } @@ -317,8 +333,7 @@ class AppSettingsScreen extends StatelessWidget { ), value: settingsService.settings.notifyOnNewChannelMessage, onChanged: notifEnabled - ? (value) => - settingsService.setNotifyOnNewChannelMessage(value) + ? (value) => settingsService.setNotifyOnNewChannelMessage(value) : null, ), const Divider(height: 1, indent: 16), @@ -367,9 +382,7 @@ class AppSettingsScreen extends StatelessWidget { ), secondary: const Icon(Icons.refresh_outlined, size: 20), title: Text(context.l10n.appSettings_clearPathOnMaxRetry), - subtitle: Text( - context.l10n.appSettings_clearPathOnMaxRetrySubtitle, - ), + subtitle: Text(context.l10n.appSettings_clearPathOnMaxRetrySubtitle), value: settingsService.settings.clearPathOnMaxRetry, onChanged: (value) { settingsService.setClearPathOnMaxRetry(value); @@ -472,9 +485,7 @@ class AppSettingsScreen extends StatelessWidget { min: 0.5, max: 5.0, divisions: 9, - label: settingsService - .settings - .initialRouteWeight + label: settingsService.settings.initialRouteWeight .toStringAsFixed(1), onChanged: (value) => settingsService.setInitialRouteWeight(value), @@ -485,9 +496,7 @@ class AppSettingsScreen extends StatelessWidget { const Divider(height: 1), ListTile( title: Text( - context - .l10n - .appSettings_routeWeightSuccessIncrement, + context.l10n.appSettings_routeWeightSuccessIncrement, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -517,9 +526,7 @@ class AppSettingsScreen extends StatelessWidget { const Divider(height: 1), ListTile( title: Text( - context - .l10n - .appSettings_routeWeightFailureDecrement, + context.l10n.appSettings_routeWeightFailureDecrement, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -548,9 +555,7 @@ class AppSettingsScreen extends StatelessWidget { ), const Divider(height: 1), ListTile( - title: Text( - context.l10n.appSettings_maxMessageRetries, - ), + title: Text(context.l10n.appSettings_maxMessageRetries), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -560,16 +565,12 @@ class AppSettingsScreen extends StatelessWidget { .appSettings_maxMessageRetriesSubtitle, ), Slider( - value: settingsService - .settings - .maxMessageRetries + value: settingsService.settings.maxMessageRetries .toDouble(), min: 2, max: 10, divisions: 8, - label: settingsService - .settings - .maxMessageRetries + label: settingsService.settings.maxMessageRetries .toString(), onChanged: (value) => settingsService .setMaxMessageRetries(value.toInt()), @@ -590,9 +591,7 @@ class AppSettingsScreen extends StatelessWidget { ), secondary: const Icon(Icons.location_searching, size: 20), title: Text(context.l10n.appSettings_enableMessageTracing), - subtitle: Text( - context.l10n.appSettings_enableMessageTracingSubtitle, - ), + subtitle: Text(context.l10n.appSettings_enableMessageTracingSubtitle), value: settingsService.settings.enableMessageTracing, onChanged: (value) { settingsService.setEnableMessageTracing(value); @@ -765,9 +764,7 @@ class AppSettingsScreen extends StatelessWidget { settingsService.settings.mapTimeFilterHours == 0 ? context.l10n.appSettings_timeFilterShowAll : context.l10n.appSettings_timeFilterShowLast( - settingsService - .settings - .mapTimeFilterHours + settingsService.settings.mapTimeFilterHours .toInt(), ), style: textTheme.bodySmall?.copyWith( @@ -925,15 +922,17 @@ class AppSettingsScreen extends StatelessWidget { title: Text( context.l10n.translation_autoIncomingTitle, style: TextStyle( - color: - translationEnabled ? null : Theme.of(context).disabledColor, + color: translationEnabled + ? null + : Theme.of(context).disabledColor, ), ), subtitle: Text( context.l10n.translation_autoIncomingSubtitle, style: TextStyle( - color: - translationEnabled ? null : Theme.of(context).disabledColor, + color: translationEnabled + ? null + : Theme.of(context).disabledColor, ), ), value: settings.autoTranslateIncomingMessages, @@ -955,15 +954,17 @@ class AppSettingsScreen extends StatelessWidget { title: Text( context.l10n.translation_composerTitle, style: TextStyle( - color: - translationEnabled ? null : Theme.of(context).disabledColor, + color: translationEnabled + ? null + : Theme.of(context).disabledColor, ), ), subtitle: Text( context.l10n.translation_composerSubtitle, style: TextStyle( - color: - translationEnabled ? null : Theme.of(context).disabledColor, + color: translationEnabled + ? null + : Theme.of(context).disabledColor, ), ), value: settings.composerTranslationEnabled, @@ -973,17 +974,12 @@ class AppSettingsScreen extends StatelessWidget { ), const Divider(height: 1, indent: 16), InkWell( - onTap: () => - _showTranslationLanguageDialog(context, settingsService), + onTap: () => _showTranslationLanguageDialog(context, settingsService), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon( - Icons.language, - size: 20, - color: scheme.onSurfaceVariant, - ), + Icon(Icons.language, size: 20, color: scheme.onSurfaceVariant), const SizedBox(width: 12), Expanded( child: Column( @@ -1106,7 +1102,8 @@ class AppSettingsScreen extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( - value: translationService.downloadFileName == + value: + translationService.downloadFileName == 'Merging chunks...' ? null : translationService.downloadProgress, @@ -1151,8 +1148,7 @@ class AppSettingsScreen extends StatelessWidget { const SizedBox(width: 12), Expanded( child: InkWell( - borderRadius: - BorderRadius.circular(MeshRadii.xs), + borderRadius: BorderRadius.circular(MeshRadii.xs), onTap: () => settingsService .setTranslationSelectedModelId(model.id), child: Column( @@ -1194,9 +1190,7 @@ class AppSettingsScreen extends StatelessWidget { const SizedBox(height: 8), Text( translationService.lastError!, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ], ], @@ -1373,8 +1367,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageEn, value: 'en', - selected: - settingsService.settings.languageOverride == 'en', + selected: settingsService.settings.languageOverride == 'en', onTap: () { settingsService.setLanguageOverride('en'); Navigator.pop(ctx); @@ -1384,8 +1377,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageFr, value: 'fr', - selected: - settingsService.settings.languageOverride == 'fr', + selected: settingsService.settings.languageOverride == 'fr', onTap: () { settingsService.setLanguageOverride('fr'); Navigator.pop(ctx); @@ -1395,8 +1387,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageEs, value: 'es', - selected: - settingsService.settings.languageOverride == 'es', + selected: settingsService.settings.languageOverride == 'es', onTap: () { settingsService.setLanguageOverride('es'); Navigator.pop(ctx); @@ -1406,8 +1397,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageDe, value: 'de', - selected: - settingsService.settings.languageOverride == 'de', + selected: settingsService.settings.languageOverride == 'de', onTap: () { settingsService.setLanguageOverride('de'); Navigator.pop(ctx); @@ -1417,8 +1407,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languagePl, value: 'pl', - selected: - settingsService.settings.languageOverride == 'pl', + selected: settingsService.settings.languageOverride == 'pl', onTap: () { settingsService.setLanguageOverride('pl'); Navigator.pop(ctx); @@ -1428,8 +1417,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageSl, value: 'sl', - selected: - settingsService.settings.languageOverride == 'sl', + selected: settingsService.settings.languageOverride == 'sl', onTap: () { settingsService.setLanguageOverride('sl'); Navigator.pop(ctx); @@ -1439,8 +1427,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languagePt, value: 'pt', - selected: - settingsService.settings.languageOverride == 'pt', + selected: settingsService.settings.languageOverride == 'pt', onTap: () { settingsService.setLanguageOverride('pt'); Navigator.pop(ctx); @@ -1450,8 +1437,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageIt, value: 'it', - selected: - settingsService.settings.languageOverride == 'it', + selected: settingsService.settings.languageOverride == 'it', onTap: () { settingsService.setLanguageOverride('it'); Navigator.pop(ctx); @@ -1461,8 +1447,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageZh, value: 'zh', - selected: - settingsService.settings.languageOverride == 'zh', + selected: settingsService.settings.languageOverride == 'zh', onTap: () { settingsService.setLanguageOverride('zh'); Navigator.pop(ctx); @@ -1472,8 +1457,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageSv, value: 'sv', - selected: - settingsService.settings.languageOverride == 'sv', + selected: settingsService.settings.languageOverride == 'sv', onTap: () { settingsService.setLanguageOverride('sv'); Navigator.pop(ctx); @@ -1483,8 +1467,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageNl, value: 'nl', - selected: - settingsService.settings.languageOverride == 'nl', + selected: settingsService.settings.languageOverride == 'nl', onTap: () { settingsService.setLanguageOverride('nl'); Navigator.pop(ctx); @@ -1494,8 +1477,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageSk, value: 'sk', - selected: - settingsService.settings.languageOverride == 'sk', + selected: settingsService.settings.languageOverride == 'sk', onTap: () { settingsService.setLanguageOverride('sk'); Navigator.pop(ctx); @@ -1505,8 +1487,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageBg, value: 'bg', - selected: - settingsService.settings.languageOverride == 'bg', + selected: settingsService.settings.languageOverride == 'bg', onTap: () { settingsService.setLanguageOverride('bg'); Navigator.pop(ctx); @@ -1516,8 +1497,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageRu, value: 'ru', - selected: - settingsService.settings.languageOverride == 'ru', + selected: settingsService.settings.languageOverride == 'ru', onTap: () { settingsService.setLanguageOverride('ru'); Navigator.pop(ctx); @@ -1527,8 +1507,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageUk, value: 'uk', - selected: - settingsService.settings.languageOverride == 'uk', + selected: settingsService.settings.languageOverride == 'uk', onTap: () { settingsService.setLanguageOverride('uk'); Navigator.pop(ctx); @@ -1538,8 +1517,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageHu, value: 'hu', - selected: - settingsService.settings.languageOverride == 'hu', + selected: settingsService.settings.languageOverride == 'hu', onTap: () { settingsService.setLanguageOverride('hu'); Navigator.pop(ctx); @@ -1549,8 +1527,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageJa, value: 'ja', - selected: - settingsService.settings.languageOverride == 'ja', + selected: settingsService.settings.languageOverride == 'ja', onTap: () { settingsService.setLanguageOverride('ja'); Navigator.pop(ctx); @@ -1560,8 +1537,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_languageKo, value: 'ko', - selected: - settingsService.settings.languageOverride == 'ko', + selected: settingsService.settings.languageOverride == 'ko', onTap: () { settingsService.setLanguageOverride('ko'); Navigator.pop(ctx); @@ -1660,8 +1636,7 @@ class AppSettingsScreen extends StatelessWidget { ctx, label: context.l10n.appSettings_unitsMetric, value: UnitSystem.metric, - selected: - settingsService.settings.unitSystem == UnitSystem.metric, + selected: settingsService.settings.unitSystem == UnitSystem.metric, onTap: () { settingsService.setUnitSystem(UnitSystem.metric); Navigator.pop(ctx); @@ -1874,9 +1849,7 @@ class AppSettingsScreen extends StatelessWidget { if (nameController.text.isEmpty) { showDismissibleSnackBar( context, - content: Text( - context.l10n.settings_cyr2latProfileNameEmpty, - ), + content: Text(context.l10n.settings_cyr2latProfileNameEmpty), ); return; } @@ -1964,9 +1937,7 @@ class AppSettingsScreen extends StatelessWidget { if (nameController.text.isEmpty) { showDismissibleSnackBar( context, - content: Text( - context.l10n.settings_cyr2latProfileNameEmpty, - ), + content: Text(context.l10n.settings_cyr2latProfileNameEmpty), ); return; } diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 080eef8e..94bbd65e 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -462,7 +462,9 @@ class _ChannelsScreenState extends State color: MeshPalette.magenta, shape: BoxShape.circle, border: Border.all( - color: Theme.of(context).colorScheme.surfaceContainerLow, + color: Theme.of( + context, + ).colorScheme.surfaceContainerLow, width: 2, ), ), @@ -488,9 +490,8 @@ class _ChannelsScreenState extends State Expanded( child: Text( channelLabel, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -544,8 +545,7 @@ class _ChannelsScreenState extends State ), const SizedBox(width: 4), ], - if (unreadCount > 0) - UnreadBadge(count: unreadCount), + if (unreadCount > 0) UnreadBadge(count: unreadCount), ], ), ], @@ -806,12 +806,8 @@ class _ChannelsScreenState extends State return MeshCard( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - borderColor: isSelected && enabled - ? MeshPalette.blueLine - : null, - color: isSelected && enabled - ? MeshPalette.blueBg - : null, + borderColor: isSelected && enabled ? MeshPalette.blueLine : null, + color: isSelected && enabled ? MeshPalette.blueBg : null, onTap: enabled ? () { setSheetState(() { @@ -828,7 +824,9 @@ class _ChannelsScreenState extends State name: title, size: 38, color: enabled - ? (isSelected ? MeshPalette.blue : cardScheme.onSurfaceVariant) + ? (isSelected + ? MeshPalette.blue + : cardScheme.onSurfaceVariant) : cardScheme.outline, icon: icon, ), @@ -840,17 +838,21 @@ class _ChannelsScreenState extends State children: [ Text( title, - style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - color: enabled ? null : cardScheme.outline, - ), + style: Theme.of(sheetContext).textTheme.bodyMedium + ?.copyWith( + fontWeight: FontWeight.w500, + color: enabled ? null : cardScheme.outline, + ), ), const SizedBox(height: 2), Text( subtitle, - style: Theme.of(sheetContext).textTheme.bodySmall?.copyWith( - color: enabled ? cardScheme.onSurfaceVariant : cardScheme.outline, - ), + style: Theme.of(sheetContext).textTheme.bodySmall + ?.copyWith( + color: enabled + ? cardScheme.onSurfaceVariant + : cardScheme.outline, + ), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -860,7 +862,9 @@ class _ChannelsScreenState extends State if (enabled) Icon( Icons.chevron_right, - color: isSelected ? MeshPalette.blue : cardScheme.onSurfaceVariant, + color: isSelected + ? MeshPalette.blue + : cardScheme.onSurfaceVariant, size: 20, ), ], @@ -998,9 +1002,7 @@ class _ChannelsScreenState extends State showDismissibleSnackBar( context, content: Text( - sheetContext - .l10n - .channels_pskMustBe32Hex, + sheetContext.l10n.channels_pskMustBe32Hex, ), ); return; @@ -1313,9 +1315,7 @@ class _ChannelsScreenState extends State addPublicChannel = value ?? true; }); }, - title: Text( - sheetContext.l10n.community_addPublicChannel, - ), + title: Text(sheetContext.l10n.community_addPublicChannel), subtitle: Text( sheetContext.l10n.community_addPublicChannelHint, ), @@ -1418,9 +1418,7 @@ class _ChannelsScreenState extends State maxChildSize: 0.95, builder: (_, scrollController) => Column( children: [ - BottomSheetHeader( - title: sheetContext.l10n.channels_addChannel, - ), + BottomSheetHeader(title: sheetContext.l10n.channels_addChannel), Expanded( child: ListView( controller: scrollController, @@ -1519,7 +1517,9 @@ class _ChannelsScreenState extends State builder: (_, scrollController) => Column( children: [ BottomSheetHeader( - title: sheetContext.l10n.channels_editChannelTitle(channel.index), + title: sheetContext.l10n.channels_editChannelTitle( + channel.index, + ), ), Expanded( child: ListView( @@ -1569,7 +1569,9 @@ class _ChannelsScreenState extends State ), SwitchListTile( contentPadding: EdgeInsets.zero, - title: Text(sheetContext.l10n.channels_cyr2latCompression), + title: Text( + sheetContext.l10n.channels_cyr2latCompression, + ), subtitle: Text( sheetContext.l10n.channels_cyr2latCompressionDscr, ), @@ -1592,14 +1594,14 @@ class _ChannelsScreenState extends State .channels_cyr2latSettingsSubheading, border: const OutlineInputBorder(), ), - items: appSettingsService.settings.cyr2latProfiles.map(( - profile, - ) { - return DropdownMenuItem( - value: profile.id, - child: Text(profile.name), - ); - }).toList(), + items: appSettingsService.settings.cyr2latProfiles + .map((profile) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }) + .toList(), onChanged: (value) => setSheetState(() { selectedCyr2LatProfileId = value; }), @@ -1633,14 +1635,20 @@ class _ChannelsScreenState extends State } on FormatException { showDismissibleSnackBar( sheetContext, - content: Text(sheetContext.l10n.channels_pskMustBe32Hex), + content: Text( + sheetContext.l10n.channels_pskMustBe32Hex, + ), ); return; } Navigator.pop(sheetContext); try { - await connector.setChannel(channel.index, name, psk); + await connector.setChannel( + channel.index, + name, + psk, + ); await connector.setChannelSmazEnabled( channel.index, smazEnabled, @@ -1656,7 +1664,9 @@ class _ChannelsScreenState extends State if (!context.mounted) return; showDismissibleSnackBar( context, - content: Text(context.l10n.channels_channelUpdated(name)), + content: Text( + context.l10n.channels_channelUpdated(name), + ), ); } catch (e, st) { debugPrint(st.toString()); @@ -1732,7 +1742,9 @@ class _ChannelsScreenState extends State }, child: Text( dialogContext.l10n.common_delete, - style: TextStyle(color: Theme.of(dialogContext).colorScheme.error), + style: TextStyle( + color: Theme.of(dialogContext).colorScheme.error, + ), ), ), ], @@ -1975,7 +1987,9 @@ class _ChannelsScreenState extends State }, child: Text( dialogContext.l10n.community_delete, - style: TextStyle(color: Theme.of(dialogContext).colorScheme.error), + style: TextStyle( + color: Theme.of(dialogContext).colorScheme.error, + ), ), ), ], diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index c7941900..77321573 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -109,10 +109,7 @@ class _CommunityQrScannerScreenState extends State { ), child: Text( context.l10n.community_scanInstructions, - style: const TextStyle( - color: MeshPalette.ink2, - fontSize: 13, - ), + style: const TextStyle(color: MeshPalette.ink2, fontSize: 13), textAlign: TextAlign.center, ), ), @@ -185,61 +182,61 @@ class _CommunityQrScannerScreenState extends State { builder: (sheetContext) { final sheetScheme = Theme.of(sheetContext).colorScheme; return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BottomSheetHeader(title: context.l10n.community_alreadyMember), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 4), - child: Text( - context.l10n.community_alreadyMemberMessage(community.name), - style: TextStyle(color: sheetScheme.onSurfaceVariant), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BottomSheetHeader(title: context.l10n.community_alreadyMember), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 4), + child: Text( + context.l10n.community_alreadyMemberMessage(community.name), + style: TextStyle(color: sheetScheme.onSurfaceVariant), + ), ), - ), - MeshCard( - child: Row( - children: [ - const Icon( - Icons.groups, - color: MeshPalette.magenta, - size: 32, - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - community.name, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 15, - ), - ), - Text( - 'ID: ${community.shortCommunityId}...', - style: MeshTheme.mono( - fontSize: 11.5, - color: sheetScheme.onSurfaceVariant, - ), - ), - ], + MeshCard( + child: Row( + children: [ + const Icon( + Icons.groups, + color: MeshPalette.magenta, + size: 32, ), - ), - ], + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + community.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + Text( + 'ID: ${community.shortCommunityId}...', + style: MeshTheme.mono( + fontSize: 11.5, + color: sheetScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: FilledButton( - onPressed: () { - Navigator.pop(sheetContext); - Navigator.pop(context); - }, - child: Text(context.l10n.common_ok), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: FilledButton( + onPressed: () { + Navigator.pop(sheetContext); + Navigator.pop(context); + }, + child: Text(context.l10n.common_ok), + ), ), - ), - ], + ], ); }, ); @@ -258,90 +255,90 @@ class _CommunityQrScannerScreenState extends State { builder: (sheetContext, setSheetState) { final joinScheme = Theme.of(sheetContext).colorScheme; return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BottomSheetHeader(title: context.l10n.community_joinTitle), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 4), - child: Text( - context.l10n.community_joinConfirmation(community.name), - style: TextStyle(color: joinScheme.onSurfaceVariant), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BottomSheetHeader(title: context.l10n.community_joinTitle), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 4), + child: Text( + context.l10n.community_joinConfirmation(community.name), + style: TextStyle(color: joinScheme.onSurfaceVariant), + ), ), - ), - MeshCard( - child: Row( - children: [ - AvatarCircle( - name: community.name, - icon: Icons.groups, - color: MeshPalette.magenta, - size: 44, - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - community.name, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 15, + MeshCard( + child: Row( + children: [ + AvatarCircle( + name: community.name, + icon: Icons.groups, + color: MeshPalette.magenta, + size: 44, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + community.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), ), - ), - Text( - 'ID: ${community.shortCommunityId}...', - style: MeshTheme.mono( - fontSize: 11.5, - color: joinScheme.onSurfaceVariant, + Text( + 'ID: ${community.shortCommunityId}...', + style: MeshTheme.mono( + fontSize: 11.5, + color: joinScheme.onSurfaceVariant, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), - ), - CheckboxListTile( - value: addPublicChannel, - onChanged: (value) { - setSheetState(() { - addPublicChannel = value ?? true; - }); - }, - title: Text(context.l10n.community_addPublicChannel), - subtitle: Text(context.l10n.community_addPublicChannelHint), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () { - completer.complete(false); - Navigator.pop(sheetContext); - }, - child: Text(context.l10n.common_cancel), - ), - ), - const SizedBox(width: 12), - Expanded( - child: FilledButton( - onPressed: () { - completer.complete(true); - Navigator.pop(sheetContext); - }, - child: Text(context.l10n.community_join), - ), - ), - ], + CheckboxListTile( + value: addPublicChannel, + onChanged: (value) { + setSheetState(() { + addPublicChannel = value ?? true; + }); + }, + title: Text(context.l10n.community_addPublicChannel), + subtitle: Text(context.l10n.community_addPublicChannelHint), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), - ), - ], + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + completer.complete(false); + Navigator.pop(sheetContext); + }, + child: Text(context.l10n.common_cancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: () { + completer.complete(true); + Navigator.pop(sheetContext); + }, + child: Text(context.l10n.community_join), + ), + ), + ], + ), + ), + ], ); }, ), diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index a36a2a6c..f7d8f46e 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -139,10 +139,7 @@ class _CompanionRadioStatsScreenState extends State { stats.lastSnrDb.toStringAsFixed(1), ), Icons.signal_cellular_alt, - MeshTheme.snrColor( - stats.lastSnrDb, - blocked: false, - ), + MeshTheme.snrColor(stats.lastSnrDb, blocked: false), ), ], ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 69fb67e0..44ec78d2 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1574,7 +1574,8 @@ class _ContactTile extends StatelessWidget { case advTypeSensor: return const Color(0xFF4ACCC4); // teal default: - return MeshPalette.blue; // chat — AvatarCircle handles deterministic hue + return MeshPalette + .blue; // chat — AvatarCircle handles deterministic hue } } @@ -1662,7 +1663,9 @@ class _ContactTile extends StatelessWidget { Icon( Icons.location_on, size: 13, - color: scheme.onSurfaceVariant.withValues(alpha: 0.55), + color: scheme.onSurfaceVariant.withValues( + alpha: 0.55, + ), ), ], ], diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index d043fbfe..529a00dd 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -351,10 +351,7 @@ class _NeighborsScreenState extends State { for (var i = 0; i < _parsedNeighbors!.length; i++) ListEntrance( index: i, - child: _buildNeighborRow( - _parsedNeighbors![i], - connector.currentSf, - ), + child: _buildNeighborRow(_parsedNeighbors![i], connector.currentSf), ), ], ); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index d955b30b..2eebdc71 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -801,9 +801,7 @@ class _PathTraceMapScreenState extends State final selected = _selectedPath; return selected != null ? [selected] : const []; } - return _displayPaths - .where((p) => !_hiddenPathIds.contains(p.id)) - .toList(); + return _displayPaths.where((p) => !_hiddenPathIds.contains(p.id)).toList(); } /// Updates the playback path, but only when the selected path's geometry @@ -1249,7 +1247,8 @@ class _PathTraceMapScreenState extends State final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); showSharedNodeSheet( context, - title: '$hex: ${contact?.name ?? context.l10n.channelPath_unknownRepeater}', + title: + '$hex: ${contact?.name ?? context.l10n.channelPath_unknownRepeater}', paths: paths, onSelect: _selectPath, ); @@ -1539,9 +1538,8 @@ class _PathTraceMapScreenState extends State tooltip: _panelCollapsed ? l10n.pathMap_expandPanel : l10n.pathMap_collapsePanel, - onPressed: () => setState( - () => _panelCollapsed = !_panelCollapsed, - ), + onPressed: () => + setState(() => _panelCollapsed = !_panelCollapsed), ), ], ), diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 5ed1b4eb..5024f334 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -470,26 +470,16 @@ class _RepeaterCliScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( - Icons.terminal, - size: 48, - color: MeshPalette.ink4, - ), + const Icon(Icons.terminal, size: 48, color: MeshPalette.ink4), const SizedBox(height: 12), Text( l10n.repeater_noCommandsSent, - style: MeshTheme.mono( - fontSize: 13, - color: MeshPalette.ink3, - ), + style: MeshTheme.mono(fontSize: 13, color: MeshPalette.ink3), ), const SizedBox(height: 4), Text( l10n.repeater_typeCommandOrUseQuick, - style: const TextStyle( - fontSize: 12, - color: MeshPalette.ink4, - ), + style: const TextStyle(fontSize: 12, color: MeshPalette.ink4), ), ], ), @@ -518,9 +508,7 @@ class _RepeaterCliScreenState extends State { style: MeshTheme.mono( fontSize: 12, fontWeight: FontWeight.w700, - color: isCommand - ? MeshPalette.blue - : MeshPalette.ink3, + color: isCommand ? MeshPalette.blue : MeshPalette.ink3, ), ), ), @@ -530,9 +518,7 @@ class _RepeaterCliScreenState extends State { entry['text']!, style: MeshTheme.mono( fontSize: 12.5, - color: isCommand - ? MeshPalette.blue - : MeshPalette.ink, + color: isCommand ? MeshPalette.blue : MeshPalette.ink, ), ), ), @@ -559,156 +545,522 @@ class _RepeaterCliScreenState extends State { void _showCommandHelp(BuildContext context) { final l10n = context.l10n; final generalCommands = [ - _CommandHelpEntry(command: 'advert', description: l10n.repeater_cliHelpAdvert), - _CommandHelpEntry(command: 'reboot', description: l10n.repeater_cliHelpReboot), - _CommandHelpEntry(command: 'clock', description: l10n.repeater_cliHelpClock), - _CommandHelpEntry(command: 'password {new-password}', description: l10n.repeater_cliHelpPassword), - _CommandHelpEntry(command: 'ver', description: l10n.repeater_cliHelpVersion), - _CommandHelpEntry(command: 'clear stats', description: l10n.repeater_cliHelpClearStats), - _CommandHelpEntry(command: 'poweroff', description: l10n.repeater_cliHelpPowerOff), - _CommandHelpEntry(command: 'shutdown', description: l10n.repeater_cliHelpPowerOff), - _CommandHelpEntry(command: 'clkreboot', description: l10n.repeater_cliHelpClkReboot), - _CommandHelpEntry(command: 'advert.zerohop', description: l10n.repeater_cliHelpAdvertZeroHop), - _CommandHelpEntry(command: 'start ota', description: l10n.repeater_cliHelpStartOta), - _CommandHelpEntry(command: 'time {epoch-seconds}', description: l10n.repeater_cliHelpTime), - _CommandHelpEntry(command: 'board', description: l10n.repeater_cliHelpBoard), - _CommandHelpEntry(command: 'discover.neighbors', description: l10n.repeater_cliHelpDiscoverNeighbors), - _CommandHelpEntry(command: 'powersaving', description: l10n.repeater_cliHelpPowersaving), - _CommandHelpEntry(command: 'powersaving {on|off}', description: l10n.repeater_cliHelpPowersavingOnOff), - _CommandHelpEntry(command: 'erase', description: l10n.repeater_cliHelpErase), - _CommandHelpEntry(command: 'stats-packets', description: l10n.repeater_cliHelpStatsPackets), - _CommandHelpEntry(command: 'stats-radio', description: l10n.repeater_cliHelpStatsRadio), - _CommandHelpEntry(command: 'stats-core', description: l10n.repeater_cliHelpStatsCore), + _CommandHelpEntry( + command: 'advert', + description: l10n.repeater_cliHelpAdvert, + ), + _CommandHelpEntry( + command: 'reboot', + description: l10n.repeater_cliHelpReboot, + ), + _CommandHelpEntry( + command: 'clock', + description: l10n.repeater_cliHelpClock, + ), + _CommandHelpEntry( + command: 'password {new-password}', + description: l10n.repeater_cliHelpPassword, + ), + _CommandHelpEntry( + command: 'ver', + description: l10n.repeater_cliHelpVersion, + ), + _CommandHelpEntry( + command: 'clear stats', + description: l10n.repeater_cliHelpClearStats, + ), + _CommandHelpEntry( + command: 'poweroff', + description: l10n.repeater_cliHelpPowerOff, + ), + _CommandHelpEntry( + command: 'shutdown', + description: l10n.repeater_cliHelpPowerOff, + ), + _CommandHelpEntry( + command: 'clkreboot', + description: l10n.repeater_cliHelpClkReboot, + ), + _CommandHelpEntry( + command: 'advert.zerohop', + description: l10n.repeater_cliHelpAdvertZeroHop, + ), + _CommandHelpEntry( + command: 'start ota', + description: l10n.repeater_cliHelpStartOta, + ), + _CommandHelpEntry( + command: 'time {epoch-seconds}', + description: l10n.repeater_cliHelpTime, + ), + _CommandHelpEntry( + command: 'board', + description: l10n.repeater_cliHelpBoard, + ), + _CommandHelpEntry( + command: 'discover.neighbors', + description: l10n.repeater_cliHelpDiscoverNeighbors, + ), + _CommandHelpEntry( + command: 'powersaving', + description: l10n.repeater_cliHelpPowersaving, + ), + _CommandHelpEntry( + command: 'powersaving {on|off}', + description: l10n.repeater_cliHelpPowersavingOnOff, + ), + _CommandHelpEntry( + command: 'erase', + description: l10n.repeater_cliHelpErase, + ), + _CommandHelpEntry( + command: 'stats-packets', + description: l10n.repeater_cliHelpStatsPackets, + ), + _CommandHelpEntry( + command: 'stats-radio', + description: l10n.repeater_cliHelpStatsRadio, + ), + _CommandHelpEntry( + command: 'stats-core', + description: l10n.repeater_cliHelpStatsCore, + ), ]; final settingsCommands = [ - _CommandHelpEntry(command: 'set af {air-time-factor}', description: l10n.repeater_cliHelpSetAf), - _CommandHelpEntry(command: 'set tx {tx-power-dbm}', description: l10n.repeater_cliHelpSetTx), - _CommandHelpEntry(command: 'set repeat {on|off}', description: l10n.repeater_cliHelpSetRepeat), - _CommandHelpEntry(command: 'set allow.read.only {on|off}', description: l10n.repeater_cliHelpSetAllowReadOnly), - _CommandHelpEntry(command: 'set flood.max {max-hops}', description: l10n.repeater_cliHelpSetFloodMax), - _CommandHelpEntry(command: 'set int.thresh {db}', description: l10n.repeater_cliHelpSetIntThresh), - _CommandHelpEntry(command: 'set agc.reset.interval {seconds}', description: l10n.repeater_cliHelpSetAgcResetInterval), - _CommandHelpEntry(command: 'set multi.acks {0|1}', description: l10n.repeater_cliHelpSetMultiAcks), - _CommandHelpEntry(command: 'set advert.interval {minutes}', description: l10n.repeater_cliHelpSetAdvertInterval), - _CommandHelpEntry(command: 'set flood.advert.interval {hours}', description: l10n.repeater_cliHelpSetFloodAdvertInterval), - _CommandHelpEntry(command: 'set guest.password {guess-password}', description: l10n.repeater_cliHelpSetGuestPassword), - _CommandHelpEntry(command: 'set name {name}', description: l10n.repeater_cliHelpSetName), - _CommandHelpEntry(command: 'set lat {latitude}', description: l10n.repeater_cliHelpSetLat), - _CommandHelpEntry(command: 'set lon {longitude}', description: l10n.repeater_cliHelpSetLon), - _CommandHelpEntry(command: 'set radio {freq},{bw},{sf},{cr}', description: l10n.repeater_cliHelpSetRadio), - _CommandHelpEntry(command: 'set rxdelay {base}', description: l10n.repeater_cliHelpSetRxDelay), - _CommandHelpEntry(command: 'set txdelay {factor}', description: l10n.repeater_cliHelpSetTxDelay), - _CommandHelpEntry(command: 'set direct.txdelay {factor}', description: l10n.repeater_cliHelpSetDirectTxDelay), - _CommandHelpEntry(command: 'set bridge.enabled {on|off}', description: l10n.repeater_cliHelpSetBridgeEnabled), - _CommandHelpEntry(command: 'set bridge.delay {0-10000}', description: l10n.repeater_cliHelpSetBridgeDelay), - _CommandHelpEntry(command: 'set bridge.source {rx|tx}', description: l10n.repeater_cliHelpSetBridgeSource), - _CommandHelpEntry(command: 'set bridge.baud {speed}', description: l10n.repeater_cliHelpSetBridgeBaud), - _CommandHelpEntry(command: 'set bridge.secret {shared-secret}', description: l10n.repeater_cliHelpSetBridgeSecret), - _CommandHelpEntry(command: 'set adc.multiplier {factor}', description: l10n.repeater_cliHelpSetAdcMultiplier), - _CommandHelpEntry(command: 'tempradio {freq},{bw},{sf},{cr},{minutes}', description: l10n.repeater_cliHelpTempRadio), - _CommandHelpEntry(command: 'setperm {pubkey-hex} {permissions}', description: l10n.repeater_cliHelpSetPerm), - _CommandHelpEntry(command: 'set dutycycle {1-100}', description: l10n.repeater_cliHelpSetDutyCycle), - _CommandHelpEntry(command: 'set prv.key {hex}', description: l10n.repeater_cliHelpSetPrvKey), - _CommandHelpEntry(command: 'set radio.rxgain {on|off}', description: l10n.repeater_cliHelpSetRadioRxGain), - _CommandHelpEntry(command: 'set owner.info {text}', description: l10n.repeater_cliHelpSetOwnerInfo), - _CommandHelpEntry(command: 'set path.hash.mode {0|1|2}', description: l10n.repeater_cliHelpSetPathHashMode), - _CommandHelpEntry(command: 'set loop.detect {off|minimal|moderate|strict}', description: l10n.repeater_cliHelpSetLoopDetect), - _CommandHelpEntry(command: 'set freq {mhz}', description: l10n.repeater_cliHelpSetFreq), - _CommandHelpEntry(command: 'set bridge.channel {1-14}', description: l10n.repeater_cliHelpSetBridgeChannel), + _CommandHelpEntry( + command: 'set af {air-time-factor}', + description: l10n.repeater_cliHelpSetAf, + ), + _CommandHelpEntry( + command: 'set tx {tx-power-dbm}', + description: l10n.repeater_cliHelpSetTx, + ), + _CommandHelpEntry( + command: 'set repeat {on|off}', + description: l10n.repeater_cliHelpSetRepeat, + ), + _CommandHelpEntry( + command: 'set allow.read.only {on|off}', + description: l10n.repeater_cliHelpSetAllowReadOnly, + ), + _CommandHelpEntry( + command: 'set flood.max {max-hops}', + description: l10n.repeater_cliHelpSetFloodMax, + ), + _CommandHelpEntry( + command: 'set int.thresh {db}', + description: l10n.repeater_cliHelpSetIntThresh, + ), + _CommandHelpEntry( + command: 'set agc.reset.interval {seconds}', + description: l10n.repeater_cliHelpSetAgcResetInterval, + ), + _CommandHelpEntry( + command: 'set multi.acks {0|1}', + description: l10n.repeater_cliHelpSetMultiAcks, + ), + _CommandHelpEntry( + command: 'set advert.interval {minutes}', + description: l10n.repeater_cliHelpSetAdvertInterval, + ), + _CommandHelpEntry( + command: 'set flood.advert.interval {hours}', + description: l10n.repeater_cliHelpSetFloodAdvertInterval, + ), + _CommandHelpEntry( + command: 'set guest.password {guess-password}', + description: l10n.repeater_cliHelpSetGuestPassword, + ), + _CommandHelpEntry( + command: 'set name {name}', + description: l10n.repeater_cliHelpSetName, + ), + _CommandHelpEntry( + command: 'set lat {latitude}', + description: l10n.repeater_cliHelpSetLat, + ), + _CommandHelpEntry( + command: 'set lon {longitude}', + description: l10n.repeater_cliHelpSetLon, + ), + _CommandHelpEntry( + command: 'set radio {freq},{bw},{sf},{cr}', + description: l10n.repeater_cliHelpSetRadio, + ), + _CommandHelpEntry( + command: 'set rxdelay {base}', + description: l10n.repeater_cliHelpSetRxDelay, + ), + _CommandHelpEntry( + command: 'set txdelay {factor}', + description: l10n.repeater_cliHelpSetTxDelay, + ), + _CommandHelpEntry( + command: 'set direct.txdelay {factor}', + description: l10n.repeater_cliHelpSetDirectTxDelay, + ), + _CommandHelpEntry( + command: 'set bridge.enabled {on|off}', + description: l10n.repeater_cliHelpSetBridgeEnabled, + ), + _CommandHelpEntry( + command: 'set bridge.delay {0-10000}', + description: l10n.repeater_cliHelpSetBridgeDelay, + ), + _CommandHelpEntry( + command: 'set bridge.source {rx|tx}', + description: l10n.repeater_cliHelpSetBridgeSource, + ), + _CommandHelpEntry( + command: 'set bridge.baud {speed}', + description: l10n.repeater_cliHelpSetBridgeBaud, + ), + _CommandHelpEntry( + command: 'set bridge.secret {shared-secret}', + description: l10n.repeater_cliHelpSetBridgeSecret, + ), + _CommandHelpEntry( + command: 'set adc.multiplier {factor}', + description: l10n.repeater_cliHelpSetAdcMultiplier, + ), + _CommandHelpEntry( + command: 'tempradio {freq},{bw},{sf},{cr},{minutes}', + description: l10n.repeater_cliHelpTempRadio, + ), + _CommandHelpEntry( + command: 'setperm {pubkey-hex} {permissions}', + description: l10n.repeater_cliHelpSetPerm, + ), + _CommandHelpEntry( + command: 'set dutycycle {1-100}', + description: l10n.repeater_cliHelpSetDutyCycle, + ), + _CommandHelpEntry( + command: 'set prv.key {hex}', + description: l10n.repeater_cliHelpSetPrvKey, + ), + _CommandHelpEntry( + command: 'set radio.rxgain {on|off}', + description: l10n.repeater_cliHelpSetRadioRxGain, + ), + _CommandHelpEntry( + command: 'set owner.info {text}', + description: l10n.repeater_cliHelpSetOwnerInfo, + ), + _CommandHelpEntry( + command: 'set path.hash.mode {0|1|2}', + description: l10n.repeater_cliHelpSetPathHashMode, + ), + _CommandHelpEntry( + command: 'set loop.detect {off|minimal|moderate|strict}', + description: l10n.repeater_cliHelpSetLoopDetect, + ), + _CommandHelpEntry( + command: 'set freq {mhz}', + description: l10n.repeater_cliHelpSetFreq, + ), + _CommandHelpEntry( + command: 'set bridge.channel {1-14}', + description: l10n.repeater_cliHelpSetBridgeChannel, + ), ]; final bridgeCommands = [ - _CommandHelpEntry(command: 'get bridge.type', description: l10n.repeater_cliHelpGetBridgeType), + _CommandHelpEntry( + command: 'get bridge.type', + description: l10n.repeater_cliHelpGetBridgeType, + ), ]; final loggingCommands = [ - _CommandHelpEntry(command: 'log start', description: l10n.repeater_cliHelpLogStart), - _CommandHelpEntry(command: 'log stop', description: l10n.repeater_cliHelpLogStop), - _CommandHelpEntry(command: 'log erase', description: l10n.repeater_cliHelpLogErase), + _CommandHelpEntry( + command: 'log start', + description: l10n.repeater_cliHelpLogStart, + ), + _CommandHelpEntry( + command: 'log stop', + description: l10n.repeater_cliHelpLogStop, + ), + _CommandHelpEntry( + command: 'log erase', + description: l10n.repeater_cliHelpLogErase, + ), ]; final neighborCommands = [ - _CommandHelpEntry(command: 'neighbors', description: l10n.repeater_cliHelpNeighbors), - _CommandHelpEntry(command: 'neighbor.remove {pubkey-prefix}', description: l10n.repeater_cliHelpNeighborRemove), + _CommandHelpEntry( + command: 'neighbors', + description: l10n.repeater_cliHelpNeighbors, + ), + _CommandHelpEntry( + command: 'neighbor.remove {pubkey-prefix}', + description: l10n.repeater_cliHelpNeighborRemove, + ), ]; final regionCommands = [ - _CommandHelpEntry(command: 'region', description: l10n.repeater_cliHelpRegion), - _CommandHelpEntry(command: 'region load', description: l10n.repeater_cliHelpRegionLoad), - _CommandHelpEntry(command: 'region get {* | name-prefix}', description: l10n.repeater_cliHelpRegionGet), - _CommandHelpEntry(command: 'region put {name} {* | parent-name-prefix}', description: l10n.repeater_cliHelpRegionPut), - _CommandHelpEntry(command: 'region remove {name}', description: l10n.repeater_cliHelpRegionRemove), - _CommandHelpEntry(command: 'region allowf {* | name-prefix}', description: l10n.repeater_cliHelpRegionAllowf), - _CommandHelpEntry(command: 'region denyf {* | name-prefix}', description: l10n.repeater_cliHelpRegionDenyf), - _CommandHelpEntry(command: 'region home', description: l10n.repeater_cliHelpRegionHome), - _CommandHelpEntry(command: 'region home {* | name-prefix}', description: l10n.repeater_cliHelpRegionHomeSet), - _CommandHelpEntry(command: 'region save', description: l10n.repeater_cliHelpRegionSave), - _CommandHelpEntry(command: 'region default', description: l10n.repeater_cliHelpRegionDefault), - _CommandHelpEntry(command: 'region default {* | name-prefix | }', description: l10n.repeater_cliHelpRegionDefaultSet), - _CommandHelpEntry(command: 'region list allowed', description: l10n.repeater_cliHelpRegionListAllowed), - _CommandHelpEntry(command: 'region list denied', description: l10n.repeater_cliHelpRegionListDenied), + _CommandHelpEntry( + command: 'region', + description: l10n.repeater_cliHelpRegion, + ), + _CommandHelpEntry( + command: 'region load', + description: l10n.repeater_cliHelpRegionLoad, + ), + _CommandHelpEntry( + command: 'region get {* | name-prefix}', + description: l10n.repeater_cliHelpRegionGet, + ), + _CommandHelpEntry( + command: 'region put {name} {* | parent-name-prefix}', + description: l10n.repeater_cliHelpRegionPut, + ), + _CommandHelpEntry( + command: 'region remove {name}', + description: l10n.repeater_cliHelpRegionRemove, + ), + _CommandHelpEntry( + command: 'region allowf {* | name-prefix}', + description: l10n.repeater_cliHelpRegionAllowf, + ), + _CommandHelpEntry( + command: 'region denyf {* | name-prefix}', + description: l10n.repeater_cliHelpRegionDenyf, + ), + _CommandHelpEntry( + command: 'region home', + description: l10n.repeater_cliHelpRegionHome, + ), + _CommandHelpEntry( + command: 'region home {* | name-prefix}', + description: l10n.repeater_cliHelpRegionHomeSet, + ), + _CommandHelpEntry( + command: 'region save', + description: l10n.repeater_cliHelpRegionSave, + ), + _CommandHelpEntry( + command: 'region default', + description: l10n.repeater_cliHelpRegionDefault, + ), + _CommandHelpEntry( + command: 'region default {* | name-prefix | }', + description: l10n.repeater_cliHelpRegionDefaultSet, + ), + _CommandHelpEntry( + command: 'region list allowed', + description: l10n.repeater_cliHelpRegionListAllowed, + ), + _CommandHelpEntry( + command: 'region list denied', + description: l10n.repeater_cliHelpRegionListDenied, + ), ]; final getCommands = [ - _CommandHelpEntry(command: 'get name', description: l10n.repeater_cliHelpGetName), - _CommandHelpEntry(command: 'get role', description: l10n.repeater_cliHelpGetRole), - _CommandHelpEntry(command: 'get public.key', description: l10n.repeater_cliHelpGetPublicKey), - _CommandHelpEntry(command: 'get prv.key', description: l10n.repeater_cliHelpGetPrvKey), - _CommandHelpEntry(command: 'get repeat', description: l10n.repeater_cliHelpGetRepeat), - _CommandHelpEntry(command: 'get tx', description: l10n.repeater_cliHelpGetTx), - _CommandHelpEntry(command: 'get freq', description: l10n.repeater_cliHelpGetFreq), - _CommandHelpEntry(command: 'get radio', description: l10n.repeater_cliHelpGetRadio), - _CommandHelpEntry(command: 'get radio.rxgain', description: l10n.repeater_cliHelpGetRadioRxGain), - _CommandHelpEntry(command: 'get af', description: l10n.repeater_cliHelpGetAf), - _CommandHelpEntry(command: 'get dutycycle', description: l10n.repeater_cliHelpGetDutyCycle), - _CommandHelpEntry(command: 'get int.thresh', description: l10n.repeater_cliHelpGetIntThresh), - _CommandHelpEntry(command: 'get agc.reset.interval', description: l10n.repeater_cliHelpGetAgcResetInterval), - _CommandHelpEntry(command: 'get multi.acks', description: l10n.repeater_cliHelpGetMultiAcks), - _CommandHelpEntry(command: 'get allow.read.only', description: l10n.repeater_cliHelpGetAllowReadOnly), - _CommandHelpEntry(command: 'get advert.interval', description: l10n.repeater_cliHelpGetAdvertInterval), - _CommandHelpEntry(command: 'get flood.advert.interval', description: l10n.repeater_cliHelpGetFloodAdvertInterval), - _CommandHelpEntry(command: 'get guest.password', description: l10n.repeater_cliHelpGetGuestPassword), - _CommandHelpEntry(command: 'get lat', description: l10n.repeater_cliHelpGetLat), - _CommandHelpEntry(command: 'get lon', description: l10n.repeater_cliHelpGetLon), - _CommandHelpEntry(command: 'get rxdelay', description: l10n.repeater_cliHelpGetRxDelay), - _CommandHelpEntry(command: 'get txdelay', description: l10n.repeater_cliHelpGetTxDelay), - _CommandHelpEntry(command: 'get direct.txdelay', description: l10n.repeater_cliHelpGetDirectTxDelay), - _CommandHelpEntry(command: 'get flood.max', description: l10n.repeater_cliHelpGetFloodMax), - _CommandHelpEntry(command: 'get owner.info', description: l10n.repeater_cliHelpGetOwnerInfo), - _CommandHelpEntry(command: 'get path.hash.mode', description: l10n.repeater_cliHelpGetPathHashMode), - _CommandHelpEntry(command: 'get loop.detect', description: l10n.repeater_cliHelpGetLoopDetect), - _CommandHelpEntry(command: 'get acl', description: l10n.repeater_cliHelpGetAcl), - _CommandHelpEntry(command: 'get bridge.enabled', description: l10n.repeater_cliHelpGetBridgeEnabled), - _CommandHelpEntry(command: 'get bridge.delay', description: l10n.repeater_cliHelpGetBridgeDelay), - _CommandHelpEntry(command: 'get bridge.source', description: l10n.repeater_cliHelpGetBridgeSource), - _CommandHelpEntry(command: 'get bridge.baud', description: l10n.repeater_cliHelpGetBridgeBaud), - _CommandHelpEntry(command: 'get bridge.channel', description: l10n.repeater_cliHelpGetBridgeChannel), - _CommandHelpEntry(command: 'get bridge.secret', description: l10n.repeater_cliHelpGetBridgeSecret), - _CommandHelpEntry(command: 'get bootloader.ver', description: l10n.repeater_cliHelpGetBootloaderVer), - _CommandHelpEntry(command: 'get adc.multiplier', description: l10n.repeater_cliHelpGetAdcMultiplier), + _CommandHelpEntry( + command: 'get name', + description: l10n.repeater_cliHelpGetName, + ), + _CommandHelpEntry( + command: 'get role', + description: l10n.repeater_cliHelpGetRole, + ), + _CommandHelpEntry( + command: 'get public.key', + description: l10n.repeater_cliHelpGetPublicKey, + ), + _CommandHelpEntry( + command: 'get prv.key', + description: l10n.repeater_cliHelpGetPrvKey, + ), + _CommandHelpEntry( + command: 'get repeat', + description: l10n.repeater_cliHelpGetRepeat, + ), + _CommandHelpEntry( + command: 'get tx', + description: l10n.repeater_cliHelpGetTx, + ), + _CommandHelpEntry( + command: 'get freq', + description: l10n.repeater_cliHelpGetFreq, + ), + _CommandHelpEntry( + command: 'get radio', + description: l10n.repeater_cliHelpGetRadio, + ), + _CommandHelpEntry( + command: 'get radio.rxgain', + description: l10n.repeater_cliHelpGetRadioRxGain, + ), + _CommandHelpEntry( + command: 'get af', + description: l10n.repeater_cliHelpGetAf, + ), + _CommandHelpEntry( + command: 'get dutycycle', + description: l10n.repeater_cliHelpGetDutyCycle, + ), + _CommandHelpEntry( + command: 'get int.thresh', + description: l10n.repeater_cliHelpGetIntThresh, + ), + _CommandHelpEntry( + command: 'get agc.reset.interval', + description: l10n.repeater_cliHelpGetAgcResetInterval, + ), + _CommandHelpEntry( + command: 'get multi.acks', + description: l10n.repeater_cliHelpGetMultiAcks, + ), + _CommandHelpEntry( + command: 'get allow.read.only', + description: l10n.repeater_cliHelpGetAllowReadOnly, + ), + _CommandHelpEntry( + command: 'get advert.interval', + description: l10n.repeater_cliHelpGetAdvertInterval, + ), + _CommandHelpEntry( + command: 'get flood.advert.interval', + description: l10n.repeater_cliHelpGetFloodAdvertInterval, + ), + _CommandHelpEntry( + command: 'get guest.password', + description: l10n.repeater_cliHelpGetGuestPassword, + ), + _CommandHelpEntry( + command: 'get lat', + description: l10n.repeater_cliHelpGetLat, + ), + _CommandHelpEntry( + command: 'get lon', + description: l10n.repeater_cliHelpGetLon, + ), + _CommandHelpEntry( + command: 'get rxdelay', + description: l10n.repeater_cliHelpGetRxDelay, + ), + _CommandHelpEntry( + command: 'get txdelay', + description: l10n.repeater_cliHelpGetTxDelay, + ), + _CommandHelpEntry( + command: 'get direct.txdelay', + description: l10n.repeater_cliHelpGetDirectTxDelay, + ), + _CommandHelpEntry( + command: 'get flood.max', + description: l10n.repeater_cliHelpGetFloodMax, + ), + _CommandHelpEntry( + command: 'get owner.info', + description: l10n.repeater_cliHelpGetOwnerInfo, + ), + _CommandHelpEntry( + command: 'get path.hash.mode', + description: l10n.repeater_cliHelpGetPathHashMode, + ), + _CommandHelpEntry( + command: 'get loop.detect', + description: l10n.repeater_cliHelpGetLoopDetect, + ), + _CommandHelpEntry( + command: 'get acl', + description: l10n.repeater_cliHelpGetAcl, + ), + _CommandHelpEntry( + command: 'get bridge.enabled', + description: l10n.repeater_cliHelpGetBridgeEnabled, + ), + _CommandHelpEntry( + command: 'get bridge.delay', + description: l10n.repeater_cliHelpGetBridgeDelay, + ), + _CommandHelpEntry( + command: 'get bridge.source', + description: l10n.repeater_cliHelpGetBridgeSource, + ), + _CommandHelpEntry( + command: 'get bridge.baud', + description: l10n.repeater_cliHelpGetBridgeBaud, + ), + _CommandHelpEntry( + command: 'get bridge.channel', + description: l10n.repeater_cliHelpGetBridgeChannel, + ), + _CommandHelpEntry( + command: 'get bridge.secret', + description: l10n.repeater_cliHelpGetBridgeSecret, + ), + _CommandHelpEntry( + command: 'get bootloader.ver', + description: l10n.repeater_cliHelpGetBootloaderVer, + ), + _CommandHelpEntry( + command: 'get adc.multiplier', + description: l10n.repeater_cliHelpGetAdcMultiplier, + ), ]; final powerMgmtCommands = [ - _CommandHelpEntry(command: 'get pwrmgt.support', description: l10n.repeater_cliHelpGetPwrMgtSupport), - _CommandHelpEntry(command: 'get pwrmgt.source', description: l10n.repeater_cliHelpGetPwrMgtSource), - _CommandHelpEntry(command: 'get pwrmgt.bootreason', description: l10n.repeater_cliHelpGetPwrMgtBootReason), - _CommandHelpEntry(command: 'get pwrmgt.bootmv', description: l10n.repeater_cliHelpGetPwrMgtBootMv), + _CommandHelpEntry( + command: 'get pwrmgt.support', + description: l10n.repeater_cliHelpGetPwrMgtSupport, + ), + _CommandHelpEntry( + command: 'get pwrmgt.source', + description: l10n.repeater_cliHelpGetPwrMgtSource, + ), + _CommandHelpEntry( + command: 'get pwrmgt.bootreason', + description: l10n.repeater_cliHelpGetPwrMgtBootReason, + ), + _CommandHelpEntry( + command: 'get pwrmgt.bootmv', + description: l10n.repeater_cliHelpGetPwrMgtBootMv, + ), ]; final sensorCommands = [ - _CommandHelpEntry(command: 'sensor get {key}', description: l10n.repeater_cliHelpSensorGet), - _CommandHelpEntry(command: 'sensor set {key} {value}', description: l10n.repeater_cliHelpSensorSet), - _CommandHelpEntry(command: 'sensor list [start]', description: l10n.repeater_cliHelpSensorList), + _CommandHelpEntry( + command: 'sensor get {key}', + description: l10n.repeater_cliHelpSensorGet, + ), + _CommandHelpEntry( + command: 'sensor set {key} {value}', + description: l10n.repeater_cliHelpSensorSet, + ), + _CommandHelpEntry( + command: 'sensor list [start]', + description: l10n.repeater_cliHelpSensorList, + ), ]; final gpsCommands = [ _CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps), - _CommandHelpEntry(command: 'gps {on|off}', description: l10n.repeater_cliHelpGpsOnOff), - _CommandHelpEntry(command: 'gps sync', description: l10n.repeater_cliHelpGpsSync), - _CommandHelpEntry(command: 'gps setloc', description: l10n.repeater_cliHelpGpsSetLoc), - _CommandHelpEntry(command: 'gps advert', description: l10n.repeater_cliHelpGpsAdvert), - _CommandHelpEntry(command: 'gps advert {none|share|prefs}', description: l10n.repeater_cliHelpGpsAdvertSet), + _CommandHelpEntry( + command: 'gps {on|off}', + description: l10n.repeater_cliHelpGpsOnOff, + ), + _CommandHelpEntry( + command: 'gps sync', + description: l10n.repeater_cliHelpGpsSync, + ), + _CommandHelpEntry( + command: 'gps setloc', + description: l10n.repeater_cliHelpGpsSetLoc, + ), + _CommandHelpEntry( + command: 'gps advert', + description: l10n.repeater_cliHelpGpsAdvert, + ), + _CommandHelpEntry( + command: 'gps advert {none|share|prefs}', + description: l10n.repeater_cliHelpGpsAdvertSet, + ), ]; showDialog( @@ -720,27 +1072,64 @@ class _RepeaterCliScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.repeater_commandsListNote, style: const TextStyle(fontSize: 13)), + Text( + l10n.repeater_commandsListNote, + style: const TextStyle(fontSize: 13), + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_general, generalCommands), + _buildHelpSection( + context, + l10n.repeater_general, + generalCommands, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_getCategory, getCommands), + _buildHelpSection( + context, + l10n.repeater_getCategory, + getCommands, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands), + _buildHelpSection( + context, + l10n.repeater_settingsCategory, + settingsCommands, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_powerMgmt, powerMgmtCommands), + _buildHelpSection( + context, + l10n.repeater_powerMgmt, + powerMgmtCommands, + ), const SizedBox(height: 16), _buildHelpSection(context, l10n.repeater_sensors, sensorCommands), const SizedBox(height: 16), _buildHelpSection(context, l10n.repeater_bridge, bridgeCommands), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_logging, loggingCommands), + _buildHelpSection( + context, + l10n.repeater_logging, + loggingCommands, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_neighborsRepeaterOnly, neighborCommands), + _buildHelpSection( + context, + l10n.repeater_neighborsRepeaterOnly, + neighborCommands, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_regionManagementRepeaterOnly, regionCommands, note: l10n.repeater_regionNote), + _buildHelpSection( + context, + l10n.repeater_regionManagementRepeaterOnly, + regionCommands, + note: l10n.repeater_regionNote, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_gpsManagement, gpsCommands, note: l10n.repeater_gpsNote), + _buildHelpSection( + context, + l10n.repeater_gpsManagement, + gpsCommands, + note: l10n.repeater_gpsNote, + ), ], ), ), @@ -764,10 +1153,16 @@ class _RepeaterCliScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + ), if (note != null) ...[ const SizedBox(height: 4), - Text(note, style: TextStyle(fontSize: 11, color: scheme.onSurfaceVariant)), + Text( + note, + style: TextStyle(fontSize: 11, color: scheme.onSurfaceVariant), + ), ], const SizedBox(height: 8), ...commands.map((entry) => _buildHelpCommandCard(context, entry)), diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index bbdec602..ab15e01e 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -70,9 +70,7 @@ class RepeaterHubScreen extends StatelessWidget { children: [ Text( repeater.name, - style: Theme.of(context) - .textTheme - .titleMedium + style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w700), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -88,9 +86,7 @@ class RepeaterHubScreen extends StatelessWidget { const SizedBox(height: 4), Text( repeater.pathLabel(l10n), - style: Theme.of(context) - .textTheme - .bodySmall + style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: scheme.onSurfaceVariant), ), if (repeater.hasLocation) ...[ @@ -122,7 +118,9 @@ class RepeaterHubScreen extends StatelessWidget { ), StatusChip( label: isAdmin ? 'ADMIN' : 'GUEST', - color: isAdmin ? MeshPalette.blue : scheme.onSurfaceVariant, + color: isAdmin + ? MeshPalette.blue + : scheme.onSurfaceVariant, ), ], ), @@ -169,7 +167,9 @@ class RepeaterHubScreen extends StatelessWidget { // ── Tools ────────────────────────────────────────────────────── SectionHeader( - isAdmin ? l10n.repeater_managementTools : l10n.repeater_guestTools, + isAdmin + ? l10n.repeater_managementTools + : l10n.repeater_guestTools, ), _HubActionTile( @@ -306,9 +306,7 @@ class _HubActionTile extends StatelessWidget { decoration: BoxDecoration( color: accentColor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all( - color: accentColor.withValues(alpha: 0.3), - ), + border: Border.all(color: accentColor.withValues(alpha: 0.3)), ), alignment: Alignment.center, child: Icon(icon, size: 22, color: accentColor), @@ -336,11 +334,7 @@ class _HubActionTile extends StatelessWidget { ], ), ), - Icon( - Icons.chevron_right, - color: scheme.onSurfaceVariant, - size: 20, - ), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20), ], ), ), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 30675e58..dca0d7f4 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -1196,9 +1196,7 @@ class _RepeaterSettingsScreenState extends State { const SizedBox(height: 12), DropdownButtonFormField( initialValue: _bandwidth, - decoration: InputDecoration( - labelText: l10n.repeater_bandwidth, - ), + decoration: InputDecoration(labelText: l10n.repeater_bandwidth), items: _bandwidthOptions.map((bw) { return DropdownMenuItem( value: bw, @@ -1478,7 +1476,9 @@ class _RepeaterSettingsScreenState extends State { child: ListTile( title: Text(l10n.repeater_localAdvertInterval), subtitle: Text( - l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + l10n.repeater_localAdvertIntervalMinutes( + _advertInterval, + ), ), trailing: Switch( value: _advertEnable, @@ -1516,7 +1516,9 @@ class _RepeaterSettingsScreenState extends State { min: 60, max: 240, divisions: 18, - label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + label: l10n.repeater_localAdvertIntervalMinutes( + _advertInterval, + ), onChanged: _advertEnable ? (value) { setState(() { @@ -2014,9 +2016,7 @@ class _RepeaterSettingsScreenState extends State { ), subtitle: Text( l10n.repeater_rebootRepeaterSubtitle, - style: const TextStyle( - color: MeshPalette.warnDim, - ), + style: const TextStyle(color: MeshPalette.warnDim), ), onTap: () => _confirmAction( l10n.repeater_rebootRepeater, @@ -2027,19 +2027,14 @@ class _RepeaterSettingsScreenState extends State { ), // Regenerate identity key - hidden until fully implemented ListTile( - leading: const Icon( - Icons.delete_forever, - color: MeshPalette.alert, - ), + leading: const Icon(Icons.delete_forever, color: MeshPalette.alert), title: Text( l10n.repeater_eraseFileSystem, style: const TextStyle(color: MeshPalette.alert), ), subtitle: Text( l10n.repeater_eraseFileSystemSubtitle, - style: const TextStyle( - color: MeshPalette.warnDim, - ), + style: const TextStyle(color: MeshPalette.warnDim), ), onTap: () => _confirmAction( l10n.repeater_eraseFileSystem, diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 080d5b7f..42c97fa5 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -605,7 +605,8 @@ class _RepeaterStatusScreenState extends State { final batteryMv = connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? _batteryMv; - if (batteryMv == null) return Theme.of(context).colorScheme.onSurfaceVariant; + if (batteryMv == null) + return Theme.of(context).colorScheme.onSurfaceVariant; final percent = estimateBatteryPercentFromMillivolts( batteryMv, _batteryChemistry(), @@ -624,12 +625,14 @@ class _RepeaterStatusScreenState extends State { crossAxisSpacing: 8, childAspectRatio: 2.2, children: items - .map((item) => StatTile( - icon: item.icon, - label: item.label, - value: item.value, - color: item.color, - )) + .map( + (item) => StatTile( + icon: item.icon, + label: item.label, + value: item.value, + color: item.color, + ), + ) .toList(), ); } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 84be2df6..a8c83723 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -482,30 +482,30 @@ class _ConnectionStatusHeader extends StatelessWidget { final (String label, Color color, bool pulse) = switch (connector.state) { MeshCoreConnectionState.scanning => ( - l10n.scanner_scanning, - MeshPalette.blue, - true, - ), + l10n.scanner_scanning, + MeshPalette.blue, + true, + ), MeshCoreConnectionState.connecting => ( - l10n.scanner_connecting, - MeshPalette.warn, - true, - ), + l10n.scanner_connecting, + MeshPalette.warn, + true, + ), MeshCoreConnectionState.connected => ( - l10n.scanner_connectedTo(connector.deviceDisplayName), - MeshPalette.signal, - false, - ), + l10n.scanner_connectedTo(connector.deviceDisplayName), + MeshPalette.signal, + false, + ), MeshCoreConnectionState.disconnecting => ( - l10n.scanner_disconnecting, - MeshPalette.warn, - true, - ), + l10n.scanner_disconnecting, + MeshPalette.warn, + true, + ), MeshCoreConnectionState.disconnected => ( - l10n.scanner_notConnected, - scheme.onSurfaceVariant, - false, - ), + l10n.scanner_notConnected, + scheme.onSurfaceVariant, + false, + ), }; return Padding( @@ -515,11 +515,7 @@ class _ConnectionStatusHeader extends StatelessWidget { child: Align( key: ValueKey(connector.state), alignment: Alignment.centerLeft, - child: StatusChip( - label: label, - color: color, - pulse: pulse, - ), + child: StatusChip(label: label, color: color, pulse: pulse), ), ), ); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 81346f5f..ef7ae08e 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -196,11 +196,7 @@ class _SettingsScreenState extends State { ), ), if (showChevron) - Icon( - Icons.chevron_right, - color: scheme.onSurfaceVariant, - size: 16, - ), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 16), ], ); } @@ -694,11 +690,7 @@ class _SettingsScreenState extends State { ], ), ), - Icon( - Icons.chevron_right, - color: scheme.onSurfaceVariant, - size: 16, - ), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 16), ], ), ), diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index cbed0277..85ac929f 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -220,9 +220,7 @@ class _TcpScreenState extends State { if (connector.isTcpTransportConnected) { return StatusChip( - label: l10n.scanner_connectedTo( - connector.activeTcpEndpoint ?? 'TCP', - ), + label: l10n.scanner_connectedTo(connector.activeTcpEndpoint ?? 'TCP'), color: MeshPalette.signal, ); } else if (connector.state == MeshCoreConnectionState.connecting && diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 7087bd2c..fafa68f2 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -604,66 +604,68 @@ class _TelemetryScreenState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildAutoRefreshNumberField( - controller: _autoRefreshIntervalController, - label: l10n.common_interval, - min: _autoRefreshMinIntervalSeconds, - max: _autoRefreshMaxIntervalSeconds, - fallback: _autoRefreshIntervalSeconds, - ), - const SizedBox(height: 12), - _buildAutoRefreshNumberField( - controller: _autoRefreshQuantityController, - label: l10n.telemetry_autoFetchQuantity, - min: _autoRefreshMinQuantity, - max: _autoRefreshMaxQuantity, - fallback: _autoRefreshDefaultQuantity, - ), - if (counterText != null) ...[ - const SizedBox(height: 12), - Text( - counterText, - textAlign: TextAlign.center, - style: TextStyle( - color: _autoRefreshLastAttemptFailed - ? Theme.of(context).colorScheme.error - : null, - fontWeight: FontWeight.w600, - ), + controller: _autoRefreshIntervalController, + label: l10n.common_interval, + min: _autoRefreshMinIntervalSeconds, + max: _autoRefreshMaxIntervalSeconds, + fallback: _autoRefreshIntervalSeconds, ), - ], - const SizedBox(height: 12), - FilledButton( - onPressed: _isLoading && !_isAutoRefreshEnabled - ? null - : _toggleAutoRefresh, - child: _isAutoRefreshEnabled - ? SizedBox( - width: double.infinity, - height: 20, - child: Stack( - alignment: Alignment.center, - children: [ - Center(child: Text(l10n.common_disable)), - Positioned( - right: 0, - child: SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.onPrimary, + const SizedBox(height: 12), + _buildAutoRefreshNumberField( + controller: _autoRefreshQuantityController, + label: l10n.telemetry_autoFetchQuantity, + min: _autoRefreshMinQuantity, + max: _autoRefreshMaxQuantity, + fallback: _autoRefreshDefaultQuantity, + ), + if (counterText != null) ...[ + const SizedBox(height: 12), + Text( + counterText, + textAlign: TextAlign.center, + style: TextStyle( + color: _autoRefreshLastAttemptFailed + ? Theme.of(context).colorScheme.error + : null, + fontWeight: FontWeight.w600, + ), + ), + ], + const SizedBox(height: 12), + FilledButton( + onPressed: _isLoading && !_isAutoRefreshEnabled + ? null + : _toggleAutoRefresh, + child: _isAutoRefreshEnabled + ? SizedBox( + width: double.infinity, + height: 20, + child: Stack( + alignment: Alignment.center, + children: [ + Center(child: Text(l10n.common_disable)), + Positioned( + right: 0, + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of( + context, + ).colorScheme.onPrimary, + ), ), ), - ), - ], - ), - ) - : Text(l10n.common_enable), - ), - ], + ], + ), + ) + : Text(l10n.common_enable), + ), + ], + ), ), - ), - ], + ], ); } @@ -912,10 +914,7 @@ class _TelemetryScreenState extends State { const SizedBox(width: 8), Text( value, - style: MeshTheme.mono( - fontSize: 13, - color: scheme.onSurface, - ), + style: MeshTheme.mono(fontSize: 13, color: scheme.onSurface), textAlign: TextAlign.end, ), ], diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 0e40da64..9bd5a5ca 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -108,9 +108,7 @@ class _UsbScreenState extends State { child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Align( - key: ValueKey( - '${connector.state}_$_isLoadingPorts', - ), + key: ValueKey('${connector.state}_$_isLoadingPorts'), alignment: Alignment.centerLeft, child: _buildStatusChip(context, connector), ), @@ -230,17 +228,11 @@ class _UsbScreenState extends State { final l10n = context.l10n; if (_isLoadingPorts) { - return EmptyState( - icon: Icons.usb, - title: l10n.usbStatus_searching, - ); + return EmptyState(icon: Icons.usb, title: l10n.usbStatus_searching); } if (_ports.isEmpty) { - return EmptyState( - icon: Icons.usb, - title: l10n.usbScreenEmptyState, - ); + return EmptyState(icon: Icons.usb, title: l10n.usbScreenEmptyState); } final isConnecting = diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 56341598..50ec64d9 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -44,7 +44,8 @@ class RetryServiceConfig { int messageBytes, { String? contactKey, int? deviceTimeoutMs, - })? calculateTimeout; + })? + calculateTimeout; final Uint8List? Function()? getSelfPublicKey; final String Function(Contact, String)? prepareContactOutboundText; final AppSettingsService? appSettingsService; @@ -436,8 +437,7 @@ class MessageRetryService extends ChangeNotifier { final outboundTextForTimeout = config.prepareContactOutboundText?.call(contact, message.text) ?? message.text; - final messageBytesForTimeout = - utf8.encode(outboundTextForTimeout).length; + final messageBytesForTimeout = utf8.encode(outboundTextForTimeout).length; int actualTimeout = timeoutMs; if (config.calculateTimeout != null) { @@ -704,13 +704,11 @@ class MessageRetryService extends ChangeNotifier { tripTimeMs > 0 && message.pathLength != null) { final outboundTextForObserved = - config!.prepareContactOutboundText?.call( - contact, - message.text, - ) ?? + config!.prepareContactOutboundText?.call(contact, message.text) ?? message.text; - final messageBytesForObserved = - utf8.encode(outboundTextForObserved).length; + final messageBytesForObserved = utf8 + .encode(outboundTextForObserved) + .length; config.onDeliveryObserved!( contact.publicKeyHex, message.pathLength!, diff --git a/lib/widgets/device_tile.dart b/lib/widgets/device_tile.dart index ef0f5441..3763b78a 100644 --- a/lib/widgets/device_tile.dart +++ b/lib/widgets/device_tile.dart @@ -30,7 +30,9 @@ class DeviceTile extends StatelessWidget { final name = device.platformName.isNotEmpty ? device.platformName : scanResult.advertisementData.advName; - final displayName = name.isNotEmpty ? name : context.l10n.common_unknownDevice; + final displayName = name.isNotEmpty + ? name + : context.l10n.common_unknownDevice; final mac = device.remoteId.toString(); final scheme = Theme.of(context).colorScheme; @@ -55,11 +57,7 @@ class DeviceTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ - AvatarCircle( - name: displayName, - size: 42, - icon: Icons.router, - ), + AvatarCircle(name: displayName, size: 42, icon: Icons.router), const SizedBox(width: 12), Expanded( child: Column( @@ -107,10 +105,7 @@ class DeviceTile extends StatelessWidget { const SizedBox(height: 3), Text( '$rssi dBm', - style: MeshTheme.mono( - fontSize: 10, - color: signalUi.color, - ), + style: MeshTheme.mono(fontSize: 10, color: signalUi.color), ), ], ), diff --git a/lib/widgets/elements_ui.dart b/lib/widgets/elements_ui.dart index 55d930db..a4995c73 100644 --- a/lib/widgets/elements_ui.dart +++ b/lib/widgets/elements_ui.dart @@ -60,10 +60,7 @@ class _FeatureToggleRow extends State { Row( mainAxisSize: MainAxisSize.min, children: [ - Switch( - value: widget.value, - onChanged: widget.onChanged, - ), + Switch(value: widget.value, onChanged: widget.onChanged), if (widget.hasRefreshing) ...[ const SizedBox(width: 4), widget.isRefreshing diff --git a/lib/widgets/jump_to_bottom_button.dart b/lib/widgets/jump_to_bottom_button.dart index 6eab28a9..5ce25542 100644 --- a/lib/widgets/jump_to_bottom_button.dart +++ b/lib/widgets/jump_to_bottom_button.dart @@ -29,10 +29,7 @@ class JumpToBottomButton extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, color: scheme.surfaceContainerHigh.withValues(alpha: 0.92), - border: Border.all( - color: scheme.outlineVariant, - width: 1, - ), + border: Border.all(color: scheme.outlineVariant, width: 1), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.18), diff --git a/lib/widgets/mesh_ui.dart b/lib/widgets/mesh_ui.dart index bfbe679f..797af7a7 100644 --- a/lib/widgets/mesh_ui.dart +++ b/lib/widgets/mesh_ui.dart @@ -350,7 +350,9 @@ class RouteChip extends StatelessWidget { Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final label = isDirect - ? (hops == null || hops == 0 ? 'DIRECT' : '$hops HOP${hops == 1 ? '' : 'S'}') + ? (hops == null || hops == 0 + ? 'DIRECT' + : '$hops HOP${hops == 1 ? '' : 'S'}') : 'FLOOD'; return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), @@ -536,9 +538,7 @@ Future showMeshSheet( useSafeArea: true, showDragHandle: false, builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.viewInsetsOf(context).bottom, - ), + padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), child: builder(context), ), ); @@ -574,10 +574,7 @@ class ErrorRetryCard extends StatelessWidget { ), ), if (onRetry != null) - TextButton( - onPressed: onRetry, - child: Text(retryLabel ?? 'Retry'), - ), + TextButton(onPressed: onRetry, child: Text(retryLabel ?? 'Retry')), ], ), ); @@ -610,10 +607,7 @@ class _ListEntranceState extends State vsync: this, duration: const Duration(milliseconds: 280), ); - _curve = CurvedAnimation( - parent: _controller, - curve: Curves.easeOutCubic, - ); + _curve = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic); final delay = Duration(milliseconds: 24 * widget.index.clamp(0, 12)); Future.delayed(delay, () { if (mounted) _controller.forward(); diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart index 1d2bbd45..c1971c15 100644 --- a/lib/widgets/message_status_icon.dart +++ b/lib/widgets/message_status_icon.dart @@ -72,11 +72,7 @@ class _MessageStatusIconState extends State if (widget.isFailed) { return Semantics( label: l10n.messageStatus_failed, - child: Icon( - Icons.cancel, - size: size, - color: colorScheme.error, - ), + child: Icon(Icons.cancel, size: size, color: colorScheme.error), ); } diff --git a/lib/widgets/path_map_ui.dart b/lib/widgets/path_map_ui.dart index cfd3672e..333301e0 100644 --- a/lib/widgets/path_map_ui.dart +++ b/lib/widgets/path_map_ui.dart @@ -160,10 +160,7 @@ List buildPacketTrailPolylines( } /// The moving packet dot and the pulse ring at the hop it just reached. -List buildPacketMarkers( - PathPlaybackController playback, - Color color, -) { +List buildPacketMarkers(PathPlaybackController playback, Color color) { if (!playback.started || !playback.hasPath) return const []; final markers = []; @@ -412,7 +409,9 @@ class PathAnimationControls extends StatelessWidget { ), controlButton( icon: playback.playing ? Icons.pause : Icons.play_arrow, - tooltip: playback.playing ? l10n.pathMap_pause : l10n.pathMap_play, + tooltip: playback.playing + ? l10n.pathMap_pause + : l10n.pathMap_play, onPressed: enabled ? playback.togglePlay : null, ), controlButton( @@ -635,7 +634,10 @@ class PathSummaryList extends StatelessWidget { parts.join(' · '), maxLines: 1, overflow: TextOverflow.ellipsis, - style: MeshTheme.mono(fontSize: 10.5, color: MeshPalette.ink3), + style: MeshTheme.mono( + fontSize: 10.5, + color: MeshPalette.ink3, + ), ), ), ), diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 207bbe5d..56834455 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -332,19 +332,12 @@ class _RepeaterLoginDialogState extends State { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.error, - size: 18, - color: scheme.error, - ), + Icon(Icons.error, size: 18, color: scheme.error), const SizedBox(width: 8), Expanded( child: Text( _loginError!, - style: TextStyle( - color: scheme.error, - fontSize: 13, - ), + style: TextStyle(color: scheme.error, fontSize: 13), ), ), ], @@ -435,9 +428,7 @@ class _RepeaterLoginDialogState extends State { Icon( Icons.auto_mode, size: 20, - color: !isFloodMode - ? scheme.primary - : null, + color: !isFloodMode ? scheme.primary : null, ), const SizedBox(width: 8), Text( @@ -458,9 +449,7 @@ class _RepeaterLoginDialogState extends State { Icon( Icons.waves, size: 20, - color: isFloodMode - ? scheme.primary - : null, + color: isFloodMode ? scheme.primary : null, ), const SizedBox(width: 8), Text( diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 34a3f994..a5e2555b 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -364,9 +364,7 @@ class _RoomLoginDialogState extends State { Icon( Icons.auto_mode, size: 20, - color: !isFloodMode - ? scheme.primary - : null, + color: !isFloodMode ? scheme.primary : null, ), const SizedBox(width: 8), Text( @@ -387,9 +385,7 @@ class _RoomLoginDialogState extends State { Icon( Icons.waves, size: 20, - color: isFloodMode - ? scheme.primary - : null, + color: isFloodMode ? scheme.primary : null, ), const SizedBox(width: 8), Text( diff --git a/lib/widgets/unread_divider.dart b/lib/widgets/unread_divider.dart index d238b1a1..4de05b32 100644 --- a/lib/widgets/unread_divider.dart +++ b/lib/widgets/unread_divider.dart @@ -15,10 +15,7 @@ class UnreadDivider extends StatelessWidget { child: Row( children: [ Expanded( - child: Container( - height: 1, - color: color.withValues(alpha: 0.25), - ), + child: Container(height: 1, color: color.withValues(alpha: 0.25)), ), const SizedBox(width: 10), Container( @@ -39,10 +36,7 @@ class UnreadDivider extends StatelessWidget { ), const SizedBox(width: 10), Expanded( - child: Container( - height: 1, - color: color.withValues(alpha: 0.25), - ), + child: Container(height: 1, color: color.withValues(alpha: 0.25)), ), ], ), From 760d8e1db37651bd9c53a40ceaaa4d36181235e4 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 13 Jun 2026 01:58:42 -0700 Subject: [PATCH 13/16] Update localization files and improve path map UI - Added new localization strings for Swedish, Ukrainian, and Chinese languages in app_sv.arb, app_uk.arb, and app_zh.arb respectively. - Enhanced the path map UI in channel_message_path_screen.dart for better readability and interaction. - Improved message retry logic in message_retry_service.dart to prevent double-pumping of queues. - Bumped version number in pubspec.yaml to 9.5.0+13. - Cleared untranslated strings in untranslated.json to reflect current localization status. --- lib/connector/meshcore_connector.dart | 23 +- lib/l10n/app_bg.arb | 580 +++-- lib/l10n/app_de.arb | 874 ++++--- lib/l10n/app_es.arb | 742 +++--- lib/l10n/app_fr.arb | 629 +++-- lib/l10n/app_hu.arb | 2152 +++++++++-------- lib/l10n/app_it.arb | 753 ++++-- lib/l10n/app_ja.arb | 928 +++++--- lib/l10n/app_ko.arb | 541 +++-- lib/l10n/app_localizations_bg.dart | 551 +++-- lib/l10n/app_localizations_de.dart | 744 +++--- lib/l10n/app_localizations_es.dart | 588 +++-- lib/l10n/app_localizations_fr.dart | 471 ++-- lib/l10n/app_localizations_hu.dart | 2198 +++++++++--------- lib/l10n/app_localizations_it.dart | 527 +++-- lib/l10n/app_localizations_ja.dart | 784 ++++--- lib/l10n/app_localizations_ko.dart | 422 ++-- lib/l10n/app_localizations_nl.dart | 71 +- lib/l10n/app_localizations_pl.dart | 85 +- lib/l10n/app_localizations_pt.dart | 76 +- lib/l10n/app_localizations_ru.dart | 84 +- lib/l10n/app_localizations_sk.dart | 80 +- lib/l10n/app_localizations_sl.dart | 84 +- lib/l10n/app_localizations_sv.dart | 78 +- lib/l10n/app_localizations_uk.dart | 85 +- lib/l10n/app_localizations_zh.dart | 81 +- lib/l10n/app_nl.arb | 96 +- lib/l10n/app_pl.arb | 96 +- lib/l10n/app_pt.arb | 96 +- lib/l10n/app_ru.arb | 96 +- lib/l10n/app_sk.arb | 96 +- lib/l10n/app_sl.arb | 96 +- lib/l10n/app_sv.arb | 96 +- lib/l10n/app_uk.arb | 96 +- lib/l10n/app_zh.arb | 96 +- lib/screens/channel_message_path_screen.dart | 208 +- lib/services/message_retry_service.dart | 14 +- pubspec.yaml | 2 +- untranslated.json | 716 +----- 39 files changed, 8823 insertions(+), 7212 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef9a9e24..b929c09d 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4459,24 +4459,27 @@ class MeshCoreConnector extends ChangeNotifier { secondsSinceLastRx: secSinceRx, ); if (mlTimeout != null) { - // Use device est_timeout as an additional ceiling when available — - // the firmware computed it from real airtime, so it's better than - // a physics guess built on a 50 ms fallback. - final ceiling = deviceTimeoutMs != null && deviceTimeoutMs > physicsMin + // Use device est_timeout as a baseline floor when available — + // the firmware computed it from real airtime. Let the learned ML + // estimate widen above it up to the hard cap, but never below it. + final floor = deviceTimeoutMs != null && deviceTimeoutMs > physicsMin ? deviceTimeoutMs.clamp(physicsMin, _hardMaxTimeoutMs) - : physicsMax; + : physicsMin.clamp(0, _hardMaxTimeoutMs); if (pathLength < 0) { - // Flood: trust ML, only enforce firmware formula as floor - if (mlTimeout < physicsMin) { - return physicsMin.clamp(0, _hardMaxTimeoutMs); + // Flood: trust ML, only enforce firmware estimate as floor + if (mlTimeout < floor) { + return floor.clamp(0, _hardMaxTimeoutMs); } } - return mlTimeout.clamp(physicsMin, ceiling).clamp(0, _hardMaxTimeoutMs); + return mlTimeout.clamp(floor, _hardMaxTimeoutMs); } // No ML data — prefer device est_timeout (it used real airtime), then physics. + // Cap the floor to the hard maximum so slow-flood physicsMin cannot exceed + // the upper bound and make clamp() throw. if (deviceTimeoutMs != null && deviceTimeoutMs > 0) { - return deviceTimeoutMs.clamp(physicsMin, _hardMaxTimeoutMs); + final floor = physicsMin.clamp(0, _hardMaxTimeoutMs); + return deviceTimeoutMs.clamp(floor, _hardMaxTimeoutMs); } return physicsMax.clamp(0, _hardMaxTimeoutMs); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index f288b774..c9923e75 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -23,13 +23,13 @@ "common_settings": "Настройки", "common_disconnect": "Прекъсни", "common_connected": "Свързано", - "common_disconnected": "Откъснато", + "common_disconnected": "Прекъснато", "common_create": "Създай", "common_continue": "Продължи", "common_share": "Сподели", "common_copy": "Копирай", "common_retry": "Опитай отново", - "common_hide": "Скриване", + "common_hide": "Скрий", "common_remove": "Изтрий", "common_enable": "Активирай", "common_disable": "Деактивирай", @@ -68,7 +68,7 @@ } }, "scanner_searchingDevices": "Търсене на устройства MeshCore...", - "scanner_tapToScan": "Натиснете Сканиране, за да намерите устройства MeshCore.", + "scanner_tapToScan": "Докоснете „Сканирай“, за да намерите устройства MeshCore.", "scanner_connectionFailed": "Връзката не успя: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -94,7 +94,7 @@ "settings_radioSettingsSubtitle": "Честота, мощност, разпространяващ фактор", "settings_radioSettingsUpdated": "Радио настройките са актуализирани", "settings_location": "Местоположение", - "settings_locationSubtitle": "Координати на GPS", + "settings_locationSubtitle": "GPS координати", "settings_locationUpdated": "Местоположението е актуализирано", "settings_locationBothRequired": "Въведете както географска ширина, така и географска дължина.", "settings_locationInvalid": "Невалидна ширина или дължина.", @@ -106,22 +106,22 @@ "settings_privacyModeEnabled": "Режим на поверителност е активиран", "settings_privacyModeDisabled": "Режим на поверителност е деактивиран", "settings_actions": "Действия", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", - "settings_sendAdvertisement": "Изпрати Реклама", - "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", - "settings_advertisementSent": "Реклама изпратена", + "settings_deleteAllPaths": "Изтрий всички пътища", + "settings_deleteAllPathsSubtitle": "Изчисти всички данни за пътищата от контактите.", + "settings_sendAdvertisement": "Изпрати реклама", + "settings_sendAdvertisementSubtitle": "Излъчи присъствието сега", + "settings_advertisementSent": "Рекламата е изпратена", "settings_syncTime": "Време за синхронизация", "settings_syncTimeSubtitle": "Задайте часовника на устройството да отговаря на времето на телефона.", "settings_timeSynchronized": "Синхронизирано във времето", "settings_refreshContacts": "Презареди контакти", "settings_refreshContactsSubtitle": "Презареди списъка с контакти от устройството", - "settings_rebootDevice": "Рестартирайте устройството", - "settings_rebootDeviceSubtitle": "Рестартирайте устройството MeshCore", - "settings_rebootDeviceConfirm": "Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.", + "settings_rebootDevice": "Рестартирай устройството", + "settings_rebootDeviceSubtitle": "Рестартирай устройството MeshCore", + "settings_rebootDeviceConfirm": "Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете прекъснати.", "settings_debug": "Отстрани", - "settings_bleDebugLog": "Лог за отстраняване на грешки на BLE", - "settings_bleDebugLogSubtitle": "Команди, отговори и сурови данни BLE", + "settings_bleDebugLog": "Дневник за отстраняване на грешки на BLE", + "settings_bleDebugLogSubtitle": "Команди, отговори и сурови BLE данни", "settings_appDebugLog": "Лог на отстраняване на грешки на приложението", "settings_appDebugLogSubtitle": "Съобщения за отстраняване на грешки на приложението", "settings_about": "За нас", @@ -134,22 +134,22 @@ } }, "settings_aboutLegalese": "Проект MeshCore с отворен код 2024 г.", - "settings_aboutDescription": "Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.", + "settings_aboutDescription": "Отворен Flutter клиент за MeshCore LoRa мрежови устройства.", "settings_infoName": "Име", "settings_infoId": "ИД", "settings_infoStatus": "Статус", "settings_infoBattery": "Батерия", - "settings_infoPublicKey": "Общ публичен ключ", + "settings_infoPublicKey": "Публичен ключ", "settings_infoContactsCount": "Брой контакти", "settings_infoChannelCount": "Брой канали", "settings_presets": "Предварителни настройки", "settings_frequency": "Честота (MHz)", "settings_frequencyHelper": "300,0 – 2500,0", "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", - "settings_bandwidth": "Ширина на честотния спектър", + "settings_bandwidth": "Ширина на честотната лента", "settings_spreadingFactor": "Фактор на разпространение", - "settings_codingRate": "Такса за кодиране", - "settings_txPower": "TX Мощност (dBm)", + "settings_codingRate": "Скорост на кодиране", + "settings_txPower": "TX мощност (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", "settings_error": "Грешка: {message}", @@ -164,8 +164,8 @@ "appSettings_appearance": "Външен вид", "appSettings_theme": "Тема", "appSettings_themeSystem": "Система по подразбиране", - "appSettings_themeLight": "Ярка", - "appSettings_themeDark": "Тъмно", + "appSettings_themeLight": "Светла", + "appSettings_themeDark": "Тъмна", "appSettings_language": "Език", "appSettings_languageSystem": "Система по подразбиране", "appSettings_languageEn": "Английски", @@ -173,35 +173,35 @@ "appSettings_languageEs": "Испански", "appSettings_languageDe": "Немски", "appSettings_languagePl": "Полски", - "appSettings_languageSl": "Словенски език", + "appSettings_languageSl": "Словенски", "appSettings_languagePt": "Португалски", "appSettings_languageIt": "Италиански", "appSettings_languageZh": "Китайски", "appSettings_languageSv": "Шведски", - "appSettings_languageNl": "Хололандски", - "appSettings_languageSk": "Словенски", + "appSettings_languageNl": "Нидерландски", + "appSettings_languageSk": "Словашки", "appSettings_languageBg": "Български", "appSettings_notifications": "Уведомления", - "appSettings_enableNotifications": "Активирай Известия", - "appSettings_enableNotificationsSubtitle": "Получете известия за съобщения и реклами", + "appSettings_enableNotifications": "Включи известията", + "appSettings_enableNotificationsSubtitle": "Получавайте известия за съобщения и реклами", "appSettings_notificationPermissionDenied": "Отказвано е разрешение за известия", - "appSettings_notificationsEnabled": "Уведомителни са активирани", - "appSettings_notificationsDisabled": "Известия са изключени", - "appSettings_messageNotifications": "Уведомления", - "appSettings_messageNotificationsSubtitle": "Покажи известие при получаване на нови съобщения", - "appSettings_channelMessageNotifications": "Уведомления за съобщения от канал", - "appSettings_channelMessageNotificationsSubtitle": "Покажи известие при получаване на съобщения от канали", + "appSettings_notificationsEnabled": "Известията са включени", + "appSettings_notificationsDisabled": "Известията са изключени", + "appSettings_messageNotifications": "Известия за съобщения", + "appSettings_messageNotificationsSubtitle": "Показвай известие при получаване на нови съобщения", + "appSettings_channelMessageNotifications": "Известия за канални съобщения", + "appSettings_channelMessageNotificationsSubtitle": "Показвай известие при получаване на съобщения от канали", "appSettings_advertisementNotifications": "Уведомления за реклами", "appSettings_advertisementNotificationsSubtitle": "Покажи известие, когато бъдат открити нови възли.", "appSettings_messaging": "Съобщения", - "appSettings_clearPathOnMaxRetry": "Изчисти Път на Макс Опит", - "appSettings_clearPathOnMaxRetrySubtitle": "Възстанови контактния път след 5 неуспешни опита за изпращане", + "appSettings_clearPathOnMaxRetry": "Изчисти пътя при максимален брой опити", + "appSettings_clearPathOnMaxRetrySubtitle": "Възстанови пътя към контакта след 5 неуспешни опита за изпращане", "appSettings_pathsWillBeCleared": "Пътищата ще бъдат почистени след 5 неуспешни опита.", "appSettings_pathsWillNotBeCleared": "Пътищата няма да бъдат автоматично изчистени.", - "appSettings_autoRouteRotation": "Автоматично маршрутизиране на завъртания", - "appSettings_autoRouteRotationSubtitle": "Превключете между най-добрите пътища и режим на наводняване", - "appSettings_autoRouteRotationEnabled": "Автоматично маршрутизиране вкл.", - "appSettings_autoRouteRotationDisabled": "Автоматично маршрутизирането е деактивирано", + "appSettings_autoRouteRotation": "Автоматична ротация на маршрутите", + "appSettings_autoRouteRotationSubtitle": "Превключвайте между най-добрите пътища и режим на наводняване", + "appSettings_autoRouteRotationEnabled": "Автоматичната ротация на маршрутите е включена", + "appSettings_autoRouteRotationDisabled": "Автоматичната ротация на маршрутите е изключена", "appSettings_battery": "Батерия", "appSettings_batteryChemistry": "Химия на батерията", "appSettings_batteryChemistryPerDevice": "Зададено за устройство ({deviceName})", @@ -254,10 +254,10 @@ } }, "appSettings_debugCard": "Отстрани", - "appSettings_appDebugLogging": "Логване за отстраняване на грешки на приложението", - "appSettings_appDebugLoggingSubtitle": "Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.", - "appSettings_appDebugLoggingEnabled": "Режимът за отстраняване на грешки в приложението е активиран.", - "appSettings_appDebugLoggingDisabled": "Логването за отстраняване на грешки в приложението е изключено.", + "appSettings_appDebugLogging": "Дневник за отстраняване на грешки на приложението", + "appSettings_appDebugLoggingSubtitle": "Записвай съобщенията за отстраняване на грешки на приложението.", + "appSettings_appDebugLoggingEnabled": "Дневникът за отстраняване на грешки на приложението е включен.", + "appSettings_appDebugLoggingDisabled": "Дневникът за отстраняване на грешки на приложението е изключен.", "contacts_title": "Контакти", "contacts_noContacts": "Няма контакти към момента.", "contacts_contactsWillAppear": "Контактите ще се появят, когато устройствата рекламират.", @@ -273,7 +273,7 @@ } } }, - "contacts_manageRepeater": "Управление на Повтарящ се Елемент", + "contacts_manageRepeater": "Управление на повторителя", "contacts_roomLogin": "Вход в стаята", "contacts_openChat": "Отвори чат", "contacts_editGroup": "Редактирай Група", @@ -287,7 +287,7 @@ } }, "contacts_newGroup": "Нова група", - "contacts_groupName": "Група", + "contacts_groupName": "Име на групата", "contacts_groupNameRequired": "Името на групата е задължително.", "contacts_groupNameReserved": "Това име на група е запазено", "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", @@ -298,11 +298,11 @@ } } }, - "contacts_filterContacts": "Филтрирайте контактите...", + "contacts_filterContacts": "Филтрирай контактите...", "contacts_noContactsMatchFilter": "Няма съвпадения с вашия филтър.", "contacts_noMembers": "Няма членове", - "contacts_lastSeenNow": "Последно видяно сега", - "contacts_lastSeenMinsAgo": "Последна активност {minutes} минути преди", + "contacts_lastSeenNow": "Видян току-що", + "contacts_lastSeenMinsAgo": "Преди {minutes} минути", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -310,8 +310,8 @@ } } }, - "contacts_lastSeenHourAgo": "Последно видяно преди час", - "contacts_lastSeenHoursAgo": "Последно видян {hours} часа преди.", + "contacts_lastSeenHourAgo": "Преди час", + "contacts_lastSeenHoursAgo": "Преди {hours} часа", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -319,8 +319,8 @@ } } }, - "contacts_lastSeenDayAgo": "Последно видяно преди 1 ден", - "contacts_lastSeenDaysAgo": "Последно видян {days} дни преди.", + "contacts_lastSeenDayAgo": "Преди 1 ден", + "contacts_lastSeenDaysAgo": "Преди {days} дни", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -342,7 +342,7 @@ } }, "channels_public": "Публично", - "channels_private": "Личен", + "channels_private": "Частен", "channels_editChannel": "Редактирай канал", "channels_muteChannel": "Заглуши канала", "channels_unmuteChannel": "Включи известията на канала", @@ -366,12 +366,12 @@ "channels_addChannel": "Добави Канал", "channels_channelIndexLabel": "Индекс на канал", "channels_channelName": "Име на канала", - "channels_usePublicChannel": "Използвайте публичен канал", + "channels_usePublicChannel": "Използвай публичен канал", "channels_standardPublicPsk": "Стандартен публичен PSK", "channels_pskHex": "PSK (шестнадесетичен код)", "channels_generateRandomPsk": "Генерирай случайна PSK", "channels_enterChannelName": "Моля, въведете име на канал.", - "channels_pskMustBe32Hex": "PSK трябва да бъде 32 шестнаредни знака.", + "channels_pskMustBe32Hex": "PSK трябва да бъде 32 шестнадесетични знака.", "channels_channelAdded": "Каналът \"{name}\" е добавен", "@channels_channelAdded": { "placeholders": { @@ -416,7 +416,7 @@ "channels_publicChannelAdded": "Публичен канал добавен", "channels_sortBy": "Сортирай по", "channels_sortManual": "Ръчно", - "channels_sortAZ": "От A до Я", + "channels_sortAZ": "От А до Я", "channels_sortLatestMessages": "Последни съобщения", "channels_sortUnread": "Непрочетено", "chat_noMessages": "Няма съобщения.", @@ -559,15 +559,15 @@ } }, "debugFrame_hexDump": "Хексадесетичен Dump:", - "chat_pathManagement": "Управление на пътища", + "chat_pathManagement": "Управление на пътищата", "chat_routingMode": "Режим на маршрутизиране", "chat_autoUseSavedPath": "Автоматично (използвай запазения път)", "chat_forceFloodMode": "Принуди режим на наводняване", - "chat_recentAckPaths": "Неотдавни ACK пътища (докоснете, за да използвате):", + "chat_recentAckPaths": "Последни ACK пътища (докоснете, за да използвате):", "chat_pathHistoryFull": "Историята на пътя е пълна. Премахнете записи, за да добавите нови.", - "chat_hopSingular": "скочи", - "chat_hopPlural": "скоци", - "chat_hopsCount": "{count} {count, plural, =1{скача} other{скача}}", + "chat_hopSingular": "скок", + "chat_hopPlural": "скока", + "chat_hopsCount": "{count} {count, plural, =1{скок} other{скока}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -579,16 +579,16 @@ "chat_removePath": "Премахни пътя", "chat_noPathHistoryYet": "Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.", "chat_pathActions": "Действия по пътя:", - "chat_setCustomPath": "Задайте персонализиран път", - "chat_setCustomPathSubtitle": "Ръчно укажете маршрутен път", - "chat_clearPath": "Почисти Път", + "chat_setCustomPath": "Задай персонализиран път", + "chat_setCustomPathSubtitle": "Ръчно задайте маршрутен път", + "chat_clearPath": "Изчисти пътя", "chat_clearPathSubtitle": "Принуди преоткриване при следващо изпращане", "chat_pathCleared": "Пътят е почистен. Следващото съобщение ще открие маршрута отново.", "chat_floodModeSubtitle": "Използвайте превключвателя за маршрутизиране в лентата на приложението.", - "chat_floodModeEnabled": "Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.", + "chat_floodModeEnabled": "Режимът на наводняване е включен. Включете го отново чрез иконата за маршрутизиране в лентата на приложението.", "chat_fullPath": "Пълен път", - "chat_pathDetailsNotAvailable": "Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.", - "chat_pathSetHops": "Пътят е зададен: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "Подробности за пътя все още не са налични. Опитайте да изпратите съобщение, за да ги обновите.", + "chat_pathSetHops": "Пътят е зададен: {hopCount} {hopCount, plural, =1{скок} other{скока}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -603,10 +603,10 @@ "chat_pathDeviceConfirmed": "Устройство потвърдено.", "chat_pathDeviceNotConfirmed": "Устройството все още не е потвърдено.", "chat_type": "Въведете", - "chat_path": "Пътекино", + "chat_path": "Път", "chat_publicKey": "Публичен ключ", "chat_compressOutgoingMessages": "Компресиране на изходящи съобщения", - "chat_floodForced": "Потоп (принуден)", + "chat_floodForced": "Наводняване (принудително)", "chat_directForced": "Директно (принудително)", "chat_hopsForced": "{count} скока (принудително)", "@chat_hopsForced": { @@ -616,9 +616,9 @@ } } }, - "chat_floodAuto": "Потоп (автоматично)", + "chat_floodAuto": "Наводняване (автоматично)", "chat_direct": "Директно", - "chat_poiShared": "Споделено място от интерес", + "chat_poiShared": "Споделена точка на интерес", "chat_unread": "Непрочетени: {count}", "@chat_unread": { "placeholders": { @@ -627,7 +627,7 @@ } } }, - "chat_openLink": "Отваряне на връзката?", + "chat_openLink": "Отворете връзката?", "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", "chat_open": "Отвори", "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", @@ -641,8 +641,8 @@ "chat_invalidLink": "Невалиден формат на връзката", "map_title": "Карта на възлите", "map_noNodesWithLocation": "Няма възли с данни за местоположение.", - "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", - "map_nodesCount": "Нодове: {count}", + "map_nodesNeedGps": "Възлите трябва да споделят GPS координатите си,\nза да се появят на картата.", + "map_nodesCount": "Възли: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -650,7 +650,7 @@ } } }, - "map_pinsCount": "Ключове: {count}", + "map_pinsCount": "Пинове: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -659,25 +659,25 @@ } }, "map_chat": "Чат", - "map_repeater": "Повтарящ се", + "map_repeater": "Повторител", "map_room": "Стая", - "map_sensor": "Датчик", - "map_pinDm": "Задържане (DM)", - "map_pinPrivate": "Задържане (Приватно)", - "map_pinPublic": "Публичен ключ", - "map_lastSeen": "Последна видяна", - "map_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", + "map_sensor": "Сензор", + "map_pinDm": "Пин (DM)", + "map_pinPrivate": "Пин (личен)", + "map_pinPublic": "Публичен пин", + "map_lastSeen": "Последно видян", + "map_disconnectConfirm": "Сигурни ли сте, че искате да прекъснете връзката с това устройство?", "map_from": "От", "map_source": "Източник", - "map_flags": "Флаг", + "map_flags": "Флагове", "map_shareMarkerHere": "Споделете маркер тук", - "map_pinLabel": "Етикетиране на пин", + "map_pinLabel": "Етикет на пина", "map_label": "Етикет", "map_pointOfInterest": "Точка на интерес", "map_sendToContact": "Изпрати на контакт", "map_sendToChannel": "Изпрати в канала", "map_noChannelsAvailable": "Няма налични канали", - "map_publicLocationShare": "Споделяне на публично място", + "map_publicLocationShare": "Споделяне на публично местоположение", "map_publicLocationShareConfirm": "Ще споделите местоположение в {channelLabel}. Този канал е публичен и всеки с PSK може да го види.", "@map_publicLocationShareConfirm": { "placeholders": { @@ -686,21 +686,21 @@ } } }, - "map_connectToShareMarkers": "Свържете се с устройство, за да споделите маркери.", - "map_filterNodes": "Филтрирайте възли", + "map_connectToShareMarkers": "Свържете се с устройство, за да споделяте маркери.", + "map_filterNodes": "Филтрирай възлите", "map_nodeTypes": "Типове възли", "map_chatNodes": "Възли на чата", "map_repeaters": "Повторители", "map_otherNodes": "Други възли", "map_keyPrefix": "Префикс на ключа", "map_filterByKeyPrefix": "Филтрирайте по префикс на ключ", - "map_publicKeyPrefix": "Префикс на публичен ключ", + "map_publicKeyPrefix": "Префикс на публичния ключ", "map_markers": "Маркери", - "map_showSharedMarkers": "Покажи споделени маркери", - "map_lastSeenTime": "Последна видяна дата", - "map_sharedPin": "Споделено копие", + "map_showSharedMarkers": "Показвай споделените маркери", + "map_lastSeenTime": "Последно видян", + "map_sharedPin": "Споделен пин", "map_joinRoom": "Присъедини се към стаята", - "map_manageRepeater": "Управление на Повтарящ се Елемент", + "map_manageRepeater": "Управление на повторителя", "mapCache_title": "Кеш на офлайн карти", "mapCache_selectAreaFirst": "Изберете област за кеширане първа", "mapCache_noTilesToDownload": "Няма плочки за изтегляне за тази област.", @@ -759,7 +759,7 @@ } } }, - "mapCache_downloadTilesButton": "Изтегли Плочки", + "mapCache_downloadTilesButton": "Изтегли плочките", "mapCache_clearCacheButton": "Изчисти кеша", "mapCache_failedDownloads": "Неуспешни изтегляния: {count}", "@mapCache_failedDownloads": { @@ -786,8 +786,8 @@ } } }, - "time_justNow": "Сега", - "time_minutesAgo": "{minutes} минути преди", + "time_justNow": "Току-що", + "time_minutesAgo": "Преди {minutes} минути", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -795,7 +795,7 @@ } } }, - "time_hoursAgo": "{hours} часа преди", + "time_hoursAgo": "Преди {hours} часа", "@time_hoursAgo": { "placeholders": { "hours": { @@ -803,7 +803,7 @@ } } }, - "time_daysAgo": "{days} дни преди", + "time_daysAgo": "Преди {days} дни", "@time_daysAgo": { "placeholders": { "days": { @@ -816,26 +816,26 @@ "time_day": "ден", "time_days": "дни", "time_week": "седмица", - "time_weeks": "секти", + "time_weeks": "седмици", "time_month": "месец", "time_months": "месеци", "time_minutes": "минути", - "time_allTime": "Всичко време", + "time_allTime": "За цялото време", "dialog_disconnect": "Прекъсни", - "dialog_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", - "login_repeaterLogin": "Повторител Вход", + "dialog_disconnectConfirm": "Сигурни ли сте, че искате да прекъснете връзката с това устройство?", + "login_repeaterLogin": "Вход за повторител", "login_roomLogin": "Вход в стаята", "login_password": "Парола", "login_enterPassword": "Въведете парола", - "login_savePassword": "Запази парола", + "login_savePassword": "Запази паролата", "login_savePasswordSubtitle": "Паролата ще бъде съхранена сигурно на това устройство.", - "login_repeaterDescription": "Въведете паролата на репитера, за да получите достъп до настройките и статуса.", + "login_repeaterDescription": "Въведете паролата на повторителя, за да получите достъп до настройките и статуса.", "login_roomDescription": "Въведете паролата на стаята, за да получите достъп до настройките и статуса.", "login_routing": "Маршрутизиране", "login_routingMode": "Режим на маршрутизиране", "login_autoUseSavedPath": "Автоматично (използвай запазения път)", "login_forceFloodMode": "Принуди режим на наводняване", - "login_managePaths": "Управление на пътища", + "login_managePaths": "Управление на пътищата", "login_login": "Вход", "login_attempt": "Опитвате {current}/{max}", "@login_attempt": { @@ -903,7 +903,7 @@ "repeater_cli": "Команден ред (CLI)", "repeater_cliSubtitle": "Изпрати команди към ретранслатора", "repeater_settings": "Настройки", - "repeater_settingsSubtitle": "Конфигурирайте параметрите на репитера", + "repeater_settingsSubtitle": "Конфигурирайте параметрите на повторителя", "repeater_statusTitle": "Статус на повтарянето", "repeater_routingMode": "Режим на маршрутизиране", "repeater_autoUseSavedPath": "Автоматично (използвай запазения път)", @@ -929,8 +929,8 @@ "repeater_lastRssi": "Последна RSSI", "repeater_lastSnr": "Последна SNR", "repeater_noiseFloor": "Ниво на шум", - "repeater_txAirtime": "TX Airtime", - "repeater_rxAirtime": "RX Airtime", + "repeater_txAirtime": "TX време в ефир", + "repeater_rxAirtime": "RX време в ефир", "repeater_packetStatistics": "Статистика на пакетите", "repeater_sent": "Изпратено", "repeater_received": "Получено", @@ -1002,7 +1002,7 @@ "repeater_settingsTitle": "Настройки на повтарящия се елемент", "repeater_basicSettings": "Основни настройки", "repeater_repeaterName": "Име на повтарящ се елемент", - "repeater_repeaterNameHelper": "Показване на името на този репитер", + "repeater_repeaterNameHelper": "Показване на името на този повторител", "repeater_adminPassword": "Парола на администратора", "repeater_adminPasswordHelper": "Пълен достъпен парола", "repeater_guestPassword": "Парола на гост", @@ -1010,11 +1010,11 @@ "repeater_radioSettings": "Настройки на радиостанцията", "repeater_frequencyMhz": "Честота (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Power", + "repeater_txPower": "TX мощност", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Ширина на честотния спектър", "repeater_spreadingFactor": "Фактор на разпространение", - "repeater_codingRate": "Такса за кодиране", + "repeater_codingRate": "Скорост на кодиране", "repeater_locationSettings": "Настройки на местоположението", "repeater_latitude": "Широчина", "repeater_latitudeHelper": "Десетични градуси (напр. 37.7749)", @@ -1022,13 +1022,13 @@ "repeater_longitudeHelper": "Градуси с десетични знаци (напр. -122.4194)", "repeater_features": "Характеристики", "repeater_packetForwarding": "Пренасочване на пакети", - "repeater_packetForwardingSubtitle": "Активирайте репитера, за да препращате пакети.", - "repeater_guestAccess": "Достъп за Гост", - "repeater_guestAccessSubtitle": "Разрешете самочетене за гости", + "repeater_packetForwardingSubtitle": "Активирайте повторителя, за да препраща пакети.", + "repeater_guestAccess": "Достъп за гости", + "repeater_guestAccessSubtitle": "Разрешете само четене за гости", "repeater_privacyMode": "Режим на поверителност", "repeater_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", "repeater_advertisementSettings": "Настройки на рекламите", - "repeater_localAdvertInterval": "Местен Рекламен Интервал", + "repeater_localAdvertInterval": "Интервал на местната реклама", "repeater_localAdvertIntervalMinutes": "{minutes} минути", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1037,7 +1037,7 @@ } } }, - "repeater_floodAdvertInterval": "Интервал на рекламата за наводнения", + "repeater_floodAdvertInterval": "Интервал на рекламата за наводняване", "repeater_floodAdvertIntervalHours": "{hours} часа", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1046,17 +1046,17 @@ } } }, - "repeater_encryptedAdvertInterval": "Криптиран Рекламен Интервал", - "repeater_dangerZone": "Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно", - "repeater_rebootRepeater": "БеРестартирай Репитер", - "repeater_rebootRepeaterSubtitle": "Рестартирайте ретранслатора.", - "repeater_rebootRepeaterConfirm": "Сигурни ли сте, че искате да рестартирате този репитер?", - "repeater_regenerateIdentityKey": "Генериране на Ключ за Идентичност", - "repeater_regenerateIdentityKeySubtitle": "Генериране на нова двойка публичен/частен ключ", - "repeater_regenerateIdentityKeyConfirm": "БеТова ще генерира нова идентичност за репитера. Продължете?", - "repeater_eraseFileSystem": "Изтрий Файлова Система", - "repeater_eraseFileSystemSubtitle": "Форматирайте файла на репитера", - "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!", + "repeater_encryptedAdvertInterval": "Криптиран интервал на рекламата", + "repeater_dangerZone": "Опасна зона", + "repeater_rebootRepeater": "Рестартирай повторителя", + "repeater_rebootRepeaterSubtitle": "Рестартира повторителя.", + "repeater_rebootRepeaterConfirm": "Сигурни ли сте, че искате да рестартирате този повторител?", + "repeater_regenerateIdentityKey": "Генерирай нов идентификационен ключ", + "repeater_regenerateIdentityKeySubtitle": "Генерирай нова двойка публичен/частен ключ", + "repeater_regenerateIdentityKeyConfirm": "Това ще генерира нова идентичност за повторителя. Продължете?", + "repeater_eraseFileSystem": "Изтрий файловата система", + "repeater_eraseFileSystemSubtitle": "Форматирай файловата система на повторителя", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: Това ще изтрие всички данни от повторителя. Това не може да бъде отменено!", "repeater_eraseSerialOnly": "Изтриването е достъпно само през серийния терминал.", "repeater_commandSent": "Командата е изпратена: {command}", "@repeater_commandSent": { @@ -1074,14 +1074,14 @@ } } }, - "repeater_confirm": "БеПотвърди", + "repeater_confirm": "Потвърди", "repeater_settingsSaved": "Настройките са запазени успешно.", - "repeater_rxGain": "Увеличен коефициент на възвръщаемост (RX)", + "repeater_rxGain": "RX усилване", "repeater_rxGainHelper": "По-висока чувствителност, по-голям ток (само за SX1262/SX1268)", - "repeater_refreshRxGain": "Възстановете повишената ефективност на RX", + "repeater_refreshRxGain": "Обнови RX усилването", "repeater_multiAcks": "Множество потвърждения", - "repeater_multiAcksSubtitle": "Признавайте съобщения по множество канали за по-добро доставяне.", - "repeater_refreshMultiAcks": "Обновете множество потвърждения", + "repeater_multiAcksSubtitle": "Потвърждавай съобщенията по множество канали за по-добро доставяне.", + "repeater_refreshMultiAcks": "Обнови множествените ACK", "repeater_networkHealth": "Състояние на мрежата", "repeater_loopDetect": "Откриване на цикли", "repeater_loopDetectHelper": "Изпратете пакети, които изглеждат като цикли в маршрутизацията.", @@ -1089,8 +1089,8 @@ "repeater_loopDetectMinimal": "Минимален", "repeater_loopDetectModerate": "Умерен", "repeater_loopDetectStrict": "Строг", - "repeater_dutyCycle": "Цикъл на работа/почивка", - "repeater_dutyCycleHelper": "Максимален процент на използване на времето на въздуха", + "repeater_dutyCycle": "Работен цикъл", + "repeater_dutyCycleHelper": "Максимален процент на използване на времето в ефир", "repeater_dutyCyclePercent": "{percent}%", "@repeater_dutyCyclePercent": { "placeholders": { @@ -1100,30 +1100,30 @@ } }, "repeater_ownerInfo": "Информация за оператора", - "repeater_ownerInfoHelper": "Публични метаданни за този репитер", + "repeater_ownerInfoHelper": "Публични метаданни за този повторител", "repeater_refreshOwnerInfo": "Обновете информацията за оператора", - "repeater_floodMax": "Максимален брой скачания при наводнение", - "repeater_floodMaxHelper": "Максималният брой пакети, които един поток може да пренесе (0-64)", - "repeater_advancedSettings": "Напреднал", - "repeater_advancedSettingsSubtitle": "Регулаторни копчета за опитни оператори", + "repeater_floodMax": "Максимален брой хопове при наводняване", + "repeater_floodMaxHelper": "Максималният брой хопове, които един пакет може да премине (0-64)", + "repeater_advancedSettings": "Разширени настройки", + "repeater_advancedSettingsSubtitle": "Експериментални настройки за опитни оператори", "repeater_pathHashMode": "Режим за хеширане на пътища", - "repeater_pathHashModeHelper": "Байтовете, използвани за кодиране на идентификатора на този репитер в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скача), 1=2 байта (65 000 идентификатора, до 32 скача), 2=3 байта (16 милиона идентификатора, до 21 скача). Версии 1.13 и по-стари версии на фърмуера използват многобайтови пътища – само след като мрежата е актуализирана до версия 1.14 или по-нова.", - "repeater_txDelay": "Забавяне на проекта \"Flood TX\"", - "repeater_txDelayHelper": "Предавайте разстоянието между пакетите за трафик при наводнения, като множител на времето за пренос на пакета (0-2, по подразбиране 0.5). По-висока стойност означава по-малко сблъсъци, но по-бавно предаване.", - "repeater_directTxDelay": "Директно забавяне на сигнала", - "repeater_directTxDelayHelper": "Предаване на интервали за директен (не-масивен) трафик, като множител на времето за пренос на пакета (0-2, по подразбиране 0.3).", - "repeater_intThresh": "Праг на интерференция", + "repeater_pathHashModeHelper": "Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.", + "repeater_txDelay": "Забавяне на Flood TX", + "repeater_txDelayHelper": "Разстоянието между пакетите при наводняване като множител на времето за пренос на пакета (0-2, по подразбиране 0.5). По-висока стойност означава по-малко сблъсъци, но по-бавно предаване.", + "repeater_directTxDelay": "Забавяне на директното предаване", + "repeater_directTxDelayHelper": "Интервал за директен (не-масов) трафик, като множител на времето за пренос на пакета (0-2, по подразбиране 0.3).", + "repeater_intThresh": "Праг на смущенията", "repeater_intThreshHelper": "Прагът е зададен на нивото на шума на радиото, така че да отхвърля смущения, които са над този праг. 0 – изключва; активирайте само, ако забележите грешки в шумна честотна лента.", - "repeater_agcResetInterval": "Интервал за рестартиране на AGC", + "repeater_agcResetInterval": "Интервал за нулиране на AGC", "repeater_agcResetIntervalHelper": "Колко често да се рестартира автоматичната настройка на усилването, за да се възстанови от състояние, в което усилването е блокирано. Времето за рестартиране е няколко секунди, като се определя като кратна на 4. 0 деактивира периодичното рестартиране.", "repeater_actionsTitle": "Действия", - "repeater_sendAdvert": "Изпратете реклама за навод", - "repeater_sendAdvertSubtitle": "Публикувайте реклама за навод в мрежата.", - "repeater_sendAdvertZeroHop": "Изпратете реклама без преминаване през други системи", - "repeater_sendAdvertZeroHopSubtitle": "Публикувайте реклама, която достига до целевата аудитория само чрез директно разпространение (без използване на посредници).", - "repeater_clockSync": "Синхронизиране на часовника сега", - "repeater_clockSyncSubtitle": "Настройте времето на телефона си да съвпада с времето на репитера.", - "repeater_actionSucceeded": "{action} succeeded", + "repeater_sendAdvert": "Изпрати реклама за наводняване", + "repeater_sendAdvertSubtitle": "Публикувай реклама за наводняване в мрежата.", + "repeater_sendAdvertZeroHop": "Изпрати реклама без хопове", + "repeater_sendAdvertZeroHopSubtitle": "Публикувай реклама, която достига до целевата аудитория само чрез директно разпространение.", + "repeater_clockSync": "Синхронизирай часовника сега", + "repeater_clockSyncSubtitle": "Настройте времето на телефона си да съвпада с времето на повторителя.", + "repeater_actionSucceeded": "{action} успешно", "@repeater_actionSucceeded": { "placeholders": { "action": { @@ -1131,7 +1131,7 @@ } } }, - "repeater_actionFailed": "{action} failed: {error}", + "repeater_actionFailed": "{action} не успя: {error}", "@repeater_actionFailed": { "placeholders": { "action": { @@ -1142,7 +1142,7 @@ } } }, - "repeater_settingsSavedRebootNeeded": "Настройки запаметени – рестартирайте ретранслатора, за да ги приложите.", + "repeater_settingsSavedRebootNeeded": "Настройките са запазени - рестартирайте повторителя, за да ги приложите.", "repeater_settingsPartialFailure": "Някои настройки не успяха: {failures}", "@repeater_settingsPartialFailure": { "placeholders": { @@ -1159,12 +1159,12 @@ } } }, - "repeater_refreshBasicSettings": "Обнови Основни Настройки", - "repeater_refreshRadioSettings": "Обнови настройките на радиопредавателите", - "repeater_refreshTxPower": "Обнови TX захранване", - "repeater_refreshPacketForwarding": "Обнови пакетно пренасочване", - "repeater_refreshGuestAccess": "Обнови достъп за гости", - "repeater_refreshPrivacyMode": "Обнови Режим на поверителност", + "repeater_refreshBasicSettings": "Обнови основните настройки", + "repeater_refreshRadioSettings": "Обнови настройките на радиото", + "repeater_refreshTxPower": "Обнови TX мощността", + "repeater_refreshPacketForwarding": "Обнови препращането на пакети", + "repeater_refreshGuestAccess": "Обнови достъпа за гости", + "repeater_refreshPrivacyMode": "Обнови режима на поверителност", "repeater_refreshed": "{label} е обновено", "@repeater_refreshed": { "placeholders": { @@ -1181,17 +1181,17 @@ } } }, - "repeater_cliTitle": "Повторител CLI", - "repeater_debugNextCommand": "Поправи Следваща Команда", + "repeater_cliTitle": "CLI на повторителя", + "repeater_debugNextCommand": "Отстрани следващата команда", "repeater_commandHelp": "Помощ", - "repeater_clearHistory": "Изчисти История", + "repeater_clearHistory": "Изчисти историята", "repeater_noCommandsSent": "Няма изпратени команди засега.", "repeater_typeCommandOrUseQuick": "Въведете команда по-долу или използвайте бързи команди", "repeater_enterCommandHint": "Въведете команда...", - "repeater_previousCommand": "Предходна команда", + "repeater_previousCommand": "Предишна команда", "repeater_nextCommand": "Следваща команда", "repeater_enterCommandFirst": "Въведете първо команда.", - "repeater_cliCommandFrameTitle": "Рамка за команда CLI", + "repeater_cliCommandFrameTitle": "Рамка на CLI команда", "repeater_cliCommandError": "Грешка: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1200,9 +1200,9 @@ } } }, - "repeater_cliQuickGetName": "Получи име", - "repeater_cliQuickGetRadio": "Получи радио", - "repeater_cliQuickGetTx": "Получи TX", + "repeater_cliQuickGetName": "Вземи име", + "repeater_cliQuickGetRadio": "Вземи радио", + "repeater_cliQuickGetTx": "Вземи TX", "repeater_cliQuickNeighbors": "Съседи", "repeater_cliQuickVersion": "Версия", "repeater_cliQuickAdvertise": "Рекламирай", @@ -1239,7 +1239,7 @@ "repeater_cliHelpSetAdcMultiplier": "Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).", "repeater_cliHelpTempRadio": "Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).", "repeater_cliHelpSetPerm": "Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).", - "repeater_cliHelpGetBridgeType": "Получава тип мост none, rs232, espnow", + "repeater_cliHelpGetBridgeType": "Получава типа на моста: none, rs232, espnow", "repeater_cliHelpLogStart": "Започва записване на пакети във файловата система.", "repeater_cliHelpLogStop": "Спира записването на пакети във файловата система.", "repeater_cliHelpLogErase": "Изтрива логовете от пакета от файловата система.", @@ -1250,8 +1250,8 @@ "repeater_cliHelpRegionGet": "Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) 'F'\"", "repeater_cliHelpRegionPut": "Добавя или актуализира дефиниция на регион с дадено име.", "repeater_cliHelpRegionRemove": "Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)", - "repeater_cliHelpRegionAllowf": "Задава 'Потоп' разрешение за посочената област. ('' за глобалния/стария обхват)", - "repeater_cliHelpRegionDenyf": "Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )", + "repeater_cliHelpRegionAllowf": "Задава разрешение 'Flood' за посочената област. ('' за глобалния/стария обхват)", + "repeater_cliHelpRegionDenyf": "Премахва разрешението \"F\" за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобално/старо ниво!!)", "repeater_cliHelpRegionHome": "Отговаря с текущия 'home' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).", "repeater_cliHelpRegionHomeSet": "Задава 'домашно' региона.", "repeater_cliHelpRegionSave": "Запазва списъка/картата с региони в съхранение.", @@ -1455,7 +1455,7 @@ } } }, - "channelPath_pathLabelTitle": "Пътекино", + "channelPath_pathLabelTitle": "Пътеки", "channelPath_observedPathHeader": "Наблюдаван път", "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { @@ -1677,7 +1677,7 @@ "pathTrace_you": "Вие", "pathTrace_notAvailable": "Пътека за проследяване не е достъпна.", "contacts_pathTrace": "Пътен проследяване", - "pathTrace_refreshTooltip": "Обнови Path Trace.", + "pathTrace_refreshTooltip": "Обнови проследяването на пътя.", "pathTrace_failed": "Пътят за проследяване не успя.", "contacts_repeaterPing": "Пингване на повторителя", "contacts_repeaterPathTrace": "Трасировка до повторител", @@ -1695,7 +1695,7 @@ "contacts_contactImported": "Контактът е импортиран.", "contacts_zeroHopAdvert": "Реклама без скок", "contacts_contactImportFailed": "Контактът не е успешно импортиран.", - "contacts_floodAdvert": "Потопна реклама", + "contacts_floodAdvert": "Реклама за наводняване", "contacts_addContactFromClipboard": "Добави контакт от клипборда", "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", "contacts_ShareContact": "Копирай контакт в клипборда", @@ -1728,20 +1728,20 @@ "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", "map_pathTraceCancelled": "Отменен е следването на пътя.", "pathTrace_clearTooltip": "Изчисти пътя", - "map_removeLast": "Премахни Последно", - "map_runTrace": "Изпълни Път на Следване", - "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", + "map_removeLast": "Премахни последното", + "map_runTrace": "Стартирай проследяването на пътя", + "map_tapToAdd": "Докоснете възлите, за да ги добавите към пътя.", "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", "scanner_chromeRequired": "Изисква се браузър Chrome", "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", "snrIndicator_lastSeen": "Последно видян", - "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", + "snrIndicator_nearByRepeaters": "Близки повторители", "chat_ShowAllPaths": "Покажи всички пътища", "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", - "settings_clientRepeat": "Без електричество – повторение", + "settings_clientRepeat": "Клиентско повторение", "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "единици", "appSettings_unitsMetric": "Метрика (m / km)", @@ -2218,10 +2218,10 @@ "repeater_cliHelpSetFreq": "(Само за серийно управление) Бързо задава само честотата. Необходимо е рестартиране. Препоръчително е да се използват настройките за \"радио\", за да се зададат всички параметри.", "repeater_cliHelpSetBridgeChannel": "(Само за моста ESPNow) Определя WiFi канала (от 1 до 14), използван от моста.", "repeater_cliHelpGetName": "Показва зададеното име на възела.", - "repeater_cliHelpGetRole": "Показва ролята на фърмуера (например, репитер, сървър за стая и т.н.).", + "repeater_cliHelpGetRole": "Показва ролята на фърмуера (например повторител, сървър на стая и т.н.).", "repeater_cliHelpGetPublicKey": "Показва публичния ключ на устройството.", "repeater_cliHelpGetPrvKey": "(Само за серийния номер) Показва личната ключа на устройството. Трябва да се третира като тайна.", - "repeater_cliHelpGetRepeat": "Показва дали функцията за пренасочване на пакети (ролята на репитер) е активирана или деактивирана.", + "repeater_cliHelpGetRepeat": "Показва дали функцията за пренасочване на пакети (ролята на повторителя) е активирана или деактивирана.", "repeater_cliHelpGetTx": "Показва текущата мощност на TX в dBm.", "repeater_cliHelpGetFreq": "Показва зададената честота в MHz.", "repeater_cliHelpGetRadio": "Показва пълните радио параметри: честота, ширина на честотния обхват, фактор на разпространение, скорост на кодиране.", @@ -2244,7 +2244,7 @@ "repeater_cliHelpGetOwnerInfo": "Показва информацията за контакт на собственика.", "repeater_cliHelpGetPathHashMode": "Показва режима на хеширане на пътя (0/1/2).", "repeater_cliHelpGetLoopDetect": "Показва чувствителността към откриване на цикли.", - "repeater_cliHelpGetAcl": "(Само за серийни номера) Изброява настройките за контрол на достъпа в репитера.", + "repeater_cliHelpGetAcl": "(Само за серийни устройства) Изброява настройките за контрол на достъпа в повторителя.", "repeater_cliHelpGetBridgeEnabled": "Показва дали мостът е активиран.", "repeater_cliHelpGetBridgeDelay": "Показва забавянето на моста в милисекунди.", "repeater_cliHelpGetBridgeSource": "Показва дали мостът изпраща или получава пакети RX или TX.", @@ -2267,10 +2267,10 @@ "repeater_cliHelpStatsPackets": "(Само за серия) Показва статистически данни на ниво пакет.", "repeater_cliHelpStatsRadio": "(Само за конкретен сериал) Показва радиостатистика.", "repeater_cliHelpStatsCore": "(Само за серийния номер) Показва основните статистически данни за фърмуера.", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "common_done": "Готово", + "background_serviceTitle": "MeshCore работи", + "background_serviceText": "Поддържа BLE връзката активна", + "appSettings_translationModelDeleted": "Изтрит {name}", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2278,7 +2278,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "Неуспешно изтриване: {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2286,7 +2286,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "Неуспешно обновяване на канала: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2294,20 +2294,20 @@ } } }, - "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", - "map_publicKeyPrefixHint": "e.g. ab12", - "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", - "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", - "chat_score": "Score", - "settings_multiAck": "Множество потвърждения", + "map_type": "Тип", + "map_path": "Път", + "map_location": "Местоположение", + "map_estLocation": "Прибл. местоположение", + "map_publicKey": "Публичен ключ", + "map_publicKeyPrefixHint": "напр. ab12", + "contact_typeChat": "Чат", + "contact_typeRepeater": "Повторител", + "contact_typeRoom": "Стая", + "contact_typeSensor": "Сензор", + "contact_typeUnknown": "Неизвестен", + "channels_via": "чрез {path}", + "chat_score": "Оценка", + "settings_multiAck": "Множество ACK", "map_sharedAt": "Споделено", "@losBlockedSpotChip": { "placeholders": { @@ -2348,11 +2348,11 @@ "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", "losBlockedSpotsHint": "Кликнете върху блокираната точка, за да я отбележите на картата.", "losBlockedSpotsTitle": "Ограничени места", - "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).", + "losSelectedObstructionDetails": "Блокирано от {obstruction} {heightUnit}, {distanceFromA} от A и {distanceFromB} от B ({distanceUnit}).", "chat_markAsUnread": "Отбелязване като непрочетено", "settings_companionDebugLogSubtitle": "Команди, отговори и сурови данни за протоколите BLE/TCP/USB", "chat_newMessages": "Нови съобщения", - "settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)", + "settings_companionDebugLog": "Дневник за отстраняване на грешки на придружаващото приложение", "repeater_chanUtil": "Използване на канала", "@routing_lastWorked": { "placeholders": { @@ -2392,59 +2392,153 @@ } } }, - "messageStatus_pending": "Изпращане", - "common_undo": "Отмяни", - "messageStatus_delivered": "Доставен", + "messageStatus_pending": "Изпраща се", + "common_undo": "Отмени", + "messageStatus_delivered": "Доставено", "messageStatus_sent": "Изпратено", - "messageStatus_failed": "Не успях да изпратя", - "messageStatus_repeated": "Слушах го многократно", + "messageStatus_failed": "Неуспешно изпращане", + "messageStatus_repeated": "Повторно чуто", "contacts_moreOptions": "Повече възможности", "contacts_searchOpen": "Търсене на контакти", "contacts_searchClose": "Затвори търсене", "routing_title": "Маршрутизиране", - "routing_modeAuto": "Автомобил", - "routing_modeFlood": "Наводнение", - "routing_modeManual": "Ръководство", - "routing_modeAutoHint": "Автоматично избира най-известния път, като при липса на информация, използва стратегия за \"запълване\" на празните пространства.", - "routing_modeFloodHint": "Излъчване през всички ретранслатори. Най-надежният начин, но изисква повече време на въздуха.", + "routing_modeAuto": "Автоматично", + "routing_modeFlood": "Наводняване", + "routing_modeManual": "Ръчно", + "routing_modeAutoHint": "Автоматично избира най-добрия известен път, а при липса на информация използва стратегия за наводняване.", + "routing_modeFloodHint": "Излъчва през всички повторители. Най-надеждният начин, но изисква повече време в ефира.", "routing_modeManualHint": "Винаги следва точно пътя, който сте определили.", "routing_currentRoute": "Текущ маршрут", - "routing_directNoHops": "Директ – без превключватели", + "routing_directNoHops": "Директно - без скокове", "routing_noPathYet": "Все още няма път. Съобщението продължава да се изпраща, докато не бъде открит маршрут.", - "routing_floodBroadcast": "Предаване през всички ретранслатори", - "routing_editPath": "Редактиране на пътя", - "routing_forgetPath": "Забравете за пътя", - "routing_knownPaths": "Известни маршрути", - "routing_knownPathsHint": "Натиснете бутона, за да превключите към него.", - "routing_inUse": "В експлоатация", + "routing_floodBroadcast": "Предаване през всички повторители", + "routing_editPath": "Редактирай пътя", + "routing_forgetPath": "Забрави пътя", + "routing_knownPaths": "Известни пътища", + "routing_knownPathsHint": "Докоснете бутона, за да превключите към него.", + "routing_inUse": "В употреба", "routing_qualityStrong": "Силен първи скок", "routing_qualityGood": "Добър първи опит", - "routing_qualityFair": "Първият добър скок", - "routing_qualityWorked": "Беше изпълнено/Доведено до край", - "routing_qualityFlood": "Получено чрез информация, разпространена в резултат на навод.", - "routing_qualityUntested": "Не тестван", - "routing_neverWorked": "никога не е потвърдено", - "routing_floodDelivery": "Доставка при навод", + "routing_qualityFair": "Приемлив първи скок", + "routing_qualityWorked": "Работил", + "routing_qualityFlood": "Получено чрез наводняване", + "routing_qualityUntested": "Нетестирано", + "routing_neverWorked": "Никога не е потвърдено", + "routing_floodDelivery": "Доставка при наводняване", "pathEditor_title": "Създаване на път", - "pathEditor_hopCounter": "{count} от 64 различни вида малц", - "pathEditor_noHops": "Все още няма добавени хмел. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите рецептата без хмел, за да я изпратите директно.", - "pathEditor_addHops": "Добавете хмела в реда, в който е посочено.", - "pathEditor_searchRepeaters": "Търсене на повтори", - "pathEditor_advancedHex": "Разширено: необработен шестничен път", - "pathEditor_hexLabel": "Префикси на шестнадесетична система", - "pathEditor_hexHelper": "Два шест-символни идентификатора на скок, разделени със запетаи", - "pathEditor_invalidTokens": "Невалидно: {tokens}", - "routing_lastWorked": "worked {when}", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "pathEditor_tooManyHops": "Максимум 64 крачета", - "pathEditor_usePath": "Използвайте този маршрут.", - "pathEditor_removeHop": "Премахнете хмела", - "pathEditor_unknownHop": "Неизвестен репитер", + "pathEditor_hopCounter": "{count} от 64 скока", + "pathEditor_noHops": "Все още няма добавени скокове. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите пътя без скокове, за да го изпратите директно.", + "pathEditor_addHops": "Добавете скоковете в посочения ред.", + "pathEditor_searchRepeaters": "Търсене на повторители", + "pathEditor_advancedHex": "Разширено: суров шестнадесетичен път", + "pathEditor_hexLabel": "Шестнадесетични префикси", + "pathEditor_hexHelper": "Два шестнадесетични идентификатора на скок, разделени със запетаи", + "pathEditor_invalidTokens": "Невалидни: {tokens}", + "routing_lastWorked": "последно работил {when}", + "routing_deliveryCounts": "{successes} доставени, {failures} неуспешни", + "pathEditor_tooManyHops": "Максимум 64 скока", + "pathEditor_usePath": "Използвай този маршрут.", + "pathEditor_removeHop": "Премахни скока", + "pathEditor_unknownHop": "Неизвестен повторител", "map_zoomIn": "Увеличи", - "map_zoomOut": "Приближете се по-малко", - "map_centerMap": "Карта на центъра", + "map_zoomOut": "Намали мащаба", + "map_centerMap": "Центрирай картата", "chrome_bluetoothRequiresChromium": "Web Bluetooth изисква браузър, базиран на Chromium.", "channels_communityShortId": "Идентификационен номер: {id}...", "pathTrace_legendGpsConfirmed": "GPS потвърдено", - "pathTrace_legendInferred": "Извлечена позиция" + "pathTrace_legendInferred": "Извлечена позиция", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_searchHint": "Търсене по име или идентификатор на възел", + "map_activity": "Дейност", + "map_online": "Онлайн", + "scanner_bluetoothWebUnsupported": "Функцията Bluetooth не е налична в браузъра. Моля, свържете се чрез USB вместо това.", + "map_recent": "Скорошни", + "map_stale": "Остарял", + "map_visible": "Видими", + "map_hidden": "Скрит", + "map_centerOnNode": "Центрирай върху възела", + "map_details": "Подробности", + "map_noGps": "Без GPS", + "map_noResults": "Няма съвпадащи възли", + "pathMap_viewSingle": "Самостоятелен", + "pathMap_viewCombined": "Комбиниран", + "pathMap_play": "Пусни", + "pathMap_pause": "Пауза", + "pathMap_replay": "Повторение", + "pathMap_stepBack": "Предишна стъпка", + "pathMap_stepForward": "Следваща стъпка", + "pathMap_animationOn": "Показвай анимацията на пакета", + "pathMap_animationOff": "Скрий анимацията на пакета", + "pathMap_hopOf": "Стъпка {current} от {total}", + "pathMap_observedPaths": "Наблюдавани пътища: {count}", + "pathMap_primary": "Основен", + "pathMap_alternate": "Алтернативен {index}", + "pathMap_hopCount": "{count, plural, =1{1 скок} other{{count} скока}}", + "pathMap_legendShared": "Споделена секция", + "pathMap_legendEstimated": "Очаквана стойност на сегмента", + "pathMap_sharedNodeCount": "Използвани от {count} пътища", + "pathMap_partialAnimation": "{count, plural, =1{1 скок няма определено местоположение — показаният път е непълен} other{{count} скока нямат определено местоположение — показаният път е непълен}}", + "pathMap_showAllPaths": "Покажи всички пътища", + "pathMap_hidePath": "Скрий пътя", + "pathMap_showPath": "Покажи пътя", + "pathMap_collapsePanel": "Сгъни панела", + "pathMap_expandPanel": "Разгъни панела", + "pathMap_noLocation": "Без посочено местоположение", + "pathMap_followPacket": "Проследи пакета", + "pathMap_unfollowPacket": "Спри проследяването на пакета", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 47062aa6..f6bccbde 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -28,15 +28,15 @@ "common_continue": "Fortfahren", "common_share": "Teilen", "common_copy": "Kopieren", - "common_retry": "Versuchen", + "common_retry": "Wiederholen", "common_hide": "Ausblenden", - "common_remove": "Löschen", + "common_remove": "Entfernen", "common_enable": "Aktivieren", "common_disable": "Deaktivieren", "common_autoRefresh": "Automatische Aktualisierung", "common_interval": "Intervall", "common_reboot": "Neustart", - "common_loading": "Laden...", + "common_loading": "Lädt...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { @@ -54,9 +54,9 @@ } } }, - "scanner_title": "MeshCore – Open-Version", - "scanner_scanning": "Scannen nach Geräten...", - "scanner_connecting": "Verbunden...", + "scanner_title": "MeshCore Open", + "scanner_scanning": "Suche nach Geräten...", + "scanner_connecting": "Verbinde...", "scanner_disconnecting": "Trenne...", "scanner_notConnected": "Nicht verbunden", "scanner_connectedTo": "Verbunden mit {deviceName}", @@ -68,8 +68,8 @@ } }, "scanner_searchingDevices": "Suche nach MeshCore-Geräten...", - "scanner_tapToScan": "Tippen Sie auf Scan, um MeshCore-Geräte zu finden.", - "scanner_connectionFailed": "Verbindungsfehler: {error}", + "scanner_tapToScan": "Tippen Sie auf Scannen, um MeshCore-Geräte zu finden.", + "scanner_connectionFailed": "Verbindung fehlgeschlagen: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -79,53 +79,53 @@ }, "scanner_stop": "Stopp", "scanner_scan": "Scannen", - "device_quickSwitch": "Schnelles Umschalten", + "device_quickSwitch": "Schnellwechsel", "device_meshcore": "MeshCore", "settings_title": "Einstellungen", "settings_deviceInfo": "Geräteinformationen", "settings_appSettings": "App-Einstellungen", - "settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung", - "settings_nodeSettings": "Knoten-Einstellungen", + "settings_appSettingsSubtitle": "Benachrichtigungen, Nachrichten und Karten-Einstellungen", + "settings_nodeSettings": "Knoteneinstellungen", "settings_nodeName": "Knotenname", "settings_nodeNameNotSet": "Nicht festgelegt", - "settings_nodeNameHint": "Gebe den Knotenamen ein", + "settings_nodeNameHint": "Geben Sie den Knotennamen ein", "settings_nodeNameUpdated": "Name aktualisiert", - "settings_radioSettings": "Funk Einstellungen", - "settings_radioSettingsSubtitle": "Frequenz, Leistung, Verbreitungsfaktor", + "settings_radioSettings": "Funk-Einstellungen", + "settings_radioSettingsSubtitle": "Frequenz, Leistung, Spreading-Faktor", "settings_radioSettingsUpdated": "Funkparameter aktualisiert", "settings_location": "Ort", "settings_locationSubtitle": "GPS-Koordinaten", - "settings_locationUpdated": "Ort aktualisiert", - "settings_locationBothRequired": "Bitte geben Sie sowohl Breite als auch Längengrad ein.", - "settings_locationInvalid": "Ungültige Breiten- oder Längengrade.", + "settings_locationUpdated": "Standort und GPS-Einstellungen aktualisiert", + "settings_locationBothRequired": "Bitte geben Sie sowohl Breiten- als auch Längengrad ein.", + "settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.", "settings_latitude": "Breitengrad", "settings_longitude": "Längengrad", - "settings_privacyMode": "Privatsphäreeinstellung", - "settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", - "settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.", - "settings_privacyModeEnabled": "Datenschutzmodus aktiviert", - "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", + "settings_privacyMode": "Privatsphärenmodus", + "settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen", + "settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.", + "settings_privacyModeEnabled": "Privatsphärenmodus aktiviert", + "settings_privacyModeDisabled": "Privatsphärenmodus deaktiviert", "settings_actions": "Aktionen", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", - "settings_sendAdvertisement": "Sende Ankündigung", - "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", + "settings_deleteAllPaths": "Alle Pfade löschen", + "settings_deleteAllPathsSubtitle": "Alle Pfaddaten aus den Kontakten entfernen.", + "settings_sendAdvertisement": "Ankündigung senden", + "settings_sendAdvertisementSubtitle": "Präsenz jetzt senden", "settings_advertisementSent": "Ankündigung gesendet", - "settings_syncTime": "Zeitsynchronisierung", - "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", + "settings_syncTime": "Zeit synchronisieren", + "settings_syncTimeSubtitle": "Geräteuhr auf die Zeit des Telefons setzen", "settings_timeSynchronized": "Zeit synchronisiert", "settings_refreshContacts": "Kontakte aktualisieren", - "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", + "settings_refreshContactsSubtitle": "Kontaktliste vom Gerät neu laden", "settings_rebootDevice": "Gerät neu starten", "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", "settings_debug": "Fehlerbehebung", "settings_bleDebugLog": "BLE-Debug-Protokoll", - "settings_bleDebugLogSubtitle": "BLE-Befehle, Antworten und Rohdaten", + "settings_bleDebugLogSubtitle": "BLE-, TCP- und USB-Befehle, Antworten und Rohdaten", "settings_appDebugLog": "App-Debug-Protokoll", - "settings_appDebugLogSubtitle": "Anwendung Debug-Nachrichten", + "settings_appDebugLogSubtitle": "App-Debug-Nachrichten", "settings_about": "Über", - "settings_aboutVersion": "MeshCore Open, Version {version}", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -133,25 +133,25 @@ } } }, - "settings_aboutLegalese": "MeshCore Open Source Projekt 2026", + "settings_aboutLegalese": "2026 MeshCore Open-Source-Projekt", "settings_aboutDescription": "Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.", "settings_infoName": "Name", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Akku", "settings_infoPublicKey": "Öffentlicher Schlüssel", - "settings_infoContactsCount": "Anzahl Kontakte", - "settings_infoChannelCount": "Anzahl Kanäle", + "settings_infoContactsCount": "Kontakte", + "settings_infoChannelCount": "Kanäle", "settings_presets": "Voreinstellungen", "settings_frequency": "Frequenz (MHz)", - "settings_frequencyHelper": "300,00 - 2.500,00", + "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", "settings_bandwidth": "Bandbreite", - "settings_spreadingFactor": "Verteilungsfaktor", + "settings_spreadingFactor": "Spreading-Faktor", "settings_codingRate": "Kodierungsrate", - "settings_txPower": "TX-Leistung (dBm)", + "settings_txPower": "Sendeleistung (dBm)", "settings_txPowerHelper": "0 – 22", - "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", + "settings_txPowerInvalid": "Ungültige Sendeleistung (0-22 dBm)", "settings_error": "Fehler: {message}", "@settings_error": { "placeholders": { @@ -161,8 +161,8 @@ } }, "appSettings_title": "App-Einstellungen", - "appSettings_appearance": "Aussehen", - "appSettings_theme": "Thema", + "appSettings_appearance": "Erscheinungsbild", + "appSettings_theme": "Design", "appSettings_themeSystem": "Systemstandard", "appSettings_themeLight": "Hell", "appSettings_themeDark": "Dunkel", @@ -179,32 +179,32 @@ "appSettings_languageZh": "Chinesisch", "appSettings_languageSv": "Schwedisch", "appSettings_languageNl": "Niederländisch", - "appSettings_languageSk": "Slowenisch", + "appSettings_languageSk": "Slowakisch", "appSettings_languageBg": "Bulgarisch", "appSettings_notifications": "Benachrichtigungen", "appSettings_enableNotifications": "Benachrichtigungen aktivieren", - "appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen", - "appSettings_notificationPermissionDenied": "Erlaubnis zur Benachrichtigung verweigert", + "appSettings_enableNotificationsSubtitle": "Benachrichtigungen für Nachrichten und Ankündigungen erhalten", + "appSettings_notificationPermissionDenied": "Benachrichtigungsberechtigung verweigert", "appSettings_notificationsEnabled": "Benachrichtigungen aktiviert", "appSettings_notificationsDisabled": "Benachrichtigungen deaktiviert", - "appSettings_messageNotifications": "Direktnachrichten Benachrichtigungen", - "appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Direktnachrichten", - "appSettings_channelMessageNotifications": "Kanalnachrichten Benachrichtigungen", - "appSettings_channelMessageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfangen von Kanalnachrichten", + "appSettings_messageNotifications": "Direktnachrichten-Benachrichtigungen", + "appSettings_messageNotificationsSubtitle": "Benachrichtigung anzeigen, wenn neue Direktnachrichten eingehen", + "appSettings_channelMessageNotifications": "Kanalnachrichten-Benachrichtigungen", + "appSettings_channelMessageNotificationsSubtitle": "Benachrichtigung anzeigen, wenn neue Kanalnachrichten eingehen", "appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen", - "appSettings_advertisementNotificationsSubtitle": "Zeige Benachrichtigung, wenn neue Knoten entdeckt werden.", + "appSettings_advertisementNotificationsSubtitle": "Benachrichtigung anzeigen, wenn neue Knoten entdeckt werden", "appSettings_messaging": "Nachrichten", - "appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen", - "appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen", - "appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.", - "appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.", + "appSettings_clearPathOnMaxRetry": "Pfad bei maximalen Wiederholungsversuchen löschen", + "appSettings_clearPathOnMaxRetrySubtitle": "Kontaktpfade nach 5 fehlgeschlagenen Sendeversuchen zurücksetzen", + "appSettings_pathsWillBeCleared": "Pfade werden nach 5 fehlgeschlagenen Wiederholungen gelöscht.", + "appSettings_pathsWillNotBeCleared": "Pfade werden nicht automatisch gelöscht.", "appSettings_autoRouteRotation": "Automatische Routenrotation", - "appSettings_autoRouteRotationSubtitle": "Wechseln zwischen den besten Pfaden und dem Fluten", + "appSettings_autoRouteRotationSubtitle": "Zwischen den besten Pfaden und dem Flood-Modus wechseln", "appSettings_autoRouteRotationEnabled": "Automatische Routenrotation aktiviert", "appSettings_autoRouteRotationDisabled": "Automatische Routenrotation deaktiviert", "appSettings_battery": "Akku", "appSettings_batteryChemistry": "Batteriechemie", - "appSettings_batteryChemistryPerDevice": "Konfiguriert pro Gerät ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Pro Gerät festgelegt ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -212,20 +212,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Verbinde ein Gerät, um zu wählen", + "appSettings_batteryChemistryConnectFirst": "Zum Auswählen mit einem Gerät verbinden", "appSettings_batteryNmc": "18650 NMC (3,0–4,2 V)", "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", "appSettings_batteryLipo": "LiPo (3,0–4,2V)", "appSettings_mapDisplay": "Kartendarstellung", - "appSettings_showRepeaters": "Zeige Repeater", - "appSettings_showRepeatersSubtitle": "Zeige Repeater-Knoten auf der Karte an", - "appSettings_showChatNodes": "Zeige Chat-Knoten", + "appSettings_showRepeaters": "Repeater anzeigen", + "appSettings_showRepeatersSubtitle": "Repeater-Knoten auf der Karte anzeigen", + "appSettings_showChatNodes": "Chat-Knoten anzeigen", "appSettings_showChatNodesSubtitle": "Chat-Knoten auf der Karte anzeigen", - "appSettings_showOtherNodes": "Zeige andere Knoten", + "appSettings_showOtherNodes": "Andere Knoten anzeigen", "appSettings_showOtherNodesSubtitle": "Andere Knotentypen auf der Karte anzeigen", "appSettings_timeFilter": "Zeitfilter", "appSettings_timeFilterShowAll": "Alle Knoten anzeigen", - "appSettings_timeFilterShowLast": "Zeige Knoten der letzten {hours} Stunden an", + "appSettings_timeFilterShowLast": "Knoten der letzten {hours} Stunden anzeigen", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -233,16 +233,16 @@ } } }, - "appSettings_mapTimeFilter": "Karten Zeitfilter", + "appSettings_mapTimeFilter": "Karten-Zeitfilter", "appSettings_showNodesDiscoveredWithin": "Zeige Knoten, die innerhalb von:", - "appSettings_allTime": "Ganzer Zeitverlauf", + "appSettings_allTime": "Gesamter Zeitraum", "appSettings_lastHour": "Letzte Stunde", "appSettings_last6Hours": "Letzte 6 Stunden", "appSettings_last24Hours": "Letzte 24 Stunden", "appSettings_lastWeek": "Letzte Woche", "appSettings_offlineMapCache": "Offline-Karten-Cache", "appSettings_noAreaSelected": "Kein Bereich ausgewählt", - "appSettings_areaSelectedZoom": "Ausgewählte Fläche (Zoom {minZoom}-{maxZoom})", + "appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -253,18 +253,28 @@ } } }, - "appSettings_debugCard": "Fehlerbehebung", + "appSettings_debugCard": "Debug", "appSettings_appDebugLogging": "App-Debug-Protokollierung", - "appSettings_appDebugLoggingSubtitle": "Protokolliere App-Debug-Nachrichten zur Fehlerbehebung", + "appSettings_appDebugLoggingSubtitle": "App-Debug-Nachrichten zur Fehlerbehebung protokollieren", "appSettings_appDebugLoggingEnabled": "App-Debug-Protokollierung aktiviert", "appSettings_appDebugLoggingDisabled": "App-Debug-Protokollierung deaktiviert", "contacts_title": "Kontakte", - "contacts_noContacts": "Noch keine Kontakte vorhanden.", - "contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.", - "contacts_searchContacts": "Suche Kontakte...", - "contacts_noUnreadContacts": "Keine ungesehene Kontakte", + "contacts_noContacts": "Noch keine Kontakte", + "contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte Ankündigungen senden.", + "contacts_searchContacts": "Kontakte suchen...", + "@contacts_searchContacts": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_noUnreadContacts": "Keine ungelesenen Kontakte", "contacts_noContactsFound": "Keine Kontakte oder Gruppen gefunden.", - "contacts_deleteContact": "Lösche den Kontakt", + "contacts_deleteContact": "Kontakt löschen", "contacts_removeConfirm": "{contactName} aus den Kontakten entfernen?", "@contacts_removeConfirm": { "placeholders": { @@ -274,11 +284,11 @@ } }, "contacts_manageRepeater": "Repeater verwalten", - "contacts_roomLogin": "Raum-Login", - "contacts_openChat": "Öffne Chat", + "contacts_roomLogin": "Raumserver-Login", + "contacts_openChat": "Chat öffnen", "contacts_editGroup": "Gruppe bearbeiten", - "contacts_deleteGroup": "Löschen Gruppe", - "contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?", + "contacts_deleteGroup": "Gruppe löschen", + "contacts_deleteGroupConfirm": "Gruppe \"{groupName}\" löschen?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -288,7 +298,7 @@ }, "contacts_newGroup": "Neue Gruppe", "contacts_groupName": "Gruppenname", - "contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.", + "contacts_groupNameRequired": "Der Gruppenname ist erforderlich.", "contacts_groupNameReserved": "Dieser Gruppenname ist reserviert", "contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.", "@contacts_groupAlreadyExists": { @@ -298,10 +308,10 @@ } } }, - "contacts_filterContacts": "Filtert Kontakte...", + "contacts_filterContacts": "Kontakte filtern...", "contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter", "contacts_noMembers": "Keine Mitglieder", - "contacts_lastSeenNow": "kürzlich", + "contacts_lastSeenNow": "gerade eben", "contacts_lastSeenMinsAgo": "~ {minutes} Min.", "@contacts_lastSeenMinsAgo": { "placeholders": { @@ -331,7 +341,7 @@ "channels_title": "Kanäle", "channels_noChannelsConfigured": "Keine Kanäle konfiguriert", "channels_addPublicChannel": "Öffentlichen Kanal hinzufügen", - "channels_searchChannels": "Suche Kanäle...", + "channels_searchChannels": "Kanäle suchen...", "channels_noChannelsFound": "Keine Kanäle gefunden", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { @@ -346,8 +356,8 @@ "channels_editChannel": "Kanal bearbeiten", "channels_muteChannel": "Kanal stummschalten", "channels_unmuteChannel": "Kanal Stummschaltung aufheben", - "channels_deleteChannel": "Lösche den Kanal", - "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", + "channels_deleteChannel": "Kanal löschen", + "channels_deleteChannelConfirm": "Kanal \"{name}\" löschen? Dies kann nicht rückgängig gemacht werden.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -366,12 +376,12 @@ "channels_addChannel": "Kanal hinzufügen", "channels_channelIndexLabel": "Kanalindex", "channels_channelName": "Kanalname", - "channels_usePublicChannel": "Verwende öffentlichen Kanal", - "channels_standardPublicPsk": "Öffentliche Standard PSK", + "channels_usePublicChannel": "Öffentlichen Kanal verwenden", + "channels_standardPublicPsk": "Öffentlicher Standard-PSK", "channels_pskHex": "PSK (Hexadezimal)", - "channels_generateRandomPsk": "Zufällige PSK generieren", - "channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.", - "channels_pskMustBe32Hex": "Die PSK muss 32 hexadezimale Zeichen haben.", + "channels_generateRandomPsk": "Zufälligen PSK generieren", + "channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein", + "channels_pskMustBe32Hex": "Der PSK muss 32 hexadezimale Zeichen haben.", "channels_channelAdded": "Kanal \"{name}\" hinzugefügt", "@channels_channelAdded": { "placeholders": { @@ -380,7 +390,7 @@ } } }, - "channels_editChannelTitle": "Bearbeiteter Kanal {index}", + "channels_editChannelTitle": "Kanal {index} bearbeiten", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -390,12 +400,17 @@ }, "channels_smazCompression": "SMAZ-Komprimierung", "channels_cyr2latCompression": "Cyr2Lat-Komprimierung", - "channels_cyr2latCompressionDscr": "Ersetzt einige kyrillische Zeichen durch lateinische Zeichen, wenn sie gesendet werden.", + "channels_cyr2latCompressionDscr": "Ersetzt beim Senden einige kyrillische Zeichen durch lateinische Zeichen.", "channels_cyr2latSettingsHeading": "Cyr2Lat-Einstellungen", "channels_cyr2latSettingsSubheading": "Ersetzungsliste", - "channels_cyr2latSettingsDscr": "JSON-Konfiguration für die Zeichenersetzung bearbeiten", + "channels_cyr2latSettingsDscr": "JSON-Konfiguration für Zeichenersetzungen bearbeiten", "channels_cyr2latSettingsDialogHint": "JSON-Ersetzungstabelle", "channels_cyr2latSettingsDialogWrongJSON": "Ungültiges JSON: {error}", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, "settings_cyr2latProfileAdd": "Cyr2Lat-Profil hinzufügen", "settings_cyr2latProfileName": "Profilname", "settings_cyr2latProfileNameEmpty": "Der Profilname darf nicht leer sein", @@ -405,6 +420,13 @@ "settings_cyr2latProfileDelete": "Cyr2Lat-Profil löschen", "settings_cyr2latProfileDeleted": "Profil erfolgreich gelöscht", "settings_cyr2latProfileDeleteDscr": "Möchten Sie das Profil \"{name}\" wirklich löschen?", + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelUpdated": "Kanal \"{name}\" aktualisiert", "@channels_channelUpdated": { "placeholders": { @@ -414,14 +436,14 @@ } }, "channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt", - "channels_sortBy": "Sortiere nach", + "channels_sortBy": "Sortieren nach", "channels_sortManual": "Manuell", "channels_sortAZ": "A bis Z", - "channels_sortLatestMessages": "Letzte Nachrichten", + "channels_sortLatestMessages": "Neueste Nachrichten", "channels_sortUnread": "Ungelesen", "chat_noMessages": "Noch keine Nachrichten.", - "chat_sendMessageToStart": "Eine Nachricht senden, um anzufangen.", - "chat_originalMessageNotFound": "Originalmeldung nicht gefunden", + "chat_sendMessageToStart": "Senden Sie eine Nachricht, um zu beginnen.", + "chat_originalMessageNotFound": "Originalnachricht nicht gefunden", "chat_replyingTo": "Antworten an {name}", "@chat_replyingTo": { "placeholders": { @@ -439,7 +461,7 @@ } }, "chat_location": "Ort", - "chat_sendMessageTo": "Sende eine Nachricht an {contactName}", + "chat_sendMessageTo": "Nachricht an {contactName} senden", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -447,8 +469,8 @@ } } }, - "chat_typeMessage": "Eine Nachricht eingeben...", - "chat_messageTooLong": "Nachricht ist zu lang (max {maxBytes} Bytes).", + "chat_typeMessage": "Nachricht eingeben...", + "chat_messageTooLong": "Nachricht zu lang (max. {maxBytes} Bytes).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -458,8 +480,8 @@ }, "chat_messageCopied": "Nachricht kopiert", "chat_messageDeleted": "Nachricht gelöscht", - "chat_retryingMessage": "Versuche es erneut.", - "chat_retryCount": "Versuche {current}/{max}", + "chat_retryingMessage": "Nachricht wird erneut gesendet.", + "chat_retryCount": "Wiederholen {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -471,7 +493,7 @@ } }, "chat_sendGif": "GIF senden", - "chat_reply": "Beantworten", + "chat_reply": "Antworten", "chat_addReaction": "Reaktion hinzufügen", "chat_me": "Ich", "emojiCategorySmileys": "Emoticons", @@ -491,8 +513,8 @@ "debugLog_clearLog": "Protokoll löschen", "debugLog_copied": "Debug-Protokoll kopiert", "debugLog_bleCopied": "BLE-Protokoll kopiert", - "debugLog_noEntries": "No Debug-Protokolle noch verfügbar", - "debugLog_enableInSettings": "Aktivieren Sie das App-Debug-Logging in den Einstellungen", + "debugLog_noEntries": "Noch keine Debug-Protokolle vorhanden", + "debugLog_enableInSettings": "App-Debug-Protokollierung in den Einstellungen aktivieren", "debugLog_frames": "Rahmen", "debugLog_rawLogRx": "Roh-Log-RX", "debugLog_noBleActivity": "Bisher keine BLE-Aktivität", @@ -658,27 +680,27 @@ } } }, - "map_chat": "Benutzer", - "map_repeater": "Wiederholungseinheit", - "map_room": "Raum", + "map_chat": "Chat", + "map_repeater": "Repeater", + "map_room": "Raumserver", "map_sensor": "Sensor", "map_pinDm": "Pin (Kontakt)", "map_pinPrivate": "Pin (Channel)", "map_pinPublic": "Kennzeichnung (Öffentlich)", - "map_lastSeen": "Letzte Sichtung", + "map_lastSeen": "Zuletzt gesehen", "map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", "map_from": "Von", "map_source": "Quelle", "map_flags": "Flaggen", - "map_shareMarkerHere": "Teilen Sie den Marker hier.", - "map_pinLabel": "Pin Name", - "map_label": "Etikett", + "map_shareMarkerHere": "Marker hier teilen", + "map_pinLabel": "Pin-Beschriftung", + "map_label": "Beschriftung", "map_pointOfInterest": "Punkt von Interesse", - "map_sendToContact": "Senden an Kontakt", - "map_sendToChannel": "Senden an Kanal", + "map_sendToContact": "An Kontakt senden", + "map_sendToChannel": "An Kanal senden", "map_noChannelsAvailable": "Keine Kanäle verfügbar", "map_publicLocationShare": "Öffentliche Standortfreigabe", - "map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.", + "map_publicLocationShareConfirm": "Sie sind dabei, einen Standort in {channelLabel} zu teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -686,26 +708,26 @@ } } }, - "map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen", - "map_filterNodes": "Knotenfilter", + "map_connectToShareMarkers": "Mit einem Gerät verbinden, um Marker zu teilen", + "map_filterNodes": "Knoten filtern", "map_nodeTypes": "Knotentypen", "map_chatNodes": "Chat-Knoten", "map_repeaters": "Repeater", "map_otherNodes": "Andere Knoten", "map_keyPrefix": "Schlüsselpräfix", - "map_filterByKeyPrefix": "Filter nach Schlüsselpräfix", - "map_publicKeyPrefix": "Schlüsselpräfix", + "map_filterByKeyPrefix": "Nach Schlüsselpräfix filtern", + "map_publicKeyPrefix": "Präfix des öffentlichen Schlüssels", "map_markers": "Marker", - "map_showSharedMarkers": "Zeige gemeinsam genutzte Marker", + "map_showSharedMarkers": "Gemeinsam genutzte Marker anzeigen", "map_lastSeenTime": "Letzte Sichtung", - "map_sharedPin": "Gemeinsames Passwort", - "map_joinRoom": "Beitreten Sie dem Raum", + "map_sharedPin": "Gemeinsamer Pin", + "map_joinRoom": "Raum beitreten", "map_manageRepeater": "Repeater verwalten", "mapCache_title": "Offline-Karten-Cache", "mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.", "mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.", - "mapCache_downloadTilesTitle": "Herunterladen von Kacheln", - "mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?", + "mapCache_downloadTilesTitle": "Kacheln herunterladen", + "mapCache_downloadTilesPrompt": "{count} Kacheln für die Offline-Nutzung herunterladen?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -714,7 +736,7 @@ } }, "mapCache_downloadAction": "Herunterladen", - "mapCache_cachedTiles": "Zwischengespeicherte {count} Kacheln", + "mapCache_cachedTiles": "{count} Kacheln zwischengespeichert", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -722,7 +744,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Zwischengespeicherte {downloaded} Kacheln ({failed} fehlgeschlagen)", + "mapCache_cachedTilesWithFailed": "{downloaded} Kacheln zwischengespeichert ({failed} fehlgeschlagen)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -733,13 +755,13 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Leere Offline-Cache", - "mapCache_clearOfflineCachePrompt": "Alle zwischengespeicherten Kartenraster entfernen?", + "mapCache_clearOfflineCacheTitle": "Offline-Cache leeren", + "mapCache_clearOfflineCachePrompt": "Alle zwischengespeicherten Kartenkacheln entfernen?", "mapCache_offlineCacheCleared": "Offline-Cache gelöscht", "mapCache_noAreaSelected": "Kein Bereich ausgewählt", - "mapCache_cacheArea": "Zwischenspeicherbereich", + "mapCache_cacheArea": "Bereich zwischenspeichern", "mapCache_useCurrentView": "Aktuelle Ansicht verwenden", - "mapCache_zoomRange": "Zoom Bereich", + "mapCache_zoomRange": "Zoombereich", "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", "@mapCache_estimatedTiles": { "placeholders": { @@ -759,7 +781,7 @@ } } }, - "mapCache_downloadTilesButton": "Herunterladen von Kacheln", + "mapCache_downloadTilesButton": "Kacheln herunterladen", "mapCache_clearCacheButton": "Cache leeren", "mapCache_failedDownloads": "Fehlgeschlagene Downloads: {count}", "@mapCache_failedDownloads": { @@ -787,7 +809,7 @@ } }, "time_justNow": "Gerade eben", - "time_minutesAgo": "{minutes} Minuten her", + "time_minutesAgo": "vor {minutes} Min.", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -795,7 +817,7 @@ } } }, - "time_hoursAgo": "{hours} Stunden her", + "time_hoursAgo": "vor {hours} Std.", "@time_hoursAgo": { "placeholders": { "hours": { @@ -803,7 +825,7 @@ } } }, - "time_daysAgo": "{days} Tage/Tage zuvor", + "time_daysAgo": "vor {days} Tagen", "@time_daysAgo": { "placeholders": { "days": { @@ -858,8 +880,8 @@ }, "login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.", "common_reload": "Neu laden", - "common_clear": "Löschen", - "path_currentPath": "Aktiver Pfad: {path}", + "common_clear": "Leeren", + "path_currentPath": "Aktueller Pfad: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -867,7 +889,7 @@ } } }, - "path_usingHopsPath": "Verwenden Sie {count} {count, plural, =1{Hop} other{Hops}} Pfad", + "path_usingHopsPath": "Pfad mit {count} {count, plural, =1{Hop} other{Hops}} verwenden", "@path_usingHopsPath": { "placeholders": { "count": { @@ -875,16 +897,16 @@ } } }, - "path_enterCustomPath": "Gebe Pfad ein", + "path_enterCustomPath": "Pfad eingeben", "path_currentPathLabel": "Aktueller Pfad", - "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", - "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", + "path_hexPrefixInstructions": "Geben Sie für jeden Zwischen-Hop das zweistellige Hex-Präfix ein, getrennt durch Kommas.", + "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet das erste Byte seines öffentlichen Schlüssels)", "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", - "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", - "path_selectFromContacts": "Oder wähle aus Kontakten aus:", + "path_helperMaxHops": "Maximal 64 Sprünge. Jedes Präfix besteht aus 2 Hex-Zeichen (1 Byte).", + "path_selectFromContacts": "Oder aus Kontakten auswählen:", "path_noRepeatersFound": "Keine Repeater oder Raumserver gefunden.", "path_customPathsRequire": "Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.", - "path_invalidHexPrefixes": "Ungültige Hexadezimal-Präfixe: {prefixes}", + "path_invalidHexPrefixes": "Ungültige Hex-Präfixe: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -892,25 +914,25 @@ } } }, - "path_tooLong": "Pfad zu lang. Maximal 64 Hops erlaubt.", + "path_tooLong": "Pfad zu lang. Maximal 64 Sprünge erlaubt.", "path_setPath": "Pfad festlegen", "repeater_management": "Repeater-Verwaltung", - "repeater_managementTools": "Verwaltungs-Tools", + "repeater_managementTools": "Verwaltungstools", "repeater_status": "Status", - "repeater_statusSubtitle": "Status, Statistiken und Nachbarn anzeigen", + "repeater_statusSubtitle": "Repeater-Status, Statistiken und Nachbarn anzeigen", "repeater_telemetry": "Telemetrie", - "repeater_telemetrySubtitle": "Sensordaten und Systemwerte anzeigen", - "repeater_cli": "Befehlszeilen-Schnittstelle", - "repeater_cliSubtitle": "Sende Befehle an den Repeater", + "repeater_telemetrySubtitle": "Telemetriedaten und Systemstatistiken anzeigen", + "repeater_cli": "CLI", + "repeater_cliSubtitle": "Befehle an den Repeater senden", "repeater_settings": "Einstellungen", - "repeater_settingsSubtitle": "Repeater-parameter konfigurieren", - "repeater_statusTitle": "Repeaterstatus", - "repeater_routingMode": "Routenmodus", + "repeater_settingsSubtitle": "Repeater-Parameter konfigurieren", + "repeater_statusTitle": "Repeater-Status", + "repeater_routingMode": "Routing-Modus", "repeater_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)", "repeater_forceFloodMode": "Flut-Modus erzwingen", "repeater_pathManagement": "Pfadverwaltung", "repeater_refresh": "Aktualisieren", - "repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.", + "repeater_statusRequestTimeout": "Statusanfrage abgelaufen.", "repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -919,13 +941,13 @@ } } }, - "repeater_systemInformation": "Systeminformation", + "repeater_systemInformation": "Systeminformationen", "repeater_battery": "Akku", "repeater_clockAtLogin": "Uhr (bei Anmeldung)", - "repeater_uptime": "Verfügbarkeit", + "repeater_uptime": "Betriebszeit", "repeater_queueLength": "Warteschlangenlänge", - "repeater_debugFlags": "Fehlerbehebungsoptionen", - "repeater_radioStatistics": "Funk-Statistik", + "repeater_debugFlags": "Debug-Flags", + "repeater_radioStatistics": "Funkstatistiken", "repeater_lastRssi": "Letzter RSSI", "repeater_lastSnr": "Letzter SNR", "repeater_noiseFloor": "Rauschpegel", @@ -952,7 +974,7 @@ } } }, - "repeater_packetTxTotal": "Gesamt: {total}, Flut: {flood}, Direkt: {direct}", + "repeater_packetTxTotal": "Gesamt: {total}, Flood: {flood}, Direkt: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -966,7 +988,7 @@ } } }, - "repeater_packetRxTotal": "Gesamt: {total}, Flut: {flood}, Direkt: {direct}", + "repeater_packetRxTotal": "Gesamt: {total}, Flood: {flood}, Direkt: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -980,7 +1002,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}", + "repeater_duplicatesFloodDirect": "Flood: {flood}, Direkt: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -999,36 +1021,36 @@ } } }, - "repeater_settingsTitle": "Repeater Einstellungen", - "repeater_basicSettings": "Grundlegende Einstellungen", - "repeater_repeaterName": "Name des Repeater", + "repeater_settingsTitle": "Repeater-Einstellungen", + "repeater_basicSettings": "Grundeinstellungen", + "repeater_repeaterName": "Repeatername", "repeater_repeaterNameHelper": "Anzeigename für diesen Repeater", "repeater_adminPassword": "Admin-Passwort", - "repeater_adminPasswordHelper": "Vollzugriffspasswort", + "repeater_adminPasswordHelper": "Passwort für Vollzugriff", "repeater_guestPassword": "Gast-Passwort", - "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", - "repeater_radioSettings": "Funk Einstellungen", + "repeater_guestPasswordHelper": "Passwort für Lesezugriff", + "repeater_radioSettings": "Funk-Einstellungen", "repeater_frequencyMhz": "Frequenz (MHz)", "repeater_frequencyHelper": "300–2500 MHz", - "repeater_txPower": "TX Power", + "repeater_txPower": "Sendeleistung", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Bandbreite", - "repeater_spreadingFactor": "Verteilungsfaktor", + "repeater_spreadingFactor": "Spreading-Faktor", "repeater_codingRate": "Kodierungsrate", - "repeater_locationSettings": "Standort Einstellungen", + "repeater_locationSettings": "Standorteinstellungen", "repeater_latitude": "Breitengrad", "repeater_latitudeHelper": "Dezimalgrad (z.B. 37,7749)", "repeater_longitude": "Längengrad", "repeater_longitudeHelper": "Dezimalgrad (z.B. -122,4194)", "repeater_features": "Funktionen", "repeater_packetForwarding": "Paketweiterleitung", - "repeater_packetForwardingSubtitle": "Aktivieren Sie den Repeater, um Pakete weiterzuleiten.", + "repeater_packetForwardingSubtitle": "Den Repeater aktivieren, um Pakete weiterzuleiten", "repeater_guestAccess": "Gastzugriff", - "repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen", - "repeater_privacyMode": "Privatsphäreeinstellung", - "repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", + "repeater_guestAccessSubtitle": "Nur-Lese-Gastzugriff erlauben", + "repeater_privacyMode": "Privatsphärenmodus", + "repeater_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen", "repeater_advertisementSettings": "Ankündigungseinstellungen", - "repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen", + "repeater_localAdvertInterval": "Lokales Ankündigungsintervall", "repeater_localAdvertIntervalMinutes": "{minutes} Minuten", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1037,7 +1059,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen", + "repeater_floodAdvertInterval": "Flood-Ankündigungsintervall", "repeater_floodAdvertIntervalHours": "{hours} Stunden", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1046,18 +1068,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung", + "repeater_encryptedAdvertInterval": "Verschlüsseltes Ankündigungsintervall", "repeater_dangerZone": "Gefahrenzone", - "repeater_rebootRepeater": "Neustart Repeater", - "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.", + "repeater_rebootRepeater": "Repeater neu starten", + "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten", "repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?", - "repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung", - "repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren", - "repeater_regenerateIdentityKeyConfirm": "Dies generiert eine neue Identität für den Repeater. Fortfahren?", + "repeater_regenerateIdentityKey": "Identitätsschlüssel neu erzeugen", + "repeater_regenerateIdentityKeySubtitle": "Neues öffentlich-privates Schlüsselpaar erzeugen", + "repeater_regenerateIdentityKeyConfirm": "Dies erzeugt eine neue Identität für den Repeater. Fortfahren?", "repeater_eraseFileSystem": "Dateisystem löschen", - "repeater_eraseFileSystemSubtitle": "Formatiere die Repeater-Dateisystemdatei", + "repeater_eraseFileSystemSubtitle": "Das Dateisystem des Repeaters formatieren", "repeater_eraseFileSystemConfirm": "WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!", - "repeater_eraseSerialOnly": "Löschen ist nur über die serielle Konsole möglich.", + "repeater_eraseSerialOnly": "Löschen ist nur über die serielle Konsole verfügbar.", "repeater_commandSent": "Befehl gesendet: {command}", "@repeater_commandSent": { "placeholders": { @@ -1076,20 +1098,20 @@ }, "repeater_confirm": "Bestätigen", "repeater_settingsSaved": "Einstellungen erfolgreich gespeichert", - "repeater_rxGain": "Erhöhter RX-Gewinn", - "repeater_rxGainHelper": "Höhere Empfindlichkeit, höherer Stromverbrauch (nur für SX1262/SX1268)", - "repeater_refreshRxGain": "Erneuerung des verstärkten RX-Effekts", - "repeater_multiAcks": "Mehrere Bestätigungen", - "repeater_multiAcksSubtitle": "Nachrichten über verschiedene Pfade senden, um die Zustellbarkeit zu verbessern.", - "repeater_refreshMultiAcks": "Mehrere Bestätigungen neu senden/aktualisieren", - "repeater_networkHealth": "Netzwerkgesundheit", - "repeater_loopDetect": "Erkennung von Schleifen", - "repeater_loopDetectHelper": "Erstellen Sie \"Flood\"-Pakete, die so aussehen, als ob sie Schleifen erzeugen.", + "repeater_rxGain": "Verstärkter RX-Gewinn", + "repeater_rxGainHelper": "Höhere Empfindlichkeit, höherer Stromverbrauch (nur SX1262/SX1268)", + "repeater_refreshRxGain": "Verstärkten RX-Gewinn aktualisieren", + "repeater_multiAcks": "Mehrfach-ACKs", + "repeater_multiAcksSubtitle": "Nachrichten über mehrere Pfade bestätigen, um die Zustellung zu verbessern", + "repeater_refreshMultiAcks": "Mehrfach-ACKs aktualisieren", + "repeater_networkHealth": "Netzwerkzustand", + "repeater_loopDetect": "Schleifenerkennung", + "repeater_loopDetectHelper": "Flood-Pakete verwerfen, die wie Routing-Schleifen aussehen", "repeater_loopDetectOff": "Aus", "repeater_loopDetectMinimal": "Minimal", "repeater_loopDetectModerate": "mäßig", - "repeater_loopDetectStrict": "streng", - "repeater_dutyCycle": "Betriebsdauer", + "repeater_loopDetectStrict": "Streng", + "repeater_dutyCycle": "Duty-Cycle", "repeater_dutyCycleHelper": "Höchster zulässiger Prozentsatz der Sendefläche", "repeater_dutyCyclePercent": "{percent}%", "@repeater_dutyCyclePercent": { @@ -1099,31 +1121,31 @@ } } }, - "repeater_ownerInfo": "Information zum Betreiber", - "repeater_ownerInfoHelper": "Öffentliche Metadaten für dieses Gerät", - "repeater_refreshOwnerInfo": "Aktualisieren Sie die Informationen zum Betreiber", - "repeater_floodMax": "Max-Hops-Flut", - "repeater_floodMaxHelper": "Maximale Anzahl an Hop-Paketen, die ein einzelnes Paket durchlaufen kann (0-64)", - "repeater_advancedSettings": "Fortgeschritten", - "repeater_advancedSettingsSubtitle": "Regler für erfahrene Bediener", + "repeater_ownerInfo": "Betreiberinformationen", + "repeater_ownerInfoHelper": "Öffentliche Metadaten für diesen Repeater", + "repeater_refreshOwnerInfo": "Betreiberinformationen aktualisieren", + "repeater_floodMax": "Maximale Flood-Sprünge", + "repeater_floodMaxHelper": "Maximale Anzahl von Sprüngen, die ein Flood-Paket zurücklegen darf (0-64)", + "repeater_advancedSettings": "Erweitert", + "repeater_advancedSettingsSubtitle": "Feinabstimmung für erfahrene Betreiber", "repeater_pathHashMode": "Hash-Modus für Pfade", "repeater_pathHashModeHelper": "Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade – ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.", - "repeater_txDelay": "Verzögerung bei Flood TX", - "repeater_txDelayHelper": "Wiederholung des Abstands für Hochwasser-Verkehr, als Multiplikator der Übertragungszeit des Pakets (0-2, Standardwert 0,5). Höherer Wert = weniger Kollisionen, aber langsamere Übertragung.", - "repeater_directTxDelay": "Direkter TX-Verzögerung", - "repeater_directTxDelayHelper": "Die Übertragungsrate für direkten (nicht-fluten) Datenverkehr wird als Vielfaches der Übertragungszeit des Pakets festgelegt (0-2, Standardwert 0,3).", - "repeater_intThresh": "Grenzwert für Störungen", - "repeater_intThreshHelper": "Der Schwellenwert wird an die Rauschpegel-Kalibrierung des Radios angepasst, sodass Störungen über diesem Wert abgefangen werden. 0 deaktiviert – erhöhen Sie diesen Wert nur, wenn Sie in einem verrauschten Frequenzbereich RX-Fehler feststellen.", - "repeater_agcResetInterval": "Intervall für die Rücksetzung von AGC", - "repeater_agcResetIntervalHelper": "Wie oft sollte die automatische Verstärkungskontrolle des Radios zurückgesetzt werden, um von einem Zustand mit zu hoher Verstärkung wieder in einen normalen Zustand zu gelangen? Die Einstellung „Sekunden“ ermöglicht eine Rücksetzung alle 4 Sekunden. Die Einstellung „0“ deaktiviert die periodische Rücksetzung.", + "repeater_txDelay": "Flood-TX-Verzögerung", + "repeater_txDelayHelper": "Abstand für Flood-Verkehr als Faktor der Paket-Sendezeit (0-2, Standard 0,5). Höher = weniger Kollisionen, aber langsamere Zustellung.", + "repeater_directTxDelay": "Direkte TX-Verzögerung", + "repeater_directTxDelayHelper": "Abstand für direkten (Nicht-Flood-)Verkehr als Faktor der Paket-Sendezeit (0-2, Standard 0,3).", + "repeater_intThresh": "Interferenzschwelle", + "repeater_intThreshHelper": "Schwelle für die Rauschboden-Kalibrierung des Radios; ignoriert Interferenzen oberhalb dieses Werts. 0 deaktiviert - nur erhöhen, wenn in einem lauten Band RX-Fehler auftreten.", + "repeater_agcResetInterval": "AGC-Reset-Intervall", + "repeater_agcResetIntervalHelper": "Wie oft die automatische Verstärkungsregelung zurückgesetzt werden soll, um aus einem festgefahrenen Verstärkungszustand zu kommen. Sekunden, auf ein Vielfaches von 4 abgerundet. 0 deaktiviert periodische Resets.", "repeater_actionsTitle": "Aktionen", - "repeater_sendAdvert": "Flood-Werbung versenden", - "repeater_sendAdvertSubtitle": "Eine Werbekampagne für Überschwemmungen über das Netzwerk verbreiten.", - "repeater_sendAdvertZeroHop": "Versenden Sie eine Anzeige ohne Zwischenvermittler.", - "repeater_sendAdvertZeroHopSubtitle": "Eine Werbekampagne mit einem einzigen Sender (ohne Weiterleitung) senden.", + "repeater_sendAdvert": "Flood-Ankündigung senden", + "repeater_sendAdvertSubtitle": "Eine Flood-Ankündigung über das Netzwerk senden", + "repeater_sendAdvertZeroHop": "Zero-Hop-Ankündigung senden", + "repeater_sendAdvertZeroHopSubtitle": "Eine Ein-Hop-Ankündigung ohne Weiterleiter senden", "repeater_clockSync": "Uhr jetzt synchronisieren", - "repeater_clockSyncSubtitle": "Übertragen Sie die Uhrzeit Ihres Telefons an den Repeater.", - "repeater_actionSucceeded": "{action} war erfolgreich", + "repeater_clockSyncSubtitle": "Die Zeit Ihres Telefons an den Repeater übertragen", + "repeater_actionSucceeded": "{action} erfolgreich", "@repeater_actionSucceeded": { "placeholders": { "action": { @@ -1142,7 +1164,7 @@ } } }, - "repeater_settingsSavedRebootNeeded": "Einstellungen gespeichert – Repeater neu starten, um die Änderungen anzuwenden.", + "repeater_settingsSavedRebootNeeded": "Einstellungen gespeichert - starten Sie den Repeater neu, um sie anzuwenden", "repeater_settingsPartialFailure": "Einige Einstellungen sind fehlgeschlagen: {failures}", "@repeater_settingsPartialFailure": { "placeholders": { @@ -1159,13 +1181,13 @@ } } }, - "repeater_refreshBasicSettings": "Grundlegende Einstellungen aktualisieren", - "repeater_refreshRadioSettings": "Radio-Einstellungen aktualisieren", + "repeater_refreshBasicSettings": "Grundeinstellungen aktualisieren", + "repeater_refreshRadioSettings": "Funk-Einstellungen aktualisieren", "repeater_refreshTxPower": "Sendeleistung aktualisieren", - "repeater_refreshPacketForwarding": "Aktualisieren Paketweiterleitung", - "repeater_refreshGuestAccess": "Aktualisieren Sie den Gastzugriff", - "repeater_refreshPrivacyMode": "Wiederherstellen des Datenschutzzustands", - "repeater_refreshed": "{label} wurde aktualisiert", + "repeater_refreshPacketForwarding": "Paketweiterleitung aktualisieren", + "repeater_refreshGuestAccess": "Gastzugriff aktualisieren", + "repeater_refreshPrivacyMode": "Privatsphärenmodus aktualisieren", + "repeater_refreshed": "{label} aktualisiert", "@repeater_refreshed": { "placeholders": { "label": { @@ -1181,17 +1203,17 @@ } } }, - "repeater_cliTitle": "Befehlszeilen-Schnittstelle (CLI) für Repeater", - "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", - "repeater_commandHelp": "Hilfe", - "repeater_clearHistory": "Löschen der Historie", - "repeater_noCommandsSent": "Noch keine Befehle gesendet.", + "repeater_cliTitle": "Repeater-CLI", + "repeater_debugNextCommand": "Nächsten Befehl debuggen", + "repeater_commandHelp": "Befehls-Hilfe", + "repeater_clearHistory": "Verlauf löschen", + "repeater_noCommandsSent": "Noch keine Befehle gesendet", "repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle", - "repeater_enterCommandHint": "Geben Sie den Befehl ein...", - "repeater_previousCommand": "Vorhergehende Aktion", - "repeater_nextCommand": "Nächste Aktion", + "repeater_enterCommandHint": "Befehl eingeben...", + "repeater_previousCommand": "Vorheriger Befehl", + "repeater_nextCommand": "Nächster Befehl", "repeater_enterCommandFirst": "Geben Sie zuerst einen Befehl ein", - "repeater_cliCommandFrameTitle": "CLI-Befehlsfenster", + "repeater_cliCommandFrameTitle": "CLI-Befehlsframe", "repeater_cliCommandError": "Fehler: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1200,12 +1222,12 @@ } } }, - "repeater_cliQuickGetName": "Name erhalten", - "repeater_cliQuickGetRadio": "Radio empfangen", - "repeater_cliQuickGetTx": "Erhalte TX", + "repeater_cliQuickGetName": "Name abrufen", + "repeater_cliQuickGetRadio": "Funkdaten abrufen", + "repeater_cliQuickGetTx": "TX abrufen", "repeater_cliQuickNeighbors": "Nachbarn", "repeater_cliQuickVersion": "Version", - "repeater_cliQuickAdvertise": "Ankündigungen", + "repeater_cliQuickAdvertise": "Ankündigen", "repeater_cliQuickClock": "Uhr", "repeater_cliHelpAdvert": "Sendet eine Ankündigung", "repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)", @@ -1214,10 +1236,10 @@ "repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.", "repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.", "repeater_cliHelpSetAf": "Legt den Luftzeitfaktor fest.", - "repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)", + "repeater_cliHelpSetTx": "Legt die LoRa-Sendeleistung in dBm fest. (Neustart erforderlich, um die Änderungen anzuwenden)", "repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.", "repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).", - "repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)", + "repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Sprüngen für eingehende Flood-Pakete fest (wenn >= max, wird das Paket nicht weitergeleitet)", "repeater_cliHelpSetIntThresh": "Legt den Interferenzeniveau (in dB) fest. Der Standardwert ist 14. Auf 0 setzen, um die Erkennung von Kanalinterferenzen zu deaktivieren.", "repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.", "repeater_cliHelpSetMultiAcks": "Aktiviert oder deaktiviert die Funktion 'Doppel-ACKs'.", @@ -1229,7 +1251,7 @@ "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", - "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", + "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Sendezeit eines Flood-Mode-Pakets und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", "repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.", @@ -1307,16 +1329,93 @@ "telemetry_switchLabel": "Schalter", "telemetry_polylineLabel": "Polylinie", "telemetry_altitudeValue": "{meters} m", + "@telemetry_altitudeValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_frequencyValue": "{hertz} Hz", + "@telemetry_frequencyValue": { + "placeholders": { + "hertz": { + "type": "String" + } + } + }, "telemetry_pressureValue": "{hpa} hPa", + "@telemetry_pressureValue": { + "placeholders": { + "hpa": { + "type": "String" + } + } + }, "telemetry_luminosityValue": "{lux} lx", + "@telemetry_luminosityValue": { + "placeholders": { + "lux": { + "type": "String" + } + } + }, "telemetry_powerValue": "{watts} W", + "@telemetry_powerValue": { + "placeholders": { + "watts": { + "type": "String" + } + } + }, "telemetry_distanceValue": "{meters} m", + "@telemetry_distanceValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_energyValue": "{kilowattHours} kWh", + "@telemetry_energyValue": { + "placeholders": { + "kilowattHours": { + "type": "String" + } + } + }, "telemetry_directionValue": "{degrees}°", + "@telemetry_directionValue": { + "placeholders": { + "degrees": { + "type": "String" + } + } + }, "telemetry_concentrationValue": "{ppm} ppm", + "@telemetry_concentrationValue": { + "placeholders": { + "ppm": { + "type": "String" + } + } + }, "telemetry_percentageValue": "{percent}%", + "@telemetry_percentageValue": { + "placeholders": { + "percent": { + "type": "String" + } + } + }, "telemetry_analogValue": "{value}", + "@telemetry_analogValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "telemetry_autoFetchQuantity": "Anzahl der Anfragen", "telemetry_error": "Daten konnten nicht abgerufen werden", "telemetry_noData": "Keine Telemetriedaten verfügbar.", @@ -1528,10 +1627,10 @@ "neighbors_heardAgo": "Gehört vor: {time}", "neighbors_unknownContact": "Unbekannt {pubkey}", "settings_locationGPSEnable": "GPS aktivieren", - "settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.", - "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", + "settings_locationGPSEnableSubtitle": "Ermöglicht GPS, den Standort automatisch zu aktualisieren.", + "settings_locationIntervalSec": "GPS-Intervall (Sekunden)", "settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.", - "contacts_manageRoom": "Raum-Server verwalten", + "contacts_manageRoom": "Raumserver verwalten", "room_management": "Raum-Server-Verwaltung", "@community_joinConfirmation": { "placeholders": { @@ -1589,7 +1688,7 @@ } } }, - "common_ok": "Alles klar", + "common_ok": "OK", "community_create": "Erstelle Community", "community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.", "community_join": "Beitreten", @@ -1694,7 +1793,7 @@ "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", - "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Zeitmetadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", @@ -1757,7 +1856,7 @@ "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!", - "map_removeLast": "Letztes Entfernen", + "map_removeLast": "Letztes entfernen", "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", "map_runTrace": "Pfadverlauf ausführen", "pathTrace_clearTooltip": "Pfad löschen", @@ -1770,9 +1869,9 @@ "snrIndicator_lastSeen": "Zuletzt gesehen", "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", "chat_ShowAllPaths": "Alle Pfade anzeigen", - "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", - "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", - "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", + "settings_clientRepeat": "Weiterleitung ohne Netzstrom", + "settings_clientRepeatFreqWarning": "Weiterleitung ohne Netzstrom erfordert 433, 869 oder 918 MHz", + "settings_clientRepeatSubtitle": "Dieses Gerät kann Mesh-Pakete für andere weiterleiten", "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Einheiten", "appSettings_unitsMetric": "Metrisch (m/km)", @@ -1983,27 +2082,27 @@ "common_deleteAll": "Alles löschen", "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen", - "map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen", - "map_guessedLocation": "Geschätzter Ort", - "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", + "map_showGuessedLocations": "Vermutete Knotenstandorte anzeigen", + "map_guessedLocation": "Vermuteter Standort", + "usbScreenSubtitle": "Wählen Sie ein erkanntes serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "Verbinden über USB", + "usbScreenTitle": "Über USB verbinden", "connectionChoiceBluetoothLabel": "Bluetooth", "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", - "usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", + "usbScreenNote": "USB-Seriell ist auf unterstützten Android-Geräten und Desktop-Plattformen verfügbar.", "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.", "usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.", "usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.", "usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.", "usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.", "usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.", - "usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.", - "usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.", - "usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.", + "usbErrorOpenFailed": "Das ausgewählte USB-Gerät konnte nicht geöffnet werden.", + "usbErrorConnectFailed": "Mit dem ausgewählten USB-Gerät konnte keine Verbindung hergestellt werden.", + "usbErrorUnsupported": "USB-Seriell wird auf dieser Plattform nicht unterstützt.", "usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.", "usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.", "usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.", - "usbFallbackDeviceName": "Web-Serielle Geräte", + "usbFallbackDeviceName": "Web-Serial-Gerät", "@usbConnectionFailed": { "placeholders": { "error": { @@ -2013,9 +2112,9 @@ }, "usbStatus_searching": "Suche nach USB-Geräten...", "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", - "usbStatus_connecting": "Verbindung zum USB-Gerät...", - "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", - "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.", + "usbStatus_connecting": "Verbindung zum USB-Gerät wird hergestellt...", + "usbConnectionFailed": "USB-Verbindung fehlgeschlagen: {error}", + "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät über USB-Companion-Firmware verfügt.", "@tcpStatus_connectingTo": { "placeholders": { "endpoint": { @@ -2030,20 +2129,20 @@ } } }, - "tcpHostLabel": "IP-Adresse", + "tcpHostLabel": "Endpunkt", "connectionChoiceTcpLabel": "TCP", - "tcpHostHint": "192.168.40.10", + "tcpHostHint": "192.168.40.10 / example.com", "tcpScreenTitle": "Verbinden über TCP", - "tcpPortLabel": "Hafen", + "tcpPortLabel": "Port", "tcpPortHint": "5000", - "tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.", + "tcpStatus_notConnected": "Endpunkt eingeben und verbinden", "tcpStatus_connectingTo": "Verbindung zu {endpoint}...", - "tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.", + "tcpErrorHostRequired": "Ein Endpunkt ist erforderlich.", "tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.", - "tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.", + "tcpErrorUnsupported": "TCP wird auf dieser Plattform nicht unterstützt.", "tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.", - "tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}", - "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen", + "tcpConnectionFailed": "TCP-Verbindung fehlgeschlagen: {error}", + "map_showDiscoveryContacts": "Entdeckte Kontakte anzeigen", "map_setAsMyLocation": "Als meine aktuelle Position festlegen", "@path_routeWeight": { "placeholders": { @@ -2055,16 +2154,16 @@ } } }, - "settings_allowByContact": "Zulassen durch Kontaktflaggen", + "settings_allowByContact": "Nach Kontakt-Flags zulassen", "settings_privacy": "Datenschutzeinstellungen", "settings_allowAll": "Alles zulassen", - "settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.", - "settings_denyAll": "Alle ablehnen", - "settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.", - "settings_telemetryLocationMode": "Telemetrie-Ortsmodus", + "settings_privacySettingsDescription": "Wählen Sie aus, welche Informationen Ihr Gerät mit anderen teilt.", + "settings_denyAll": "Alles verweigern", + "settings_privacySubtitle": "Steuern Sie, welche Informationen freigegeben werden.", + "settings_telemetryLocationMode": "Telemetrie-Standortmodus", "settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus", - "settings_advertLocation": "Anzeigenort", - "settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen", + "settings_advertLocation": "Standort in Ankündigung", + "settings_advertLocationSubtitle": "Standort in die Ankündigung einschließen.", "settings_telemetryBaseMode": "Telemetrie-Basismodus", "contact_teleBase": "Telemetriebasis", "contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie", @@ -2089,7 +2188,7 @@ "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Telemetriemodus aktualisiert", - "map_showOverlaps": "Überlappungen der Repeater-Taste", + "map_showOverlaps": "Repeater-Schlüsselüberlappungen", "map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.", "@radioStats_noiseFloor": { "placeholders": { @@ -2141,7 +2240,7 @@ "appSettings_languageKo": "Koreanisch", "radioStats_tooltip": "Daten zu Radio- und Mesh-Netzwerken", "radioStats_screenTitle": "Senderinformationen", - "radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.", + "radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatistiken anzuzeigen.", "radioStats_firmwareTooOld": "Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.", "radioStats_waiting": "Warte auf Daten…", "radioStats_noiseFloor": "Rauschpegel: {noiseDbm} dBm", @@ -2210,8 +2309,8 @@ "scanner_linuxPairingHidePin": "PIN ausblenden", "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", - "repeater_cliQuickClockSync": "Uhr Synchronisieren", - "repeater_cliQuickDiscovery": "Entdecke Nachbarn", + "repeater_cliQuickClockSync": "Uhr synchronisieren", + "repeater_cliQuickDiscovery": "Nachbarn entdecken", "@repeater_clockSyncAfterLogin": { "description": "Repeater setting: auto sync device clock after successful login" }, @@ -2268,7 +2367,7 @@ "repeater_cliHelpGetRxDelay": "Zeigt den Basiswert für die Verzögerungszeit an.", "repeater_cliHelpGetTxDelay": "Zeigt den Faktor für die Übertragungsverzögerung im Notfallmodus an.", "repeater_cliHelpGetDirectTxDelay": "Zeigt den Faktor für die Verzögerung im Direktmodus an.", - "repeater_cliHelpGetFloodMax": "Zeigt die maximale Anzahl von Überschwemmungsphasen an.", + "repeater_cliHelpGetFloodMax": "Zeigt die maximale Anzahl von Sprüngen für Flood-Pakete an.", "repeater_cliHelpGetOwnerInfo": "Zeigt die Zeichenkette mit den Kontaktinformationen des Eigentümers an.", "repeater_cliHelpGetPathHashMode": "Zeigt den Pfad-Hash-Modus (0/1/2) an.", "repeater_cliHelpGetLoopDetect": "Zeigt die Empfindlichkeit der Schleifenerkennung an.", @@ -2290,15 +2389,15 @@ "repeater_cliHelpSensorList": "Zeigt alle benutzerdefinierten Sensoreinstellungen an, wobei die Seitennummerierung optional von einem Startindex abhängt.", "repeater_cliHelpRegionDefault": "Zeigt den aktuellen Standard-Region-Bereich an.", "repeater_cliHelpRegionDefaultSet": "Definiert den Standard-Regionenbereich. Verwenden Sie \"\", um diesen zu löschen.", - "repeater_cliHelpRegionListAllowed": "Nennt die Regionen, die Überschwemmungsverkehr zulassen.", - "repeater_cliHelpRegionListDenied": "Auflistung von Regionen, die den Verkehr aufgrund von Überschwemmungen verbieten.", + "repeater_cliHelpRegionListAllowed": "Nennt die Regionen, die Flood-Verkehr zulassen.", + "repeater_cliHelpRegionListDenied": "Nennt die Regionen, die Flood-Verkehr verbieten.", "repeater_cliHelpStatsPackets": "(Nur für serielle Verbindungen) Zeigt Statistiken auf Paketebene.", "repeater_cliHelpStatsRadio": "(Nur für Serien) Zeigt Radiostatistiken an.", "repeater_cliHelpStatsCore": "(Nur für serielle Schnittstellen) Zeigt grundlegende Firmware-Statistiken.", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "common_done": "Fertig", + "background_serviceTitle": "MeshCore läuft", + "background_serviceText": "BLE-Verbindung bleibt aktiv", + "appSettings_translationModelDeleted": "Übersetzungsmodell {name} gelöscht", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2306,7 +2405,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "Löschen fehlgeschlagen: {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2314,7 +2413,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "Kanal konnte nicht aktualisiert werden: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2322,21 +2421,28 @@ } } }, - "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", - "map_publicKeyPrefixHint": "e.g. ab12", + "map_type": "Typ", + "map_path": "Pfad", + "map_location": "Standort", + "map_estLocation": "Geschätzter Standort", + "map_publicKey": "Öffentlicher Schlüssel", + "map_publicKeyPrefixHint": "z. B. ab12", "contact_typeChat": "Chat", "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", + "contact_typeRoom": "Raumserver", "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", - "chat_score": "Score", - "settings_multiAck": "Mehrere Bestätigungen", - "map_sharedAt": "Geteilt", + "contact_typeUnknown": "Unbekannt", + "channels_via": "über {path}", + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "chat_score": "Punktzahl", + "settings_multiAck": "Mehrfach-ACKs", + "map_sharedAt": "Geteilt am", "@losBlockedSpotChip": { "placeholders": { "distance": { @@ -2377,7 +2483,7 @@ "losBlockedSpotChip": "{distance} • {distanceUnit} • {obstruction} {heightUnit}", "losBlockedSpotsHint": "Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.", "losSelectedObstructionDetails": "Blockiert durch {obstruction} in einer Höhe von {heightUnit}, {distanceFromA} von A und {distanceFromB} von B ({distanceUnit}).", - "chat_markAsUnread": "Als nicht gelesen markieren", + "chat_markAsUnread": "Als ungelesen markieren", "chat_newMessages": "Neue Nachrichten", "settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware", "settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten", @@ -2421,58 +2527,152 @@ } }, "messageStatus_sent": "Gesendet", - "messageStatus_delivered": "Geliefert", + "messageStatus_delivered": "Zugestellt", "common_undo": "Rückgängig machen", - "messageStatus_pending": "Versenden", - "messageStatus_failed": "Nicht gesendet", - "messageStatus_repeated": "Wiederholt gehört", + "messageStatus_pending": "Wird gesendet", + "messageStatus_failed": "Senden fehlgeschlagen", + "messageStatus_repeated": "Mehrfach gehört", "contacts_moreOptions": "Weitere Optionen", - "contacts_searchOpen": "Kontakte suchen", - "contacts_searchClose": "Erweiterte Suche", + "contacts_searchOpen": "Suche öffnen", + "contacts_searchClose": "Suche schließen", "routing_title": "Routenplanung", - "routing_modeAuto": "Auto", - "routing_modeFlood": "Überschwemmung", - "routing_modeManual": "Handbuch", - "routing_modeFloodHint": "Übertragung über alle Repeater. Die zuverlässigste Methode, jedoch mit höherem Datenverbrauch.", - "routing_modeAutoHint": "Wählt automatisch den bekanntesten Pfad aus und verwendet eine Flutungsmethode, wenn kein Pfad bekannt ist.", - "routing_modeManualHint": "Sendet immer genau den von Ihnen festgelegten Weg.", + "routing_modeAuto": "Automatisch", + "routing_modeFlood": "Flut", + "routing_modeManual": "Manuell", + "routing_modeFloodHint": "Über alle Repeater senden. Am zuverlässigsten, aber mit höherem Funkzeitbedarf.", + "routing_modeAutoHint": "Wählt automatisch den besten bekannten Pfad und wechselt auf Flut, wenn keiner bekannt ist.", + "routing_modeManualHint": "Sendet immer genau den von Ihnen festgelegten Pfad.", "routing_currentRoute": "Aktuelle Route", - "routing_directNoHops": "Direkt – ohne Zwischenverstärkung", - "routing_noPathYet": "Noch kein Pfad gefunden. Die Nachricht wird gesendet, bis ein Weg entdeckt wurde.", - "routing_floodBroadcast": "Übertragung über jeden Repeater", + "routing_directNoHops": "Direkt - keine Repeater-Sprünge", + "routing_noPathYet": "Noch kein Pfad gefunden. Die nächste Nachricht wird geflutet, bis eine Route entdeckt ist.", + "routing_floodBroadcast": "Über alle Repeater senden", "routing_editPath": "Pfad bearbeiten", - "routing_forgetPath": "Vergiss den Weg", - "routing_knownPaths": "Bekannte Routen", - "routing_knownPathsHint": "Wählen Sie den Pfad, um zu diesem zu wechseln.", - "routing_inUse": "Im Gebrauch", - "routing_qualityStrong": "Ein starker erster Sprung", - "routing_qualityGood": "Ein guter erster Schritt", - "routing_qualityFair": "Erster erfolgreicher Schritt", - "routing_qualityWorked": "Hat erfolgreich geliefert", - "routing_qualityFlood": "Information erhalten durch Nachrichten über die Überschwemmung", + "routing_forgetPath": "Pfad vergessen", + "routing_knownPaths": "Bekannte Pfade", + "routing_knownPathsHint": "Tippen Sie auf einen Pfad, um zu ihm zu wechseln.", + "routing_inUse": "In Verwendung", + "routing_qualityStrong": "Starker erster Hop", + "routing_qualityGood": "Guter erster Hop", + "routing_qualityFair": "Ausreichender erster Hop", + "routing_qualityWorked": "Hat zugestellt", + "routing_qualityFlood": "Per Flood gehört", "routing_qualityUntested": "Nicht getestet", - "routing_lastWorked": "war beschäftigt {when}", + "routing_lastWorked": "funktionierte {when}", "routing_neverWorked": "nie bestätigt", - "routing_floodDelivery": "Lieferung bei Überschwemmung", + "routing_floodDelivery": "Flood-Zustellung", "pathEditor_title": "Pfad erstellen", - "pathEditor_hopCounter": "{count} von 64 Hopfengewächsen", - "pathEditor_noHops": "Noch keine Hopfen hinzugefügt. Klicken Sie auf die Schaltflächen unten, um sie nacheinander hinzuzufügen, oder speichern Sie die Rezepter ohne Hopfen, um sie direkt zu versenden.", - "pathEditor_addHops": "Fügen Sie die Hopfen in der richtigen Reihenfolge hinzu.", - "pathEditor_searchRepeaters": "Suche nach wiederholten Nachrichten", - "pathEditor_advancedHex": "Fortgeschritten: Roh-Hex-Pfad", + "pathEditor_hopCounter": "{count} von 64 Sprüngen", + "pathEditor_noHops": "Noch keine Sprünge hinzugefügt. Tippen Sie unten auf Repeater, um sie in Reihenfolge hinzuzufügen, oder speichern Sie ohne Sprünge, um direkt zu senden.", + "pathEditor_addHops": "Sprünge in Reihenfolge hinzufügen", + "pathEditor_searchRepeaters": "Repeater suchen", + "pathEditor_advancedHex": "Erweitert: roher Hex-Pfad", "pathEditor_hexLabel": "Hex-Präfixe", - "pathEditor_hexHelper": "Zwei Hexadezimalzeichen pro Sprung, getrennt durch Kommas", + "pathEditor_hexHelper": "Zwei Hex-Zeichen pro Sprung, durch Kommas getrennt", "pathEditor_invalidTokens": "Ungültig: {tokens}", - "pathEditor_tooManyHops": "Maximal 64 Hopfengreifer", - "pathEditor_usePath": "Verwenden Sie diesen Pfad.", - "pathEditor_removeHop": "Hop entfernen", + "pathEditor_tooManyHops": "Maximal 64 Sprünge", + "pathEditor_usePath": "Diesen Pfad verwenden", + "pathEditor_removeHop": "Sprung entfernen", "pathEditor_unknownHop": "Unbekannter Repeater", - "map_zoomIn": "Zoomen", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "map_zoomOut": "Auszoomen", - "map_centerMap": "Zentralkarte", + "map_zoomIn": "Vergrößern", + "routing_deliveryCounts": "{successes} zugestellt, {failures} fehlgeschlagen", + "map_zoomOut": "Verkleinern", + "map_centerMap": "Karte zentrieren", "chrome_bluetoothRequiresChromium": "Web Bluetooth benötigt einen Chromium-Browser.", "channels_communityShortId": "ID: {id}…", - "pathTrace_legendGpsConfirmed": "GPS-Bestätigung", - "pathTrace_legendInferred": "Abgeleitete Position" + "pathTrace_legendGpsConfirmed": "GPS bestätigt", + "pathTrace_legendInferred": "Abgeleitete Position", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_activity": "Aktivität", + "scanner_bluetoothWebUnsupported": "Bluetooth ist im Browser nicht verfügbar. Verwenden Sie stattdessen eine USB-Verbindung.", + "map_searchHint": "Nach Knotenname oder ID suchen", + "map_online": "Online", + "map_recent": "Kürzlich", + "map_stale": "Veraltet", + "map_hidden": "Versteckt", + "map_visible": "Sichtbar", + "map_centerOnNode": "Auf Knoten zentrieren", + "map_details": "Details", + "map_noGps": "Kein GPS", + "map_noResults": "Keine passenden Knoten gefunden", + "pathMap_viewSingle": "Einzeln", + "pathMap_viewCombined": "Kombiniert", + "pathMap_play": "Abspielen", + "pathMap_pause": "Pause", + "pathMap_replay": "Erneut abspielen", + "pathMap_stepBack": "Vorheriger Sprung", + "pathMap_stepForward": "Nächster Sprung", + "pathMap_animationOn": "Paketanimation anzeigen", + "pathMap_animationOff": "Paketanimation ausblenden", + "pathMap_hopOf": "{current} von {total}", + "pathMap_observedPaths": "Beobachtete Pfade: {count}", + "pathMap_primary": "Primär", + "pathMap_alternate": "Alternative {index}", + "pathMap_hopCount": "{count, plural, =1{1 Sprung} other{{count} Sprünge}}", + "pathMap_legendShared": "Gemeinsamer Abschnitt", + "pathMap_legendEstimated": "Geschätzter Abschnitt", + "pathMap_sharedNodeCount": "Verwendet von {count} Pfaden", + "pathMap_partialAnimation": "{count, plural, =1{1 Sprung hat keinen Standort - der angezeigte Pfad ist unvollständig} other{{count} Sprünge haben keinen Standort - der angezeigte Pfad ist unvollständig}}", + "pathMap_showAllPaths": "Alle anzeigen", + "pathMap_hidePath": "Pfad ausblenden", + "pathMap_showPath": "Pfad anzeigen", + "pathMap_collapsePanel": "Panel einklappen", + "pathMap_expandPanel": "Panel ausklappen", + "pathMap_noLocation": "Keine Standortdaten", + "pathMap_followPacket": "Ansicht auf Paket fixieren", + "pathMap_unfollowPacket": "Fixierung aufheben", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 31b6dc7d..303f8cb5 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -28,7 +28,7 @@ "common_continue": "Continuar", "common_share": "Compartir", "common_copy": "Copiar", - "common_retry": "Intentar", + "common_retry": "Reintentar", "common_hide": "Ocultar", "common_remove": "Eliminar", "common_enable": "Activar", @@ -54,11 +54,11 @@ } } }, - "scanner_title": "MeshCore: Versión abierta", + "scanner_title": "MeshCore Open", "scanner_scanning": "Escaneando dispositivos...", "scanner_connecting": "Conectando...", "scanner_disconnecting": "Desconectando...", - "scanner_notConnected": "No está conectado", + "scanner_notConnected": "No conectado", "scanner_connectedTo": "Conectado a {deviceName}", "@scanner_connectedTo": { "placeholders": { @@ -68,7 +68,7 @@ } }, "scanner_searchingDevices": "Buscando dispositivos MeshCore...", - "scanner_tapToScan": "Toca Escanear para encontrar dispositivos MeshCore", + "scanner_tapToScan": "Pulsa Escanear para encontrar dispositivos MeshCore", "scanner_connectionFailed": "Error de conexión: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -78,7 +78,7 @@ } }, "scanner_stop": "Detener", - "scanner_scan": "Escanea", + "scanner_scan": "Escanear", "device_quickSwitch": "Cambiar rápidamente", "device_meshcore": "MeshCore", "settings_title": "Configuración", @@ -100,23 +100,23 @@ "settings_locationInvalid": "Latitud o longitud inválidos.", "settings_latitude": "Latitud", "settings_longitude": "Longitud", - "settings_privacyMode": "Modo Privacidad", + "settings_privacyMode": "Modo de privacidad", "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", "settings_privacyModeEnabled": "Modo de privacidad activado", "settings_privacyModeDisabled": "Modo de privacidad desactivado", "settings_actions": "Acciones", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", - "settings_sendAdvertisement": "Enviar Anuncio", - "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", + "settings_deleteAllPaths": "Eliminar todas las rutas", + "settings_deleteAllPathsSubtitle": "Borrar todos los datos de ruta de los contactos.", + "settings_sendAdvertisement": "Enviar anuncio", + "settings_sendAdvertisementSubtitle": "Difundir la presencia ahora", "settings_advertisementSent": "Anuncio enviado", - "settings_syncTime": "Tiempo de Sincronización", - "settings_syncTimeSubtitle": "Establecer la hora del dispositivo al tiempo del teléfono", - "settings_timeSynchronized": "Sincronizado en el tiempo", - "settings_refreshContacts": "Actualizar Contactos", + "settings_syncTime": "Sincronizar hora", + "settings_syncTimeSubtitle": "Establecer la hora del dispositivo con la del teléfono", + "settings_timeSynchronized": "Hora sincronizada", + "settings_refreshContacts": "Actualizar contactos", "settings_refreshContactsSubtitle": "Recargar lista de contactos del dispositivo", - "settings_rebootDevice": "Reiniciar Dispositivo", + "settings_rebootDevice": "Reiniciar dispositivo", "settings_rebootDeviceSubtitle": "Reiniciar el dispositivo MeshCore", "settings_rebootDeviceConfirm": "¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.", "settings_debug": "Depurar", @@ -125,7 +125,7 @@ "settings_appDebugLog": "Registro de Depuración de la App", "settings_appDebugLogSubtitle": "Mensajes de depuración de la aplicación", "settings_about": "Acerca de", - "settings_aboutVersion": "MeshCore Open versión {version}", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -133,13 +133,13 @@ } } }, - "settings_aboutLegalese": "2026 Proyecto Open Source MeshCore", - "settings_aboutDescription": "Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.", + "settings_aboutLegalese": "Proyecto MeshCore Open Source 2026", + "settings_aboutDescription": "Un cliente Flutter de código abierto para dispositivos MeshCore de malla LoRa.", "settings_infoName": "Nombre", "settings_infoId": "ID", "settings_infoStatus": "Estado", "settings_infoBattery": "Batería", - "settings_infoPublicKey": "Clave Pública", + "settings_infoPublicKey": "Clave pública", "settings_infoContactsCount": "Número de contactos", "settings_infoChannelCount": "Número de canales", "settings_presets": "Preajustes", @@ -148,8 +148,8 @@ "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", "settings_bandwidth": "Ancho de banda", "settings_spreadingFactor": "Factor de propagación", - "settings_codingRate": "Tasa de Programación", - "settings_txPower": "TX Potencia (dBm)", + "settings_codingRate": "Tasa de codificación", + "settings_txPower": "Potencia TX (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", "settings_error": "Error: {message}", @@ -164,7 +164,7 @@ "appSettings_appearance": "Apariencia", "appSettings_theme": "Tema", "appSettings_themeSystem": "Valor predeterminado del sistema", - "appSettings_themeLight": "Luz", + "appSettings_themeLight": "Claro", "appSettings_themeDark": "Oscuro", "appSettings_language": "Idioma", "appSettings_languageSystem": "Predeterminado del sistema", @@ -179,32 +179,32 @@ "appSettings_languageZh": "Chino", "appSettings_languageSv": "Sueco", "appSettings_languageNl": "Neerlandés", - "appSettings_languageSk": "Esloveno", - "appSettings_languageBg": "Bulgaro", + "appSettings_languageSk": "Eslovaco", + "appSettings_languageBg": "Búlgaro", "appSettings_notifications": "Notificaciones", - "appSettings_enableNotifications": "Habilitar Notificaciones", + "appSettings_enableNotifications": "Habilitar notificaciones", "appSettings_enableNotificationsSubtitle": "Recibir notificaciones para mensajes y anuncios", "appSettings_notificationPermissionDenied": "Permiso de notificación denegado", "appSettings_notificationsEnabled": "Notificaciones activadas", "appSettings_notificationsDisabled": "Notificaciones desactivadas", - "appSettings_messageNotifications": "Notificaciones de Mensaje", + "appSettings_messageNotifications": "Notificaciones de mensajes", "appSettings_messageNotificationsSubtitle": "Mostrar notificación al recibir nuevos mensajes", - "appSettings_channelMessageNotifications": "Notificaciones de Mensajes del Canal", + "appSettings_channelMessageNotifications": "Notificaciones de mensajes del canal", "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificación al recibir mensajes del canal", - "appSettings_advertisementNotifications": "Notificaciones de Anuncios", + "appSettings_advertisementNotifications": "Notificaciones de anuncios", "appSettings_advertisementNotificationsSubtitle": "Mostrar notificación cuando se descubren nuevos nodos", "appSettings_messaging": "Mensajería", - "appSettings_clearPathOnMaxRetry": "Borrar Camino en Max Reintentos", + "appSettings_clearPathOnMaxRetry": "Borrar ruta al máximo de reintentos", "appSettings_clearPathOnMaxRetrySubtitle": "Restablecer la ruta de contacto después de 5 intentos de envío fallidos", - "appSettings_pathsWillBeCleared": "Los caminos se limpiarán después de 5 intentos fallidos.", + "appSettings_pathsWillBeCleared": "Las rutas se borrarán después de 5 intentos fallidos.", "appSettings_pathsWillNotBeCleared": "Las rutas no se eliminarán automáticamente.", - "appSettings_autoRouteRotation": "Rotación de Ruta Automática", + "appSettings_autoRouteRotation": "Rotación automática de rutas", "appSettings_autoRouteRotationSubtitle": "Alternar entre las mejores rutas y el modo inundación", "appSettings_autoRouteRotationEnabled": "Rotación de ruta automática habilitada", "appSettings_autoRouteRotationDisabled": "Rotación de ruta automática desactivada", "appSettings_battery": "Batería", "appSettings_batteryChemistry": "Química de la batería", - "appSettings_batteryChemistryPerDevice": "Configuración por dispositivo ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Configurar por dispositivo ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -212,18 +212,18 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Conéctate a un dispositivo para elegir", + "appSettings_batteryChemistryConnectFirst": "Conéctate a un dispositivo para elegirlo", "appSettings_batteryNmc": "18650 NMC (3,0-4,2 V)", - "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65 V)", - "appSettings_batteryLipo": "LiPo (3.0-4.2V)", - "appSettings_mapDisplay": "Visualización del Mapa", - "appSettings_showRepeaters": "Mostrar Repetidores", + "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65 V)", + "appSettings_batteryLipo": "LiPo (3,0-4,2 V)", + "appSettings_mapDisplay": "Visualización del mapa", + "appSettings_showRepeaters": "Mostrar repetidores", "appSettings_showRepeatersSubtitle": "Mostrar nodos de repetidor en el mapa", - "appSettings_showChatNodes": "Mostrar Nodos de Chat", + "appSettings_showChatNodes": "Mostrar nodos de chat", "appSettings_showChatNodesSubtitle": "Mostrar nodos de chat en el mapa", "appSettings_showOtherNodes": "Mostrar otros nodos", "appSettings_showOtherNodesSubtitle": "Mostrar otros tipos de nodo en el mapa", - "appSettings_timeFilter": "Filtro de Tiempo", + "appSettings_timeFilter": "Filtro de tiempo", "appSettings_timeFilterShowAll": "Mostrar todos los nodos", "appSettings_timeFilterShowLast": "Mostrar nodos de las últimas {hours} horas", "@appSettings_timeFilterShowLast": { @@ -233,14 +233,14 @@ } } }, - "appSettings_mapTimeFilter": "Filtro de Tiempo del Mapa", + "appSettings_mapTimeFilter": "Filtro de tiempo del mapa", "appSettings_showNodesDiscoveredWithin": "Mostrar nodos descubiertos dentro de:", "appSettings_allTime": "Todo el tiempo", "appSettings_lastHour": "Última hora", "appSettings_last6Hours": "Últimas 6 horas", "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "La semana pasada", - "appSettings_offlineMapCache": "Caché de Mapa Offline", + "appSettings_offlineMapCache": "Caché de mapa sin conexión", "appSettings_noAreaSelected": "No se ha seleccionado ningún área", "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { @@ -255,17 +255,27 @@ }, "appSettings_debugCard": "Depurar", "appSettings_appDebugLogging": "Registro de Depuración de la App", - "appSettings_appDebugLoggingSubtitle": "Registrar mensajes de depuración de la app de registro para solucionar problemas", + "appSettings_appDebugLoggingSubtitle": "Registrar mensajes de depuración de la app para solucionar problemas", "appSettings_appDebugLoggingEnabled": "Registro de depuración de la aplicación habilitado", "appSettings_appDebugLoggingDisabled": "El registro de depuración de la aplicación está desactivado", "contacts_title": "Contactos", "contacts_noContacts": "Aún no hay contactos.", "contacts_contactsWillAppear": "Los contactos aparecerán cuando los dispositivos anuncien.", - "contacts_searchContacts": "Buscar contactos...", - "contacts_noUnreadContacts": "No contactos sin leer", + "contacts_searchContacts": "Buscar {number}{str} contactos...", + "@contacts_searchContacts": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_noUnreadContacts": "No hay contactos sin leer", "contacts_noContactsFound": "No se encontraron contactos ni grupos.", - "contacts_deleteContact": "Eliminar Contacto", - "contacts_removeConfirm": "Eliminar {contactName} de los contactos?", + "contacts_deleteContact": "Eliminar contacto", + "contacts_removeConfirm": "¿Eliminar {contactName} de los contactos?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -273,11 +283,11 @@ } } }, - "contacts_manageRepeater": "Gestionar Repetidor", - "contacts_roomLogin": "Inicio de Sala", - "contacts_openChat": "Abrir Chat", - "contacts_editGroup": "Editar Grupo", - "contacts_deleteGroup": "Eliminar Grupo", + "contacts_manageRepeater": "Gestionar repetidor", + "contacts_roomLogin": "Inicio de sesión en sala", + "contacts_openChat": "Abrir chat", + "contacts_editGroup": "Editar grupo", + "contacts_deleteGroup": "Eliminar grupo", "contacts_deleteGroupConfirm": "Eliminar {groupName}?", "@contacts_deleteGroupConfirm": { "placeholders": { @@ -396,6 +406,11 @@ "channels_cyr2latSettingsDscr": "Editar la configuración JSON de sustitución de caracteres", "channels_cyr2latSettingsDialogHint": "Mapa JSON de sustituciones", "channels_cyr2latSettingsDialogWrongJSON": "JSON incorrecto: {error}", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, "settings_cyr2latProfileAdd": "Añadir perfil Cyr2Lat", "settings_cyr2latProfileName": "Nombre del perfil", "settings_cyr2latProfileNameEmpty": "El nombre del perfil no puede estar vacío", @@ -405,6 +420,13 @@ "settings_cyr2latProfileDelete": "Eliminar perfil Cyr2Lat", "settings_cyr2latProfileDeleted": "Perfil eliminado correctamente", "settings_cyr2latProfileDeleteDscr": "¿Está seguro de que desea eliminar el perfil \"{name}\"?", + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelUpdated": "Canal \"{name}\" actualizado", "@channels_channelUpdated": { "placeholders": { @@ -494,7 +516,7 @@ "debugLog_noEntries": "Aún no hay registros de depuración.", "debugLog_enableInSettings": "Habilitar el registro de depuración de la aplicación en la configuración", "debugLog_frames": "Marcos", - "debugLog_rawLogRx": "Registro Crudo-RX", + "debugLog_rawLogRx": "Registro bruto RX", "debugLog_noBleActivity": "Aún no hay actividad BLE", "debugFrame_length": "Longitud del Marco: {count} bytes", "@debugFrame_length": { @@ -548,8 +570,8 @@ } } }, - "debugFrame_textTypeCli": "Interfaz de línea de comandos", - "debugFrame_textTypePlain": "Sencillo", + "debugFrame_textTypeCli": "CLI", + "debugFrame_textTypePlain": "Plano", "debugFrame_text": "- Texto: \"{text}\"", "@debugFrame_text": { "placeholders": { @@ -558,15 +580,15 @@ } } }, - "debugFrame_hexDump": "Mapeo Hexadecimal:", - "chat_pathManagement": "Gestión de Rutas", + "debugFrame_hexDump": "Volcado hexadecimal:", + "chat_pathManagement": "Gestión de rutas", "chat_routingMode": "Modo de enrutamiento", "chat_autoUseSavedPath": "Auto (usar la ruta guardada)", - "chat_forceFloodMode": "Modo Inundación Forzado", - "chat_recentAckPaths": "Rutas de ACK Recientes (tocar para usar):", - "chat_pathHistoryFull": "El historial de rutas está completo. Eliminar entradas para añadir nuevas.", - "chat_hopSingular": "salta", - "chat_hopPlural": "salta", + "chat_forceFloodMode": "Modo inundación forzado", + "chat_recentAckPaths": "Rutas recientes de ACK (toca para usar):", + "chat_pathHistoryFull": "El historial de rutas está completo. Elimina entradas para añadir nuevas.", + "chat_hopSingular": "salto", + "chat_hopPlural": "saltos", "chat_hopsCount": "{count} {count, plural, =1{salto} other{saltos}}", "@chat_hopsCount": { "placeholders": { @@ -578,17 +600,17 @@ "chat_successes": "Éxitos", "chat_removePath": "Eliminar ruta", "chat_noPathHistoryYet": "Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.", - "chat_pathActions": "Acciones de Ruta:", - "chat_setCustomPath": "Establecer Ruta Personalizada", + "chat_pathActions": "Acciones de ruta:", + "chat_setCustomPath": "Establecer ruta personalizada", "chat_setCustomPathSubtitle": "Especificar manualmente la ruta de enrutamiento", - "chat_clearPath": "Limpiar Ruta", + "chat_clearPath": "Borrar ruta", "chat_clearPathSubtitle": "Forzar redescubrimiento en el próximo envío", - "chat_pathCleared": "Ruta eliminada. El siguiente mensaje redescubrirá la ruta.", - "chat_floodModeSubtitle": "Utilizar el interruptor de enrutamiento en la barra de herramientas", - "chat_floodModeEnabled": "El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.", + "chat_pathCleared": "Ruta borrada. El siguiente mensaje redescubrirá la ruta.", + "chat_floodModeSubtitle": "Usar el selector de enrutamiento en la barra de herramientas", + "chat_floodModeEnabled": "El modo de inundación está activado. Desactívalo con el icono de enrutamiento de la barra de herramientas de la app.", "chat_fullPath": "Ruta completa", "chat_pathDetailsNotAvailable": "Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.", - "chat_pathSetHops": "Ruta establecida: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathSetHops": "Ruta establecida: {hopCount} {hopCount, plural, =1{salto} other{saltos}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -602,9 +624,9 @@ "chat_pathSavedLocally": "Guardado localmente. Conéctate para sincronizar.", "chat_pathDeviceConfirmed": "Dispositivo confirmado.", "chat_pathDeviceNotConfirmed": "Dispositivo aún no confirmado.", - "chat_type": "Escribe", + "chat_type": "Tipo", "chat_path": "Ruta", - "chat_publicKey": "Clave Pública", + "chat_publicKey": "Clave pública", "chat_compressOutgoingMessages": "Comprimir mensajes salientes", "chat_floodForced": "Inundación (forzada)", "chat_directForced": "Directo (forzado)", @@ -617,8 +639,8 @@ } }, "chat_floodAuto": "Inundación (automática)", - "chat_direct": "Guardar", - "chat_poiShared": "Punto de Interés Compartido", + "chat_direct": "Directo", + "chat_poiShared": "Punto de interés compartido", "chat_unread": "Sin leer: {count}", "@chat_unread": { "placeholders": { @@ -628,7 +650,7 @@ } }, "chat_openLink": "¿Abrir enlace?", - "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", + "chat_openLinkConfirmation": "¿Quieres abrir este enlace en tu navegador?", "chat_open": "Abrir", "chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}", "@chat_couldNotOpenLink": { @@ -639,7 +661,7 @@ } }, "chat_invalidLink": "Formato de enlace no válido", - "map_title": "Mapa de Nodos", + "map_title": "Mapa de nodos", "map_noNodesWithLocation": "No hay nodos con datos de ubicación", "map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa", "map_nodesCount": "Nodos: {count}", @@ -650,7 +672,7 @@ } } }, - "map_pinsCount": "Ganchos: {count}", + "map_pinsCount": "Pines: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -660,25 +682,25 @@ }, "map_chat": "Chat", "map_repeater": "Repetidor", - "map_room": "Habitación", + "map_room": "Sala", "map_sensor": "Sensor", - "map_pinDm": "Etiqueta (DM)", - "map_pinPrivate": "Bloqueo (Privado)", - "map_pinPublic": "Clave (Pública)", - "map_lastSeen": "Última vez que se vio", + "map_pinDm": "Pin (DM)", + "map_pinPrivate": "Pin (privado)", + "map_pinPublic": "Pin (público)", + "map_lastSeen": "Última vez visto", "map_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", "map_from": "De", "map_source": "Fuente", "map_flags": "Banderas", "map_shareMarkerHere": "Compartir marcador aquí", - "map_pinLabel": "Etiqueta de marcador", + "map_pinLabel": "Etiqueta del pin", "map_label": "Etiqueta", "map_pointOfInterest": "Punto de interés", "map_sendToContact": "Enviar a contacto", "map_sendToChannel": "Enviar a canal", "map_noChannelsAvailable": "No hay canales disponibles", "map_publicLocationShare": "Compartir ubicación pública", - "map_publicLocationShareConfirm": "Estás a punto de compartir una ubicación en {channelLabel}. Este canal es público y cualquiera con la PSK puede verlo.", + "map_publicLocationShareConfirm": "Estás a punto de compartir una ubicación en {channelLabel}. Este canal es público y cualquiera con la PSK puede verla.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -687,25 +709,25 @@ } }, "map_connectToShareMarkers": "Conéctate a un dispositivo para compartir marcadores", - "map_filterNodes": "Filtrar Nodos", + "map_filterNodes": "Filtrar nodos", "map_nodeTypes": "Tipos de nodo", - "map_chatNodes": "Nodos de Chat", + "map_chatNodes": "Nodos de chat", "map_repeaters": "Repetidores", - "map_otherNodes": "Otros Nodos", + "map_otherNodes": "Otros nodos", "map_keyPrefix": "Prefijo de clave", "map_filterByKeyPrefix": "Filtrar por prefijo clave", "map_publicKeyPrefix": "Prefijo de clave pública", "map_markers": "Marcadores", "map_showSharedMarkers": "Mostrar marcadores compartidos", - "map_lastSeenTime": "Última vez que se vio", + "map_lastSeenTime": "Hora de última vez visto", "map_sharedPin": "Pin compartido", - "map_joinRoom": "Únete a la sala", - "map_manageRepeater": "Gestionar Repetidor", - "mapCache_title": "Caché de Mapa Offline", - "mapCache_selectAreaFirst": "Seleccionar un área para cachear primero", - "mapCache_noTilesToDownload": "No hay azulejos para descargar para este área.", - "mapCache_downloadTilesTitle": "Descargar ficheros", - "mapCache_downloadTilesPrompt": "Descargar {count} ficheros para usar sin conexión?", + "map_joinRoom": "Unirse a la sala", + "map_manageRepeater": "Gestionar repetidor", + "mapCache_title": "Caché de mapa sin conexión", + "mapCache_selectAreaFirst": "Selecciona primero un área para almacenar en caché", + "mapCache_noTilesToDownload": "No hay teselas para descargar para esta área.", + "mapCache_downloadTilesTitle": "Descargar teselas", + "mapCache_downloadTilesPrompt": "¿Descargar {count} teselas para uso sin conexión?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -714,7 +736,7 @@ } }, "mapCache_downloadAction": "Descargar", - "mapCache_cachedTiles": "Almacenados {count} azulejos", + "mapCache_cachedTiles": "Teselas almacenadas: {count}", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -722,7 +744,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Archivados {downloaded} azulejos ({failed} fallidos)", + "mapCache_cachedTilesWithFailed": "Teselas almacenadas: {downloaded} ({failed} fallidas)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -733,14 +755,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Borrar caché offline", - "mapCache_clearOfflineCachePrompt": "Eliminar todas las baldosas en caché del mapa?", - "mapCache_offlineCacheCleared": "Almacén en caché sin conexión eliminado", + "mapCache_clearOfflineCacheTitle": "Borrar caché sin conexión", + "mapCache_clearOfflineCachePrompt": "¿Eliminar todas las teselas del mapa almacenadas en caché?", + "mapCache_offlineCacheCleared": "Caché sin conexión borrada", "mapCache_noAreaSelected": "No se ha seleccionado ningún área", - "mapCache_cacheArea": "Área de Caché", - "mapCache_useCurrentView": "Usar Vista Actual", - "mapCache_zoomRange": "Rango de Zoom", - "mapCache_estimatedTiles": "Tiles estimados: {count}", + "mapCache_cacheArea": "Área de caché", + "mapCache_useCurrentView": "Usar vista actual", + "mapCache_zoomRange": "Rango de zoom", + "mapCache_estimatedTiles": "Teselas estimadas: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -759,8 +781,8 @@ } } }, - "mapCache_downloadTilesButton": "Descargar Mosaicos", - "mapCache_clearCacheButton": "Borrar Caché", + "mapCache_downloadTilesButton": "Descargar teselas", + "mapCache_clearCacheButton": "Borrar caché", "mapCache_failedDownloads": "Descargas fallidas: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -786,8 +808,8 @@ } } }, - "time_justNow": "Hace un momento", - "time_minutesAgo": "{minutes} minutos hace.", + "time_justNow": "Justo ahora", + "time_minutesAgo": "hace {minutes} min.", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -795,7 +817,7 @@ } } }, - "time_hoursAgo": "{hours}h hace", + "time_hoursAgo": "hace {hours} h", "@time_hoursAgo": { "placeholders": { "hours": { @@ -803,7 +825,7 @@ } } }, - "time_daysAgo": "{days} días hace", + "time_daysAgo": "hace {days} días", "@time_daysAgo": { "placeholders": { "days": { @@ -820,24 +842,24 @@ "time_month": "mes", "time_months": "meses", "time_minutes": "minutos", - "time_allTime": "Todas las veces", + "time_allTime": "Todo el tiempo", "dialog_disconnect": "Desconectar", "dialog_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", - "login_repeaterLogin": "Iniciar sesión en el Repetidor", - "login_roomLogin": "Inicio de Sala", + "login_repeaterLogin": "Inicio de sesión del repetidor", + "login_roomLogin": "Inicio de sesión en la sala", "login_password": "Contraseña", - "login_enterPassword": "Introducir contraseña", + "login_enterPassword": "Introduce la contraseña", "login_savePassword": "Guardar contraseña", "login_savePasswordSubtitle": "La contraseña se almacenará de forma segura en este dispositivo.", - "login_repeaterDescription": "Ingrese la contraseña del repetidor para acceder a la configuración y el estado.", - "login_roomDescription": "Ingrese la contraseña de la sala para acceder a la configuración y el estado.", + "login_repeaterDescription": "Introduce la contraseña del repetidor para acceder como invitado o administrador.", + "login_roomDescription": "Introduce la contraseña de la sala para acceder como invitado o administrador.", "login_routing": "Enrutamiento", "login_routingMode": "Modo de enrutamiento", "login_autoUseSavedPath": "Auto (usar la ruta guardada)", - "login_forceFloodMode": "Activar Modo Inundación Forzada", - "login_managePaths": "Gestionar Rutas", + "login_forceFloodMode": "Forzar modo inundación", + "login_managePaths": "Gestionar rutas", "login_login": "Iniciar sesión", - "login_attempt": "Intentar {current}/{max}", + "login_attempt": "Intento {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -848,7 +870,7 @@ } } }, - "login_failed": "Inicio fallido: {error}", + "login_failed": "Error de inicio de sesión: {error}", "@login_failed": { "placeholders": { "error": { @@ -856,7 +878,7 @@ } } }, - "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", + "login_failedMessage": "El inicio de sesión ha fallado. La contraseña es incorrecta o el repetidor no está disponible.", "common_reload": "Recargar", "common_clear": "Borrar", "path_currentPath": "Ruta actual: {path}", @@ -867,7 +889,7 @@ } } }, - "path_usingHopsPath": "Usando {count} {count, plural, =1{hop} other{hops}} ruta", + "path_usingHopsPath": "Usando {count} {count, plural, =1{salto} other{saltos}} de ruta", "@path_usingHopsPath": { "placeholders": { "count": { @@ -875,15 +897,15 @@ } } }, - "path_enterCustomPath": "Introducir Ruta Personalizada", + "path_enterCustomPath": "Introducir ruta personalizada", "path_currentPathLabel": "Ruta actual", "path_hexPrefixInstructions": "Introduzca los prefijos hexadecimales de 2 caracteres para cada salto, separados por comas.", - "path_hexPrefixExample": "Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).", + "path_hexPrefixExample": "Ejemplo: A1,F2,3C (cada nodo usa el primer byte de su clave pública).", "path_labelHexPrefixes": "Prefijos hexadecimales", "path_helperMaxHops": "Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).", - "path_selectFromContacts": "O seleccionar de contactos:", + "path_selectFromContacts": "O seleccionar de los contactos:", "path_noRepeatersFound": "No se encontraron repetidores ni servidores de sala.", - "path_customPathsRequire": "Las rutas personalizadas requieren saltos intermedios que pueden transmitir mensajes.", + "path_customPathsRequire": "Las rutas personalizadas requieren saltos intermedios que puedan transmitir mensajes.", "path_invalidHexPrefixes": "Prefijos hexadecimales inválidos: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { @@ -893,24 +915,24 @@ } }, "path_tooLong": "La ruta es demasiado larga. Se permiten un máximo de 64 saltos.", - "path_setPath": "Establecer Ruta", - "repeater_management": "Gestión de Repetidores", - "repeater_managementTools": "Herramientas de Gestión", + "path_setPath": "Establecer ruta", + "repeater_management": "Gestión de repetidores", + "repeater_managementTools": "Herramientas de gestión", "repeater_status": "Estado", "repeater_statusSubtitle": "Ver el estado, las estadísticas y los vecinos del repetidor", "repeater_telemetry": "Telemetría", "repeater_telemetrySubtitle": "Ver la telemetría de los sensores y las estadísticas del sistema", - "repeater_cli": "Interfaz de línea de comandos", + "repeater_cli": "CLI", "repeater_cliSubtitle": "Enviar comandos al repetidor", "repeater_settings": "Configuración", "repeater_settingsSubtitle": "Configurar parámetros del repetidor", "repeater_statusTitle": "Estado del Repetidor", "repeater_routingMode": "Modo de enrutamiento", "repeater_autoUseSavedPath": "Auto (usar la ruta guardada)", - "repeater_forceFloodMode": "Modo Inundación Forzado", + "repeater_forceFloodMode": "Modo inundación forzado", "repeater_pathManagement": "Gestión de rutas", "repeater_refresh": "Actualizar", - "repeater_statusRequestTimeout": "Solicitud de estado caducó.", + "repeater_statusRequestTimeout": "Se agotó el tiempo de espera de la solicitud de estado.", "repeater_errorLoadingStatus": "Error al cargar el estado: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -924,18 +946,18 @@ "repeater_clockAtLogin": "Reloj (al inicio de sesión)", "repeater_uptime": "Tiempo de actividad", "repeater_queueLength": "Longitud de la cola", - "repeater_debugFlags": "Marcadores de Depuración", - "repeater_radioStatistics": "Estadísticas de Radio", + "repeater_debugFlags": "Banderas de depuración", + "repeater_radioStatistics": "Estadísticas de radio", "repeater_lastRssi": "Último RSSI", "repeater_lastSnr": "Último SNR", - "repeater_noiseFloor": "Nivel de Ruido", - "repeater_txAirtime": "TX Airtime", - "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Estadísticas del Paquete", - "repeater_sent": "Enviado", - "repeater_received": "Recibido", + "repeater_noiseFloor": "Nivel de ruido", + "repeater_txAirtime": "Tiempo de aire TX", + "repeater_rxAirtime": "Tiempo de aire RX", + "repeater_packetStatistics": "Estadísticas de paquetes", + "repeater_sent": "Enviados", + "repeater_received": "Recibidos", "repeater_duplicates": "Duplicados", - "repeater_daysHoursMinsSecs": "{days} días {hours}h {minutes}m {seconds}s", + "repeater_daysHoursMinsSecs": "{days} días {hours}h {minutes} min {seconds} s", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -999,36 +1021,36 @@ } } }, - "repeater_settingsTitle": "Configuración del Repetidor", - "repeater_basicSettings": "Configuración Básica", - "repeater_repeaterName": "Nombre del Repetidor", - "repeater_repeaterNameHelper": "Mostrar nombre para este repetidor", - "repeater_adminPassword": "Contraseña de Administrador", + "repeater_settingsTitle": "Ajustes del repetidor", + "repeater_basicSettings": "Ajustes básicos", + "repeater_repeaterName": "Nombre del repetidor", + "repeater_repeaterNameHelper": "Nombre visible para este repetidor", + "repeater_adminPassword": "Contraseña de administrador", "repeater_adminPasswordHelper": "Contraseña de acceso completo", "repeater_guestPassword": "Contraseña de invitado", - "repeater_guestPasswordHelper": "Acceso de solo lectura con contraseña", - "repeater_radioSettings": "Configuración de Radio", + "repeater_guestPasswordHelper": "Contraseña de acceso de solo lectura", + "repeater_radioSettings": "Ajustes de radio", "repeater_frequencyMhz": "Frecuencia (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Potencia", + "repeater_txPower": "Potencia TX", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Ancho de banda", "repeater_spreadingFactor": "Factor de propagación", - "repeater_codingRate": "Tasa de Programación", - "repeater_locationSettings": "Configuración de Ubicación", + "repeater_codingRate": "Tasa de codificación", + "repeater_locationSettings": "Ajustes de ubicación", "repeater_latitude": "Latitud", "repeater_latitudeHelper": "Grados decimales (por ejemplo, 37.7749)", "repeater_longitude": "Longitud", "repeater_longitudeHelper": "Grados decimales (por ejemplo, -122.4194)", - "repeater_features": "Características", - "repeater_packetForwarding": "Enrutamiento de Paquetes", - "repeater_packetForwardingSubtitle": "Habilitar el repetidor para reenviar paquetes", - "repeater_guestAccess": "Acceso de Invitados", - "repeater_guestAccessSubtitle": "Permitir acceso de invitado en solo lectura", - "repeater_privacyMode": "Modo Privacidad", + "repeater_features": "Funciones", + "repeater_packetForwarding": "Reenvío de paquetes", + "repeater_packetForwardingSubtitle": "Permitir que el repetidor reenvíe paquetes", + "repeater_guestAccess": "Acceso de invitado", + "repeater_guestAccessSubtitle": "Permitir acceso de invitado de solo lectura", + "repeater_privacyMode": "Modo de privacidad", "repeater_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", - "repeater_advertisementSettings": "Configuración de Anuncios", - "repeater_localAdvertInterval": "Intervalo de Anuncio Local", + "repeater_advertisementSettings": "Ajustes de anuncios", + "repeater_localAdvertInterval": "Intervalo de anuncio local", "repeater_localAdvertIntervalMinutes": "{minutes} minutos", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1037,7 +1059,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalo de Anuncio de Inundación", + "repeater_floodAdvertInterval": "Intervalo de anuncio por inundación", "repeater_floodAdvertIntervalHours": "{hours} horas", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1046,18 +1068,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervalo de Anuncio Cifrado", - "repeater_dangerZone": "Zona de Peligro", - "repeater_rebootRepeater": "Reiniciar Repetidor", + "repeater_encryptedAdvertInterval": "Intervalo de anuncio cifrado", + "repeater_dangerZone": "Zona de peligro", + "repeater_rebootRepeater": "Reiniciar repetidor", "repeater_rebootRepeaterSubtitle": "Reiniciar el dispositivo repetidor", "repeater_rebootRepeaterConfirm": "¿Está seguro de que desea reiniciar este repetidor?", - "repeater_regenerateIdentityKey": "Regenerar Clave de Identidad", - "repeater_regenerateIdentityKeySubtitle": "Generar nueva pareja de clave pública/privada", - "repeater_regenerateIdentityKeyConfirm": "Esto generará una nueva identidad para el repetidor. Continuar?", - "repeater_eraseFileSystem": "Borrar Sistema de Archivos", + "repeater_regenerateIdentityKey": "Regenerar clave de identidad", + "repeater_regenerateIdentityKeySubtitle": "Generar una nueva pareja de claves pública/privada", + "repeater_regenerateIdentityKeyConfirm": "Esto generará una nueva identidad para el repetidor. ¿Continuar?", + "repeater_eraseFileSystem": "Borrar sistema de archivos", "repeater_eraseFileSystemSubtitle": "Formatear el sistema de archivos del repetidor", "repeater_eraseFileSystemConfirm": "ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!", - "repeater_eraseSerialOnly": "Borrar solo está disponible a través de la consola serial.", + "repeater_eraseSerialOnly": "Borrar solo está disponible a través de la consola serie.", "repeater_commandSent": "Comando enviado: {command}", "@repeater_commandSent": { "placeholders": { @@ -1075,22 +1097,22 @@ } }, "repeater_confirm": "Confirmar", - "repeater_settingsSaved": "Guardado de ajustes exitoso", - "repeater_rxGain": "Aumento en la ganancia de RX", + "repeater_settingsSaved": "Ajustes guardados correctamente", + "repeater_rxGain": "Ganancia RX", "repeater_rxGainHelper": "Mayor sensibilidad, mayor consumo de corriente (solo para SX1262/SX1268)", - "repeater_refreshRxGain": "Aumenta el rendimiento de RX con la nueva versión.", - "repeater_multiAcks": "Múltiples respuestas de confirmación", - "repeater_multiAcksSubtitle": "Reconocer mensajes a través de múltiples vías para una mejor entrega.", - "repeater_refreshMultiAcks": "Reenviar múltiples confirmaciones", + "repeater_refreshRxGain": "Mejora el rendimiento de RX con la nueva versión.", + "repeater_multiAcks": "ACK múltiples", + "repeater_multiAcksSubtitle": "Reconocer mensajes a través de múltiples rutas para una mejor entrega.", + "repeater_refreshMultiAcks": "Actualizar ACK múltiples", "repeater_networkHealth": "Salud de la red", "repeater_loopDetect": "Detección de bucles", "repeater_loopDetectHelper": "Crea paquetes de \"flujo\" que parezcan bucles de enrutamiento.", - "repeater_loopDetectOff": "Fuera", + "repeater_loopDetectOff": "Desactivado", "repeater_loopDetectMinimal": "Mínimo", "repeater_loopDetectModerate": "Moderado", - "repeater_loopDetectStrict": "Estrictos", + "repeater_loopDetectStrict": "Estricto", "repeater_dutyCycle": "Ciclo de trabajo", - "repeater_dutyCycleHelper": "Porcentaje máximo de tiempo de antena", + "repeater_dutyCycleHelper": "Porcentaje máximo de tiempo de aire", "repeater_dutyCyclePercent": "{percent}%", "@repeater_dutyCyclePercent": { "placeholders": { @@ -1099,31 +1121,31 @@ } } }, - "repeater_ownerInfo": "Información del operador", + "repeater_ownerInfo": "Información del propietario", "repeater_ownerInfoHelper": "Metadatos públicos para este repetidor", - "repeater_refreshOwnerInfo": "Actualizar información del operador", - "repeater_floodMax": "Máximo número de saltos en caso de inundación", - "repeater_floodMaxHelper": "Número máximo de paquetes de flujo que un nodo puede enviar (0-64)", - "repeater_advancedSettings": "Avanzado", - "repeater_advancedSettingsSubtitle": "Perillas de ajuste para operadores experimentados", + "repeater_refreshOwnerInfo": "Actualizar información del propietario", + "repeater_floodMax": "Máximo de saltos por inundación", + "repeater_floodMaxHelper": "Número máximo de paquetes de inundación que un nodo puede enviar (0-64)", + "repeater_advancedSettings": "Ajustes avanzados", + "repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experimentados", "repeater_pathHashMode": "Modo de hash de ruta", "repeater_pathHashModeHelper": "Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.", - "repeater_txDelay": "Retraso en Flood, TX", + "repeater_txDelay": "Retraso TX por inundación", "repeater_txDelayHelper": "Ajuste de retransmisión para el tráfico de inundación, como un multiplicador del tiempo de transmisión del paquete (0-2, valor predeterminado 0.5). Un valor más alto significa menos colisiones, pero una entrega más lenta.", - "repeater_directTxDelay": "Retraso directo en TX", + "repeater_directTxDelay": "Retraso TX directo", "repeater_directTxDelayHelper": "Reenvío de espacios para el tráfico directo (no masivo), como un multiplicador del tiempo de transmisión del paquete (0-2, valor predeterminado 0.3).", "repeater_intThresh": "Límite de interferencia", - "repeater_intThreshHelper": "Se establece un umbral para la calibración del nivel de ruido de la radio, de modo que rechaza las interferencias que superen este nivel. 0 deshabilita — solo aumente este valor si observa errores en una banda de frecuencia con mucho ruido.", + "repeater_intThreshHelper": "Se establece un umbral para la calibración del nivel de ruido de la radio, de modo que rechaza las interferencias que superen este nivel. 0 deshabilita: solo aumente este valor si observa errores en una banda de frecuencia con mucho ruido.", "repeater_agcResetInterval": "Intervalo de reinicio de AGC", - "repeater_agcResetIntervalHelper": "¿Con qué frecuencia se debe restablecer el control automático de ganancia del radio para recuperarse de un estado de ganancia bloqueada? Se puede restablecer cada pocos segundos, o cada 4 segundos. Desactivar la función de restablecimiento periódico.", + "repeater_agcResetIntervalHelper": "¿Con qué frecuencia se debe restablecer el control automático de ganancia del radio para recuperarse de un estado de ganancia bloqueada? Se puede restablecer cada pocos segundos o cada 4 segundos. Desactiva la función de restablecimiento periódico.", "repeater_actionsTitle": "Acciones", - "repeater_sendAdvert": "Enviar anuncio sobre inundaciones", - "repeater_sendAdvertSubtitle": "Transmite un anuncio sobre inundaciones a través de la red.", - "repeater_sendAdvertZeroHop": "Enviar anuncio sin intermediarios", + "repeater_sendAdvert": "Enviar anuncio por inundación", + "repeater_sendAdvertSubtitle": "Transmitir un anuncio por inundación a través de la red.", + "repeater_sendAdvertZeroHop": "Enviar anuncio sin saltos", "repeater_sendAdvertZeroHopSubtitle": "Transmite un anuncio de un solo salto (sin retransmisiones).", "repeater_clockSync": "Sincronizar reloj ahora", "repeater_clockSyncSubtitle": "Envía la hora de tu teléfono al repetidor.", - "repeater_actionSucceeded": "{action} succeeded", + "repeater_actionSucceeded": "{action} completado correctamente", "@repeater_actionSucceeded": { "placeholders": { "action": { @@ -1131,7 +1153,7 @@ } } }, - "repeater_actionFailed": "{action} failed: {error}", + "repeater_actionFailed": "Error en {action}: {error}", "@repeater_actionFailed": { "placeholders": { "action": { @@ -1301,22 +1323,99 @@ "telemetry_energyLabel": "Energía", "telemetry_directionLabel": "Dirección", "telemetry_timeLabel": "Hora", - "telemetry_gyrometerLabel": "Girómetro", + "telemetry_gyrometerLabel": "Giroscopio", "telemetry_colourLabel": "Color", "telemetry_gpsLabel": "GPS", "telemetry_switchLabel": "Interruptor", "telemetry_polylineLabel": "Polilínea", "telemetry_altitudeValue": "{meters} m", + "@telemetry_altitudeValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_frequencyValue": "{hertz} Hz", + "@telemetry_frequencyValue": { + "placeholders": { + "hertz": { + "type": "String" + } + } + }, "telemetry_pressureValue": "{hpa} hPa", + "@telemetry_pressureValue": { + "placeholders": { + "hpa": { + "type": "String" + } + } + }, "telemetry_luminosityValue": "{lux} lx", + "@telemetry_luminosityValue": { + "placeholders": { + "lux": { + "type": "String" + } + } + }, "telemetry_powerValue": "{watts} W", + "@telemetry_powerValue": { + "placeholders": { + "watts": { + "type": "String" + } + } + }, "telemetry_distanceValue": "{meters} m", + "@telemetry_distanceValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_energyValue": "{kilowattHours} kWh", + "@telemetry_energyValue": { + "placeholders": { + "kilowattHours": { + "type": "String" + } + } + }, "telemetry_directionValue": "{degrees}°", + "@telemetry_directionValue": { + "placeholders": { + "degrees": { + "type": "String" + } + } + }, "telemetry_concentrationValue": "{ppm} ppm", + "@telemetry_concentrationValue": { + "placeholders": { + "ppm": { + "type": "String" + } + } + }, "telemetry_percentageValue": "{percent}%", + "@telemetry_percentageValue": { + "placeholders": { + "percent": { + "type": "String" + } + } + }, "telemetry_analogValue": "{value}", + "@telemetry_analogValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "telemetry_autoFetchQuantity": "Número de solicitudes", "telemetry_error": "No se pudieron obtener los datos", "telemetry_noData": "No hay datos de telemetría disponibles.", @@ -1332,7 +1431,7 @@ "telemetry_voltageLabel": "Voltaje", "telemetry_mcuTemperatureLabel": "Temperatura del MCU", "telemetry_temperatureLabel": "Temperatura", - "telemetry_currentLabel": "Actual", + "telemetry_currentLabel": "Corriente", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1371,14 +1470,14 @@ } } }, - "channelPath_title": "Ruta del Paquete", + "channelPath_title": "Ruta del paquete", "channelPath_viewMap": "Ver mapa", - "channelPath_otherObservedPaths": "Otros caminos observados", - "channelPath_repeaterHops": "Saltos del Repetidor", + "channelPath_otherObservedPaths": "Otras rutas observadas", + "channelPath_repeaterHops": "Saltos del repetidor", "channelPath_noHopDetails": "Los detalles del paquete no están disponibles.", "channelPath_messageDetails": "Detalles del mensaje", "channelPath_senderLabel": "Remitente", - "channelPath_timeLabel": "Tiempo", + "channelPath_timeLabel": "Hora", "channelPath_repeatsLabel": "Repetir", "channelPath_pathLabel": "Ruta {index}", "channelPath_observedLabel": "Observado", @@ -1523,13 +1622,13 @@ } }, "neighbors_unknownContact": "Clave pública desconocida {pubkey}", - "neighbors_heardAgo": "Escuchado: {time} hace atrás", + "neighbors_heardAgo": "Escuchado hace {time}", "settings_locationGPSEnable": "Habilitar GPS", "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", - "contacts_manageRoom": "Gestionar Servidor de Habitación", - "room_management": "Administración del Servidor de Habitación", + "contacts_manageRoom": "Gestionar servidor de sala", + "room_management": "Administración del servidor de sala", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1598,7 +1697,7 @@ "community_publicChannel": "Comunidad Pública", "community_hashtagChannel": "Hashtag de la Comunidad", "community_name": "Nombre de la comunidad", - "common_ok": "De acuerdo", + "common_ok": "Aceptar", "community_enterName": "Introducir nombre de comunidad", "community_created": "Comunidad \"{name}\" creada", "community_joined": "Se unió a la comunidad \"{name}\"", @@ -1676,34 +1775,34 @@ }, "pathTrace_you": "Tú", "pathTrace_failed": "El trazado de ruta falló.", - "pathTrace_refreshTooltip": "Actualizar Path Trace", - "contacts_pathTrace": "Rastreo de caminos", - "contacts_repeaterPathTrace": "Rastrear ruta al repetidor", - "contacts_repeaterPing": "Pingar repetidor", + "pathTrace_refreshTooltip": "Actualizar trazado de ruta", + "contacts_pathTrace": "Traza de ruta", + "contacts_repeaterPathTrace": "Traza de ruta al repetidor", + "contacts_repeaterPing": "Hacer ping al repetidor", "contacts_ping": "Ping", "pathTrace_notAvailable": "El trazado de ruta no está disponible.", - "contacts_roomPing": "Pingar servidor de sala", - "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", - "contacts_pathTraceTo": "Rastrear ruta a {name}", + "contacts_roomPing": "Hacer ping al servidor de sala", + "contacts_roomPathTrace": "Traza de ruta al servidor de sala", + "contacts_pathTraceTo": "Traza de ruta a {name}", "contacts_chatTraceRoute": "Ruta de trazado", "appSettings_languageUk": "Ucraniano", "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", - "contacts_invalidAdvertFormat": "Datos de contacto no válidos", + "contacts_invalidAdvertFormat": "Formato de anuncio no válido", "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", "contacts_contactImportFailed": "Contacto no se importó correctamente.", - "contacts_zeroHopAdvert": "Anuncio de Zero Hop", - "contacts_ShareContactZeroHop": "Compartir contacto por anuncio", - "contacts_ShareContact": "Copiar contacto al Portapapeles", + "contacts_zeroHopAdvert": "Anuncio de un solo salto", + "contacts_ShareContactZeroHop": "Compartir contacto por anuncio de un solo salto", + "contacts_ShareContact": "Copiar contacto al portapapeles", "contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles", "contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles", "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", - "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", - "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + "contacts_zeroHopContactAdvertSent": "Contacto enviado por anuncio.", + "contacts_contactAdvertCopied": "Anuncio copiado al portapapeles.", + "contacts_contactAdvertCopyFailed": "No se pudo copiar el anuncio al portapapeles.", "notification_activityTitle": "Actividad de MeshCore", "notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}", "@notification_messagesCount": { @@ -1738,27 +1837,27 @@ } }, "notification_receivedNewMessage": "Nuevo mensaje recibido", - "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", - "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", + "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a un archivo GPX.", + "settings_gpxExportRepeaters": "Exportar repetidores / servidores de sala a GPX", "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", "settings_gpxExportNoContacts": "No hay contactos para exportar.", - "settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO", + "settings_gpxExportNotAvailable": "No compatible con tu dispositivo o sistema operativo", "settings_gpxExportError": "Hubo un error al exportar.", - "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o servidores de sala con una ubicación a un archivo GPX.", "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", "settings_gpxExportAll": "Exportar todos los contactos a GPX", "settings_gpxExportContacts": "Exportar compañeros a GPX", - "settings_gpxExportChat": "Ubicaciones de compañero", - "settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala", + "settings_gpxExportChat": "Ubicaciones de compañeros", + "settings_gpxExportRepeatersRoom": "Ubicaciones de repetidores y servidores de sala", "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", - "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", + "settings_gpxExportShareSubject": "Exportación GPX de datos de mapa de meshcore-open", + "pathTrace_someHopsNoLocation": "Uno o más de los saltos carecen de una ubicación", "pathTrace_clearTooltip": "Borrar ruta", - "map_runTrace": "Ejecutar Rastreo de Ruta", - "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", + "map_runTrace": "Ejecutar traza de ruta", + "map_tapToAdd": "Toque los nodos para añadirlos a la ruta.", "map_removeLast": "Eliminar último", - "map_pathTraceCancelled": "Rastreo de ruta cancelado.", + "map_pathTraceCancelled": "Traza de ruta cancelada.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_chromeRequired": "Navegador Chrome requerido", "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", @@ -1766,7 +1865,7 @@ "scanner_enableBluetooth": "Habilitar Bluetooth", "snrIndicator_nearByRepeaters": "Repetidores cercanos", "snrIndicator_lastSeen": "Visto por última vez", - "chat_ShowAllPaths": "Mostrar todos los caminos", + "chat_ShowAllPaths": "Mostrar todas las rutas", "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", "settings_clientRepeat": "Repetir sin conexión", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.", @@ -1954,9 +2053,9 @@ }, "contacts_searchContactsNoNumber": "Buscar contactos...", "contacts_unread": "No leído", - "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", - "contacts_searchUsers": "Buscar {number}{str} Usuarios...", - "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", + "contacts_searchFavorites": "Buscar {number}{str} favoritos...", + "contacts_searchUsers": "Buscar {number}{str} usuarios...", + "contacts_searchRepeaters": "Buscar {number}{str} repetidores...", "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...", "contactsSettings_autoAddTitle": "Detección automática", "settings_contactSettings": "Configuración de contacto", @@ -2043,7 +2142,7 @@ "tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.", "tcpErrorTimedOut": "La conexión TCP ha caducado.", "tcpConnectionFailed": "Error en la conexión TCP: {error}", - "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento", + "map_showDiscoveryContacts": "Mostrar contactos de descubrimiento", "map_setAsMyLocation": "Establecer mi ubicación", "@path_routeWeight": { "placeholders": { @@ -2084,14 +2183,14 @@ "appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito", "appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.", "appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo", - "appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.", + "appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de una ruta después de un intento de entrega fallido.", "appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes", "appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Modo de telemetría actualizado", "settings_multiAck": "Múltiples respuestas de confirmación", "map_showOverlaps": "Superposiciones de tecla repetidora", - "map_runTraceWithReturnPath": "Volver atrás por el mismo camino.", + "map_runTraceWithReturnPath": "Volver por la misma ruta.", "@radioStats_noiseFloor": { "placeholders": { "noiseDbm": { @@ -2296,10 +2395,10 @@ "repeater_cliHelpStatsPackets": "(Solo para series) Muestra estadísticas a nivel de paquetes.", "repeater_cliHelpStatsRadio": "(Solo para transmisiones en serie) Muestra estadísticas de radio.", "repeater_cliHelpStatsCore": "(Solo para series) Muestra estadísticas clave del firmware.", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "common_done": "Hecho", + "background_serviceTitle": "MeshCore en ejecución", + "background_serviceText": "Manteniendo BLE conectado", + "appSettings_translationModelDeleted": "Eliminado {name}", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2307,7 +2406,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "No se pudo eliminar: {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2315,7 +2414,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "No se pudo actualizar el canal: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2330,12 +2429,19 @@ "map_publicKey": "Public Key", "map_publicKeyPrefixHint": "e.g. ab12", "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", + "contact_typeRepeater": "Repetidor", + "contact_typeRoom": "Sala", "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", - "chat_score": "Score", + "contact_typeUnknown": "Desconocido", + "channels_via": "vía {path}", + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "chat_score": "Puntuación", "map_sharedAt": "Compartido", "@losBlockedSpotChip": { "placeholders": { @@ -2420,59 +2526,153 @@ } } }, - "messageStatus_pending": "Enviar", + "messageStatus_pending": "Enviando", "common_undo": "Deshacer", - "messageStatus_sent": "Sentido", + "messageStatus_sent": "Enviado", "messageStatus_delivered": "Entregado", "messageStatus_failed": "No se pudo enviar", - "messageStatus_repeated": "Escuché repetidamente", + "messageStatus_repeated": "Escuchado repetidamente", "contacts_moreOptions": "Más opciones", "contacts_searchOpen": "Buscar contactos", - "contacts_searchClose": "Búsqueda avanzada", - "routing_title": "Ruteo", - "routing_modeAuto": "Coche", + "contacts_searchClose": "Cerrar búsqueda", + "routing_title": "Enrutamiento", + "routing_modeAuto": "Automático", "routing_modeFlood": "Inundación", "routing_modeManual": "Manual", - "routing_modeAutoHint": "Selecciona automáticamente la ruta más conocida, y si no hay ninguna ruta conocida, utiliza la ruta más directa.", - "routing_modeFloodHint": "Transmisiones a través de todos los repetidores. Es la opción más fiable, pero utiliza más tiempo de transmisión.", - "routing_modeManualHint": "Siempre sigue exactamente la ruta que usted ha definido.", + "routing_modeAutoHint": "Selecciona automáticamente la mejor ruta conocida y, si no hay ninguna, usa el modo de inundación.", + "routing_modeFloodHint": "Transmite por todos los repetidores. Es la opción más fiable, pero usa más tiempo de aire.", + "routing_modeManualHint": "Siempre sigue exactamente la ruta que has definido.", "routing_currentRoute": "Ruta actual", "routing_directNoHops": "Directo — sin saltos de repetidor", - "routing_noPathYet": "Aún no hay un camino definido. El mensaje se envía continuamente hasta que se encuentre una ruta.", - "routing_floodBroadcast": "Transmisión a través de todos los repetidores.", + "routing_noPathYet": "Aún no hay una ruta. El siguiente mensaje se enviará por inundación hasta que se descubra una ruta.", + "routing_floodBroadcast": "Transmisión por todos los repetidores", "routing_editPath": "Editar ruta", - "routing_forgetPath": "Olvídate del camino", + "routing_forgetPath": "Olvidar ruta", "routing_knownPaths": "Rutas conocidas", - "routing_knownPathsHint": "Seleccione una opción para cambiar a esa.", + "routing_knownPathsHint": "Toca una ruta para cambiar a ella.", "routing_inUse": "En uso", - "routing_qualityStrong": "Primer salto exitoso", - "routing_qualityGood": "Primer paso exitoso", - "routing_qualityWorked": "Ha cumplido", - "routing_qualityFair": "Primer salto de calidad", - "routing_qualityFlood": "Se ha escuchado a través de rumores.", + "routing_qualityStrong": "Primer salto fuerte", + "routing_qualityGood": "Primer salto bueno", + "routing_qualityWorked": "Ha entregado", + "routing_qualityFair": "Primer salto aceptable", + "routing_qualityFlood": "Escuchado por inundación", "routing_qualityUntested": "Sin probar", - "routing_lastWorked": "trabajó {when}", + "routing_lastWorked": "funcionó {when}", "routing_neverWorked": "nunca confirmado", "routing_floodDelivery": "Entrega por inundación", "pathEditor_title": "Crear ruta", - "pathEditor_hopCounter": "{count} de 64 granos de lúpulo", - "pathEditor_noHops": "Aún no se han añadido lúpulos. Haga clic en los repetidores para añadirlos en el orden deseado, o guarde la receta sin lúpulos para enviarla directamente.", - "pathEditor_addHops": "Añadir los lúpulos en el orden adecuado.", + "pathEditor_hopCounter": "{count} de 64 saltos", + "pathEditor_noHops": "Aún no se han añadido saltos. Toca los repetidores de abajo para añadirlos en orden, o guarda la ruta sin saltos para enviarla directamente.", + "pathEditor_addHops": "Añadir los saltos en orden", "pathEditor_searchRepeaters": "Buscar repetidores", "pathEditor_advancedHex": "Avanzado: ruta hexadecimal sin procesar", "pathEditor_hexLabel": "Prefijos hexadecimales", "pathEditor_hexHelper": "Dos caracteres hexadecimales por salto, separados por comas.", "pathEditor_invalidTokens": "Inválido: {tokens}", "pathEditor_tooManyHops": "Máximo 64 saltos", - "pathEditor_usePath": "Utilice esta ruta.", - "pathEditor_removeHop": "Eliminar el lúpulo", + "pathEditor_usePath": "Usar esta ruta", + "pathEditor_removeHop": "Eliminar salto", "pathEditor_unknownHop": "Repetidor desconocido", "map_zoomIn": "Acercar", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "map_zoomOut": "Acercar", - "map_centerMap": "Mapa del centro", + "routing_deliveryCounts": "{successes} entregados, {failures} fallidos", + "map_zoomOut": "Alejar", + "map_centerMap": "Centrar mapa", "chrome_bluetoothRequiresChromium": "Web Bluetooth requiere un navegador Chromium.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "Confirmado mediante GPS", - "pathTrace_legendInferred": "Posición inferida" + "pathTrace_legendInferred": "Posición estimada", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_searchHint": "Buscar por nombre o ID del nodo", + "map_activity": "Actividad", + "map_online": "En línea", + "scanner_bluetoothWebUnsupported": "La funcionalidad Bluetooth no está disponible en el navegador. Conéctese mediante USB en su lugar.", + "map_recent": "Reciente", + "map_visible": "Visible", + "map_stale": "Antiguo; pasado de fecha", + "map_hidden": "Oculto", + "map_centerOnNode": "Enfocar en el nodo", + "map_details": "Detalles", + "map_noGps": "Sin GPS", + "map_noResults": "No se encontraron nodos coincidentes.", + "pathMap_viewSingle": "Individual", + "pathMap_viewCombined": "Combinado", + "pathMap_play": "Reproducir", + "pathMap_pause": "Pausa", + "pathMap_replay": "Repetir", + "pathMap_stepBack": "Salto anterior", + "pathMap_stepForward": "Siguiente salto", + "pathMap_animationOn": "Mostrar animación del paquete", + "pathMap_animationOff": "Ocultar la animación del paquete", + "pathMap_hopOf": "Saltar {current} de {total}", + "pathMap_observedPaths": "Rutas observadas: {count}", + "pathMap_primary": "Principal", + "pathMap_alternate": "Alternativo {index}", + "pathMap_hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}", + "pathMap_legendShared": "Segmento compartido", + "pathMap_legendEstimated": "Segmento estimado", + "pathMap_sharedNodeCount": "Utilizado en {count} rutas.", + "pathMap_partialAnimation": "{count, plural, =1{1 salto no tiene ubicación — la ruta mostrada es parcial} other{{count} saltos no tienen ubicación — la ruta mostrada es parcial}}", + "pathMap_showAllPaths": "Mostrar todas", + "pathMap_hidePath": "Ocultar ruta", + "pathMap_showPath": "Mostrar ruta", + "pathMap_collapsePanel": "Cerrar panel", + "pathMap_expandPanel": "Ampliar panel", + "pathMap_noLocation": "Sin ubicación", + "pathMap_followPacket": "Seguir paquete", + "pathMap_unfollowPacket": "Dejar de seguir el paquete", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 76d6244b..2c90f639 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,5 +1,5 @@ { - "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", + "channels_channelDeleteFailed": "Échec de la suppression du canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,7 +9,7 @@ }, "@@locale": "fr", "appTitle": "MeshCore Open", - "nav_contacts": "Coordonnées", + "nav_contacts": "Contacts", "nav_channels": "Canaux", "nav_map": "Carte", "common_cancel": "Annuler", @@ -101,31 +101,31 @@ "settings_latitude": "Latitude", "settings_longitude": "Longitude", "settings_privacyMode": "Mode de confidentialité", - "settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces", - "settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", + "settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces", + "settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", "settings_privacyModeEnabled": "Mode de confidentialité activé", "settings_privacyModeDisabled": "Mode de confidentialité désactivé", "settings_actions": "Actions", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", + "settings_deleteAllPaths": "Supprimer tous les chemins", + "settings_deleteAllPathsSubtitle": "Effacer toutes les données de chemin des contacts.", "settings_sendAdvertisement": "S'annoncer", - "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", + "settings_sendAdvertisementSubtitle": "Diffuser la présence maintenant", "settings_advertisementSent": "Annonce envoyée", - "settings_syncTime": "Temps de synchronisation", - "settings_syncTimeSubtitle": "Définir l'heure de l'appareil sur l'heure du téléphone.", - "settings_timeSynchronized": "Synchronisation temporelle", - "settings_refreshContacts": "Rafraîchir les Contacts", + "settings_syncTime": "Synchroniser l'heure", + "settings_syncTimeSubtitle": "Régler l'heure de l'appareil sur celle du téléphone.", + "settings_timeSynchronized": "Heure synchronisée", + "settings_refreshContacts": "Actualiser les contacts", "settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil", "settings_rebootDevice": "Redémarrer l'appareil", "settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore", "settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.", - "settings_debug": "Déboguer", - "settings_bleDebugLog": "Journal de débogage BLE", - "settings_bleDebugLogSubtitle": "Commandes BLE, réponses et données brutes", + "settings_debug": "Débogage", + "settings_bleDebugLog": "Journal de débogage du compagnon", + "settings_bleDebugLogSubtitle": "Commandes, réponses et données brutes BLE/TCP/USB", "settings_appDebugLog": "Journal de débogage de l'application", "settings_appDebugLogSubtitle": "Messages de débogage de l'application", "settings_about": "À propos", - "settings_aboutVersion": "MeshCore Open {version}", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -133,13 +133,13 @@ } } }, - "settings_aboutLegalese": "Projet MeshCore Open Source 2026", - "settings_aboutDescription": "Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.", + "settings_aboutLegalese": "Projet open source MeshCore 2026", + "settings_aboutDescription": "Client Flutter open source pour les appareils de réseau maillé LoRa MeshCore.", "settings_infoName": "Nom", - "settings_infoId": "Numéro d'identification", + "settings_infoId": "ID", "settings_infoStatus": "État", "settings_infoBattery": "Batterie", - "settings_infoPublicKey": "Clé Publique", + "settings_infoPublicKey": "Clé publique", "settings_infoContactsCount": "Nombre de contacts", "settings_infoChannelCount": "Nombre de canaux", "settings_presets": "Préréglages", @@ -147,9 +147,9 @@ "settings_frequencyHelper": "300,0 - 2 500,0", "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", "settings_bandwidth": "Bande passante", - "settings_spreadingFactor": "Facteur de répartition (SF)", + "settings_spreadingFactor": "Facteur d'étalement (SF)", "settings_codingRate": "Taux de codage (CR)", - "settings_txPower": "TX Puissance (dBm)", + "settings_txPower": "Puissance TX (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)", "settings_error": "Erreur : {message}", @@ -164,7 +164,7 @@ "appSettings_appearance": "Apparence", "appSettings_theme": "Thème", "appSettings_themeSystem": "Défaut système", - "appSettings_themeLight": "Lumière", + "appSettings_themeLight": "Clair", "appSettings_themeDark": "Sombre", "appSettings_language": "Langue", "appSettings_languageSystem": "Par défaut du système", @@ -173,35 +173,35 @@ "appSettings_languageEs": "Espagnol", "appSettings_languageDe": "Allemand", "appSettings_languagePl": "Polonais", - "appSettings_languageSl": "Sloveno", + "appSettings_languageSl": "Slovène", "appSettings_languagePt": "Portugais", "appSettings_languageIt": "Italien", "appSettings_languageZh": "Chinois", "appSettings_languageSv": "Suédois", "appSettings_languageNl": "Néerlandais", - "appSettings_languageSk": "Slovène", + "appSettings_languageSk": "Slovaque", "appSettings_languageBg": "Bulgare", "appSettings_notifications": "Notifications", - "appSettings_enableNotifications": "Activer les Notifications", + "appSettings_enableNotifications": "Activer les notifications", "appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les annonces", "appSettings_notificationPermissionDenied": "Permission de notification refusée", "appSettings_notificationsEnabled": "Notifications activées", "appSettings_notificationsDisabled": "Notifications désactivées", - "appSettings_messageNotifications": "Notifications de Messages", + "appSettings_messageNotifications": "Notifications de messages", "appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages", - "appSettings_channelMessageNotifications": "Notifications des Messages de Canal", + "appSettings_channelMessageNotifications": "Notifications des messages de canal", "appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal", "appSettings_advertisementNotifications": "Notifications d'annonces", "appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nœuds", "appSettings_messaging": "Messagerie", - "appSettings_clearPathOnMaxRetry": "Effacer le chemin sur Max Retry", + "appSettings_clearPathOnMaxRetry": "Effacer le chemin après le nombre maximal de tentatives", "appSettings_clearPathOnMaxRetrySubtitle": "Réinitialiser le chemin de contact après 5 tentatives d'envoi infructueuses", - "appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.", + "appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 échecs.", "appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.", - "appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique", - "appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)", - "appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée", - "appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée", + "appSettings_autoRouteRotation": "Rotation automatique du routage", + "appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode flood", + "appSettings_autoRouteRotationEnabled": "Rotation automatique du routage activée", + "appSettings_autoRouteRotationDisabled": "Rotation automatique du routage désactivée", "appSettings_battery": "Batterie", "appSettings_batteryChemistry": "Chimie de la batterie", "appSettings_batteryChemistryPerDevice": "Définir par appareil ({deviceName})", @@ -233,14 +233,14 @@ } } }, - "appSettings_mapTimeFilter": "Filtre du Temps de la Carte", - "appSettings_showNodesDiscoveredWithin": "Afficher les nœuds découverts dans :", + "appSettings_mapTimeFilter": "Filtre temporel de la carte", + "appSettings_showNodesDiscoveredWithin": "Afficher les nœuds découverts au cours de :", "appSettings_allTime": "Tout le temps", "appSettings_lastHour": "Dernière heure", "appSettings_last6Hours": "Dernières 6 heures", "appSettings_last24Hours": "Dernières 24 heures", "appSettings_lastWeek": "La semaine dernière", - "appSettings_offlineMapCache": "Cache de Carte Hors Ligne", + "appSettings_offlineMapCache": "Cache de carte hors ligne", "appSettings_noAreaSelected": "Aucune zone sélectionnée", "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { @@ -253,15 +253,25 @@ } } }, - "appSettings_debugCard": "Déboguer", - "appSettings_appDebugLogging": "Journalisation de débogage de l'application", - "appSettings_appDebugLoggingSubtitle": "Enregistrez les messages de débogage de l'application Log pour le dépannage.", + "appSettings_debugCard": "Débogage", + "appSettings_appDebugLogging": "Journalisation des débogages de l'application", + "appSettings_appDebugLoggingSubtitle": "Journaliser les messages de débogage de l'application pour le dépannage.", "appSettings_appDebugLoggingEnabled": "Journalisation de débogage de l'application activée", - "appSettings_appDebugLoggingDisabled": "Le débogage de l'application est désactivé.", - "contacts_title": "Coordonnées", + "appSettings_appDebugLoggingDisabled": "Journalisation de débogage de l'application désactivée", + "contacts_title": "Contacts", "contacts_noContacts": "Aucun contact trouvé.", - "contacts_contactsWillAppear": "Les contacts apparaîtront lorsque les appareils font leur annonce.", - "contacts_searchContacts": "Rechercher des contacts...", + "contacts_contactsWillAppear": "Les contacts apparaîtront lorsque les appareils diffuseront leur annonce.", + "contacts_searchContacts": "Rechercher {number}{str} contacts...", + "@contacts_searchContacts": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, "contacts_noUnreadContacts": "Aucun contact non lu", "contacts_noContactsFound": "Aucun contact ou groupe trouvé.", "contacts_deleteContact": "Supprimer le contact", @@ -274,11 +284,11 @@ } }, "contacts_manageRepeater": "Gérer le répéteur", - "contacts_roomLogin": "Connexion Room Server", - "contacts_openChat": "Ouverture du Chat", + "contacts_roomLogin": "Connexion au room server", + "contacts_openChat": "Ouvrir le chat", "contacts_editGroup": "Modifier le groupe", "contacts_deleteGroup": "Supprimer le groupe", - "contacts_deleteGroupConfirm": "Supprimer {groupName}?", + "contacts_deleteGroupConfirm": "Supprimer le groupe \"{groupName}\" ?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -286,7 +296,7 @@ } } }, - "contacts_newGroup": "Nouveau Groupe", + "contacts_newGroup": "Nouveau groupe", "contacts_groupName": "Nom du groupe", "contacts_groupNameRequired": "Le nom du groupe est obligatoire.", "contacts_groupNameReserved": "Ce nom de groupe est réservé", @@ -301,8 +311,8 @@ "contacts_filterContacts": "Filtrer les contacts...", "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", "contacts_noMembers": "Aucun membre", - "contacts_lastSeenNow": "Vu maintenant", - "contacts_lastSeenMinsAgo": "~ {minutes} minutes.", + "contacts_lastSeenNow": "À l'instant", + "contacts_lastSeenMinsAgo": "~ {minutes} min", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -347,7 +357,7 @@ "channels_muteChannel": "Désactiver les notifications du canal", "channels_unmuteChannel": "Réactiver les notifications du canal", "channels_deleteChannel": "Supprimer le canal", - "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", + "channels_deleteChannelConfirm": "Supprimer \"{name}\" ? Cette action est irréversible.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -355,7 +365,7 @@ } } }, - "channels_channelDeleted": "Le canal \"{name}\" a été supprimé", + "channels_channelDeleted": "Canal \"{name}\" supprimé", "@channels_channelDeleted": { "placeholders": { "name": { @@ -363,15 +373,15 @@ } } }, - "channels_addChannel": "Ajouter un Canal", - "channels_channelIndexLabel": "Index de canal", + "channels_addChannel": "Ajouter un canal", + "channels_channelIndexLabel": "Index du canal", "channels_channelName": "Nom du canal", "channels_usePublicChannel": "Utiliser le canal public", "channels_standardPublicPsk": "PSK public standard", "channels_pskHex": "PSK (Hexadécimal)", - "channels_generateRandomPsk": "Générer une clé de modulation PSK aléatoire", + "channels_generateRandomPsk": "Générer une PSK aléatoire", "channels_enterChannelName": "Veuillez entrer un nom de canal", - "channels_pskMustBe32Hex": "Le PKS doit être composé de 32 caractères hexadécimaux.", + "channels_pskMustBe32Hex": "La PSK doit contenir 32 caractères hexadécimaux.", "channels_channelAdded": "Le canal \"{name}\" a été ajouté", "@channels_channelAdded": { "placeholders": { @@ -396,6 +406,11 @@ "channels_cyr2latSettingsDscr": "Modifier la configuration JSON des remplacements de caractères", "channels_cyr2latSettingsDialogHint": "Tableau de remplacement JSON", "channels_cyr2latSettingsDialogWrongJSON": "JSON incorrect : {error}", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, "settings_cyr2latProfileAdd": "Ajouter un profil Cyr2Lat", "settings_cyr2latProfileName": "Nom du profil", "settings_cyr2latProfileNameEmpty": "Le nom du profil ne peut pas être vide", @@ -404,7 +419,14 @@ "settings_cyr2latProfileEdit": "Modifier le profil Cyr2Lat", "settings_cyr2latProfileDelete": "Supprimer le profil Cyr2Lat", "settings_cyr2latProfileDeleted": "Profil supprimé avec succès", - "settings_cyr2latProfileDeleteDscr": "Êtes-vous sûr de vouloir supprimer le profil \"{name}\"?", + "settings_cyr2latProfileDeleteDscr": "Êtes-vous sûr de vouloir supprimer le profil \"{name}\" ?", + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelUpdated": "Le canal \"{name}\" a été mis à jour", "@channels_channelUpdated": { "placeholders": { @@ -413,13 +435,13 @@ } } }, - "channels_publicChannelAdded": "Le canal public a été ajouté", + "channels_publicChannelAdded": "Canal public ajouté", "channels_sortBy": "Trier par", "channels_sortManual": "Manuel", "channels_sortAZ": "A à Z", "channels_sortLatestMessages": "Derniers messages", - "channels_sortUnread": "Non lu", - "chat_noMessages": "Aucun message pour le moment.", + "channels_sortUnread": "Non lus", + "chat_noMessages": "Aucun message pour le moment", "chat_sendMessageToStart": "Envoyer un message pour commencer", "chat_originalMessageNotFound": "Message d'origine non trouvé", "chat_replyingTo": "Répondre à {name}", @@ -458,7 +480,7 @@ }, "chat_messageCopied": "Message copié", "chat_messageDeleted": "Message supprimé", - "chat_retryingMessage": "Tentative de récupération.", + "chat_retryingMessage": "Nouvelle tentative d'envoi du message", "chat_retryCount": "Essai {current}/{max}", "@chat_retryCount": { "placeholders": { @@ -470,9 +492,9 @@ } } }, - "chat_sendGif": "Envoyer GIF", + "chat_sendGif": "Envoyer un GIF", "chat_reply": "Répondre", - "chat_addReaction": "Ajouter une Réaction", + "chat_addReaction": "Ajouter une réaction", "chat_me": "Moi", "emojiCategorySmileys": "Émojis", "emojiCategoryGestures": "Gestes", @@ -549,7 +571,7 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Simple", + "debugFrame_textTypePlain": "Texte brut", "debugFrame_text": "- Texte : \"{text}\"", "@debugFrame_text": { "placeholders": { @@ -558,7 +580,7 @@ } } }, - "debugFrame_hexDump": "Vidéo de Dump Hexadécimal :", + "debugFrame_hexDump": "Dump hexadécimal :", "chat_pathManagement": "Gestion des chemins", "chat_routingMode": "Mode de routage", "chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", @@ -577,15 +599,15 @@ }, "chat_successes": "Succès", "chat_removePath": "Supprimer le chemin", - "chat_noPathHistoryYet": "Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.", + "chat_noPathHistoryYet": "Aucun historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.", "chat_pathActions": "Actions du chemin :", "chat_setCustomPath": "Définir un chemin personnalisé", "chat_setCustomPathSubtitle": "Spécifier manuellement le chemin de routage", "chat_clearPath": "Effacer le chemin", - "chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi", - "chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.", - "chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d'application pour rebasculer en mode auto par la suite.", - "chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.", + "chat_clearPathSubtitle": "Forcer la redécouverte lors du prochain envoi", + "chat_pathCleared": "Chemin effacé. Le prochain message redécouvrira la route.", + "chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utilisez le sélecteur de routage de la barre d'application pour revenir ensuite au mode auto.", + "chat_floodModeEnabled": "Le mode flood est activé. Utilisez l'icône de routage de la barre d'outils pour changer cela.", "chat_fullPath": "Chemin complet", "chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.", "chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", @@ -602,11 +624,11 @@ "chat_pathSavedLocally": "Sauvegardé localement. Connectez-vous pour synchroniser.", "chat_pathDeviceConfirmed": "Appareil confirmé.", "chat_pathDeviceNotConfirmed": "L'appareil n'a pas encore été confirmé.", - "chat_type": "Saisir", + "chat_type": "Type", "chat_path": "Chemin", - "chat_publicKey": "Clé Publique", + "chat_publicKey": "Clé publique", "chat_compressOutgoingMessages": "Compresser les messages sortants", - "chat_floodForced": "Tout le réseau (forcée)", + "chat_floodForced": "Flood (forcé)", "chat_directForced": "Direct (forcé)", "chat_hopsForced": "{count} sauts (forcés)", "@chat_hopsForced": { @@ -616,9 +638,9 @@ } } }, - "chat_floodAuto": "Tout le réseau (auto)", - "chat_direct": "Afficher", - "chat_poiShared": "Point d'intérêt Partagé", + "chat_floodAuto": "Flood (auto)", + "chat_direct": "Direct", + "chat_poiShared": "Point d'intérêt partagé", "chat_unread": "Non lu : {count}", "@chat_unread": { "placeholders": { @@ -650,7 +672,7 @@ } } }, - "map_pinsCount": "Epingles: {count}", + "map_pinsCount": "Épingles : {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -660,25 +682,25 @@ }, "map_chat": "Conversation", "map_repeater": "Répéteur", - "map_room": "Room Server", + "map_room": "Room server", "map_sensor": "Capteur", - "map_pinDm": "Clé (DM)", - "map_pinPrivate": "Verrouiller (Privé)", - "map_pinPublic": "Clé (Public)", - "map_lastSeen": "Dernière fois vu", + "map_pinDm": "Épingle (DM)", + "map_pinPrivate": "Épingle (privé)", + "map_pinPublic": "Épingle (public)", + "map_lastSeen": "Vu pour la dernière fois", "map_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "map_from": "À partir de", + "map_from": "Depuis", "map_source": "Source", "map_flags": "Drapeaux", "map_shareMarkerHere": "Partager le marqueur ici", - "map_pinLabel": "Étiquete de repin", + "map_pinLabel": "Libellé de l'épingle", "map_label": "Étiquette", "map_pointOfInterest": "Point d'intérêt", "map_sendToContact": "Envoyer au contact", "map_sendToChannel": "Envoyer sur le canal", "map_noChannelsAvailable": "Aucun canal disponible", - "map_publicLocationShare": "Partager dans un lieu public", - "map_publicLocationShareConfirm": "Vous êtes sur le point de partager un emplacement dans {channelLabel}. Ce canal est public et toute personne disposant de la clé PSK peut le voir.", + "map_publicLocationShare": "Partage public de localisation", + "map_publicLocationShareConfirm": "Vous êtes sur le point de partager une localisation dans {channelLabel}. Ce canal est public et toute personne disposant de la PSK peut la voir.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -689,23 +711,23 @@ "map_connectToShareMarkers": "Connectez-vous à un appareil pour partager des marqueurs", "map_filterNodes": "Filtrer les nœuds", "map_nodeTypes": "Types de nœuds", - "map_chatNodes": "Nœuds de Chat", + "map_chatNodes": "Nœuds de chat", "map_repeaters": "Répéteurs", "map_otherNodes": "Autres nœuds", - "map_keyPrefix": "Préfixe clé", + "map_keyPrefix": "Préfixe de clé", "map_filterByKeyPrefix": "Filtrer par préfixe de clé", "map_publicKeyPrefix": "Préfixe de clé publique", "map_markers": "Marqueurs", "map_showSharedMarkers": "Afficher les marqueurs partagés", - "map_lastSeenTime": "Dernière fois vu", - "map_sharedPin": "Clé partagée", + "map_lastSeenTime": "Vu pour la dernière fois", + "map_sharedPin": "Épingle partagée", "map_joinRoom": "Rejoindre le room server", "map_manageRepeater": "Gérer le répéteur", - "mapCache_title": "Cache de Carte Hors Ligne", - "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", - "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", + "mapCache_title": "Cache de carte hors ligne", + "mapCache_selectAreaFirst": "Sélectionnez d'abord une zone à mettre en cache", + "mapCache_noTilesToDownload": "Aucune tuile à télécharger pour cette zone.", "mapCache_downloadTilesTitle": "Télécharger les tuiles", - "mapCache_downloadTilesPrompt": "Télécharger {count} tuiles pour un usage hors ligne ?", + "mapCache_downloadTilesPrompt": "Télécharger {count} tuiles pour une utilisation hors ligne ?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -714,7 +736,7 @@ } }, "mapCache_downloadAction": "Télécharger", - "mapCache_cachedTiles": "Cachez {count} tuiles", + "mapCache_cachedTiles": "{count} tuiles mises en cache", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -722,7 +744,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)", + "mapCache_cachedTilesWithFailed": "{downloaded} tuiles mises en cache ({failed} en échec)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -733,12 +755,12 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Vider le cache hors ligne", + "mapCache_clearOfflineCacheTitle": "Effacer le cache hors ligne", "mapCache_clearOfflineCachePrompt": "Supprimer toutes les tuiles de carte mises en cache ?", "mapCache_offlineCacheCleared": "Le cache hors ligne a été effacé.", "mapCache_noAreaSelected": "Aucune zone sélectionnée", - "mapCache_cacheArea": "Zone de cache", - "mapCache_useCurrentView": "Utiliser la Vue Actuelle", + "mapCache_cacheArea": "Zone à mettre en cache", + "mapCache_useCurrentView": "Utiliser la vue actuelle", "mapCache_zoomRange": "Plage de zoom", "mapCache_estimatedTiles": "Estimation des tuiles : {count}", "@mapCache_estimatedTiles": { @@ -748,7 +770,7 @@ } } }, - "mapCache_downloadedTiles": "Téléchargé {completed} / {total}", + "mapCache_downloadedTiles": "{completed} / {total} téléchargées", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -760,7 +782,7 @@ } }, "mapCache_downloadTilesButton": "Télécharger les tuiles", - "mapCache_clearCacheButton": "Vider le Cache", + "mapCache_clearCacheButton": "Vider le cache", "mapCache_failedDownloads": "Téléchargements échoués : {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -824,7 +846,7 @@ "dialog_disconnect": "Déconnecter", "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", "login_repeaterLogin": "Connexion au répéteur", - "login_roomLogin": "Connexion Room Server", + "login_roomLogin": "Connexion au room server", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", @@ -867,7 +889,7 @@ } } }, - "path_usingHopsPath": "Utiliser {count} {count, plural, =1{hop} other{hops}} chemin", + "path_usingHopsPath": "Utiliser un chemin de {count} {count, plural, =1{saut} other{sauts}}", "@path_usingHopsPath": { "placeholders": { "count": { @@ -877,13 +899,13 @@ }, "path_enterCustomPath": "Entrer un chemin personnalisé", "path_currentPathLabel": "Chemin actuel", - "path_hexPrefixInstructions": "Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.", + "path_hexPrefixInstructions": "Entrez des préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.", "path_hexPrefixExample": "Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).", "path_labelHexPrefixes": "Préfixes hexadécimaux", - "path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)", - "path_selectFromContacts": "Sélectionner à partir des contacts :", - "path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.", - "path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.", + "path_helperMaxHops": "Max. 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet).", + "path_selectFromContacts": "Sélectionner parmi les contacts :", + "path_noRepeatersFound": "Aucun répéteur ou room server trouvé.", + "path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires capables de transmettre des messages.", "path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { @@ -1026,7 +1048,7 @@ "repeater_guestAccess": "Accès Invité", "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", "repeater_privacyMode": "Mode de confidentialité", - "repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces", + "repeater_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces", "repeater_advertisementSettings": "Paramètres d'annonces", "repeater_localAdvertInterval": "Intervalle des annonces Locale (0 saut)", "repeater_localAdvertIntervalMinutes": "{minutes} minutes", @@ -1085,7 +1107,7 @@ "repeater_networkHealth": "Santé du réseau", "repeater_loopDetect": "Détection de boucles", "repeater_loopDetectHelper": "Envoyer des paquets de données qui semblent former des boucles de routage.", - "repeater_loopDetectOff": "Prix", + "repeater_loopDetectOff": "Désactivé", "repeater_loopDetectMinimal": "Minimal", "repeater_loopDetectModerate": "Modéré", "repeater_loopDetectStrict": "Strict", @@ -1099,28 +1121,28 @@ } } }, - "repeater_ownerInfo": "Informations sur l'opérateur", - "repeater_ownerInfoHelper": "Métadonnées publiques pour cet émetteur", - "repeater_refreshOwnerInfo": "Rafraîchir les informations sur l'opérateur", - "repeater_floodMax": "Nombre maximal de sauts lors des inondations", + "repeater_ownerInfo": "Informations sur le propriétaire", + "repeater_ownerInfoHelper": "Métadonnées publiques pour cet appareil", + "repeater_refreshOwnerInfo": "Rafraîchir les informations du propriétaire", + "repeater_floodMax": "Nombre maximal de sauts en flood", "repeater_floodMaxHelper": "Nombre maximal de paquets de données qu'un flux peut transmettre (0-64)", "repeater_advancedSettings": "Avancé", - "repeater_advancedSettingsSubtitle": "Molettes de réglage pour les opérateurs expérimentés", + "repeater_advancedSettingsSubtitle": "Réglages avancés pour les opérateurs expérimentés", "repeater_pathHashMode": "Mode de hachage de chemin", "repeater_pathHashModeHelper": "Octets utilisés pour encoder l'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu'à 32 sauts), 2 = 3 octets (16 millions d'ID, jusqu'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n'est plus nécessaire.", - "repeater_txDelay": "Retard dû aux inondations à Texas", - "repeater_txDelayHelper": "Rétransmettre l'espacement pour le trafic de secours en cas de inondation, en multipliant le temps d'émission du paquet (0-2, valeur par défaut : 0,5). Une valeur plus élevée signifie moins de collisions, mais une vitesse de transmission plus lente.", - "repeater_directTxDelay": "Retard de transmission direct", - "repeater_directTxDelayHelper": "Rétransmettre l'espacement pour le trafic direct (non-inondation), en multipliant le temps de transmission des paquets (0-2, valeur par défaut : 0,3).", + "repeater_txDelay": "Délai de transmission en flood", + "repeater_txDelayHelper": "Intervalle de retransmission pour le trafic flood, en multipliant le temps d'émission des paquets (0-2, valeur par défaut : 0,5). Une valeur plus élevée réduit les collisions, mais ralentit la transmission.", + "repeater_directTxDelay": "Délai de transmission direct", + "repeater_directTxDelayHelper": "Intervalle de retransmission pour le trafic direct (non flood), en multipliant le temps de transmission des paquets (0-2, valeur par défaut : 0,3).", "repeater_intThresh": "Seuil de perturbation", "repeater_intThreshHelper": "Seuil dépassé pour la calibration du niveau de bruit de la radio, afin de rejeter les interférences supérieures à ce niveau. 0 désactive – ne mettez cette valeur que si vous constatez des erreurs RX dans une bande de fréquences bruyante.", - "repeater_agcResetInterval": "Interval de réinitialisation de l'AGC", + "repeater_agcResetInterval": "Intervalle de réinitialisation de l'AGC", "repeater_agcResetIntervalHelper": "À quelle fréquence réinitialiser le contrôle automatique du gain de la radio pour revenir à un état normal ? Chaque seconde, ou à chaque multiple de 4. Désactiver la réinitialisation périodique avec 0.", "repeater_actionsTitle": "Actions", - "repeater_sendAdvert": "Envoyer une publicité sur les inondations", - "repeater_sendAdvertSubtitle": "Diffuser une publicité sur les inondations via le réseau.", - "repeater_sendAdvertZeroHop": "Envoyer une publicité sans intermédiaire", - "repeater_sendAdvertZeroHopSubtitle": "Diffuser une publicité d'un seul saut (sans relais)", + "repeater_sendAdvert": "Envoyer une annonce flood", + "repeater_sendAdvertSubtitle": "Diffuser une annonce flood via le réseau.", + "repeater_sendAdvertZeroHop": "Envoyer une annonce sans relais", + "repeater_sendAdvertZeroHopSubtitle": "Diffuser une annonce d'un seul saut (sans relais)", "repeater_clockSync": "Synchroniser l'heure maintenant", "repeater_clockSyncSubtitle": "Envoyez l'heure de votre téléphone au répéteur.", "repeater_actionSucceeded": "{action} a réussi", @@ -1161,7 +1183,7 @@ }, "repeater_refreshBasicSettings": "Rafraîchir les paramètres de base", "repeater_refreshRadioSettings": "Rafraîchir les paramètres Radio", - "repeater_refreshTxPower": "Rafraîchir la tension TX", + "repeater_refreshTxPower": "Rafraîchir la puissance TX", "repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets", "repeater_refreshGuestAccess": "Rafraîchir l'accès invité", "repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité", @@ -1181,8 +1203,8 @@ } } }, - "repeater_cliTitle": "Répéteur CLI", - "repeater_debugNextCommand": "Déboguer Prochaine Commande", + "repeater_cliTitle": "CLI du répéteur", + "repeater_debugNextCommand": "Déboguer la prochaine commande", "repeater_commandHelp": "Aide", "repeater_clearHistory": "Effacer l'historique", "repeater_noCommandsSent": "Aucune commande n'a encore été envoyée.", @@ -1201,11 +1223,11 @@ } }, "repeater_cliQuickGetName": "Obtenir le nom", - "repeater_cliQuickGetRadio": "Obtenir la Radio", + "repeater_cliQuickGetRadio": "Obtenir la radio", "repeater_cliQuickGetTx": "Obtenir TX", "repeater_cliQuickNeighbors": "Voisins", "repeater_cliQuickVersion": "Version", - "repeater_cliQuickAdvertise": "Publier", + "repeater_cliQuickAdvertise": "Diffuser", "repeater_cliQuickClock": "Horloge", "repeater_cliHelpAdvert": "Envoie un paquet d'annonce", "repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)", @@ -1317,6 +1339,83 @@ "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", "telemetry_analogValue": "{value}", + "@telemetry_altitudeValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, + "@telemetry_frequencyValue": { + "placeholders": { + "hertz": { + "type": "String" + } + } + }, + "@telemetry_pressureValue": { + "placeholders": { + "hpa": { + "type": "String" + } + } + }, + "@telemetry_luminosityValue": { + "placeholders": { + "lux": { + "type": "String" + } + } + }, + "@telemetry_powerValue": { + "placeholders": { + "watts": { + "type": "String" + } + } + }, + "@telemetry_distanceValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, + "@telemetry_energyValue": { + "placeholders": { + "kilowattHours": { + "type": "String" + } + } + }, + "@telemetry_directionValue": { + "placeholders": { + "degrees": { + "type": "String" + } + } + }, + "@telemetry_concentrationValue": { + "placeholders": { + "ppm": { + "type": "String" + } + } + }, + "@telemetry_percentageValue": { + "placeholders": { + "percent": { + "type": "String" + } + } + }, + "@telemetry_analogValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "telemetry_autoFetchQuantity": "Nombre de requêtes", "telemetry_error": "Impossible de récupérer les données", "telemetry_noData": "Aucune donnée de télémétrie disponible.", @@ -1525,11 +1624,11 @@ "neighbors_unknownContact": "Clé publique inconnue {pubkey}", "neighbors_heardAgo": "Écouté : {time} auparavant", "settings_locationGPSEnable": "Activer le GPS", - "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", - "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", + "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la localisation via GPS", + "settings_locationIntervalSec": "Intervalle de mise à jour du GPS (secondes)", "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", - "contacts_manageRoom": "Gérer le Room Server", - "room_management": "Administrattion Room Server", + "contacts_manageRoom": "Gérer le room server", + "room_management": "Gestion du room server", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1675,9 +1774,9 @@ } }, "pathTrace_you": "Vous", - "pathTrace_refreshTooltip": "Actualiser Path Trace", - "pathTrace_failed": "Traçage du chemin échoué.", - "pathTrace_notAvailable": "Tracé de chemin non disponible.", + "pathTrace_refreshTooltip": "Actualiser le traçage du chemin", + "pathTrace_failed": "Échec du traçage du chemin.", + "pathTrace_notAvailable": "Le traçage du chemin est indisponible.", "contacts_pathTrace": "Traçage de chemin", "contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur", "contacts_repeaterPing": "Pinguer le répéteur", @@ -1709,6 +1808,27 @@ "notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}", "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", "notification_newTypeDiscovered": "Nouveau {contactType} découvert", + "@notification_channelMessagesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@notification_newNodesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": { + "type": "String" + } + } + }, "notification_receivedNewMessage": "Nouveau message reçu", "settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX", "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", @@ -1736,11 +1856,11 @@ "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", - "snrIndicator_lastSeen": "Dernière fois vu", + "snrIndicator_lastSeen": "Vu pour la dernière fois", "snrIndicator_nearByRepeaters": "Répéteurs à proximité", "chat_ShowAllPaths": "Afficher tous les chemins", "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", - "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", + "settings_clientRepeatSubtitle": "Permettre à cet appareil de relayer les paquets du maillage pour les autres.", "settings_clientRepeat": "Répétition hors réseau", "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Unités", @@ -1952,11 +2072,11 @@ "discoveredContacts_Title": "Contacts découverts", "discoveredContacts_searchHint": "Rechercher des contacts découverts", "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.", - "common_deleteAll": "Supprimer tout", + "common_deleteAll": "Tout supprimer", "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?", - "map_showGuessedLocations": "Afficher les emplacements des nœuds estimés", - "map_guessedLocation": "Lieu deviné", + "map_showGuessedLocations": "Afficher les emplacements estimés des nœuds", + "map_guessedLocation": "Localisation estimée", "connectionChoiceUsbLabel": "USB", "usbScreenTitle": "Connectez via USB", "connectionChoiceBluetoothLabel": "Bluetooth", @@ -2029,10 +2149,10 @@ }, "settings_privacy": "Paramètres de confidentialité", "settings_privacySubtitle": "Contrôlez les informations partagées", - "settings_telemetryLocationMode": "Mode d'emplacement de télémétrie", - "settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie", - "settings_advertLocation": "Emplacement de l'annonce", - "settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce", + "settings_telemetryLocationMode": "Mode de localisation de la télémétrie", + "settings_telemetryEnvironmentMode": "Mode d'environnement de la télémétrie", + "settings_advertLocation": "Localisation de l'annonce", + "settings_advertLocationSubtitle": "Inclure la localisation dans l'annonce", "settings_denyAll": "Refuser tout", "settings_allowByContact": "Autoriser par drapeaux de contact", "settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.", @@ -2046,22 +2166,22 @@ "contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement", "contact_telemetry": "Télémétrie", "contact_settings": "Paramètres de contact", - "contact_lastSeen": "Dernière fois vu", + "contact_lastSeen": "Vu pour la dernière fois", "contact_clearChat": "Effacer la conversation", "contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base", - "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.", + "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un chemin peut accumuler grâce aux livraisons réussies.", "appSettings_initialRouteWeight": "Poids initial de l'itinéraire", - "appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet", + "appSettings_maxRouteWeight": "Poids maximal du chemin", "appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts", - "appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite", - "appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.", - "appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité", - "appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.", - "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages", - "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.", + "appSettings_routeWeightSuccessIncrement": "Incrément du poids de succès", + "appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un chemin après une livraison réussie.", + "appSettings_routeWeightFailureDecrement": "Décrément du poids d'échec", + "appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un chemin après une tentative de livraison infructueuse.", + "appSettings_maxMessageRetries": "Nombre maximal de tentatives d'envoi", + "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives avant de marquer un message comme ayant échoué.", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour", - "map_showOverlaps": "Chevauchement de la touche répétitive", + "map_showOverlaps": "Chevauchements de clés de répéteur", "map_runTraceWithReturnPath": "Revenir sur le même chemin.", "@radioStats_noiseFloor": { "placeholders": { @@ -2119,8 +2239,8 @@ "radioStats_noiseFloor": "Niveau de bruit : {noiseDbm} dBm", "radioStats_lastRssi": "Dernier RSSI : {rssiDbm} dBm", "radioStats_lastSnr": "Dernier SNR : {snr} dB", - "radioStats_txAir": "Temps d'antenne à la télévision du Texas (total) : {seconds} s", - "radioStats_rxAir": "Temps d'utilisation de l'appareil RX (total) : {seconds} s", + "radioStats_txAir": "Temps d'antenne TX (total) : {seconds} s", + "radioStats_rxAir": "Temps d'antenne RX (total) : {seconds} s", "radioStats_chartCaption": "Niveau de bruit (dBm) sur les échantillons récents.", "radioStats_stripNoise": "Niveau de bruit : {noiseDbm} dBm", "radioStats_stripWaiting": "Récupération des statistiques de la radio…", @@ -2245,7 +2365,7 @@ "repeater_cliHelpGetLat": "Affiche la latitude configurée.", "repeater_cliHelpGetLon": "Affiche la longitude configurée.", "repeater_cliHelpGetRxDelay": "Affiche la valeur de base de rxdelay.", - "repeater_cliHelpGetTxDelay": "Indique le facteur de délai de transmission en mode inondation.", + "repeater_cliHelpGetTxDelay": "Indique le facteur de délai de transmission en mode flood.", "repeater_cliHelpGetDirectTxDelay": "Indique le facteur de délai direct.", "repeater_cliHelpGetFloodMax": "Indique le nombre maximal de fois où le niveau de l'eau a atteint son point culminant.", "repeater_cliHelpGetOwnerInfo": "Affiche la chaîne d'informations de contact du propriétaire.", @@ -2269,15 +2389,15 @@ "repeater_cliHelpSensorList": "Affiche toutes les configurations de capteurs personnalisées, avec une pagination à partir d'un index de départ optionnel.", "repeater_cliHelpRegionDefault": "Affiche la portée régionale par défaut actuelle.", "repeater_cliHelpRegionDefaultSet": "Définit la portée régionale par défaut. Utilisez \"\" pour la réinitialiser.", - "repeater_cliHelpRegionListAllowed": "Énumère les régions autorisant la circulation des véhicules en cas de inondation.", - "repeater_cliHelpRegionListDenied": "Liste des régions qui interdisent la circulation en cas de inondation.", + "repeater_cliHelpRegionListAllowed": "Énumère les régions autorisant la circulation des véhicules en cas de flood.", + "repeater_cliHelpRegionListDenied": "Liste les régions qui interdisent la circulation en cas de flood.", "repeater_cliHelpStatsPackets": "(Uniquement pour les séries) Affiche des statistiques au niveau des paquets.", "repeater_cliHelpStatsRadio": "(Uniquement pour les séries) Affiche les statistiques de la radio.", "repeater_cliHelpStatsCore": "(Uniquement pour les séries) Affiche les statistiques du micrologicem intégré.", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "common_done": "Terminé", + "background_serviceTitle": "MeshCore en cours d'exécution", + "background_serviceText": "Maintien de la connexion BLE", + "appSettings_translationModelDeleted": "Supprimé {name}", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2285,7 +2405,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "Échec de la suppression : {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2293,7 +2413,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "Échec de la mise à jour du canal : {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2302,17 +2422,24 @@ } }, "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", - "map_publicKeyPrefixHint": "e.g. ab12", + "map_path": "Chemin", + "map_location": "Localisation", + "map_estLocation": "Localisation estimée", + "map_publicKey": "Clé publique", + "map_publicKeyPrefixHint": "p. ex. ab12", "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", + "contact_typeRepeater": "Répéteur", "contact_typeRoom": "Room", - "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", + "contact_typeSensor": "Capteur", + "contact_typeUnknown": "Inconnu", "channels_via": "via {path}", + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, "chat_score": "Score", "settings_multiAck": "Plusieurs accusés de réception", "map_sharedAt": "Partagé", @@ -2400,58 +2527,152 @@ } }, "common_undo": "Annuler", - "messageStatus_sent": "Envoyer", + "messageStatus_sent": "Envoyé", "messageStatus_delivered": "Livré", - "messageStatus_pending": "Envoyer", + "messageStatus_pending": "Envoi en cours", "messageStatus_failed": "Échec de l'envoi", - "messageStatus_repeated": "Répété plusieurs fois", + "messageStatus_repeated": "Reçu plusieurs fois", "contacts_searchOpen": "Rechercher des contacts", "contacts_moreOptions": "Plus d'options", "contacts_searchClose": "Recherche avancée", - "routing_title": "Planification des itinéraires", - "routing_modeAuto": "Voiture", - "routing_modeFlood": "Inondation", + "routing_title": "Routage", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Flood", "routing_modeManual": "Manuel", "routing_modeFloodHint": "Diffusion via tous les répéteurs. La méthode la plus fiable, mais qui utilise plus de temps d'antenne.", - "routing_modeAutoHint": "Sélectionne automatiquement le chemin le plus connu, et utilise la méthode de \"inondation\" si aucun chemin n'est connu.", - "routing_modeManualHint": "Il suit toujours le chemin précis que vous avez défini.", - "routing_currentRoute": "Itinéraire actuel", + "routing_modeAutoHint": "Sélectionne automatiquement le meilleur chemin connu, et utilise le flood si aucun chemin n'est connu.", + "routing_modeManualHint": "Envoie toujours selon le chemin exact que vous avez défini.", + "routing_currentRoute": "Route actuelle", "routing_directNoHops": "Direct — sans relais", - "routing_noPathYet": "Aucune voie encore trouvée. Le message suivant est envoyé jusqu'à ce qu'une route soit découverte.", + "routing_noPathYet": "Aucun chemin pour le moment. Le prochain message passera en flood jusqu'à ce qu'une route soit découverte.", "routing_floodBroadcast": "Diffusion via tous les répéteurs", "routing_editPath": "Modifier le chemin", - "routing_forgetPath": "Oubliez le chemin", + "routing_forgetPath": "Oublier le chemin", "routing_knownPaths": "Chemins connus", - "routing_knownPathsHint": "Créez un raccourci pour y accéder.", + "routing_knownPathsHint": "Touchez un chemin pour l'utiliser.", "routing_inUse": "En cours d'utilisation", - "routing_qualityStrong": "Première étape réussie", - "routing_qualityGood": "Première étape réussie", - "routing_qualityFair": "Première étape réussie", - "routing_qualityWorked": "A livré", - "routing_qualityFlood": "Rapporté par des informations provenant de plusieurs sources.", + "routing_qualityStrong": "Premier saut fort", + "routing_qualityGood": "Premier saut correct", + "routing_qualityFair": "Premier saut moyen", + "routing_qualityWorked": "A déjà fonctionné", + "routing_qualityFlood": "Entendu via flood", "routing_qualityUntested": "Non testé", - "routing_lastWorked": "a travaillé {when}", + "routing_lastWorked": "a fonctionné {when}", "routing_neverWorked": "jamais confirmé", - "routing_floodDelivery": "Livraison en cas de inondation", - "pathEditor_hopCounter": "{count} parmi 64 houblons", + "routing_floodDelivery": "Livraison flood", + "pathEditor_hopCounter": "{count} sur 64 sauts", "pathEditor_title": "Créer un chemin", - "pathEditor_noHops": "Aucun houblon ajouté pour le moment. Cliquez sur les répétiteurs ci-dessous pour les ajouter dans l'ordre souhaité, ou enregistrez sans houblon pour envoyer directement.", - "pathEditor_addHops": "Ajoutez les houblons dans l'ordre souhaité.", + "pathEditor_noHops": "Aucun saut pour le moment. Touchez les répéteurs ci-dessous pour les ajouter dans l'ordre, ou enregistrez sans saut pour envoyer directement.", + "pathEditor_addHops": "Ajouter les sauts dans l'ordre", "pathEditor_searchRepeaters": "Rechercher des répétiteurs", "pathEditor_advancedHex": "Avancé : chemin hexadécimal brut", "pathEditor_hexLabel": "Préfixes hexadécimaux", "pathEditor_hexHelper": "Deux caractères hexadécimaux par saut, séparés par des virgules.", - "pathEditor_invalidTokens": "Incorrect : {tokens}", + "pathEditor_invalidTokens": "Invalide : {tokens}", "pathEditor_tooManyHops": "Maximum 64 sauts", - "pathEditor_usePath": "Utilisez ce chemin.", - "pathEditor_removeHop": "Éliminer le haricot", + "pathEditor_usePath": "Utiliser ce chemin", + "pathEditor_removeHop": "Supprimer le saut", "pathEditor_unknownHop": "Répéteur non identifié", - "map_zoomIn": "Zoomez", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "map_zoomOut": "Zoomez", - "map_centerMap": "Carte du centre", + "map_zoomIn": "Zoom avant", + "routing_deliveryCounts": "{successes} livrés, {failures} échoués", + "map_zoomOut": "Zoom arrière", + "map_centerMap": "Centrer la carte", "chrome_bluetoothRequiresChromium": "Web Bluetooth nécessite un navigateur Chromium.", "channels_communityShortId": "ID : {id}…", - "pathTrace_legendGpsConfirmed": "Le GPS a confirmé.", - "pathTrace_legendInferred": "Position déduite" + "pathTrace_legendGpsConfirmed": "GPS confirmé", + "pathTrace_legendInferred": "Position déduite", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "scanner_bluetoothWebUnsupported": "La fonctionnalité Bluetooth n'est pas disponible dans le navigateur. Veuillez vous connecter via USB à la place.", + "map_activity": "Activité", + "map_searchHint": "Rechercher par nom ou ID de nœud", + "map_online": "En ligne", + "map_recent": "Récents", + "map_stale": "Rancide", + "map_visible": "Visible", + "map_hidden": "Masqué", + "map_centerOnNode": "Centrer sur le nœud", + "map_details": "Détails", + "map_noGps": "Aucun GPS", + "map_noResults": "Aucun nœud correspondant", + "pathMap_viewSingle": "Simple", + "pathMap_viewCombined": "Combiné", + "pathMap_play": "Lecture", + "pathMap_pause": "Pause", + "pathMap_replay": "Rejouer", + "pathMap_stepBack": "Saut précédent", + "pathMap_stepForward": "Saut suivant", + "pathMap_animationOn": "Afficher l'animation des paquets", + "pathMap_animationOff": "Masquer l'animation des paquets", + "pathMap_hopOf": "Saut {current} sur {total}", + "pathMap_observedPaths": "Chemins observés : {count}", + "pathMap_primary": "Principal", + "pathMap_alternate": "Alternative {index}", + "pathMap_hopCount": "{count, plural, =1{1 saut} other{{count} sauts}}", + "pathMap_legendShared": "Segment partagé", + "pathMap_legendEstimated": "Segment estimé", + "pathMap_sharedNodeCount": "Utilisé par {count} chemins", + "pathMap_partialAnimation": "{count, plural, =1{Un saut n'a pas de localisation — le chemin indiqué est partiel} other{{count} sauts n'ont pas de localisation — le chemin indiqué est partiel}}", + "pathMap_showAllPaths": "Afficher tout", + "pathMap_hidePath": "Masquer le chemin", + "pathMap_showPath": "Afficher le chemin", + "pathMap_collapsePanel": "Réduire le panneau", + "pathMap_expandPanel": "Développer le panneau", + "pathMap_noLocation": "Aucune localisation", + "pathMap_followPacket": "Verrouiller la vue sur le paquet", + "pathMap_unfollowPacket": "Déverrouiller la vue du paquet", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index cd5ed8c8..044f1490 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -4,33 +4,33 @@ "nav_contacts": "Kapcsolatok", "nav_channels": "Csatornák", "nav_map": "Térkép", - "common_cancel": "Át kell venni", - "common_ok": "Rendben", - "common_connect": "Kapcsolódj", - "common_unknownDevice": "Tudatlan eszköz", - "common_save": "Mentés", + "common_cancel": "Mégsem", + "common_ok": "RENDBEN", + "common_connect": "Csatlakozás", + "common_unknownDevice": "Ismeretlen eszköz", + "common_save": "Megtakarítás", "common_delete": "Töröl", - "common_deleteAll": "Minden törlés", - "common_close": "Bezárás", + "common_deleteAll": "Összes törlése", + "common_close": "Közeli", "common_edit": "Szerkesztés", - "common_add": "Hozzáad", - "common_settings": "Beállítások", - "common_disconnect": "Csatlakozást megszakasztani", - "common_connected": "Kapcsolódó", - "common_disconnected": "Elválasztva", - "common_create": "Készítsd", + "common_add": "Hozzáadás", + "common_settings": "Beállítások elemre", + "common_disconnect": "Leválasztás", + "common_connected": "Csatlakozva", + "common_disconnected": "Szétkapcsolt", + "common_create": "Teremt", "common_continue": "Folytatás", - "common_share": "Ossza meg", + "common_share": "Részesedés", "common_copy": "Másolat", - "common_retry": "Újrapróbálja", + "common_retry": "Próbálja újra", "common_hide": "Elrejt", - "common_remove": "Eltávolít", + "common_remove": "Távolítsa el", "common_enable": "Engedélyezés", - "common_disable": "Leteteszt", + "common_disable": "Letiltás", "common_autoRefresh": "Automatikus frissítés", "common_interval": "Intervallum", - "common_reboot": "Újraindítás", - "common_loading": "Betöltés...", + "common_reboot": "Indítsa újra", + "common_loading": "Terhelés...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { @@ -48,17 +48,17 @@ } } }, - "scanner_title": "MeshCore nyitott", + "scanner_title": "MeshCore Open", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceTcpLabel": "TCP", - "tcpScreenTitle": "TCP-n keresztül kapcsolódjon", - "tcpHostLabel": "IP-cím", - "tcpHostHint": "192.168.40.10", - "tcpPortLabel": "Múzeum", + "tcpScreenTitle": "Csatlakozzon TCP-n keresztül", + "tcpHostLabel": "Végpont", + "tcpHostHint": "192.168.40.10 / example.com", + "tcpPortLabel": "Kikötő", "tcpPortHint": "5000", - "tcpStatus_notConnected": "Adja meg a célpontot, majd kapcsolja össze.", - "tcpStatus_connectingTo": "Kapcsolat a {endpoint}-hez...", + "tcpStatus_notConnected": "Adja meg a végpontot, és csatlakozzon", + "tcpStatus_connectingTo": "Csatlakozás a következőhöz: {endpoint}...", "@tcpStatus_connectingTo": { "placeholders": { "endpoint": { @@ -66,10 +66,10 @@ } } }, - "tcpErrorHostRequired": "Az IP-címet meg kell adni.", - "tcpErrorPortInvalid": "Az érték 1 és 65535 között kell lennie.", - "tcpErrorUnsupported": "A TCP-protokoll nem támogatott ez a platformon.", - "tcpErrorTimedOut": "A TCP-kapcsolat időtúllépett.", + "tcpErrorHostRequired": "Házigazda szükséges.", + "tcpErrorPortInvalid": "A portnak 1 és 65535 között kell lennie.", + "tcpErrorUnsupported": "A TCP-átvitel nem támogatott ezen a platformon.", + "tcpErrorTimedOut": "A TCP-kapcsolat időtúllépése lejárt.", "tcpConnectionFailed": "A TCP-kapcsolat sikertelen: {error}", "@tcpConnectionFailed": { "placeholders": { @@ -78,28 +78,28 @@ } } }, - "usbScreenTitle": "USB-en keresztül csatlakoztassuk", - "usbScreenSubtitle": "Válasszon egy azonosított soros eszközt, és közvetlenül csatlakoztassa a MeshCore-hoz.", - "usbScreenStatus": "Válasszon egy USB-es eszközt", - "usbScreenNote": "Az USB-es soros kommunikáció a támogatott Android eszközökön és asztali rendszereken is elérhető.", - "usbScreenEmptyState": "Nincs USB eszköz megtalálva. Csatlakoztasson egyet, majd frissítse a rendszert.", - "usbErrorPermissionDenied": "A USB-es hozzáférés megtagadva.", - "usbErrorDeviceMissing": "Az kiválasztott USB eszköz már nem elérhető.", - "usbErrorInvalidPort": "Válasszon egy érvényes USB-eszközt.", - "usbErrorBusy": "Egy másik USB-csatlakozás kérése már folyamatban van.", - "usbErrorNotConnected": "Nincs csatlakoztatott USB eszköz.", - "usbErrorOpenFailed": "Nem sikerült megnyitni a kiválasztott USB-eszközöt.", - "usbErrorConnectFailed": "Nem sikerült kapcsolatot létesíteni a kiválasztott USB-eszközhöz.", - "usbErrorUnsupported": "Ez a platform nem támogat USB-es soros kommunikációt.", - "usbErrorAlreadyActive": "Az USB-kapcsolat már be van állítva.", - "usbErrorNoDeviceSelected": "Nincs kiválasztva USB eszköz.", - "usbErrorPortClosed": "Az USB-kapcsolat nem aktív.", - "usbErrorConnectTimedOut": "Kapcsolódás sikertelen. Ellenőrizze, hogy a eszköz rendelkezik-e USB-hez tartozó firmware-rel.", - "usbFallbackDeviceName": "Web-szériás eszköz", - "usbStatus_notConnected": "Válasszon egy USB-es eszközt", - "usbStatus_connecting": "USB eszközhez való csatlakozás...", - "usbStatus_searching": "USB eszközök keresése...", - "usbConnectionFailed": "USB-kapcsolat sikertelen: {error}", + "usbScreenTitle": "Csatlakoztassa USB-n keresztül", + "usbScreenSubtitle": "Válasszon egy észlelt soros eszközt, és csatlakozzon közvetlenül a MeshCore csomóponthoz.", + "usbScreenStatus": "Válasszon ki egy USB-eszközt", + "usbScreenNote": "Az USB-soros aktív a támogatott Android-eszközökön és asztali platformokon.", + "usbScreenEmptyState": "Nem található USB-eszköz. Csatlakoztasson egyet, és frissítse.", + "usbErrorPermissionDenied": "Az USB-engedély megtagadva.", + "usbErrorDeviceMissing": "A kiválasztott USB-eszköz már nem érhető el.", + "usbErrorInvalidPort": "Válasszon ki egy érvényes USB-eszközt.", + "usbErrorBusy": "Egy másik USB-csatlakozási kérelem már folyamatban van.", + "usbErrorNotConnected": "Nincs USB-eszköz csatlakoztatva.", + "usbErrorOpenFailed": "Nem sikerült megnyitni a kiválasztott USB-eszközt.", + "usbErrorConnectFailed": "Nem sikerült csatlakozni a kiválasztott USB-eszközhöz.", + "usbErrorUnsupported": "Az USB-soros nem támogatott ezen a platformon.", + "usbErrorAlreadyActive": "Az USB-kapcsolat már aktív.", + "usbErrorNoDeviceSelected": "Nem lett kiválasztva USB-eszköz.", + "usbErrorPortClosed": "Az USB-csatlakozás nincs nyitva.", + "usbErrorConnectTimedOut": "A kapcsolat időtúllépése lejárt. Győződjön meg arról, hogy az eszköz rendelkezik USB Companion firmware-rel.", + "usbFallbackDeviceName": "Web soros eszköz", + "usbStatus_notConnected": "Válasszon ki egy USB-eszközt", + "usbStatus_connecting": "Csatlakozás USB-eszközhöz...", + "usbStatus_searching": "USB-eszközök keresése...", + "usbConnectionFailed": "USB csatlakozás sikertelen: {error}", "@usbConnectionFailed": { "placeholders": { "error": { @@ -107,11 +107,11 @@ } } }, - "scanner_scanning": "Készülékek keresése...", - "scanner_connecting": "Kapcsolódás...", - "scanner_disconnecting": "Kapcsolat megszakad...", - "scanner_notConnected": "Nem csatlakozva", - "scanner_connectedTo": "Kapcsolódik a {deviceName}-hez", + "scanner_scanning": "Eszközök keresése...", + "scanner_connecting": "Csatlakozás...", + "scanner_disconnecting": "Leválasztás...", + "scanner_notConnected": "Nincs csatlakoztatva", + "scanner_connectedTo": "Csatlakozva a következőhöz: {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -120,8 +120,8 @@ } }, "scanner_searchingDevices": "MeshCore eszközök keresése...", - "scanner_tapToScan": "A Tap Scan funkció segítségével kereshet MeshCore eszközöket.", - "scanner_connectionFailed": "Kapcsolódás sikertelen: {error}", + "scanner_tapToScan": "Koppintson a Scan elemre a MeshCore-eszközök megkereséséhez", + "scanner_connectionFailed": "A csatlakozás sikertelen: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -129,66 +129,66 @@ } } }, - "scanner_stop": "Megállj", - "scanner_scan": "Szkenálás", - "scanner_bluetoothOff": "A Bluetooth kikapcsolva", - "scanner_bluetoothOffMessage": "Kérjük, kapcsolja be a Bluetooth-ot, hogy eszközök keresése lehessen.", + "scanner_stop": "Stop", + "scanner_scan": "Letapogatás", + "scanner_bluetoothOff": "Bluetooth ki van kapcsolva", + "scanner_bluetoothOffMessage": "Kérjük, kapcsolja be a Bluetooth-t az eszközök kereséséhez", "scanner_chromeRequired": "Chrome böngésző szükséges", - "scanner_chromeRequiredMessage": "Ez az alkalmazás a Bluetooth funkcióhoz Google Chrome-ot vagy Chromium alapú böngészőt igényel.", - "scanner_enableBluetooth": "Engedje be a Bluetooth funkciót", + "scanner_chromeRequiredMessage": "Ehhez a webes alkalmazáshoz Google Chrome vagy Chromium-alapú böngésző szükséges a Bluetooth támogatásához.", + "scanner_enableBluetooth": "Bluetooth engedélyezése", "device_quickSwitch": "Gyors váltás", "device_meshcore": "MeshCore", - "settings_title": "Beállítások", - "settings_deviceInfo": "A készülék információi", - "settings_appSettings": "Alkalmazási beállítások", - "settings_appSettingsSubtitle": "Értesítések, üzenetküldés és térképi beállítások", - "settings_nodeSettings": "Műközép beállítások", - "settings_nodeName": "Vonal neve", - "settings_nodeNameNotSet": "Nem megállapított", - "settings_nodeNameHint": "Adja meg a csomópont nevét", - "settings_nodeNameUpdated": "Neve frissítve", + "settings_title": "Beállítások elemre", + "settings_deviceInfo": "Készülék Info", + "settings_appSettings": "Alkalmazásbeállítások", + "settings_appSettingsSubtitle": "Értesítések, üzenetküldés és térképbeállítások", + "settings_nodeSettings": "Csomópont beállításai", + "settings_nodeName": "Csomópont neve", + "settings_nodeNameNotSet": "Nincs beállítva", + "settings_nodeNameHint": "Írja be a csomópont nevét", + "settings_nodeNameUpdated": "Név frissítve", "settings_radioSettings": "Rádióbeállítások", - "settings_radioSettingsSubtitle": "Frekvencia, teljesítmény, szélesítési tényező", - "settings_radioSettingsUpdated": "A rádió beállítások frissítve", - "settings_location": "Helyszín", + "settings_radioSettingsSubtitle": "Frekvencia, teljesítmény, szórási tényező", + "settings_radioSettingsUpdated": "A rádió beállításai frissítve", + "settings_location": "hely", "settings_locationSubtitle": "GPS koordináták", - "settings_locationUpdated": "A helyzet és a GPS beállítások frissítve", - "settings_locationBothRequired": "Kérjük, adja meg a földrajzi szélességet és hosszúságot.", - "settings_locationInvalid": "Érvénytelen szélesszög vagy hosszszög.", - "settings_locationGPSEnable": "GPS engedélyezve", - "settings_locationGPSEnableSubtitle": "Lehetővé teszi, hogy a GPS automatikusan frissítse a helyzetet.", - "settings_locationIntervalSec": "GPS-számolási intervallum (másodpercek)", - "settings_locationIntervalInvalid": "Az intervallum legalább 60 másodpercnek, de legfeljebb 86400 másodpercnak kell lennie.", - "settings_latitude": "Nyugat-–––––––––––––––––––––––––––––––", - "settings_longitude": "hosszúság", - "settings_contactSettings": "Kapcsolat beállítások", - "settings_contactSettingsSubtitle": "Beállítások, amelyek meghatározzák, hogyan lehet új kapcsolatokat hozzáadni.", - "settings_privacyMode": "Adatvédelem mód", - "settings_privacyModeSubtitle": "Elrejtsük a nevét/a helyszínt az űrianyagokban", - "settings_privacyModeToggle": "Engedje be a privát üzemmódot, hogy elrejtse a nevét és a helyét az online hirdetésekben.", - "settings_privacyModeEnabled": "Adatvédelem mód beállítva", - "settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva", - "settings_actions": "Tevékenységek", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", + "settings_locationUpdated": "A hely- és GPS-beállítások frissítve", + "settings_locationBothRequired": "Adja meg a szélességi és hosszúsági fokokat is.", + "settings_locationInvalid": "Érvénytelen szélesség vagy hosszúság.", + "settings_locationGPSEnable": "GPS engedélyezése", + "settings_locationGPSEnableSubtitle": "Lehetővé teszi a GPS számára a hely automatikus frissítését.", + "settings_locationIntervalSec": "GPS intervallum (másodperc)", + "settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.", + "settings_latitude": "Szélesség", + "settings_longitude": "Hosszúság", + "settings_contactSettings": "Kapcsolati beállítások", + "settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.", + "settings_privacyMode": "Adatvédelmi mód", + "settings_privacyModeSubtitle": "Név/hely elrejtése a hirdetésekben", + "settings_privacyModeToggle": "Kapcsolja be az adatvédelmi módot, hogy elrejtse nevét és tartózkodási helyét a hirdetésekben.", + "settings_privacyModeEnabled": "Az adatvédelmi mód engedélyezve", + "settings_privacyModeDisabled": "Az adatvédelmi mód letiltva", + "settings_actions": "Akciók", + "settings_deleteAllPaths": "Az összes elérési út törlése", + "settings_deleteAllPathsSubtitle": "Törölje az összes elérési utat a névjegyekből.", "settings_sendAdvertisement": "Hirdetés küldése", - "settings_sendAdvertisementSubtitle": "A nyilvános megjelenés", - "settings_advertisementSent": "Hirdetés elküldve", - "settings_syncTime": "Szinkronizációs idő", - "settings_syncTimeSubtitle": "Állítsa a készülék időzítését a telefon időjére", - "settings_timeSynchronized": "Időben szinkronizált", - "settings_refreshContacts": "Újraindítsd a kapcsolatok listát", - "settings_refreshContactsSubtitle": "Újra töltse a kontaktlista-adatokat a készülékről", - "settings_rebootDevice": "Újraindítás", - "settings_rebootDeviceSubtitle": "Indítsa újra a MeshCore eszközt.", - "settings_rebootDeviceConfirm": "Biztosan szeretné újraindítani a készüléket? Ebben az esetben a kapcsolat megszűnik.", + "settings_sendAdvertisementSubtitle": "Jelenlét a közvetítésben most", + "settings_advertisementSent": "Reklám elküldve", + "settings_syncTime": "Szinkronizálási idő", + "settings_syncTimeSubtitle": "Az eszköz órájának beállítása a telefon idejére", + "settings_timeSynchronized": "Idő szinkronizálva", + "settings_refreshContacts": "Névjegyek frissítése", + "settings_refreshContactsSubtitle": "Névjegylista újratöltése az eszközről", + "settings_rebootDevice": "Eszköz újraindítása", + "settings_rebootDeviceSubtitle": "Indítsa újra a MeshCore eszközt", + "settings_rebootDeviceConfirm": "Biztosan újra akarja indítani az eszközt? Megszakad a kapcsolat.", "settings_debug": "Hibakeresés", "settings_bleDebugLog": "BLE hibaelhárítási napló", "settings_bleDebugLogSubtitle": "BLE parancsok, válaszok és alapvető adatok", - "settings_appDebugLog": "App-debug log", - "settings_appDebugLogSubtitle": "Programozási hibajelzések", - "settings_about": "Ról", - "settings_aboutVersion": "MeshCore Open {version} verzió", + "settings_appDebugLog": "Alkalmazás hibakeresési naplója", + "settings_appDebugLogSubtitle": "Alkalmazás hibakeresési üzenetei", + "settings_about": "Körülbelül", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -196,29 +196,29 @@ } } }, - "settings_aboutLegalese": "2026-os MeshCore nyílt forráskódú projekt", - "settings_aboutDescription": "Egy nyílt forráskódú Flutter kliens a MeshCore LoRa hálózati eszközök számára.", + "settings_aboutLegalese": "2026 MeshCore nyílt forráskódú projekt", + "settings_aboutDescription": "Nyílt forráskódú Flutter kliens MeshCore LoRa mesh hálózati eszközökhöz.", "settings_aboutOpenMeteoAttribution": "LOS magassági adatok: Open-Meteo (CC BY 4.0)", "settings_infoName": "Név", - "settings_infoId": "Az azonosító", + "settings_infoId": "ID", "settings_infoStatus": "Állapot", - "settings_infoBattery": "Akku", - "settings_infoPublicKey": "Nyelvkönyv", - "settings_infoContactsCount": "Kapcsolatok száma", - "settings_infoChannelCount": "Csatorna száma", - "settings_presets": "Előre beállított beállítások", + "settings_infoBattery": "Akkumulátor", + "settings_infoPublicKey": "Nyilvános kulcs", + "settings_infoContactsCount": "Névjegyek száma", + "settings_infoChannelCount": "Csatornaszám", + "settings_presets": "Előbeállítások", "settings_frequency": "Frekvencia (MHz)", - "settings_frequencyHelper": "300,0 – 2500,0", + "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Érvénytelen frekvencia (300-2500 MHz)", - "settings_bandwidth": "Kapacitás", - "settings_spreadingFactor": "Terjesztési tényező", + "settings_bandwidth": "Sávszélesség", + "settings_spreadingFactor": "Spreading Factor", "settings_codingRate": "Kódolási sebesség", "settings_txPower": "TX teljesítmény (dBm)", - "settings_txPowerHelper": "0 – 22", + "settings_txPowerHelper": "0-22", "settings_txPowerInvalid": "Érvénytelen TX teljesítmény (0-22 dBm)", - "settings_clientRepeat": "Autonóm rendszer újra", - "settings_clientRepeatSubtitle": "Engedje, hogy ez a eszköz mások számára is ismételje a hálózati csomagokat.", - "settings_clientRepeatFreqWarning": "A hálózat nélküli kommunikációhoz 433, 869 vagy 918 MHz frekvenciát igényel.", + "settings_clientRepeat": "Hálózaton kívüli ismétlés", + "settings_clientRepeatSubtitle": "Engedélyezze az eszköznek, hogy megismételje a mesh-csomagokat mások számára", + "settings_clientRepeatFreqWarning": "A hálózaton kívüli ismétlés 433, 869 vagy 918 MHz frekvenciát igényel", "settings_error": "Hiba: {message}", "@settings_error": { "placeholders": { @@ -227,62 +227,62 @@ } } }, - "appSettings_title": "Alkalmazási beállítások", + "appSettings_title": "Alkalmazásbeállítások", "appSettings_appearance": "Megjelenés", "appSettings_theme": "Téma", - "appSettings_themeSystem": "Alapértékek", - "appSettings_themeLight": "Világítás", + "appSettings_themeSystem": "Rendszer alapértelmezett", + "appSettings_themeLight": "Fény", "appSettings_themeDark": "Sötét", "appSettings_language": "Nyelv", - "appSettings_languageSystem": "Alapértékek", - "appSettings_languageEn": "Angol", - "appSettings_languageFr": "Francia", - "appSettings_languageEs": "Spanyol", - "appSettings_languageDe": "Német", - "appSettings_languagePl": "Lengyel", - "appSettings_languageSl": "szlovén nyelv", - "appSettings_languagePt": "Portugál", - "appSettings_languageIt": "Olasz", - "appSettings_languageZh": "Kínai", - "appSettings_languageSv": "Svéd", - "appSettings_languageNl": "Hollandi", - "appSettings_languageSk": "Szlovén nyelvre fordítás", - "appSettings_languageBg": "Bulgár", - "appSettings_languageRu": "Orosz", - "appSettings_languageUk": "Украинский", - "appSettings_enableMessageTracing": "Engedje meg a üzenetek nyomon követését", - "appSettings_enableMessageTracingSubtitle": "Adja meg a üzenetek részletes útvonal- és időzítési adatokat.", + "appSettings_languageSystem": "Rendszer alapértelmezett", + "appSettings_languageEn": "angol", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", + "appSettings_languageDe": "Deutsch", + "appSettings_languagePl": "Polski", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", + "appSettings_languageIt": "Italiano", + "appSettings_languageZh": "中文", + "appSettings_languageSv": "Svenska", + "appSettings_languageNl": "Nederlands", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_languageRu": "Русский", + "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Üzenetkövetés engedélyezése", + "appSettings_enableMessageTracingSubtitle": "Az üzenetek részletes útválasztási és időzítési metaadatainak megjelenítése", "appSettings_notifications": "Értesítések", - "appSettings_enableNotifications": "Engedélyezze az értesítéseket", - "appSettings_enableNotificationsSubtitle": "Kapjon értesítéseket üzenetekről és hirdetésekről.", - "appSettings_notificationPermissionDenied": "A értesítési engedély megtagadva", - "appSettings_notificationsEnabled": "A figyelmeztetések engedélyezve", - "appSettings_notificationsDisabled": "A figyelmeztetések kikapcsolva", - "appSettings_messageNotifications": "Üzenet értesítések", - "appSettings_messageNotificationsSubtitle": "A figyelmeztetést megjelenítve, amikor új üzenet érkezik", - "appSettings_channelMessageNotifications": "Csatorna-üzenetek értesítése", - "appSettings_channelMessageNotificationsSubtitle": "A figyelmeztetést megjelenítve, amikor új üzenet érkezik a csatornáról", - "appSettings_advertisementNotifications": "Reklám értesítések", - "appSettings_advertisementNotificationsSubtitle": "A figyelmeztetést megjelenítve, amikor új csomópontok kerülnek felfedezésre.", - "appSettings_messaging": "Üzenetek küldése", - "appSettings_clearPathOnMaxRetry": "Egyértelmű út a Max Retry funkció használatával", - "appSettings_clearPathOnMaxRetrySubtitle": "A kapcsolat visszaállítás 5 sikertelen továbbítás után", - "appSettings_pathsWillBeCleared": "Ha 5-szer sikertelenül próbálunk, a útvonalat automatikusan tisztítjuk.", - "appSettings_pathsWillNotBeCleared": "A utak automatikusan nem tisztítódnak.", - "appSettings_autoRouteRotation": "Autóútok forgása", - "appSettings_autoRouteRotationSubtitle": "Válasszon a legjobb útvonalak között, vagy válassza a vízözön-módot.", - "appSettings_autoRouteRotationEnabled": "Az automatikus útvonalváltás engedélyezve", - "appSettings_autoRouteRotationDisabled": "Az automatikus útvonal-választás funkció kikapcsolva.", - "appSettings_maxRouteWeight": "Maximális útvonal súly", - "appSettings_maxRouteWeightSubtitle": "A lehető legnagyobb súly, amit egy útvonal sikeres szállítmányok során összegyűjthet.", - "appSettings_initialRouteWeight": "A kezdeti útvonal súlya", - "appSettings_initialRouteWeightSubtitle": "Az új, felfedezett útvonalakhoz tartozó kezdeti súly", - "appSettings_routeWeightSuccessIncrement": "Sikerhez vezető növelés", - "appSettings_routeWeightSuccessIncrementSubtitle": "A sikeresen teljesített útvonalhoz hozzáadott súly.", - "appSettings_routeWeightFailureDecrement": "Hibás súly csökkenése", - "appSettings_routeWeightFailureDecrementSubtitle": "A jártatásból eltávolított súly, ami a sikertelen szállítás következménye.", - "appSettings_maxMessageRetries": "Maximális üzenetek újraküldési próbálkozások", - "appSettings_maxMessageRetriesSubtitle": "A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.", + "appSettings_enableNotifications": "Értesítések engedélyezése", + "appSettings_enableNotificationsSubtitle": "Értesítések fogadása üzenetekről és hirdetésekről", + "appSettings_notificationPermissionDenied": "Értesítési engedély megtagadva", + "appSettings_notificationsEnabled": "Értesítések engedélyezve", + "appSettings_notificationsDisabled": "Az értesítések letiltva", + "appSettings_messageNotifications": "Üzenetértesítések", + "appSettings_messageNotificationsSubtitle": "Értesítés megjelenítése új üzenetek fogadásakor", + "appSettings_channelMessageNotifications": "Csatorna üzenetek értesítései", + "appSettings_channelMessageNotificationsSubtitle": "Értesítés megjelenítése csatornaüzenetek fogadásakor", + "appSettings_advertisementNotifications": "Reklám Értesítések", + "appSettings_advertisementNotificationsSubtitle": "Értesítés megjelenítése új csomópontok felfedezésekor", + "appSettings_messaging": "Üzenetküldés", + "appSettings_clearPathOnMaxRetry": "Útvonal törlése a Max. újrapróbálkozásnál", + "appSettings_clearPathOnMaxRetrySubtitle": "Állítsa vissza a kapcsolati elérési utat 5 sikertelen küldési kísérlet után", + "appSettings_pathsWillBeCleared": "Az elérési utak 5 sikertelen újrapróbálkozás után törlődnek", + "appSettings_pathsWillNotBeCleared": "Az útvonalak nem törlődnek automatikusan", + "appSettings_autoRouteRotation": "Auto Route Rotation", + "appSettings_autoRouteRotationSubtitle": "Váltson a legjobb útvonalak és az elárasztási mód között", + "appSettings_autoRouteRotationEnabled": "Az automatikus útvonalforgatás engedélyezve", + "appSettings_autoRouteRotationDisabled": "Az automatikus útvonalforgatás letiltva", + "appSettings_maxRouteWeight": "Maximális útvonalsúly", + "appSettings_maxRouteWeightSubtitle": "Maximális súly, amelyet egy útvonal felhalmozhat a sikeres szállításokból", + "appSettings_initialRouteWeight": "Az útvonal kezdeti súlya", + "appSettings_initialRouteWeightSubtitle": "Kezdősúly az újonnan felfedezett utakhoz", + "appSettings_routeWeightSuccessIncrement": "Siker súlynövekedés", + "appSettings_routeWeightSuccessIncrementSubtitle": "Súly hozzáadva egy útvonalhoz a sikeres szállítás után", + "appSettings_routeWeightFailureDecrement": "Hiba súlycsökkentés", + "appSettings_routeWeightFailureDecrementSubtitle": "Sikertelen szállítás után eltávolított súly az útvonalról", + "appSettings_maxMessageRetries": "Üzenet újrapróbálkozások maximális száma", + "appSettings_maxMessageRetriesSubtitle": "Az üzenet sikertelenként való megjelölése előtti újrapróbálkozások száma", "path_routeWeight": "{weight}/{max}", "@path_routeWeight": { "placeholders": { @@ -294,9 +294,9 @@ } } }, - "appSettings_battery": "Akku", - "appSettings_batteryChemistry": "Aakkum töltés kémia", - "appSettings_batteryChemistryPerDevice": "Beállítások {deviceName}-hez", + "appSettings_battery": "Akkumulátor", + "appSettings_batteryChemistry": "Akkumulátor kémia", + "appSettings_batteryChemistryPerDevice": "Beállítás eszközenként ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -304,20 +304,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Csatlakozzon egy eszközhez, hogy kiválassza", + "appSettings_batteryChemistryConnectFirst": "A választáshoz csatlakozzon egy eszközhöz", "appSettings_batteryNmc": "18650 NMC (3,0-4,2 V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65 V)", "appSettings_batteryLipo": "LiPo (3,0-4,2 V)", "appSettings_mapDisplay": "Térkép megjelenítése", - "appSettings_showRepeaters": "Megismétlés", - "appSettings_showRepeatersSubtitle": "A térképen megjelenítsük a repeater-eket.", - "appSettings_showChatNodes": "Megjeleníts kommunikációs pontokat", - "appSettings_showChatNodesSubtitle": "A chat-szobákat megjelenítsük a térképen", - "appSettings_showOtherNodes": "Mutasson további csomópontokat", - "appSettings_showOtherNodesSubtitle": "Mutassa meg a többi hálózati elemet a térképen", - "appSettings_timeFilter": "Időbeli szűrés", - "appSettings_timeFilterShowAll": "Mutassa meg az összes csomópontot", - "appSettings_timeFilterShowLast": "Mutasson az utolsó {hours} órából származó adatokat.", + "appSettings_showRepeaters": "Ismétlők megjelenítése", + "appSettings_showRepeatersSubtitle": "Az átjátszó csomópontok megjelenítése a térképen", + "appSettings_showChatNodes": "Chat csomópontok megjelenítése", + "appSettings_showChatNodesSubtitle": "A csevegési csomópontok megjelenítése a térképen", + "appSettings_showOtherNodes": "Más csomópontok megjelenítése", + "appSettings_showOtherNodesSubtitle": "Más csomóponttípusok megjelenítése a térképen", + "appSettings_timeFilter": "Időszűrő", + "appSettings_timeFilterShowAll": "Az összes csomópont megjelenítése", + "appSettings_timeFilterShowLast": "Az elmúlt {hours} óra csomópontjainak megjelenítése", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -326,18 +326,18 @@ } }, "appSettings_mapTimeFilter": "Térkép időszűrő", - "appSettings_showNodesDiscoveredWithin": "Megjeleníts olyan węzveket, amelyek a következő területen lettek felfedezve:", - "appSettings_allTime": "Minden időpont", - "appSettings_lastHour": "Az utolsó óra", - "appSettings_last6Hours": "Az utóban 6 óra", - "appSettings_last24Hours": "Az utóbbi 24 óra", - "appSettings_lastWeek": "A múlt héten", - "appSettings_offlineMapCache": "Offline térkép tárolás", + "appSettings_showNodesDiscoveredWithin": "Itt talált csomópontok megjelenítése:", + "appSettings_allTime": "Minden alkalommal", + "appSettings_lastHour": "Utolsó óra", + "appSettings_last6Hours": "Utolsó 6 óra", + "appSettings_last24Hours": "Az elmúlt 24 óra", + "appSettings_lastWeek": "Múlt héten", + "appSettings_offlineMapCache": "Offline térképgyorsítótár", "appSettings_unitsTitle": "Egységek", - "appSettings_unitsMetric": "Méter (m / kilométer)", - "appSettings_unitsImperial": "Királyi (láb / mérföld)", - "appSettings_noAreaSelected": "Nincs kiválasztott terület.", - "appSettings_areaSelectedZoom": "Kiválasztott terület (zoom: {minZoom}-{maxZoom})", + "appSettings_unitsMetric": "Metrikus (m/km)", + "appSettings_unitsImperial": "birodalmi (ft/mi)", + "appSettings_noAreaSelected": "Nincs kiválasztott terület", + "appSettings_areaSelectedZoom": "Kijelölt terület (nagyítás {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -349,16 +349,16 @@ } }, "appSettings_debugCard": "Hibakeresés", - "appSettings_appDebugLogging": "App-ban történő hibakereséshez használt naplózás", - "appSettings_appDebugLoggingSubtitle": "Log alkalmazás hibaelhárítási üzenetek", - "appSettings_appDebugLoggingEnabled": "Az alkalmazás hibaelhárítási naplózás engedélyezve", - "appSettings_appDebugLoggingDisabled": "Az alkalmazás hibaelhárítási naplózatának bekapcsolása kiküszöbölve", + "appSettings_appDebugLogging": "Alkalmazások hibakeresési naplózása", + "appSettings_appDebugLoggingSubtitle": "A hibaelhárításhoz naplózza az alkalmazás hibakeresési üzeneteit", + "appSettings_appDebugLoggingEnabled": "Alkalmazások hibakeresési naplózása engedélyezve", + "appSettings_appDebugLoggingDisabled": "Az alkalmazáshibakeresési naplózás letiltva", "contacts_title": "Kapcsolatok", - "contacts_noContacts": "Jelenleg még nincs kapcsolat.", - "contacts_contactsWillAppear": "A kapcsolatok megjelennek, amikor a eszközök hirdetnek.", - "contacts_unread": "Olvasatlan", - "contacts_searchContactsNoNumber": "Kapcsolatok keresése...", - "contacts_searchContacts": "Keresés {number}-ban {str}…", + "contacts_noContacts": "Még nincsenek elérhetőségek", + "contacts_contactsWillAppear": "A névjegyek akkor jelennek meg, amikor az eszközök hirdetnek", + "contacts_unread": "Nem olvasott", + "contacts_searchContactsNoNumber": "Névjegyek keresése...", + "contacts_searchContacts": "Keresés a {number}{str} névjegyekben...", "@contacts_searchContacts": { "placeholders": { "number": { @@ -369,7 +369,7 @@ } } }, - "contacts_searchFavorites": "Keresés {number}{str}... Kedvencek", + "contacts_searchFavorites": "Keresés a {number}{str} Kedvencek között...", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -380,7 +380,7 @@ } } }, - "contacts_searchUsers": "Search {number}{str} Users...", + "contacts_searchUsers": "Keresés a {number}{str} felhasználók között...", "@contacts_searchUsers": { "placeholders": { "number": { @@ -391,7 +391,7 @@ } } }, - "contacts_searchRepeaters": "Keresés {number}-on, {str} típusú adóállomások között...", + "contacts_searchRepeaters": "Keresés a {number}{str} átjátszók között...", "@contacts_searchRepeaters": { "placeholders": { "number": { @@ -402,7 +402,7 @@ } } }, - "contacts_searchRoomServers": "Keresés {number}-ban {str}...", + "contacts_searchRoomServers": "Keresés a {number}{str} szobaszervereken...", "@contacts_searchRoomServers": { "placeholders": { "number": { @@ -413,10 +413,10 @@ } } }, - "contacts_noUnreadContacts": "Nincs olvasatlan üzenetek", - "contacts_noContactsFound": "Nincs megtalálva semmilyen kapcsolat vagy csoport.", - "contacts_deleteContact": "Kapcsolattól töröl", - "contacts_removeConfirm": "Hogy töröljem a {contactName} nevű személyt a kontaktlistából?", + "contacts_noUnreadContacts": "Nincsenek olvasatlan névjegyek", + "contacts_noContactsFound": "Nem található névjegy vagy csoport", + "contacts_deleteContact": "Névjegy törlése", + "contacts_removeConfirm": "Eltávolítja a {contactName} alkalmazást a névjegyek közül?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -424,13 +424,13 @@ } } }, - "contacts_manageRepeater": "Ellenőriző eszköz kezelése", - "contacts_manageRoom": "A szobai szerver kezelése", - "contacts_roomLogin": "Szoba szerverbe való bejelentkezés", - "contacts_openChat": "Nyitott beszélgetés", - "contacts_editGroup": "Edit csoport", + "contacts_manageRepeater": "Repeater kezelése", + "contacts_manageRoom": "Szobaszerver kezelése", + "contacts_roomLogin": "Szobaszerver bejelentkezés", + "contacts_openChat": "Nyissa meg a Chat lehetőséget", + "contacts_editGroup": "Csoport szerkesztése", "contacts_deleteGroup": "Csoport törlése", - "contacts_deleteGroupConfirm": "Hogy töröljem a \"{groupName}\"-t?", + "contacts_deleteGroupConfirm": "Eltávolítja a következőt: \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -440,9 +440,9 @@ }, "contacts_newGroup": "Új csoport", "contacts_groupName": "Csoport neve", - "contacts_groupNameRequired": "A csoportnak meg kell adni a nevét.", - "contacts_groupNameReserved": "Ez a csoportnév foglalt", - "contacts_groupAlreadyExists": "A \"{name}\" nevű csoport már létezik.", + "contacts_groupNameRequired": "A csoportnév megadása kötelező", + "contacts_groupNameReserved": "Ez a csoportnév fenntartva", + "contacts_groupAlreadyExists": "A \"{name}\" csoport már létezik", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -450,11 +450,11 @@ } } }, - "contacts_filterContacts": "Szűrj kontaktokat...", - "contacts_noContactsMatchFilter": "Nincs találat a megadott szűrés alapján.", + "contacts_filterContacts": "Névjegyek szűrése...", + "contacts_noContactsMatchFilter": "Egyetlen névjegy sem felel meg a szűrőnek", "contacts_noMembers": "Nincsenek tagok", - "contacts_lastSeenNow": "utóbbi időben", - "contacts_lastSeenMinsAgo": "~ {minutes} perc", + "contacts_lastSeenNow": "nemrég", + "contacts_lastSeenMinsAgo": "~ {minutes} min.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -462,7 +462,7 @@ } } }, - "contacts_lastSeenHourAgo": "Kb. 1 óra", + "contacts_lastSeenHourAgo": "~ 1 óra", "contacts_lastSeenHoursAgo": "~ {hours} óra", "@contacts_lastSeenHoursAgo": { "placeholders": { @@ -471,8 +471,8 @@ } } }, - "contacts_lastSeenDayAgo": "Kb. 1 nap", - "contacts_lastSeenDaysAgo": "~ {days} days", + "contacts_lastSeenDayAgo": "~ 1 nap", + "contacts_lastSeenDaysAgo": "~ {days} nap", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -481,11 +481,11 @@ } }, "channels_title": "Csatornák", - "channels_noChannelsConfigured": "Nincs konfigurált csatorna.", - "channels_addPublicChannel": "Hozzon létre nyilvános csatornát", - "channels_searchChannels": "Keresési opciók...", - "channels_noChannelsFound": "Nincs megtalálható csatorna", - "channels_channelIndex": "{index}-os csatorna", + "channels_noChannelsConfigured": "Nincsenek konfigurálva csatornák", + "channels_addPublicChannel": "Nyilvános csatorna hozzáadása", + "channels_searchChannels": "Csatornák keresése...", + "channels_noChannelsFound": "Nem található csatorna", + "channels_channelIndex": "Csatorna {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -493,13 +493,13 @@ } } }, - "channels_public": "A nyilvánosság számára", - "channels_private": "Személyes", + "channels_public": "Nyilvános", + "channels_private": "Magán", "channels_editChannel": "Csatorna szerkesztése", - "channels_muteChannel": "Csendes csatorna", - "channels_unmuteChannel": "Engedje be a hangot", - "channels_deleteChannel": "Mozdony törlése", - "channels_deleteChannelConfirm": "Törlés {name}? Ez nem visszafordítható.", + "channels_muteChannel": "Csatorna némítása", + "channels_unmuteChannel": "Csatorna némításának feloldása", + "channels_deleteChannel": "Csatorna törlése", + "channels_deleteChannelConfirm": "Törli a következőt: \"{name}\"? Ezt nem lehet visszavonni.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -507,7 +507,7 @@ } } }, - "channels_channelDeleteFailed": "Nem sikerült törölni a \"{name}\" nevű csatornát.", + "channels_channelDeleteFailed": "Nem sikerült törölni a \"{name}\" csatornát", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -515,7 +515,7 @@ } } }, - "channels_channelDeleted": "A \"{name}\" nevű csatorna törölve", + "channels_channelDeleted": "A \"{name}\" csatorna törölve", "@channels_channelDeleted": { "placeholders": { "name": { @@ -523,15 +523,15 @@ } } }, - "channels_addChannel": "Csatorna hozzáadása", - "channels_channelIndexLabel": "Csatorna index", + "channels_addChannel": "Csatorna hozzáadása lehetőségre", + "channels_channelIndexLabel": "Csatorna indexe", "channels_channelName": "Csatorna neve", - "channels_usePublicChannel": "Használja a nyilvános csatornát", - "channels_standardPublicPsk": "Általános, állami által finanszírozott PSK", - "channels_pskHex": "PSK (Hexadecimális kód)", - "channels_generateRandomPsk": "Véletlenszerűen generáljon PSK-t", - "channels_enterChannelName": "Kérjük, adja meg egy csatorna nevét", - "channels_pskMustBe32Hex": "A PSK 32-bázisú hexadecimális karakterből áll.", + "channels_usePublicChannel": "Nyilvános csatorna használata", + "channels_standardPublicPsk": "Normál nyilvános PSK", + "channels_pskHex": "PSK (Hex)", + "channels_generateRandomPsk": "Véletlenszerű PSK generálása", + "channels_enterChannelName": "Kérjük, adja meg a csatorna nevét", + "channels_pskMustBe32Hex": "A PSK-nak 32 hexadecimális karakterből kell állnia", "channels_channelAdded": "A \"{name}\" csatorna hozzáadva", "@channels_channelAdded": { "placeholders": { @@ -540,7 +540,7 @@ } } }, - "channels_editChannelTitle": "Módosítsd a csatornát {index}", + "channels_editChannelTitle": "Csatorna szerkesztése {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -548,24 +548,24 @@ } } }, - "channels_smazCompression": "SMAZ kompresszió", - "channels_cyr2latCompression": "Cyr2Lat kompresszió", - "channels_cyr2latCompressionDscr": "Néhány Cirill betűt Latin betűkkel helyettesít küldéskor.", - "channels_cyr2latSettingsHeading": "Cyr2Lat beállítások", - "channels_cyr2latSettingsSubheading": "Helyettesítési lista", - "channels_cyr2latSettingsDscr": "A karakterhelyettesítési JSON-konfiguráció szerkesztése", - "channels_cyr2latSettingsDialogHint": "JSON-csere táblázat", - "channels_cyr2latSettingsDialogWrongJSON": "Hibás JSON: {error}", - "settings_cyr2latProfileAdd": "Cyr2Lat-profil hozzáadása", + "channels_smazCompression": "SMAZ tömörítés", + "channels_cyr2latCompression": "Cyr2Lat tömörítés", + "channels_cyr2latCompressionDscr": "Küldéskor egyes cirill karaktereket latin karakterekre cserél.", + "channels_cyr2latSettingsHeading": "Cyr2Lat beállítás", + "channels_cyr2latSettingsSubheading": "A helyettesítők listája", + "channels_cyr2latSettingsDscr": "Szerkessze a karaktercsere JSON-konfigurációját", + "channels_cyr2latSettingsDialogHint": "JSON cseretérkép", + "channels_cyr2latSettingsDialogWrongJSON": "Érvénytelen JSON: {error}", + "settings_cyr2latProfileAdd": "Cyr2Lat profil hozzáadása", "settings_cyr2latProfileName": "Profil neve", - "settings_cyr2latProfileNameEmpty": "A profil neve nem lehet üres", - "settings_cyr2latProfileAdded": "A profil hozzáadása sikeres", - "settings_cyr2latProfileUpdated": "A profil frissítése sikeres", - "settings_cyr2latProfileEdit": "Cyr2Lat profil szerkesztése", + "settings_cyr2latProfileNameEmpty": "A profilnév nem lehet üres", + "settings_cyr2latProfileAdded": "A profil sikeresen hozzáadva", + "settings_cyr2latProfileUpdated": "A profil sikeresen frissítve", + "settings_cyr2latProfileEdit": "Szerkessze a Cyr2Lat profilt", "settings_cyr2latProfileDelete": "Cyr2Lat profil törlése", - "settings_cyr2latProfileDeleted": "A profil törlése sikeresen megtörtént", - "settings_cyr2latProfileDeleteDscr": "Biztosan törölni szeretné a \"{name}\" profilt?", - "channels_channelUpdated": "A {name} csatorna frissítve", + "settings_cyr2latProfileDeleted": "A profil sikeresen törölve", + "settings_cyr2latProfileDeleteDscr": "Biztos benne, hogy törölni kívánja a(z) \"{name}\" profilt?", + "channels_channelUpdated": "A \"{name}\" csatorna frissítve", "@channels_channelUpdated": { "placeholders": { "name": { @@ -573,28 +573,28 @@ } } }, - "channels_publicChannelAdded": "A nyilvános csatorna hozzáadva", - "channels_sortBy": "Szűrés", - "channels_sortManual": "Használati útmutató", + "channels_publicChannelAdded": "Nyilvános csatorna hozzáadva", + "channels_sortBy": "Rendezés", + "channels_sortManual": "Kézikönyv", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Legfrissebb üzenetek", - "channels_sortUnread": "Olvasatlan", - "channels_createPrivateChannel": "Létrehoz egy privát csatornát", - "channels_createPrivateChannelDesc": "Titkos kulcs segítségével védelem.", - "channels_joinPrivateChannel": "Csatlakozzon egy privát csatornához", - "channels_joinPrivateChannelDesc": "Kézzel adja meg a titkos kulcsot.", + "channels_sortLatestMessages": "Legújabb üzenetek", + "channels_sortUnread": "Nem olvasott", + "channels_createPrivateChannel": "Hozzon létre egy privát csatornát", + "channels_createPrivateChannelDesc": "Titkos kulccsal biztosítva.", + "channels_joinPrivateChannel": "Csatlakozz egy privát csatornához", + "channels_joinPrivateChannelDesc": "Adjon meg kézzel egy titkos kulcsot.", "channels_joinPublicChannel": "Csatlakozzon a nyilvános csatornához", "channels_joinPublicChannelDesc": "Bárki csatlakozhat ehhez a csatornához.", - "channels_joinHashtagChannel": "Csatlakozzon egy hashtage-os csatornához", - "channels_joinHashtagChannelDesc": "Bárkinek lehet csatlakoznia a hashtagekhez tartozó csatornához.", - "channels_scanQrCode": "Scanned egy QR-kódot", + "channels_joinHashtagChannel": "Csatlakozz egy Hashtag csatornához", + "channels_joinHashtagChannelDesc": "Bárki csatlakozhat a hashtag csatornákhoz.", + "channels_scanQrCode": "QR-kód beolvasása", "channels_scanQrCodeComingSoon": "Hamarosan", "channels_enterHashtag": "Írja be a hashtaget", - "channels_hashtagHint": "pl. #csapat", - "chat_noMessages": "Még nincs üzenet.", - "chat_sendMessageToStart": "Küldj egy üzenetet, hogy elindulj!", - "chat_originalMessageNotFound": "A eredeti üzenet nem található.", - "chat_replyingTo": "Replying to {name}", + "channels_hashtagHint": "például #csapat", + "chat_noMessages": "Még nincsenek üzenetek", + "chat_sendMessageToStart": "A kezdéshez küldjön üzenetet", + "chat_originalMessageNotFound": "Az eredeti üzenet nem található", + "chat_replyingTo": "Válasz erre: {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -602,7 +602,7 @@ } } }, - "chat_replyTo": "Reply to {name}", + "chat_replyTo": "Válasz erre: {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -610,8 +610,8 @@ } } }, - "chat_location": "Helyszín", - "chat_sendMessageTo": "Küldj üzenetet {contactName}-nek", + "chat_location": "hely", + "chat_sendMessageTo": "Üzenet küldése a következő címre: {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -619,8 +619,8 @@ } } }, - "chat_typeMessage": "Írjon üzenetet...", - "chat_messageTooLong": "A üzenet túl hosszú (a maximális {maxBytes} bájt).", + "chat_typeMessage": "Írjon be egy üzenetet...", + "chat_messageTooLong": "Az üzenet túl hosszú (max. {maxBytes} bájt).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -630,8 +630,8 @@ }, "chat_messageCopied": "Üzenet másolva", "chat_messageDeleted": "Üzenet törölve", - "chat_retryingMessage": "Újrapróbálási üzenet", - "chat_retryCount": "Újrapróbál {current}/{max}", + "chat_retryingMessage": "Üzenet újrapróbálkozása", + "chat_retryCount": "Próbálja újra {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -642,32 +642,32 @@ } } }, - "chat_sendGif": "Küldj GIF-ot", + "chat_sendGif": "GIF küldése", "chat_reply": "Válasz", - "chat_addReaction": "Hozzon létre reakciót", - "chat_me": "Én", - "emojiCategorySmileys": "Emoji", - "emojiCategoryGestures": "Testmozgások", - "emojiCategoryHearts": "Szívak", - "emojiCategoryObjects": "Tárgyak", - "gifPicker_title": "Válasszon egy GIF-et", + "chat_addReaction": "Reakció hozzáadása", + "chat_me": "Nekem", + "emojiCategorySmileys": "Hangulatjelek", + "emojiCategoryGestures": "Gesztusok", + "emojiCategoryHearts": "Szívek", + "emojiCategoryObjects": "Objektumok", + "gifPicker_title": "Válassz egy GIF-et", "gifPicker_searchHint": "GIF-ek keresése...", - "gifPicker_poweredBy": "Forrás: GIPHY", - "gifPicker_noGifsFound": "Nincsenek GIF-ek megtalálva.", - "gifPicker_failedLoad": "Nem sikerült betölteni a GIF-fájlokat.", - "gifPicker_failedSearch": "Nem sikerült a GIF-eket megtalálni.", - "gifPicker_noInternet": "Nincs internetkapcsolat.", - "debugLog_appTitle": "App-debug log", - "debugLog_bleTitle": "BLE hibajelentő napló", - "debugLog_copyLog": "Másolat napló", - "debugLog_clearLog": "Jelzett napló", - "debugLog_copied": "Hibajelentő napló másolva", - "debugLog_bleCopied": "BLE-log másolva", - "debugLog_noEntries": "Jelenleg még nem léteznek hibaelhárítási naplókat.", - "debugLog_enableInSettings": "Engedje be az alkalmazás hibaelhárítási naplózását a beállítások menüben.", + "gifPicker_poweredBy": "Üzemeltető: GIPHY", + "gifPicker_noGifsFound": "Nem található GIF", + "gifPicker_failedLoad": "Nem sikerült betölteni a GIF-eket", + "gifPicker_failedSearch": "Nem sikerült a GIF-ek keresése", + "gifPicker_noInternet": "Nincs internet kapcsolat", + "debugLog_appTitle": "Alkalmazás hibakeresési naplója", + "debugLog_bleTitle": "BLE hibakeresési napló", + "debugLog_copyLog": "Napló másolása", + "debugLog_clearLog": "Napló törlése", + "debugLog_copied": "Hibakeresési napló másolva", + "debugLog_bleCopied": "BLE napló másolva", + "debugLog_noEntries": "Még nincsenek hibakeresési naplók", + "debugLog_enableInSettings": "Engedélyezze az alkalmazás hibakeresési bejelentkezési beállításait", "debugLog_frames": "Keretek", - "debugLog_rawLogRx": "Az eredeti Log-RX", - "debugLog_noBleActivity": "Jelenleg nincs BLE-hez kapcsolódó tevékenység.", + "debugLog_rawLogRx": "Nyers Log-RX", + "debugLog_noBleActivity": "Még nincs BLE tevékenység", "debugFrame_length": "Keret hossza: {count} bájt", "@debugFrame_length": { "placeholders": { @@ -684,8 +684,8 @@ } } }, - "debugFrame_textMessageHeader": "Címzett:", - "debugFrame_destinationPubKey": "- Célhely: {pubKey}", + "debugFrame_textMessageHeader": "Szöveges üzenet keret:", + "debugFrame_destinationPubKey": "- Cél PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -701,7 +701,7 @@ } } }, - "debugFrame_flags": "- Jelvények: 0x{value}", + "debugFrame_flags": "- Zászlók: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -709,7 +709,7 @@ } } }, - "debugFrame_textType": "- Tartalom típusa: {type} ({label})", + "debugFrame_textType": "- Szöveg típusa: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -720,9 +720,9 @@ } } }, - "debugFrame_textTypeCli": "Parancssori felület (CLI)", - "debugFrame_textTypePlain": "Egyszerű, alap, hagyományos", - "debugFrame_text": "- Tartalom: \"{text}\"", + "debugFrame_textTypeCli": "CLI", + "debugFrame_textTypePlain": "Egyszerű", + "debugFrame_text": "- Szöveg: \"{text}\"", "@debugFrame_text": { "placeholders": { "text": { @@ -730,7 +730,7 @@ } } }, - "debugFrame_hexDump": "Hex-dump:", + "debugFrame_hexDump": "Hex dump:", "chat_pathManagement": "Útvonal-kezelés", "chat_ShowAllPaths": "Mutasson meg minden útvonalat", "chat_routingMode": "Útvonal-kezelési mód", @@ -749,17 +749,17 @@ } }, "chat_successes": "sikerek", - "chat_removePath": "Törölje a elérési útvonalat", - "chat_noPathHistoryYet": "Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.", + "chat_removePath": "Távolítsa el az útvonalat", + "chat_noPathHistoryYet": "Még nincs úttörténet.\nÜzenet küldése utak felfedezéséhez.", "chat_pathActions": "Céltúrások:", "chat_setCustomPath": "Beállítsd a saját útvonalat", "chat_setCustomPathSubtitle": "Kézzel megadott útvonal", "chat_clearPath": "Egyértelmű út", "chat_clearPathSubtitle": "A parancs új küldéskor újra kell aktivizálnia.", - "chat_pathCleared": "Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.", + "chat_pathCleared": "Út megtisztítva. A következő üzenet újra felfedezi az útvonalat.", "chat_floodModeSubtitle": "Használja a \"útvonal\" kapcsolót az alkalmazás sávjában.", "chat_floodModeEnabled": "Árvízvédelmi mód bekapcsolva. A visszaállítás a alkalmazásban található útvonal ikon segítségével.", - "chat_fullPath": "Teljes elérési út", + "chat_fullPath": "Teljes útvonal", "chat_pathDetailsNotAvailable": "Az útvonal részletei még nem elérhetők. Próbálja meg küldeni egy üzenetet, hogy frissítse az információkat.", "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { @@ -772,16 +772,16 @@ } } }, - "chat_pathSavedLocally": "Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.", - "chat_pathDeviceConfirmed": "A készülék megvan.", - "chat_pathDeviceNotConfirmed": "A készülék még nem bizonyított.", - "chat_type": "Típus", - "chat_path": "Út", - "chat_publicKey": "Nyelvkönyv", - "chat_compressOutgoingMessages": "A küldött üzenetek tömörítése", - "chat_floodForced": "Áradás (kényszerített)", - "chat_directForced": "Közvetlen (erélyes)", - "chat_hopsForced": "{count} ánusz (erővel)", + "chat_pathSavedLocally": "Helyben mentve. Csatlakozás a szinkronizáláshoz.", + "chat_pathDeviceConfirmed": "Az eszköz megerősítve.", + "chat_pathDeviceNotConfirmed": "Az eszköz még nincs megerősítve.", + "chat_type": "Írja be", + "chat_path": "Útvonal", + "chat_publicKey": "Nyilvános kulcs", + "chat_compressOutgoingMessages": "A kimenő üzenetek tömörítése", + "chat_floodForced": "Árvíz (kényszerített)", + "chat_directForced": "Közvetlen (kényszerített)", + "chat_hopsForced": "{count} ugrás (kényszerített)", "@chat_hopsForced": { "placeholders": { "count": { @@ -789,9 +789,9 @@ } } }, - "chat_floodAuto": "Vízosztás (autó)", + "chat_floodAuto": "Árvíz (automatikus)", "chat_direct": "Közvetlen", - "chat_poiShared": "Közös erőforrás", + "chat_poiShared": "POI megosztva", "chat_unread": "Olvasatlan: {count}", "@chat_unread": { "placeholders": { @@ -800,10 +800,10 @@ } } }, - "chat_openLink": "Nyisd meg a linket?", - "chat_openLinkConfirmation": "Szeretnéd megnyitni ezt a linket a böngésződben?", + "chat_openLink": "Link megnyitása?", + "chat_openLinkConfirmation": "Meg akarja nyitni ezt a hivatkozást a böngészőjében?", "chat_open": "Nyitott", - "chat_couldNotOpenLink": "Nem sikerült megnyitni a hivat: {url}", + "chat_couldNotOpenLink": "Nem sikerült megnyitni a linket: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -811,13 +811,13 @@ } } }, - "chat_invalidLink": "Érvénytelen hivatkozás formátum", - "map_title": "Grafikus ábrázás", - "map_lineOfSight": "Látási vonal", - "map_losScreenTitle": "Látási vonal", - "map_noNodesWithLocation": "Nincs olyan adatpont, amelyhez helyszín-információk tartoznak.", - "map_nodesNeedGps": "A pontoknak meg kell osztaniuk GPS koordinátáikat, hogy megjelenjenek a térképen.", - "map_nodesCount": "Csúcsok: {count}", + "chat_invalidLink": "Érvénytelen linkformátum", + "map_title": "Csomópont térkép", + "map_lineOfSight": "Látóvonal", + "map_losScreenTitle": "Látóvonal", + "map_noNodesWithLocation": "Nincsenek helyadatokkal rendelkező csomópontok", + "map_nodesNeedGps": "A csomópontoknak meg kell osztaniuk GPS-koordinátáikat\nhogy megjelenjen a térképen", + "map_nodesCount": "Csomópontok: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -825,7 +825,7 @@ } } }, - "map_pinsCount": "Csapok: {count}", + "map_pinsCount": "Pins: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -835,26 +835,26 @@ }, "map_chat": "Csevegés", "map_repeater": "Ismétlő", - "map_room": "szoba", + "map_room": "Szoba", "map_sensor": "Érzékelő", - "map_pinDm": "Jel (DM)", - "map_pinPrivate": "Titkos (privát)", - "map_pinPublic": "Jelmez (nyilvános)", - "map_lastSeen": "Utoljára látva", - "map_disconnectConfirm": "Biztosan szeretné kiírni ezt a készüléket?", - "map_from": "Attól", + "map_pinDm": "Pin (DM)", + "map_pinPrivate": "Pin (privát)", + "map_pinPublic": "Pin (nyilvános)", + "map_lastSeen": "Utoljára látott", + "map_disconnectConfirm": "Biztosan le akarja kapcsolni ezt az eszközt?", + "map_from": "Tól", "map_source": "Forrás", - "map_flags": "Zászló", - "map_shareMarkerHere": "Osztja ezt a tartalmat itt", - "map_setAsMyLocation": "Állítsa be a jelenlegi helyzetemként", - "map_pinLabel": "Címkét ragasztani", + "map_flags": "Zászlók", + "map_shareMarkerHere": "Oszd meg a jelölőt itt", + "map_setAsMyLocation": "Beállítás helyemként", + "map_pinLabel": "Kitűzött címke", "map_label": "Címke", - "map_pointOfInterest": "Érdekes hely", - "map_sendToContact": "Kapcsolatfelvételi űrlap", - "map_sendToChannel": "Küldés a csatornán", - "map_noChannelsAvailable": "Nincs elérhető csatorna.", - "map_publicLocationShare": "Térköz, nyilvános hely", - "map_publicLocationShareConfirm": "Most egy helyszínt megosztasz a {channelLabel} csatornán. Ez a csatorna nyilvános, és bárki, aki rendelkezik a PSK-val, megtekintheti.", + "map_pointOfInterest": "Érdekes pont", + "map_sendToContact": "Elküldés a kapcsolattartónak", + "map_sendToChannel": "Küldés a csatornára", + "map_noChannelsAvailable": "Nincs elérhető csatorna", + "map_publicLocationShare": "Nyilvános helymegosztás", + "map_publicLocationShareConfirm": "Arra készül, hogy megosszon egy helyet itt: {channelLabel}. Ez a csatorna nyilvános, és a PSK birtokában bárki láthatja.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -862,33 +862,33 @@ } } }, - "map_connectToShareMarkers": "Kapcsolódjon egy eszközhöz, hogy megoszthassa a vonalzókat.", - "map_filterNodes": "Szűrési pontok", - "map_nodeTypes": "Vonalak típusai", - "map_chatNodes": "Csevegési pontok", - "map_repeaters": "Újraküldők", + "map_connectToShareMarkers": "Csatlakozzon egy eszközhöz a jelölők megosztásához", + "map_filterNodes": "Csomópontok szűrése", + "map_nodeTypes": "Csomópont típusok", + "map_chatNodes": "Chat csomópontok", + "map_repeaters": "Ismétlők", "map_otherNodes": "Egyéb csomópontok", - "map_keyPrefix": "Kulcsfontosságú előtag", - "map_filterByKeyPrefix": "Szűrj a kulcsos előtér szerint", - "map_publicKeyPrefix": "Névfelhasználó kulc-prefix", + "map_keyPrefix": "Kulcselőtag", + "map_filterByKeyPrefix": "Szűrés kulcs előtagja szerint", + "map_publicKeyPrefix": "Nyilvános kulcs előtagja", "map_markers": "Jelölők", - "map_showSharedMarkers": "Mutassa meg a közös jeleket", - "map_showGuessedLocations": "Megjelenítsa a megjósolt csomópontok helyét", - "map_showDiscoveryContacts": "Megjelenítse a Discovery-nál elérhet kontaktokat", - "map_guessedLocation": "Tippolt hely", - "map_lastSeenTime": "Utoljára megjelent idő", - "map_sharedPin": "Gemeinsames PIN-kód", - "map_joinRoom": "Csatlakozás a szobához", - "map_manageRepeater": "Ellenőriző eszköz kezelése", - "map_tapToAdd": "Nyomj meg a csomópontokhoz, hogy hozzáadd őket az útvonalhoz.", - "map_runTrace": "Útvonal követés", - "map_removeLast": "Törölj utolsó", - "map_pathTraceCancelled": "Az útvonal követés megszakadt.", - "mapCache_title": "Offline térkép tárolás", - "mapCache_selectAreaFirst": "Válasszon egy területet, amelyet először cache-oljon.", - "mapCache_noTilesToDownload": "Nincsenek letölthető tile-ok ebben a területben.", - "mapCache_downloadTilesTitle": "Letöltsd a tile-okat", - "mapCache_downloadTilesPrompt": "Töltse le {count} darab tile-t offline használatra?", + "map_showSharedMarkers": "Megosztott jelölők megjelenítése", + "map_showGuessedLocations": "Talált csomópont-helyek megjelenítése", + "map_showDiscoveryContacts": "Felfedezési névjegyek megjelenítése", + "map_guessedLocation": "Talált hely", + "map_lastSeenTime": "Utoljára látott időpont", + "map_sharedPin": "Megosztott gombostű", + "map_joinRoom": "Csatlakozz a szobához", + "map_manageRepeater": "Repeater kezelése", + "map_tapToAdd": "Érintse meg a csomópontokat, hogy hozzáadja őket az útvonalhoz.", + "map_runTrace": "Futtatási útvonal nyomkövetése", + "map_removeLast": "Utolsó eltávolítása", + "map_pathTraceCancelled": "Útvonal nyomkövetés törölve.", + "mapCache_title": "Offline térképgyorsítótár", + "mapCache_selectAreaFirst": "Válassza ki a gyorsítótárba helyezendő területet", + "mapCache_noTilesToDownload": "Ehhez a területhez nem lehet letölteni csempét", + "mapCache_downloadTilesTitle": "Csempe letöltése", + "mapCache_downloadTilesPrompt": "Letölti a {count} csempét offline használatra?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -897,7 +897,7 @@ } }, "mapCache_downloadAction": "Letöltés", - "mapCache_cachedTiles": "Tárolt {count} darab", + "mapCache_cachedTiles": "Gyorsítótárazott {count} csempe", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -905,7 +905,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", + "mapCache_cachedTilesWithFailed": "Gyorsítótárazott {downloaded} csempe ({failed} sikertelen)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -916,14 +916,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Tiszta offline tárhely", - "mapCache_clearOfflineCachePrompt": "Távolítsa el az összes tárolt térképmegjelenítőt?", - "mapCache_offlineCacheCleared": "A helyi memóriát töröltük.", - "mapCache_noAreaSelected": "Nincs kiválasztott terület.", - "mapCache_cacheArea": "Tároló terület", - "mapCache_useCurrentView": "Használja a jelenlegi nézetet", + "mapCache_clearOfflineCacheTitle": "Offline gyorsítótár törlése", + "mapCache_clearOfflineCachePrompt": "Eltávolítja az összes gyorsítótárazott térképcsempét?", + "mapCache_offlineCacheCleared": "Offline gyorsítótár törölve", + "mapCache_noAreaSelected": "Nincs kiválasztott terület", + "mapCache_cacheArea": "Gyorsítótár terület", + "mapCache_useCurrentView": "Az aktuális nézet használata", "mapCache_zoomRange": "Zoom tartomány", - "mapCache_estimatedTiles": "Becsült kerámiák: {count}", + "mapCache_estimatedTiles": "Becsült csempék: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -942,8 +942,8 @@ } } }, - "mapCache_downloadTilesButton": "Letöltsd a tile-okat", - "mapCache_clearCacheButton": "Ósztótt adatokat", + "mapCache_downloadTilesButton": "Letöltés csempe", + "mapCache_clearCacheButton": "Törölje a gyorsítótárat", "mapCache_failedDownloads": "Sikertelen letöltések: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -952,7 +952,7 @@ } } }, - "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", + "mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -969,8 +969,8 @@ } } }, - "time_justNow": "Most", - "time_minutesAgo": "{minutes} perckel ezelőtt", + "time_justNow": "Éppen most", + "time_minutesAgo": "{minutes} perce", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -978,7 +978,7 @@ } } }, - "time_hoursAgo": "{hours} óva", + "time_hoursAgo": "{hours}órája", "@time_hoursAgo": { "placeholders": { "hours": { @@ -986,7 +986,7 @@ } } }, - "time_daysAgo": "{days}d ago", + "time_daysAgo": "{days}napja", "@time_daysAgo": { "placeholders": { "days": { @@ -995,32 +995,32 @@ } }, "time_hour": "óra", - "time_hours": "órák", + "time_hours": "óra", "time_day": "nap", - "time_days": "napok", - "time_week": "het", - "time_weeks": "het, hetek", + "time_days": "napokon", + "time_week": "hét", + "time_weeks": "hét", "time_month": "hónap", - "time_months": "hónapok", - "time_minutes": "percek", - "time_allTime": "Bármely időpont", - "dialog_disconnect": "Csatlakozást megszakasztani", - "dialog_disconnectConfirm": "Biztosan szeretné kiírni ezt a készüléket?", - "login_repeaterLogin": "Ismételt bejelentkezés", - "login_roomLogin": "Szoba szerverbe való bejelentkezés", + "time_months": "hónap", + "time_minutes": "jegyzőkönyv", + "time_allTime": "Minden idők", + "dialog_disconnect": "Leválasztás", + "dialog_disconnectConfirm": "Biztosan le akarja kapcsolni ezt az eszközt?", + "login_repeaterLogin": "Ismétlő bejelentkezés", + "login_roomLogin": "Szobaszerver bejelentkezés", "login_password": "Jelszó", "login_enterPassword": "Adja meg a jelszót", - "login_savePassword": "Mentse el a jelszót", - "login_savePasswordSubtitle": "A jelszó biztonságosan tárolódik ezen a készüléken.", - "login_repeaterDescription": "Adja meg a repeater (ismétítő) jelszót, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.", - "login_roomDescription": "Adja meg a belépési kódot, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.", - "login_routing": "Útvonal meghatározás", - "login_routingMode": "Útvonal-kezelési mód", - "login_autoUseSavedPath": "Automatikus (az eddigi útvonal használata)", - "login_forceFloodMode": "Erőforrás-alapú áramlás mód", + "login_savePassword": "Jelszó mentése", + "login_savePasswordSubtitle": "A jelszó biztonságosan lesz tárolva ezen az eszközön", + "login_repeaterDescription": "Adja meg az ismétlő jelszavát a vendég vagy adminisztrátori hozzáféréshez.", + "login_roomDescription": "Adja meg a szoba jelszavát a vendég vagy adminisztrátori hozzáféréshez.", + "login_routing": "Útválasztás", + "login_routingMode": "Útválasztási mód", + "login_autoUseSavedPath": "Automatikus (mentett útvonal használata)", + "login_forceFloodMode": "Force Flood mód", "login_managePaths": "Útvonalak kezelése", "login_login": "Bejelentkezés", - "login_attempt": "Megpróbálás {current}/{max}-adik", + "login_attempt": "Kísérlet {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -1031,7 +1031,7 @@ } } }, - "login_failed": "Belépés sikertelen: {error}", + "login_failed": "Sikertelen bejelentkezés: {error}", "@login_failed": { "placeholders": { "error": { @@ -1039,9 +1039,9 @@ } } }, - "login_failedMessage": "Belépés sikertelen. Vagy a jelszó helytelen, vagy a hálózati kapcsolat nem létesül.", - "common_reload": "Újra töltés", - "common_clear": "Egyértelmű", + "login_failedMessage": "Sikertelen bejelentkezés. Vagy hibás a jelszó, vagy az átjátszó nem érhető el.", + "common_reload": "Újratöltés", + "common_clear": "Világos", "path_currentPath": "Jelenlegi útvonal: {path}", "@path_currentPath": { "placeholders": { @@ -1065,7 +1065,7 @@ "path_labelHexPrefixes": "Út (hex-prefixek)", "path_helperMaxHops": "A maximális hossz 64 karakter. Minden előző rész 2 hatos számjegyből áll (1 bájt).", "path_selectFromContacts": "Válasszon a kontaktlista elembek közül:", - "path_noRepeatersFound": "Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.", + "path_noRepeatersFound": "Nem található átjátszó vagy szobaszerver.", "path_customPathsRequire": "Az egyedi útvonalaknak szükségük van átjáró pontokra, amelyek képesek üzeneteket továbbítani.", "path_invalidHexPrefixes": "Érvénytelen hexadecimális előtagok: {prefixes}", "@path_invalidHexPrefixes": { @@ -1077,27 +1077,27 @@ }, "path_tooLong": "Az út túl hosszú. A maximális engedélyezett lépések száma 64.", "path_setPath": "Útvonal meghatározása", - "repeater_management": "Adatkapcsolás kezelése", - "room_management": "Szoba-szerver kezelés", - "repeater_managementTools": "Menedzsmentes eszközök", + "repeater_management": "Repeater Management", + "room_management": "Szobaszerver-kezelés", + "repeater_managementTools": "Kezelőeszközök", "repeater_status": "Állapot", - "repeater_statusSubtitle": "Megtekintheted a repeater állapotát, statisztikáit és a környező eszközök adatait.", - "repeater_telemetry": "Adatvisszaadás", - "repeater_telemetrySubtitle": "Tekintsük a szenzorok és a rendszer állapotának adatát", - "repeater_cli": "Parancssori felület (CLI)", - "repeater_cliSubtitle": "Küldj parancsokat a repeaternek.", + "repeater_statusSubtitle": "Tekintse meg az átjátszó állapotát, a statisztikákat és a szomszédokat", + "repeater_telemetry": "Telemetria", + "repeater_telemetrySubtitle": "Tekintse meg az érzékelők telemetriáját és a rendszerstatisztikát", + "repeater_cli": "CLI", + "repeater_cliSubtitle": "Parancsok küldése az átjátszónak", "repeater_neighbors": "Szomszédok", - "repeater_neighborsSubtitle": "Tekintsük a nullás lépésű szomszédokat.", - "repeater_settings": "Beállítások", - "repeater_settingsSubtitle": "Állítsa be a repeater paramétereket", - "repeater_statusTitle": "Adatkapcsolódás állapot", - "repeater_routingMode": "Útvonal-kezelési mód", + "repeater_neighborsSubtitle": "Tekintse meg a zero hop szomszédokat.", + "repeater_settings": "Beállítások elemre", + "repeater_settingsSubtitle": "Állítsa be az ismétlő paramétereit", + "repeater_statusTitle": "Repeater állapota", + "repeater_routingMode": "Útválasztási mód", "repeater_autoUseSavedPath": "Automatikus (az eddigi útvonal használata)", "repeater_forceFloodMode": "Erőforrás-alapú áramlás mód", "repeater_pathManagement": "Útvonal-kezelés", - "repeater_refresh": "Újrafriszol", - "repeater_statusRequestTimeout": "Az állapotkérés időtúlt.", - "repeater_errorLoadingStatus": "Hiba a státusz betöltés közben: {error}", + "repeater_refresh": "Frissítés", + "repeater_statusRequestTimeout": "Az állapotkérelem időtúllépése lejárt.", + "repeater_errorLoadingStatus": "Hiba az állapot betöltésekor: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -1106,22 +1106,22 @@ } }, "repeater_systemInformation": "Rendszerinformációk", - "repeater_battery": "Akku", + "repeater_battery": "Akkumulátor", "repeater_clockAtLogin": "Óra (bejelentkezéskor)", - "repeater_uptime": "A rendszer elérhetősége", - "repeater_queueLength": "Várakozási sor hossza", - "repeater_debugFlags": "Hibakeresési beállítások", - "repeater_radioStatistics": "Rádió statisztika", - "repeater_lastRssi": "Utolsó RSSI érték", + "repeater_uptime": "Üzemidő", + "repeater_queueLength": "Sor hossza", + "repeater_debugFlags": "Debug Flags", + "repeater_radioStatistics": "Rádióstatisztika", + "repeater_lastRssi": "Utolsó RSSI", "repeater_lastSnr": "Utolsó SNR", - "repeater_noiseFloor": "Háttérzaj szint", - "repeater_txAirtime": "TX Airtime", - "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Csomagok statisztikája", - "repeater_sent": "Elküldve", - "repeater_received": "Megérkezett", - "repeater_duplicates": "Duplák", - "repeater_daysHoursMinsSecs": "{days} days {hours}h {minutes}m {seconds}s", + "repeater_noiseFloor": "Zajpadló", + "repeater_txAirtime": "TX műsoridő", + "repeater_rxAirtime": "RX műsoridő", + "repeater_packetStatistics": "Csomagstatisztika", + "repeater_sent": "Küldött", + "repeater_received": "Fogadva", + "repeater_duplicates": "Ismétlődések", + "repeater_daysHoursMinsSecs": "{days} nap {hours}ó {minutes}p {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -1166,7 +1166,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Áradás: {flood}, Közvetlen: {direct}", + "repeater_duplicatesFloodDirect": "Árvíz: {flood}, Közvetlen: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -1185,36 +1185,36 @@ } } }, - "repeater_settingsTitle": "Adatátvisszaadási beállítások", + "repeater_settingsTitle": "Repeater beállítások", "repeater_basicSettings": "Alapbeállítások", - "repeater_repeaterName": "Adóállomás neve", - "repeater_repeaterNameHelper": "Ez a repeater neve", - "repeater_adminPassword": "Adminisztrátori jelszó", - "repeater_adminPasswordHelper": "Teljes jogosultságú jelszó", - "repeater_guestPassword": "Vendég felhasználói név/jelszó", - "repeater_guestPasswordHelper": "Csak olvasási jogosítást biztosító jelszó", + "repeater_repeaterName": "Ismétlő neve", + "repeater_repeaterNameHelper": "Az átjátszó megjelenítési neve", + "repeater_adminPassword": "Admin jelszó", + "repeater_adminPasswordHelper": "Teljes hozzáférési jelszó", + "repeater_guestPassword": "Vendégjelszó", + "repeater_guestPasswordHelper": "Csak olvasható hozzáférési jelszó", "repeater_radioSettings": "Rádióbeállítások", "repeater_frequencyMhz": "Frekvencia (MHz)", - "repeater_frequencyHelper": "300–2500 MHz", - "repeater_txPower": "TX Power", + "repeater_frequencyHelper": "300-2500 MHz", + "repeater_txPower": "TX teljesítmény", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Adatkapacitás", - "repeater_spreadingFactor": "Terjesztési tényező", + "repeater_bandwidth": "Sávszélesség", + "repeater_spreadingFactor": "Spreading Factor", "repeater_codingRate": "Kódolási sebesség", - "repeater_locationSettings": "Helyszínbeállítások", - "repeater_latitude": "Nyugat–keleti szélesség", - "repeater_latitudeHelper": "Desztes fokok (pl. 37,7749)", - "repeater_longitude": "hosszúság", - "repeater_longitudeHelper": "Desztes fokok (pl. -122.4194)", + "repeater_locationSettings": "Helybeállítások", + "repeater_latitude": "Szélesség", + "repeater_latitudeHelper": "Tizedes fokozatok (pl. 37,7749)", + "repeater_longitude": "Hosszúság", + "repeater_longitudeHelper": "Tizedes fokok (pl. -122,4194)", "repeater_features": "Jellemzők", - "repeater_packetForwarding": "Csomagok továbbítás", - "repeater_packetForwardingSubtitle": "Engedje, hogy a repeater továbbítsa a csomagokat.", - "repeater_guestAccess": "Vendégek számára elérhető", - "repeater_guestAccessSubtitle": "Engedje meg a vendégek számára, hogy csak olvassák a tartalmat", - "repeater_privacyMode": "Adatvédelem mód", - "repeater_privacyModeSubtitle": "Elrejtse a nevét/a helyszínt az űrlapon", + "repeater_packetForwarding": "Csomagtovábbítás", + "repeater_packetForwardingSubtitle": "Ismétlő engedélyezése a csomagok továbbításához", + "repeater_guestAccess": "Vendég hozzáférés", + "repeater_guestAccessSubtitle": "Csak olvasási hozzáférés engedélyezése a vendégek számára", + "repeater_privacyMode": "Adatvédelmi mód", + "repeater_privacyModeSubtitle": "Név/hely elrejtése a hirdetésekben", "repeater_advertisementSettings": "Reklámbeállítások", - "repeater_localAdvertInterval": "Helyi hirdetés időtartama", + "repeater_localAdvertInterval": "Helyi hirdetési intervallum", "repeater_localAdvertIntervalMinutes": "{minutes} perc", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1223,7 +1223,7 @@ } } }, - "repeater_floodAdvertInterval": "Vízosztály-hirdetés időtartama", + "repeater_floodAdvertInterval": "Árvízi hirdetési intervallum", "repeater_floodAdvertIntervalHours": "{hours} óra", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1232,18 +1232,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Kódolt hirdetés-szünet", + "repeater_encryptedAdvertInterval": "Titkosított hirdetési időköz", "repeater_dangerZone": "Veszélyzóna", - "repeater_rebootRepeater": "Újraindítás", - "repeater_rebootRepeaterSubtitle": "Indítsa újra a repeater-t.", - "repeater_rebootRepeaterConfirm": "Biztosan szeretné újraindítani ezt a repeatert?", - "repeater_regenerateIdentityKey": "Újra generálja az azonosító kulcsot", - "repeater_regenerateIdentityKeySubtitle": "Új nyilvános/személyes kulcs-párt generáljon", - "repeater_regenerateIdentityKeyConfirm": "Ez új azonosítást fog létrehozni a repeater számára. Folytatni?", - "repeater_eraseFileSystem": "Törölje a fájlrendszert", - "repeater_eraseFileSystemSubtitle": "Formázza a duplázó fájlrendszert.", - "repeater_eraseFileSystemConfirm": "FIGYELEM: Ez törli az összes adatot a repeater-en. Ez nem visszafordítható!", - "repeater_eraseSerialOnly": "Az Erase funkció csak a soros konzolon érhető el.", + "repeater_rebootRepeater": "Reboot Repeater", + "repeater_rebootRepeaterSubtitle": "Indítsa újra az átjátszó eszközt", + "repeater_rebootRepeaterConfirm": "Biztosan újraindítja ezt az átjátszót?", + "repeater_regenerateIdentityKey": "Identitáskulcs újragenerálása", + "repeater_regenerateIdentityKeySubtitle": "Új nyilvános/privát kulcspár létrehozása", + "repeater_regenerateIdentityKeyConfirm": "Ez új identitást generál az átjátszó számára. Folytatja?", + "repeater_eraseFileSystem": "Fájlrendszer törlése", + "repeater_eraseFileSystemSubtitle": "Formázza meg az átjátszó fájlrendszert", + "repeater_eraseFileSystemConfirm": "FIGYELMEZTETÉS: Ezzel törli az átjátszón lévő összes adatot. Ezt nem lehet visszavonni!", + "repeater_eraseSerialOnly": "A törlés csak soros konzolon érhető el.", "repeater_commandSent": "Parancs elküldve: {command}", "@repeater_commandSent": { "placeholders": { @@ -1252,7 +1252,7 @@ } } }, - "repeater_errorSendingCommand": "Hibás parancs küldés: {error}", + "repeater_errorSendingCommand": "Hiba a parancs küldésekor: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1260,23 +1260,23 @@ } } }, - "repeater_confirm": "Beküldve", - "repeater_settingsSaved": "Beállítások sikeresen mentve", - "repeater_rxGain": "Nagyobb RX-jel erősítés", - "repeater_rxGainHelper": "Magasabb érzékenység, nagyobb áramfelvétel (csak SX1262/SX1268 esetén)", - "repeater_refreshRxGain": "Újraindított, fokozott RX hatás", - "repeater_multiAcks": "Többszörös visszaigazolások", - "repeater_multiAcksSubtitle": "Ismerje el üzeneteket több úton is, hogy biztosítsa a jobb átadást.", - "repeater_refreshMultiAcks": "Frissítse a többször is kapott visszaigazolásokat.", - "repeater_networkHealth": "Hálózati állapot", - "repeater_loopDetect": "Ciklusok azonosítása", - "repeater_loopDetectHelper": "Készíts olyan \"vízfolyást\" megjelenítő csomagokat, amelyek úgy néznek ki, mint egy hibaút.", - "repeater_loopDetectOff": "Le, kikap", + "repeater_confirm": "Erősítse meg", + "repeater_settingsSaved": "A beállítások sikeresen elmentve", + "repeater_rxGain": "Megnövelt RX nyereség", + "repeater_rxGainHelper": "Nagyobb érzékenység, nagyobb áramfelvétel (csak SX1262/SX1268)", + "repeater_refreshRxGain": "Frissítse a megnövelt RX-erősítést", + "repeater_multiAcks": "Multi-ACK", + "repeater_multiAcksSubtitle": "Nyugtázza az üzeneteket több úton a jobb kézbesítés érdekében", + "repeater_refreshMultiAcks": "Többszörös ACK-ek frissítése", + "repeater_networkHealth": "Hálózat állapota", + "repeater_loopDetect": "Hurokérzékelés", + "repeater_loopDetectHelper": "Dobd el az útválasztó huroknak tűnő árvízcsomagokat", + "repeater_loopDetectOff": "Le", "repeater_loopDetectMinimal": "Minimális", - "repeater_loopDetectModerate": "Közepes", + "repeater_loopDetectModerate": "Mérsékelt", "repeater_loopDetectStrict": "Szigorú", - "repeater_dutyCycle": "Munka- és pihenőidő aránya", - "repeater_dutyCycleHelper": "A maximális időszámítás százalékos aránya", + "repeater_dutyCycle": "Üzemi ciklus", + "repeater_dutyCycleHelper": "A műsoridő maximális százaléka", "repeater_dutyCyclePercent": "{percent}%", "@repeater_dutyCyclePercent": { "placeholders": { @@ -1286,30 +1286,30 @@ } }, "repeater_ownerInfo": "Üzemeltető információ", - "repeater_ownerInfoHelper": "A nyilvánosan elérhető metadatak a repeaterhez", - "repeater_refreshOwnerInfo": "Frissítse az üzemeltető adatokat", - "repeater_floodMax": "A vízmaximumos ugrások", - "repeater_floodMaxHelper": "A legmagasabb szám, amely egy vízszint-csomagban szerepelhet (0-64)", - "repeater_advancedSettings": "Haladó", - "repeater_advancedSettingsSubtitle": "Erkélő kapcsolók tapasztalt kezelők számára", - "repeater_pathHashMode": "Út-hash mód", - "repeater_pathHashModeHelper": "A byte-ok, amelyek az alábbi repeater-ek azonosítójának kódolására szolgálnak a flood-útvonal/ciklus-észlelő címkékben. 0=1 byte (256 azonosító, akár 64 útvonal), 1=2 byte (65 000 azonosító, akár 32 útvonal), 2=3 byte (16 millió azonosító, akár 21 útvonal). A v1.13-as verziótól kezdődően és az azt követő verziókban a több byte-os útvonalak megszűntek – csak egyetlen útvonal létesül, miután a hálózat a v1.14-es verzióra vagy az azt követő verzióra frissült.", - "repeater_txDelay": "Flood TX késés", - "repeater_txDelayHelper": "Újraküldési intervallum árvíz esetén, amely a csomag átviteli idejének (0-2, alapérték 0,5) szorzata. Minél nagyobb az érték, annál kevesebb ütközés, de lassabb a továbbítás.", - "repeater_directTxDelay": "Közvetlen TX késés", - "repeater_directTxDelayHelper": "A közvetlen (nem tömeges) forgalomhoz tartozó adatcsomagok újrádiózására szolgáló intervallum, amely a csomag átviteli idejének (0-2, alapértelmezett érték 0,3) szorzata.", - "repeater_intThresh": "Interferencia határ", - "repeater_intThreshHelper": "A határt a rádió zajszintjének kalibrálására állították, így elutasítja a fenti szint feletti interferenciákat. 0 kikapcsol – csak akkor állítsa be, ha zajos frekvencián RX hibákat észlel.", - "repeater_agcResetInterval": "AGC visszazárási intervallum", - "repeater_agcResetIntervalHelper": "Mennyi időnként kell a rádió automatikus hangerőszabályozását visszaállítani, hogy kijavítsa a problémát? A visszaállítás időtartama: másoderek, amely 4-szeresével osztható. A 0 érték a periodikus visszaállítás kikapcsolását jelzi.", - "repeater_actionsTitle": "Tevékenységek", - "repeater_sendAdvert": "Eljuttass flood hirdetést", - "repeater_sendAdvertSubtitle": "Terjesztse egy árvíz elleni reklámot a hálózaton keresztül.", - "repeater_sendAdvertZeroHop": "Küldj egy közvetlen hirdetést", - "repeater_sendAdvertZeroHopSubtitle": "Adja közzé egyetlen átjáró hirdetést (nincs átjátszás).", - "repeater_clockSync": "Synchronizálja az órát", - "repeater_clockSyncSubtitle": "Állítsa be a telefon időzítését a repeaterhez.", - "repeater_actionSucceeded": "{action} sikert aratott", + "repeater_ownerInfoHelper": "Nyilvános metaadatok ehhez az átjátszóhoz", + "repeater_refreshOwnerInfo": "Frissítse az operátor adatait", + "repeater_floodMax": "Árvíz max ugrás", + "repeater_floodMaxHelper": "Maximum ugrások, amelyeket egy árvízcsomag utazhat (0-64)", + "repeater_advancedSettings": "Fejlett", + "repeater_advancedSettingsSubtitle": "Hangológombok tapasztalt kezelőknek", + "repeater_pathHashMode": "Útvonal hash mód", + "repeater_pathHashModeHelper": "Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat – csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.", + "repeater_txDelay": "Áradási TX késleltetés", + "repeater_txDelayHelper": "Újraküldési térköz az árvízi forgalomhoz, a csomag sugárzási idejének szorzójaként (0-2, alapértelmezett 0,5). Magasabb = kevesebb ütközés, de lassabb szállítás.", + "repeater_directTxDelay": "Közvetlen TX késleltetés", + "repeater_directTxDelayHelper": "A közvetlen (nem elárasztásos) forgalom újraküldési távolsága a csomag sugárzási idejének szorzójaként (0-2, alapértelmezett 0,3).", + "repeater_intThresh": "Interferencia küszöb", + "repeater_intThreshHelper": "A küszöbérték átkerült a rádió zajszint-kalibrációjához, így az e szint feletti interferenciát elutasítja. 0 letilt – csak akkor emel, ha zajos sávban RX hibákat lát.", + "repeater_agcResetInterval": "AGC reset intervallum", + "repeater_agcResetIntervalHelper": "Milyen gyakran kell visszaállítani a rádió automatikus erősítésszabályozását, hogy helyreálljon a beragadt erősítési állapotból. A másodpercek, 4 többszörösére csökkentve. 0 letiltja az időszakos visszaállításokat.", + "repeater_actionsTitle": "Akciók", + "repeater_sendAdvert": "Árvízhirdetés küldése", + "repeater_sendAdvertSubtitle": "Adjon árvízreklámot a hálózaton keresztül", + "repeater_sendAdvertZeroHop": "Zéró ugrású hirdetés küldése", + "repeater_sendAdvertZeroHopSubtitle": "Egyugrásos hirdetés sugárzása (közvetítés nélkül)", + "repeater_clockSync": "Óra szinkronizálása most", + "repeater_clockSyncSubtitle": "Tolja a telefon idejét az átjátszóhoz", + "repeater_actionSucceeded": "{action} sikerült", "@repeater_actionSucceeded": { "placeholders": { "action": { @@ -1328,8 +1328,8 @@ } } }, - "repeater_settingsSavedRebootNeeded": "Beállítások mentve – újraindítsa a repeatert, hogy alkalmazza", - "repeater_settingsPartialFailure": "Bizonyos beállítások nem sikerültek: {failures}", + "repeater_settingsSavedRebootNeeded": "A beállítások mentve – az átjátszó újraindítása az alkalmazáshoz", + "repeater_settingsPartialFailure": "Néhány beállítás nem sikerült: {failures}", "@repeater_settingsPartialFailure": { "placeholders": { "failures": { @@ -1337,7 +1337,7 @@ } } }, - "repeater_errorSavingSettings": "Hibás beállítások mentése: {error}", + "repeater_errorSavingSettings": "Hiba a beállítások mentésekor: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1345,12 +1345,12 @@ } } }, - "repeater_refreshBasicSettings": "Visszaállítás az alapértékekre", - "repeater_refreshRadioSettings": "Frissítse a rádió beállításait", - "repeater_refreshTxPower": "Újraindítás TX-támogatással", - "repeater_refreshPacketForwarding": "Csomagok továbbításának frissítése", - "repeater_refreshGuestAccess": "Újraindítás vendégHozzáférés", - "repeater_refreshPrivacyMode": "Visszaállítás a magánéletvédő módra", + "repeater_refreshBasicSettings": "Frissítse az alapvető beállításokat", + "repeater_refreshRadioSettings": "Frissítse a rádióbeállításokat", + "repeater_refreshTxPower": "TX teljesítmény frissítése", + "repeater_refreshPacketForwarding": "Csomagtovábbítás frissítése", + "repeater_refreshGuestAccess": "Vendég hozzáférés frissítése", + "repeater_refreshPrivacyMode": "Frissítse az adatvédelmi módot", "repeater_refreshed": "{label} frissítve", "@repeater_refreshed": { "placeholders": { @@ -1359,7 +1359,7 @@ } } }, - "repeater_errorRefreshing": "Hiba a {label} frissítés közben", + "repeater_errorRefreshing": "Hiba a {label} frissítésekor", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1367,17 +1367,17 @@ } } }, - "repeater_cliTitle": "CLI (parancssori felület)", - "repeater_debugNextCommand": "Hibakeresés, következő parancs", - "repeater_commandHelp": "Segítség", - "repeater_clearHistory": "Egyértelmű történet", - "repeater_noCommandsSent": "Még egyik parancsot sem küldtünk.", - "repeater_typeCommandOrUseQuick": "Írja be a parancsot alább, vagy használja a gyors parancsokat.", + "repeater_cliTitle": "Repeater CLI", + "repeater_debugNextCommand": "Debug Next Command", + "repeater_commandHelp": "Command Help", + "repeater_clearHistory": "Törölje az előzményeket", + "repeater_noCommandsSent": "Még nem küldtek parancsot", + "repeater_typeCommandOrUseQuick": "Írjon be egy parancsot alább, vagy használjon gyorsparancsokat", "repeater_enterCommandHint": "Írja be a parancsot...", "repeater_previousCommand": "Előző parancs", "repeater_nextCommand": "Következő parancs", - "repeater_enterCommandFirst": "Add meg először egy parancsot", - "repeater_cliCommandFrameTitle": "CLI parancssor felépítése", + "repeater_enterCommandFirst": "Először írjon be egy parancsot", + "repeater_cliCommandFrameTitle": "CLI parancskeret", "repeater_cliCommandError": "Hiba: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1386,81 +1386,81 @@ } } }, - "repeater_cliQuickGetName": "Kapcsold össze a nevet", - "repeater_cliQuickGetRadio": "Szerezd a rádiót", - "repeater_cliQuickGetTx": "Szerezd a TX-t", + "repeater_cliQuickGetName": "Get Name", + "repeater_cliQuickGetRadio": "Szerezz rádiót", + "repeater_cliQuickGetTx": "Szerezd meg a TX-et", "repeater_cliQuickNeighbors": "Szomszédok", - "repeater_cliQuickVersion": "Verzió", - "repeater_cliQuickAdvertise": "Hirdetés", - "repeater_cliQuickClock": "óra", - "repeater_cliHelpAdvert": "Elküldi egy hirdetési csomagot", - "repeater_cliHelpReboot": "Újraindítja a készüléket. (Kérjük, vegye figyelembe, hogy valószínűleg \"Időhiba\" üzenetet fog kapni, ami normális)", - "repeater_cliHelpClock": "A jelenlegi időt mutatja az egyes eszközök karórája alapján.", - "repeater_cliHelpPassword": "Új adminisztrációs jelszót állít be a eszköz számára.", - "repeater_cliHelpVersion": "Megjeleníti a készülék verzióját és a szoftver verziószámát.", - "repeater_cliHelpClearStats": "Visszaállítja a különböző statisztikai mérőszámokat a nullára.", - "repeater_cliHelpSetAf": "Beállítja az idő-szabályozási tényezőt.", - "repeater_cliHelpSetTx": "Beállítja a LoRa átviteli teljesítményt dBm-ben (a rendszer újraindításával alkalmazható).", - "repeater_cliHelpSetRepeat": "Engedélyezi vagy tiltja meg a repeater szerepet ezen a csomón.", - "repeater_cliHelpSetAllowReadOnly": "(Szoba szerver) Ha \"igen\", akkor üres jelszóval történő bejelentkezés engedélyezett lesz, de nem lehet üzeneteket küldeni a szobában. (Csak olvasási funkció)", - "repeater_cliHelpSetFloodMax": "Beállítja a bejövő adatcsomagok maximális számát (ha ez a érték nagyobb vagy egyenlő a maximális értékkel, a csomag nem továbbítódik).", - "repeater_cliHelpSetIntThresh": "Beállítja az interferencia határértéket (dB-ben). Az alapérték 14. Ha 0-ra állítja, kiküntheti a csatornák közötti interferencia detektálást.", - "repeater_cliHelpSetAgcResetInterval": "Beállítja az intervallumot, amely a \"Automatikus gain\" szabályozó újraindításához szükséges. Beállítás értéke 0, ha a funkciót le kell tiltani.", - "repeater_cliHelpSetMultiAcks": "Engedélyezi vagy kikapcsolja a „dupla visszaigazolás” funkciót.", - "repeater_cliHelpSetAdvertInterval": "Beállítja az időzítő intervallumot percenként, hogy egy helyi (nincs átjáró) hirdetési csomagot küldjen. Beállítás értéke 0, ha a funkciót le szeretné tiltani.", - "repeater_cliHelpSetFloodAdvertInterval": "Beállítja az időzítő intervallumot órában, hogy egy \"áramló\" hirdetési üzenetet küldjön. Beállítás értéke 0, ha a funkciót kikapcsolni kell.", - "repeater_cliHelpSetGuestPassword": "Beállítja/frissíti a vendég felhasználói fiókot. (Ez lehetővé teszi a visszatérő felhasználók számára, hogy a \"Statistika lekérdezése\" kérést elküldjék)", - "repeater_cliHelpSetName": "Megadja az űrlap neve.", - "repeater_cliHelpSetLat": "Beállítja az hirdetés térképen megjelenő pont koordinátájának (tizedes fokokban) a latitude-ját.", - "repeater_cliHelpSetLon": "Beállítja az hirdetés térképen megjelenő hosszúság koordinátát (tizedes fokokban).", - "repeater_cliHelpSetRadio": "Teljesen új rádióparamétereket állít be, és azokat a beállításokba menti. Az alkalmazásához \"újraindítás\" parancs szükséges.", - "repeater_cliHelpSetRxDelay": "Beállítások (kísérleti): Alapérték (legalább 1 értékre kell állítani, hogy hatás legyen), amely alapján a fogadott csomagokhoz enyhe késést alkalmazunk, a jelet ereje/pontszám alapján. 0-ra állítva a funkciót lekapcsoljuk.", - "repeater_cliHelpSetTxDelay": "Beállítja egy tényezőt, amely a légköri idővel szorozva, egy áramlás-üzem módú csomaghoz, valamint egy véletlenszerű slot-rendszerhez, hogy késleltesse a továbbítását. (az ütközések valószínűségének csökkentése érdekében)", - "repeater_cliHelpSetDirectTxDelay": "Hasonló a txdelay-hez, de ebben az esetben egy véletlenszerű késést alkalmazunk a közvetlen módú csomagok továbbításakor.", - "repeater_cliHelpSetBridgeEnabled": "Engedélyez/Tiltás a híd funkciójának.", - "repeater_cliHelpSetBridgeDelay": "Állíts be egy késleztatást a csomagok újbóli továbbításakor.", - "repeater_cliHelpSetBridgeSource": "Döntse el, hogy a híd fogadott vagy elküldött csomagokat fogja-e továbbítani.", - "repeater_cliHelpSetBridgeBaud": "Állítsa be a soros kommunikáció sebességét az RS232 hídok számára.", - "repeater_cliHelpSetBridgeSecret": "Állítsa be a titkos kapcsolatot az ESPNOW hídokhoz.", - "repeater_cliHelpSetAdcMultiplier": "Lehetővé teszi a felhasználónak, hogy egyedi tényezőt állíts be a riportolt akkumulátor feszültségének módosításához (ez csak bizonyos alkatrészeken támogatott).", - "repeater_cliHelpTempRadio": "Időjárás szerinti rádióparamétereket állít be a megadott időtartamra, majd visszaállítja az eredeti beállításokat. (Nem menti a beállításokat a beállítások részben).", - "repeater_cliHelpSetPerm": "A ACL-t módosítja. Ha a \"permissions\" érték 0, akkor eltávolítja a megfelelő bejegyzést (a pubkey előtag alapján). Új bejegyzést hoz létre, ha a pubkey-hex teljes hossza, és jelenleg nem szerepel az ACL-ben. A bejegyzést frissíti a megfelelő pubkey előtag alapján. A engedélyek különbözőek a különböző firmware szerepek között, de az alsó 2 bit a következő értékeket képviseli: 0 (Vendég), 1 (Csak olvasás), 2 (Olvasás és írás), 3 (Adminisztrátor)", - "repeater_cliHelpGetBridgeType": "Kapcsolatok: hid típusú, RS232, ESPNOW", - "repeater_cliHelpLogStart": "Elindítja a csomagok naplózását a fájlrendszerbe.", - "repeater_cliHelpLogStop": "Megállítja a csomagok naplózását a fájlrendszerbe.", - "repeater_cliHelpLogErase": "Törli a fájlrendszerből a csomagok log-fájljait.", - "repeater_cliHelpNeighbors": "Mutat egy listát, amely tartalmazza a más repeater-ek által hallott adatok listáját, amelyek 0-hop hirdetések révén érhetők el. Minden sor az alábbi formát követi: id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Törli az első, a megadott kulcs-prefix (hexadecimális formában) alapján megegyező bejegyzést a szomszédok listájából.", - "repeater_cliHelpRegion": "(sorozat) Lista az összes meghatározott területet és a jelenlegi árvízvédelmi engedélyeket.", - "repeater_cliHelpRegionLoad": "FIGYELEM: ez egy speciális, több parancsot tartalmazó futtatás. Minden következő parancs egy területtel kapcsolatos, amely egyenletes szóközökkel (a szülő-gyermek kapcsolatot jelző) megkülönböztethető. A parancs végrehajtása egy üres sor/parancs küldésével történik.", - "repeater_cliHelpRegionGet": "Keresések egy adott név előtérrel (vagy \"*\" globális hatókörre). Válasz: \"-> region-név (szülő-név) 'F'\"", - "repeater_cliHelpRegionPut": "Hozzáad vagy frissíti egy régió definíciót megadott néven.", - "repeater_cliHelpRegionRemove": "Eltávolítja a megadott nevet használó régió-definíciót. (pontosan meg kell egyeznie, és nem lehet gyermekrégiója)", - "repeater_cliHelpRegionAllowf": "Beállítja a megadott területre vonatkozó \"víz\" jogosultságot. (A globális/régi beállítások esetén a \"*\" jelölő)", - "repeater_cliHelpRegionDenyf": "Eltávolítja a megadott területre vonatkozó \"F\"lood (víz) engedélyt. (FIGYELEM: jelenleg nem javasolt ezt a globális/régi verzióban használni!!)", - "repeater_cliHelpRegionHome": "Visszaállítja a jelenlegi „otthoni” régiót. (Ez a beállítás még nem került alkalmazásra, csak jövőbeli használatra fenyelve)", - "repeater_cliHelpRegionHomeSet": "Beállítja a \"házi\" régiót.", - "repeater_cliHelpRegionSave": "Megőrzi a régió listát/térképet a tárolóban.", - "repeater_cliHelpGps": "Megadja a GPS állapotát. Ha a GPS kikapcsolva van, akkor csak \"ki\" választot ad, ha be van, akkor \"be\", \"állapot\", \"pozíció\", \"satellitok száma\" értékeket ad.", - "repeater_cliHelpGpsOnOff": "Engedi a GPS működés állapotát.", - "repeater_cliHelpGpsSync": "A hálózati időt az GPS óra időjével szinkronizálja.", - "repeater_cliHelpGpsSetLoc": "Beállítja a węsz pozícióját GPS koordináták alapján, és menti a beállításokat.", - "repeater_cliHelpGpsAdvert": "Adja meg a hirdetés konfigurációjának helyszín-információját:\n- none: ne tartalmazza a helyszínt a hirdetésekben\n- share: megosztja a GPS-helyszínt (SensorManager-ből)\n- prefs: hirdeti a beállításokban tárolt helyszínt", - "repeater_cliHelpGpsAdvertSet": "Beállítja a hirdetés helyszín-specifikus beállításait.", - "repeater_commandsListTitle": "Parancsok listája", - "repeater_commandsListNote": "FIGYELEM: a különböző \"set ...\" parancsok mellett létezik egy \"get ...\" parancs is.", + "repeater_cliQuickVersion": "Változat", + "repeater_cliQuickAdvertise": "Hirdet", + "repeater_cliQuickClock": "Óra", + "repeater_cliHelpAdvert": "Reklámcsomagot küld", + "repeater_cliHelpReboot": "Újraindítja az eszközt. (megjegyzendő, hogy \"Időtúllépés\" jelenik meg, ami normális)", + "repeater_cliHelpClock": "Megjeleníti az aktuális időt az eszköz órájánként.", + "repeater_cliHelpPassword": "Új rendszergazdai jelszót állít be az eszközhöz.", + "repeater_cliHelpVersion": "Megmutatja az eszköz verzióját és a firmware felépítési dátumát.", + "repeater_cliHelpClearStats": "Nullára állítja a különböző statisztikai számlálókat.", + "repeater_cliHelpSetAf": "Beállítja a műsoridő-tényezőt.", + "repeater_cliHelpSetTx": "A LoRa adási teljesítményét dBm-ben állítja be. (újraindítás az alkalmazáshoz)", + "repeater_cliHelpSetRepeat": "Engedélyezi vagy letiltja a csomópont ismétlő szerepét.", + "repeater_cliHelpSetAllowReadOnly": "(Szobaszerver) Ha 'be', akkor az üres jelszó bejelentkezés engedélyezett, de nem lehet postázni a szobába. (csak olvasható)", + "repeater_cliHelpSetFloodMax": "Beállítja a bejövő árvízcsomag ugrásainak maximális számát (ha >= max, a csomag nem kerül továbbításra)", + "repeater_cliHelpSetIntThresh": "Beállítja az interferencia küszöböt (DB-ben). Az alapértelmezett érték 14. Állítsa 0-ra a csatornainterferencia-érzékelés letiltásához.", + "repeater_cliHelpSetAgcResetInterval": "Beállítja az Auto Gain Controller alaphelyzetbe állításának intervallumát. A letiltáshoz állítsa 0-ra.", + "repeater_cliHelpSetMultiAcks": "Engedélyezi vagy letiltja a „dupla ACK” funkciót.", + "repeater_cliHelpSetAdvertInterval": "Beállítja a helyi (nulla ugrású) hirdetési csomag küldésének időzítési időközét percekben. A letiltáshoz állítsa 0-ra.", + "repeater_cliHelpSetFloodAdvertInterval": "Beállítja az időzítő intervallumát órákban az árvízhirdetési csomag küldéséhez. A letiltáshoz állítsa 0-ra.", + "repeater_cliHelpSetGuestPassword": "Beállítja/frissíti a vendég jelszavát. (Repeaterek esetén a vendégbejelentkezés elküldheti a \"Statisztikák lekérése\" kérést)", + "repeater_cliHelpSetName": "Beállítja a hirdetés nevét.", + "repeater_cliHelpSetLat": "Beállítja a hirdetéstérkép szélességi fokát. (tizedes fok)", + "repeater_cliHelpSetLon": "Beállítja a hirdetési térkép hosszúsági fokát. (tizedes fok)", + "repeater_cliHelpSetRadio": "Teljesen új rádióparamétereket állít be, és elmenti a beállításokhoz. Az alkalmazáshoz \"reboot\" parancs szükséges.", + "repeater_cliHelpSetRxDelay": "Beállítja a (kísérleti) bázist (az effektushoz > 1-nek kell lennie) a fogadott csomagok enyhe késleltetéséhez, a jelerősség/pontszám alapján. A letiltáshoz állítsa 0-ra.", + "repeater_cliHelpSetTxDelay": "Beállít egy tényezőt, megszorozva az adásidővel egy elárasztásos módú csomaghoz és egy véletlenszerű résrendszerhez, hogy késleltesse a továbbítását. (az ütközések valószínűségének csökkentése érdekében)", + "repeater_cliHelpSetDirectTxDelay": "Ugyanaz, mint a txdelay, de véletlenszerű késleltetés alkalmazására a közvetlen módú csomagok továbbítására.", + "repeater_cliHelpSetBridgeEnabled": "Bridge engedélyezése/letiltása.", + "repeater_cliHelpSetBridgeDelay": "Állítsa be a késleltetést a csomagok újraküldése előtt.", + "repeater_cliHelpSetBridgeSource": "Válassza ki, hogy a híd újraküldje-e a fogadott vagy továbbított csomagokat.", + "repeater_cliHelpSetBridgeBaud": "Soros kapcsolat átviteli sebességének beállítása rs232 hidakhoz.", + "repeater_cliHelpSetBridgeSecret": "Állítsa be a hídtitkot espnow hidakhoz.", + "repeater_cliHelpSetAdcMultiplier": "Egyéni tényezőt állít be a jelentett akkumulátorfeszültség beállításához (csak bizonyos kártyákon támogatott).", + "repeater_cliHelpTempRadio": "Ideiglenes rádióparamétereket állít be a megadott számú percre, majd visszaáll az eredeti rádióparaméterekre. (NEM menti a beállításokba).", + "repeater_cliHelpSetPerm": "Módosítja az ACL-t. Eltávolítja a megfelelő bejegyzést (pubkey előtag alapján), ha az „engedélyek” értéke nulla. Új bejegyzést ad hozzá, ha a pubkey-hex teljes hosszúságú, és jelenleg nincs az ACL-ben. Frissíti a bejegyzést a pubkey előtag egyeztetésével. Az engedélybitek firmware-szerepkörönként változnak, de az alacsony 2 bit a következő: 0 (Vendég), 1 (Csak olvasható), 2 (Olvasás, írás), 3 (Adminisztrátor)", + "repeater_cliHelpGetBridgeType": "Hidatípust nem kap, rs232, espnow", + "repeater_cliHelpLogStart": "Elindítja a csomagnaplózást a fájlrendszerbe.", + "repeater_cliHelpLogStop": "Leállítja a csomagok naplózását a fájlrendszerbe.", + "repeater_cliHelpLogErase": "Törli a csomagnaplókat a fájlrendszerből.", + "repeater_cliHelpNeighbors": "Megjeleníti a nulla ugrású hirdetéseken keresztül hallható egyéb átjátszó csomópontok listáját. Minden sor id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Eltávolítja az első egyező bejegyzést (a pubkey előtag (hex) alapján) a szomszédok listájából.", + "repeater_cliHelpRegion": "(csak soros) Felsorolja az összes meghatározott régiót és az aktuális árvízi engedélyeket.", + "repeater_cliHelpRegionLoad": "MEGJEGYZÉS: ez egy speciális többparancsos hívás. Minden következő parancs egy régiónév (szóközökkel behúzva a szülőhierarchiát jelölve, legalább egy szóközzel). Üres sor/parancs küldésével megszűnik.", + "repeater_cliHelpRegionGet": "Régiót keres adott név előtaggal (vagy \"*\" a globális hatókörhöz). A válasz a következővel: \"-> régiónév (szülőnév) 'F'\"", + "repeater_cliHelpRegionPut": "Adott névvel rendelkező régiódefiníció hozzáadása vagy frissítése.", + "repeater_cliHelpRegionRemove": "Eltávolítja a megadott nevű régiódefiníciót. (pontosan meg kell egyeznie, és nem lehetnek alárendelt régiók)", + "repeater_cliHelpRegionAllowf": "Beállítja az 'F'lood engedélyt az adott régióhoz. (\"*\" a globális/örökölt hatókörre)", + "repeater_cliHelpRegionDenyf": "Eltávolítja az 'F'lood engedélyt az adott régióhoz. (MEGJEGYZÉS: ebben a szakaszban NEM tanácsos ezt használni a globális/örökölt hatókörön!!)", + "repeater_cliHelpRegionHome": "Az aktuális „otthoni” régióval válaszol. (A megjegyzés még bárhol érvényes, jövőre fenntartva)", + "repeater_cliHelpRegionHomeSet": "Beállítja az „otthoni” régiót.", + "repeater_cliHelpRegionSave": "Megőrzi a régiólistát/leképezést a tárhelyen.", + "repeater_cliHelpGps": "Megadja a gps állapotát. Ha a gps ki van kapcsolva, akkor csak kikapcsolva válaszol, ha be van kapcsolva, akkor válaszol: be, állapot, javítás, sat count", + "repeater_cliHelpGpsOnOff": "Bekapcsolja a gps tápellátási állapotát.", + "repeater_cliHelpGpsSync": "Szinkronizálja a csomópont idejét a gps órával.", + "repeater_cliHelpGpsSetLoc": "Beállítja a csomópont pozícióját a gps koordinátákra és menti a beállításokat.", + "repeater_cliHelpGpsAdvert": "Megadja a csomópont helyhirdetési konfigurációját:\n- nincs: ne adja meg a helyet a hirdetésekben\n- megosztás: GPS hely megosztása (a SensorManagerből)\n- preferenciák: a beállításokban tárolt hely hirdetése", + "repeater_cliHelpGpsAdvertSet": "Beállítja a helyhirdetés konfigurációját.", + "repeater_commandsListTitle": "Parancslista", + "repeater_commandsListNote": "MEGJEGYZÉS: a különféle \"set ...\" parancsokhoz van egy \"get ...\" parancs is.", "repeater_general": "Általános", - "repeater_settingsCategory": "Beállítások", + "repeater_settingsCategory": "Beállítások elemre", "repeater_bridge": "Híd", - "repeater_logging": "Naplózás", - "repeater_neighborsRepeaterOnly": "Szomszédok (Csak ismétlő funkció)", - "repeater_regionManagementRepeaterOnly": "Regionális menedzsment (Csak egyirányú kommunikáció)", - "repeater_regionNote": "Region-specifikus parancsokat vezettek be a régiók definiálására és a hozzájuk tartozó engedélyek kezelésére.", - "repeater_gpsManagement": "GPS-vezérlés", - "repeater_gpsNote": "Az GPS-al kapcsolatos funkciók lehetővé teszik a helyszín-személyesítéssel kapcsolatos feladatok kezelését.", - "telemetry_receivedData": "Kapott adatokat a szenzorokról", - "telemetry_requestTimeout": "Az adatkapcsolati kérés sikertelen.", - "telemetry_errorLoading": "Hiba az adatok begyűjtésében: {error}", + "repeater_logging": "Fakitermelés", + "repeater_neighborsRepeaterOnly": "Szomszédok (csak átjátszó)", + "repeater_regionManagementRepeaterOnly": "Régiókezelés (csak ismétlő)", + "repeater_regionNote": "Régióparancsok kerültek bevezetésre a régiódefiníciók és engedélyek kezelésére.", + "repeater_gpsManagement": "GPS kezelés", + "repeater_gpsNote": "A gps parancs bevezetésre került a helyhez kapcsolódó témák kezeléséhez.", + "telemetry_receivedData": "Fogadott telemetriai adatok", + "telemetry_requestTimeout": "A telemetriai kérés időtúllépése lejárt.", + "telemetry_errorLoading": "Hiba a telemetria betöltésekor: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1473,25 +1473,25 @@ "telemetry_analogInputLabel": "Analóg bemenet", "telemetry_analogOutputLabel": "Analóg kimenet", "telemetry_genericLabel": "Általános érzékelő", - "telemetry_luminosityLabel": "Fényerő", + "telemetry_luminosityLabel": "Fényesség", "telemetry_presenceLabel": "Jelenlét", - "telemetry_humidityLabel": "Páratartalom", + "telemetry_humidityLabel": "Nedvesség", "telemetry_accelerometerLabel": "Gyorsulásmérő", "telemetry_pressureLabel": "Nyomás", "telemetry_altitudeLabel": "Magasság", "telemetry_frequencyLabel": "Frekvencia", "telemetry_percentageLabel": "Százalék", "telemetry_concentrationLabel": "Koncentráció", - "telemetry_powerLabel": "Teljesítmény", + "telemetry_powerLabel": "Hatalom", "telemetry_distanceLabel": "Távolság", "telemetry_energyLabel": "Energia", "telemetry_directionLabel": "Irány", "telemetry_timeLabel": "Idő", - "telemetry_gyrometerLabel": "Giroszkóp", + "telemetry_gyrometerLabel": "Girométer", "telemetry_colourLabel": "Szín", "telemetry_gpsLabel": "GPS", "telemetry_switchLabel": "Kapcsoló", - "telemetry_polylineLabel": "Töröttvonal", + "telemetry_polylineLabel": "Vonallánc", "telemetry_altitudeValue": "{meters} m", "telemetry_frequencyValue": "{hertz} Hz", "telemetry_pressureValue": "{hpa} hPa", @@ -1503,10 +1503,10 @@ "telemetry_concentrationValue": "{ppm} ppm", "telemetry_percentageValue": "{percent}%", "telemetry_analogValue": "{value}", - "telemetry_autoFetchQuantity": "Kérések száma", + "telemetry_autoFetchQuantity": "Mennyiséget kér", "telemetry_error": "Nem sikerült lekérni az adatokat", - "telemetry_noData": "Nincsenek elérhető telemetriadatok.", - "telemetry_channelTitle": "{channel} csatorna", + "telemetry_noData": "Nincsenek telemetriai adatok.", + "telemetry_channelTitle": "Csatorna {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1514,7 +1514,7 @@ } } }, - "telemetry_batteryLabel": "Akku", + "telemetry_batteryLabel": "Akkumulátor", "telemetry_voltageLabel": "Feszültség", "telemetry_mcuTemperatureLabel": "MCU hőmérséklet", "telemetry_temperatureLabel": "Hőmérséklet", @@ -1546,7 +1546,7 @@ } } }, - "telemetry_temperatureValue": "{celsius} °C / {fahrenheit} °F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1557,9 +1557,9 @@ } } }, - "neighbors_receivedData": "Kapott szomszédok adatait", - "neighbors_requestTimedOut": "A szomszédok kérik, hogy tiltsák le a kamerát.", - "neighbors_errorLoading": "Hiba a szomszédok betöltésében: {error}", + "neighbors_receivedData": "Szomszédok adatok fogadása", + "neighbors_requestTimedOut": "A szomszédok kérése lejárt.", + "neighbors_errorLoading": "Hiba a szomszédok betöltésekor: {error}", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1567,9 +1567,9 @@ } } }, - "neighbors_repeatersNeighbors": "Ismétlő eszközök, szomszédok", - "neighbors_noData": "Nincsenek elérhető szomszédokról adatok.", - "neighbors_unknownContact": "Tudatlan {pubkey}", + "neighbors_repeatersNeighbors": "Ismétlők Szomszédok", + "neighbors_noData": "A szomszédokról nem állnak rendelkezésre adatok.", + "neighbors_unknownContact": "Ismeretlen {pubkey}", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1577,7 +1577,7 @@ } } }, - "neighbors_heardAgo": "Értsd: {time} sitten", + "neighbors_heardAgo": "Hallva: {time} ezelőtt", "@neighbors_heardAgo": { "placeholders": { "time": { @@ -1585,18 +1585,18 @@ } } }, - "channelPath_title": "Csomagok útvonala", - "channelPath_viewMap": "Megtekinthető térkép", + "channelPath_title": "Csomag elérési útja", + "channelPath_viewMap": "Térkép megtekintése", "channelPath_otherObservedPaths": "Egyéb megfigyelt utak", - "channelPath_repeaterHops": "Adat továbbító lépések", - "channelPath_noHopDetails": "Ez a csomag nem tartalmaz részletes információkat a \"hop\" (vagy más hasonló) szót használó kifejezésekről.", + "channelPath_repeaterHops": "Repeater Hops", + "channelPath_noHopDetails": "Ennél a csomagnál a komlórészletek nincsenek megadva.", "channelPath_messageDetails": "Üzenet részletei", - "channelPath_senderLabel": "Megküldő", + "channelPath_senderLabel": "Feladó", "channelPath_timeLabel": "Idő", - "channelPath_repeatsLabel": "Ismétli", - "channelPath_pathLabel": "Útvonal {index}", + "channelPath_repeatsLabel": "Ismétlődik", + "channelPath_pathLabel": "{index} elérési út", "channelPath_observedLabel": "Megfigyelt", - "channelPath_observedPathTitle": "Megfigyelt útvonal: {index} • {hops}", + "channelPath_observedPathTitle": "Megfigyelt útvonal {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1607,7 +1607,7 @@ } } }, - "channelPath_noLocationData": "Nincs helyszínadat.", + "channelPath_noLocationData": "Nincsenek helyadatok", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1630,10 +1630,10 @@ } } }, - "channelPath_unknownPath": "Megfejt", + "channelPath_unknownPath": "Ismeretlen", "channelPath_floodPath": "Árvíz", "channelPath_directPath": "Közvetlen", - "channelPath_observedZeroOf": "0-ból {total}", + "channelPath_observedZeroOf": "0 / {total} ugrás", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1641,7 +1641,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} of {total} hops", + "channelPath_observedSomeOf": "{observed} / {total} ugrás", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1653,8 +1653,8 @@ } }, "channelPath_mapTitle": "Útvonal térkép", - "channelPath_noRepeaterLocations": "Ez a útvonal nem támogat repeater-t.", - "channelPath_primaryPath": "Útvonal {index} (Elsődleges)", + "channelPath_noRepeaterLocations": "Nincs elérhető átjátszó hely ehhez az útvonalhoz.", + "channelPath_primaryPath": "{index} elérési út (elsődleges)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1669,8 +1669,8 @@ } } }, - "channelPath_pathLabelTitle": "Út", - "channelPath_observedPathHeader": "Megfigyelt útvonal", + "channelPath_pathLabelTitle": "Útvonal", + "channelPath_observedPathHeader": "Megfigyelt ösvény", "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { @@ -1682,14 +1682,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "Ez a csomag nem tartalmaz részletes információkat a szállításhoz.", - "channelPath_unknownRepeater": "Tudatlan erősítő", - "community_title": "Helyi közösség", - "community_create": "Teremtsd meg a közösséget", - "community_createDesc": "Légyon létre egy új közösséget, és osszák meg QR-kód segítségével.", - "community_join": "Csatlakozjon", - "community_joinTitle": "Csatlakozzon a közösséghez", - "community_joinConfirmation": "Szeretne csatlakozni a közösséghez, {name}?", + "channelPath_noHopDetailsAvailable": "Ehhez a csomaghoz nem állnak rendelkezésre ugrási részletek.", + "channelPath_unknownRepeater": "Ismeretlen Repeater", + "community_title": "Közösség", + "community_create": "Közösség létrehozása", + "community_createDesc": "Hozzon létre egy új közösséget, és ossza meg QR-kóddal.", + "community_join": "Csatlakozik", + "community_joinTitle": "Csatlakozz a közösséghez", + "community_joinConfirmation": "Szeretnél csatlakozni a \"{name}\" közösséghez?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1697,14 +1697,14 @@ } } }, - "community_scanQr": "QR-kód olvasó a közösség számára", - "community_scanInstructions": "Fordítsa a kamerát egy közösségi QR-kód irányába.", - "community_showQr": "Megjelenítse a QR-kódot", - "community_publicChannel": "Összetartó, közösségi", - "community_hashtagChannel": "Helyi hashtaget", - "community_name": "Helyi közösség neve", - "community_enterName": "Kérjük, a közösség nevét írja be.", - "community_created": "A \"{name}\" nevű közösség létrehozva", + "community_scanQr": "Scan Community QR", + "community_scanInstructions": "Irányítsa a kamerát egy közösségi QR-kódra", + "community_showQr": "QR-kód megjelenítése", + "community_publicChannel": "Közösségi Nyilvános", + "community_hashtagChannel": "Közösségi hashtag", + "community_name": "Közösség neve", + "community_enterName": "Adja meg a közösség nevét", + "community_created": "A \"{name}\" közösség létrehozva", "@community_created": { "placeholders": { "name": { @@ -1712,7 +1712,7 @@ } } }, - "community_joined": "Csatlakozott a {name} közösséghez", + "community_joined": "Csatlakozott a \"{name}\" közösséghez", "@community_joined": { "placeholders": { "name": { @@ -1720,8 +1720,8 @@ } } }, - "community_qrTitle": "Osszpontosítás a közösségben", - "community_qrInstructions": "Scanned this QR-kódot, hogy csatlakozhat a {name} csoporthoz.", + "community_qrTitle": "Közösség megosztása", + "community_qrInstructions": "Olvassa be ezt a QR-kódot a \"{name}\" csoporthoz való csatlakozáshoz", "@community_qrInstructions": { "placeholders": { "name": { @@ -1729,10 +1729,10 @@ } } }, - "community_hashtagPrivacyHint": "A közösségi hashtagekhez tartozó csatornák csak a közösség tagjai számára érhetők el.", + "community_hashtagPrivacyHint": "A közösségi hashtag csatornákhoz csak a közösség tagjai csatlakozhatnak", "community_invalidQrCode": "Érvénytelen közösségi QR-kód", - "community_alreadyMember": "Már tag vagy", - "community_alreadyMemberMessage": "Már tagja {name}-nek.", + "community_alreadyMember": "Már tag", + "community_alreadyMemberMessage": "Ön már tagja a(z) \"{name}\".", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1740,13 +1740,13 @@ } } }, - "community_addPublicChannel": "Hozzon létre egy közösségi nyilvános csatornát", - "community_addPublicChannelHint": "Automatikusan hozzon létre ezt a csatornát a közösség számára.", - "community_noCommunities": "Még egyik közösség sem csatlakozott.", - "community_scanOrCreate": "Scelle egy QR-kódot, vagy hozzon létre egy közösséget, hogy elinduljon.", + "community_addPublicChannel": "Közösségi nyilvános csatorna hozzáadása", + "community_addPublicChannelHint": "Nyilvános csatorna automatikus hozzáadása ehhez a közösséghez", + "community_noCommunities": "Még nem csatlakozott közösség", + "community_scanOrCreate": "Olvassa be a QR-kódot, vagy hozzon létre egy közösséget a kezdéshez", "community_manageCommunities": "Közösségek kezelése", - "community_delete": "Hagyományos közösségi élet", - "community_deleteConfirm": "Hagyom {name}-et?", + "community_delete": "Kilépés a közösségből", + "community_deleteConfirm": "Kilép a(z) „{name}” programból?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1754,7 +1754,7 @@ } } }, - "community_deleteChannelsWarning": "Ezem törli is {count} csatornát és a hozzá tartozó üzeneteket.", + "community_deleteChannelsWarning": "Ezzel {count} csatornát és azok üzeneteit is törli.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1762,7 +1762,7 @@ } } }, - "community_deleted": "A közösség, amely {name}", + "community_deleted": "Kilépett a \"{name}\" közösségből", "@community_deleted": { "placeholders": { "name": { @@ -1770,8 +1770,8 @@ } } }, - "community_regenerateSecret": "Titkos visszaállítás", - "community_regenerateSecretConfirm": "Újra kell generálni a titkos kulcsot {name} számára? Minden tagnak be kell szkennelnie az új QR-kódot, hogy továbbra is kommunikálhasson.", + "community_regenerateSecret": "Regeneráld a Titkot", + "community_regenerateSecretConfirm": "Újragenerálja a titkos kulcsot a következőhöz: \"{name}\"? A kommunikáció folytatásához minden tagnak be kell olvasnia az új QR-kódot.", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1779,8 +1779,8 @@ } } }, - "community_regenerate": "Újraalakítás", - "community_secretRegenerated": "Titkos kulcs megújult {name} számára.", + "community_regenerate": "Regenerátum", + "community_secretRegenerated": "Titok újra létrehozva a következőhöz: \"{name}\"", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1788,8 +1788,8 @@ } } }, - "community_updateSecret": "Frissítési titok", - "community_secretUpdated": "Titkos információ frissítve {name} számára", + "community_updateSecret": "Frissítse a Titkot", + "community_secretUpdated": "Titok frissítve a következőhöz: \"{name}\"", "@community_secretUpdated": { "placeholders": { "name": { @@ -1797,7 +1797,7 @@ } } }, - "community_scanToUpdateSecret": "Scanned a új QR-kódot, hogy frissítsük a {name} számára megőrzött titkos információt.", + "community_scanToUpdateSecret": "Olvassa be az új QR-kódot a „{name}” titkának frissítéséhez", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1805,14 +1805,14 @@ } } }, - "community_addHashtagChannel": "Adjon egy közösségi hashtaget", - "community_addHashtagChannelDesc": "Hozz létre egy hashtage-os csatornát ennek a közösségnek", - "community_selectCommunity": "Válasszon közösséget", + "community_addHashtagChannel": "Közösségi hashtag hozzáadása", + "community_addHashtagChannelDesc": "Adj hozzá egy hashtag-csatornát ehhez a közösséghez", + "community_selectCommunity": "Válassza a Közösség lehetőséget", "community_regularHashtag": "Rendszeres hashtag", - "community_regularHashtagDesc": "Önmagas szintű hashtaget (bárki csatlakozhat)", - "community_communityHashtag": "Helyi hashtaget", - "community_communityHashtagDesc": "Csak a közösség tagjai számára", - "community_forCommunity": "{name} számára", + "community_regularHashtagDesc": "Nyilvános hashtag (bárki csatlakozhat)", + "community_communityHashtag": "Közösségi hashtag", + "community_communityHashtagDesc": "Privát a közösség tagjai számára", + "community_forCommunity": "{name}", "@community_forCommunity": { "placeholders": { "name": { @@ -1821,28 +1821,28 @@ } }, "listFilter_tooltip": "Szűrés és rendezés", - "listFilter_sortBy": "Szűrés", - "listFilter_latestMessages": "Legfrissebb üzenetek", - "listFilter_heardRecently": "Úgy hallottam, hogy...", + "listFilter_sortBy": "Rendezés", + "listFilter_latestMessages": "Legújabb üzenetek", + "listFilter_heardRecently": "Nemrég hallottam", "listFilter_az": "A-Z", "listFilter_filters": "Szűrők", - "listFilter_all": "Mind", + "listFilter_all": "Minden", "listFilter_favorites": "Kedvencek", - "listFilter_addToFavorites": "Megerősítés kívánságlistára", - "listFilter_removeFromFavorites": "Törölj a kedvencekből", + "listFilter_addToFavorites": "Hozzáadás a kedvencekhez", + "listFilter_removeFromFavorites": "Eltávolítás a kedvencek közül", "listFilter_users": "Felhasználók", - "listFilter_repeaters": "Újraküldők", - "listFilter_roomServers": "Szoba-szolgálatok", + "listFilter_repeaters": "Ismétlők", + "listFilter_roomServers": "Szobaszerverek", "listFilter_unreadOnly": "Csak olvasatlan", "listFilter_newGroup": "Új csoport", "pathTrace_you": "Te", - "pathTrace_failed": "A útvonal követése sikertelen.", - "pathTrace_notAvailable": "Az útvonal követési funkció nem elérhető.", - "pathTrace_refreshTooltip": "Út mentesség frissítése.", - "pathTrace_someHopsNoLocation": "Egy vagy több búzavirág hiányozik a helyszínéről!", - "pathTrace_clearTooltip": "Egyértelmű út.", - "losSelectStartEnd": "Válassza ki a kezdő és a végpontokat a LOS-hoz.", - "losRunFailed": "A látószög ellenőrzése sikertelen: {error}", + "pathTrace_failed": "Az útvonal nyomon követése nem sikerült.", + "pathTrace_notAvailable": "Útvonal nyomkövetés nem érhető el.", + "pathTrace_refreshTooltip": "Path Trace frissítése.", + "pathTrace_someHopsNoLocation": "Egy vagy több komló helye hiányzik!", + "pathTrace_clearTooltip": "Tiszta útvonal.", + "losSelectStartEnd": "Válassza ki a LOS kezdő és záró csomópontját.", + "losRunFailed": "A rálátás ellenőrzése sikertelen: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1850,13 +1850,13 @@ } } }, - "losClearAllPoints": "Teljesen tisztázzuk az összes pontot", - "losRunToViewElevationProfile": "Használja a LOS-t, hogy megtekinthesse a magasságkülönbségek diagramját.", + "losClearAllPoints": "Törölje az összes pontot", + "losRunToViewElevationProfile": "Futtassa a LOS-t a magassági profil megtekintéséhez", "losMenuTitle": "LOS menü", - "losMenuSubtitle": "A térképen található pontok kiválasztására vagy a térképen hosszúra nyomva, hogy egyedi pontokat definiálhassunk.", - "losShowDisplayNodes": "Megjelenítsen a megjelenítési egységeket", + "losMenuSubtitle": "Érintse meg a csomópontokat, vagy tartsa lenyomva a térképet az egyéni pontokhoz", + "losShowDisplayNodes": "Megjelenítési csomópontok megjelenítése", "losCustomPoints": "Egyedi pontok", - "losCustomPointLabel": "Egyedi {index}", + "losCustomPointLabel": "Egyéni {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1864,8 +1864,8 @@ } } }, - "losPointA": "A pont A", - "losPointB": "Pont B", + "losPointA": "A pont", + "losPointB": "B pont", "losAntennaA": "Antenna A: {value} {unit}", "@losAntennaA": { "placeholders": { @@ -1877,7 +1877,7 @@ } } }, - "losAntennaB": "Antenna B: {value} {unit}", + "losAntennaB": "B antenna: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1888,9 +1888,9 @@ } } }, - "losRun": "Futtass a LOS-on", - "losNoElevationData": "Nincsenek emelkedési adatok.", - "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}", + "losRun": "Futtassa a LOS-t", + "losNoElevationData": "Nincsenek magassági adatok", + "losProfileClear": "{distance} {distanceUnit}, tiszta LOS, minimális távolság {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1907,7 +1907,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, amelyet {obstruction} akadályoz meg {heightUnit}-ban", + "losProfileBlocked": "{distance} {distanceUnit}, blokkolta: {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1926,7 +1926,7 @@ }, "losStatusChecking": "LOS: ellenőrzés...", "losStatusNoData": "LOS: nincs adat", - "losStatusSummary": "LOS: {clear}/{total} tisztított, {blocked} blokkolt, {unknown} ismeretlen", + "losStatusSummary": "LOS: {clear}/{total} tiszta, {blocked} blokkolva, {unknown} ismeretlen", "@losStatusSummary": { "placeholders": { "clear": { @@ -1943,20 +1943,20 @@ } } }, - "losErrorElevationUnavailable": "Az alábbi minták esetében nem áll rendelkezésre magasságadat.", - "losErrorInvalidInput": "Hibás vagy hiányos táblázatok a LOS (Loss of Signal) számításához.", - "losRenameCustomPoint": "Állítsa meg a saját pont nevét", + "losErrorElevationUnavailable": "Egy vagy több mintához nem állnak rendelkezésre magassági adatok.", + "losErrorInvalidInput": "Érvénytelen pontok/emelkedési adatok a LOS számításhoz.", + "losRenameCustomPoint": "Egyéni pont átnevezése", "losPointName": "Pont neve", - "losShowPanelTooltip": "Megjelenítse a LOS paneelt", - "losHidePanelTooltip": "Rejtse el a LOS paneelt", - "losElevationAttribution": "Magasságadatok: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Radio Horizont", - "losLegendLosBeam": "LOS jelzés", - "losLegendTerrain": "Terület", - "losFrequencyLabel": "Hatósság", - "losFrequencyInfoTooltip": "Lásd a számítás részleteit", - "losFrequencyDialogTitle": "A rádióhullámok hatótávolságának kiszámítása", - "losFrequencyDialogDescription": "A {baselineK} értékből kezdve, {baselineFreq} MHz-os frekvencián, a számítás az aktuális {frequencyMHz} MHz-os sávhoz igazítja a k-tényezőt, amely meghatározza a görbös rádióhatótávolság határát.", + "losShowPanelTooltip": "LOS panel megjelenítése", + "losHidePanelTooltip": "LOS panel elrejtése", + "losElevationAttribution": "Magassági adatok: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádióhorizont", + "losLegendLosBeam": "LOS gerenda", + "losLegendTerrain": "Terep", + "losFrequencyLabel": "Frekvencia", + "losFrequencyInfoTooltip": "Tekintse meg a számítás részleteit", + "losFrequencyDialogTitle": "Rádióhorizont számítás", + "losFrequencyDialogDescription": "A k={baselineK} értéktől kezdve {baselineFreq} MHz-en a számítás az aktuális {frequencyMHz} MHz-es sávhoz igazítja a k-tényezőt, amely meghatározza az ívelt rádióhorizont-sapkát.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1974,14 +1974,14 @@ } } }, - "contacts_pathTrace": "Útvonal követése", + "contacts_pathTrace": "Út nyom", "contacts_ping": "Ping", - "contacts_repeaterPathTrace": "Az útvonal követése a repeaterig", - "contacts_repeaterPing": "Ping-szinkronizáló", - "contacts_roomPathTrace": "Kapcsolat a szobai szerverrel", - "contacts_roomPing": "Ping-szolgáló szerver", - "contacts_chatTraceRoute": "Útvonal meghatározása", - "contacts_pathTraceTo": "Keresse meg a {name} címét.", + "contacts_repeaterPathTrace": "Útvonal nyomkövetése az átjátszóhoz", + "contacts_repeaterPing": "Ping átjátszó", + "contacts_roomPathTrace": "Útvonal nyomkövetése a szobaszerverhez", + "contacts_roomPing": "Ping szoba szerver", + "contacts_chatTraceRoute": "Út nyomvonala", + "contacts_pathTraceTo": "Útvonal nyomon követése ide: {name}", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1989,21 +1989,21 @@ } } }, - "contacts_clipboardEmpty": "A kiválasztott szöveg üres.", - "contacts_invalidAdvertFormat": "Érvénytelen kontaktinformáció", - "contacts_contactImported": "Kapcsolat létrejött.", - "contacts_contactImportFailed": "Nem sikerült a kapcsolatot importálni.", - "contacts_zeroHopAdvert": "Zero Hop reklám", - "contacts_floodAdvert": "Árvízre vonatkozó hirdetés", - "contacts_copyAdvertToClipboard": "Másolja a hirdetést a kiválasztási ablakba", - "contacts_addContactFromClipboard": "Adjon hozzá egy kapcsolatot a kiválasztott listából", - "contacts_ShareContact": "Másolja a kapcsolatot a kiválasztóba", - "contacts_ShareContactZeroHop": "Ossza meg a kapcsolatot hirdetés segítségével", - "contacts_zeroHopContactAdvertSent": "Kapcsolatot a hirdetésen keresztül.", - "contacts_zeroHopContactAdvertFailed": "Nem sikerült a kapcsolatot elküldeni.", - "contacts_contactAdvertCopied": "A hirdetés másolva a vágólapra.", - "contacts_contactAdvertCopyFailed": "Az hirdetés másolása a vágólapra sikertelen.", - "notification_activityTitle": "MeshCore tevékenységek", + "contacts_clipboardEmpty": "A vágólap üres.", + "contacts_invalidAdvertFormat": "Érvénytelen kapcsolattartási adatok", + "contacts_contactImported": "A névjegy importálása megtörtént.", + "contacts_contactImportFailed": "Nem sikerült importálni a névjegyet.", + "contacts_zeroHopAdvert": "Zero Hop hirdetés", + "contacts_floodAdvert": "Árvíz hirdetés", + "contacts_copyAdvertToClipboard": "Hirdetés másolása a vágólapra", + "contacts_addContactFromClipboard": "Névjegy hozzáadása a vágólapról", + "contacts_ShareContact": "Névjegy másolása a vágólapra", + "contacts_ShareContactZeroHop": "Kapcsolat megosztása hirdetéssel", + "contacts_zeroHopContactAdvertSent": "Az elérhetőséget hirdetéssel küldték el.", + "contacts_zeroHopContactAdvertFailed": "Nem sikerült elküldeni a névjegyet.", + "contacts_contactAdvertCopied": "A hirdetés a vágólapra másolva.", + "contacts_contactAdvertCopyFailed": "A hirdetés vágólapra másolása nem sikerült.", + "notification_activityTitle": "MeshCore tevékenység", "notification_messagesCount": "{count} {count, plural, =1{üzenet} other{üzenetek}}", "@notification_messagesCount": { "placeholders": { @@ -2028,7 +2028,7 @@ } } }, - "notification_newTypeDiscovered": "Új {contactType} megtalálva", + "notification_newTypeDiscovered": "Új {contactType} felfedezve", "@notification_newTypeDiscovered": { "placeholders": { "contactType": { @@ -2036,58 +2036,58 @@ } } }, - "notification_receivedNewMessage": "Új üzenetet kaptam", - "settings_gpxExportRepeaters": "Külső eszközök / helyi szerver a GPX formátumba", - "settings_gpxExportRepeatersSubtitle": "Exportálható repeater/szobaterm-szerver, amely egy GPX fájlban tárolja a helyzetet.", - "settings_gpxExportContacts": "GPX export funkciók", - "settings_gpxExportContactsSubtitle": "Az export funkció lehetővé teszi, hogy a GPS fájlban megadott helyszínen is megőrizzük az útvonalat.", - "settings_gpxExportAll": "Exportálja az összes kapcsolatot GPX formátumban.", - "settings_gpxExportAllSubtitle": "Az összes elérhetőséget, amelyekhez egy helyszín tartozik, egy GPX fájlba exportálja.", - "settings_gpxExportSuccess": "A GPX fájl sikeresen exportálva lett.", - "settings_gpxExportNoContacts": "Nincs exportálható kapcsolatok.", - "settings_gpxExportNotAvailable": "Nem támogatott a jelenlegi eszközön/rendszeren.", - "settings_gpxExportError": "Hiba történt az export során.", - "settings_gpxExportRepeatersRoom": "Adatátvisszaadó eszközök és helyiségi szerverek helyei", - "settings_gpxExportChat": "Kapcsolódó helyszínek", - "settings_gpxExportAllContacts": "Az összes kapcsolat helyszíne", - "settings_gpxExportShareText": "A meshcore-open-ból exportált térkéadatumok", - "settings_gpxExportShareSubject": "meshcore-open GPX formátumú térképi adatok export", - "snrIndicator_nearByRepeaters": "Helyszíni erősítők", - "snrIndicator_lastSeen": "Utoljára, amikor látták", - "contactsSettings_title": "Kapcsolatok beállításai", + "notification_receivedNewMessage": "Új üzenet érkezett", + "settings_gpxExportRepeaters": "Ismétlők / szobaszerver exportálása GPX-be", + "settings_gpxExportRepeatersSubtitle": "Exportálja az átjátszókat / szobaszervereket a hellyel GPX-fájlba.", + "settings_gpxExportContacts": "Exportáljon társokat a GPX-be", + "settings_gpxExportContactsSubtitle": "A hellyel rendelkező kísérőket GPX-fájlba exportálja.", + "settings_gpxExportAll": "Az összes névjegy exportálása a GPX-be", + "settings_gpxExportAllSubtitle": "A hellyel rendelkező összes névjegyet GPX-fájlba exportálja.", + "settings_gpxExportSuccess": "A GPX fájl sikeresen exportálva.", + "settings_gpxExportNoContacts": "Nincsenek exportálandó névjegyek.", + "settings_gpxExportNotAvailable": "Az Ön eszköze/OS nem támogatja", + "settings_gpxExportError": "Hiba történt az exportálás során.", + "settings_gpxExportRepeatersRoom": "Repeater és szobaszerver helyei", + "settings_gpxExportChat": "Társ helyek", + "settings_gpxExportAllContacts": "Minden kapcsolattartó hely", + "settings_gpxExportShareText": "A térképadatok a meshcore-openből exportálva", + "settings_gpxExportShareSubject": "meshcore-open GPX térképadatok exportálása", + "snrIndicator_nearByRepeaters": "Közeli átjátszók", + "snrIndicator_lastSeen": "Utoljára látott", + "contactsSettings_title": "Névjegyek beállításai", "contactsSettings_autoAddTitle": "Automatikus felfedezés", - "contactsSettings_otherTitle": "Egyéb kapcsolattal kapcsolatos beállítások", - "contactsSettings_autoAddUsersTitle": "Automatikus felhasználói hozzáadás", - "contactsSettings_autoAddUsersSubtitle": "Engedje, hogy a segítő automatikusan hozzáadja az új felhasználókat.", - "contactsSettings_autoAddRepeatersTitle": "Automatikus visszatöltés", - "contactsSettings_autoAddRepeatersSubtitle": "Engedje, hogy a segítő eszköz automatikusan hozzáadja az új, megtalált jelzőállomásokat.", - "contactsSettings_autoAddRoomServersTitle": "Automatikus szobák szerverek hozzáadása", - "contactsSettings_autoAddRoomServersSubtitle": "Engedje, hogy a segítő automatikusan hozzáadja az új, megtalált hálózati szervereket.", - "contactsSettings_autoAddSensorsTitle": "Automatikus érzékelők hozzáadása", - "contactsSettings_autoAddSensorsSubtitle": "Engedje, hogy a kísérő automatikusan hozzáadja az új, megtalált szenzorokat.", - "contactsSettings_overwriteOldestTitle": "Felülírja a legrégebbet", - "contactsSettings_overwriteOldestSubtitle": "Amikor a névsor telítődik, a legidősebb, de még nem kedvencként jelölt személyt helyettesíti egy újabb.", - "discoveredContacts_Title": "Megtalált kapcsolatok", - "discoveredContacts_noMatching": "Nincs megegyező kapcsolat.", - "discoveredContacts_searchHint": "Keress új kapcsolatokat", + "contactsSettings_otherTitle": "Egyéb kapcsolattartási beállítások", + "contactsSettings_autoAddUsersTitle": "Felhasználók automatikus hozzáadása", + "contactsSettings_autoAddUsersSubtitle": "Engedélyezze a társ számára, hogy automatikusan hozzáadja a felfedezett felhasználókat.", + "contactsSettings_autoAddRepeatersTitle": "Ismétlők automatikus hozzáadása", + "contactsSettings_autoAddRepeatersSubtitle": "Engedélyezze a társnak, hogy automatikusan hozzáadja a felfedezett ismétlőket.", + "contactsSettings_autoAddRoomServersTitle": "Szobaszerverek automatikus hozzáadása", + "contactsSettings_autoAddRoomServersSubtitle": "Engedélyezze a társ számára, hogy automatikusan hozzáadja a felfedezett szobaszervereket.", + "contactsSettings_autoAddSensorsTitle": "Érzékelők automatikus hozzáadása", + "contactsSettings_autoAddSensorsSubtitle": "Engedélyezze a társ számára a felfedezett érzékelők automatikus hozzáadását.", + "contactsSettings_overwriteOldestTitle": "A legrégebbi felülírása", + "contactsSettings_overwriteOldestSubtitle": "Amikor a névjegylista megtelik, a legrégebbi, nem kedvenc névjegy lecserélődik.", + "discoveredContacts_Title": "Felfedezett kapcsolatok", + "discoveredContacts_noMatching": "Nincsenek megfelelő névjegyek", + "discoveredContacts_searchHint": "Keresés a talált névjegyek között", "discoveredContacts_contactAdded": "Kapcsolat hozzáadva", - "discoveredContacts_addContact": "Adjon személyhez", - "discoveredContacts_copyContact": "Másolja a kapcsolatot a vágólapra", - "discoveredContacts_deleteContact": "Törölj a feltalált kapcsolatot", - "discoveredContacts_deleteContactAll": "Törölj minden megtalált kapcsolatot", - "discoveredContacts_deleteContactAllContent": "Biztos, hogy szeretné törölni az összes eddig megtalált kapcsolatot?", - "chat_sendCooldown": "Kérjük, várjon egy pillanatot, mielőtt újra elküldené.", - "appSettings_jumpToOldestUnread": "Jelentkezzen az legörebb, olvasatlan üzenetre", - "appSettings_jumpToOldestUnreadSubtitle": "Amikor egy új csevet indítunk, amelyben vannak olvashatatlan üzenetek, görgessük a listát, hogy a legelső, olvashatatlan üzenet megjelenjen, nem pedig az utolsó.", - "appSettings_languageHu": "Magyar", - "appSettings_languageJa": "Japán", - "appSettings_languageKo": "Koreai", - "radioStats_tooltip": "Rádió és hálózati statisztikák", - "radioStats_screenTitle": "Rádió statisztikák", - "radioStats_notConnected": "Csatlakozzon egy eszközhöz, hogy megtekinthesse a rádió adatok statisztikáit.", - "radioStats_firmwareTooOld": "A rádió statisztikákhoz v8 vagy újabb verziójú szoftver szükséges.", - "radioStats_waiting": "Adatokra vár…", - "radioStats_noiseFloor": "Háttérzaj szint: {noiseDbm} dBm", + "discoveredContacts_addContact": "Névjegy hozzáadása elemre", + "discoveredContacts_copyContact": "Névjegy másolása a vágólapra", + "discoveredContacts_deleteContact": "A felfedezett névjegy törlése", + "discoveredContacts_deleteContactAll": "Törölje az összes felfedezett névjegyet", + "discoveredContacts_deleteContactAllContent": "Biztosan törli az összes felfedezett névjegyet?", + "chat_sendCooldown": "Kérjük, várjon egy pillanatot, mielőtt újra elküldi.", + "appSettings_jumpToOldestUnread": "Ugrás a legrégebbi olvasatlanra", + "appSettings_jumpToOldestUnreadSubtitle": "Ha olvasatlan üzeneteket tartalmazó csevegést nyit meg, görgessen az első olvasatlanra a legutóbbi helyett.", + "appSettings_languageHu": "magyar", + "appSettings_languageJa": "japán", + "appSettings_languageKo": "koreai", + "radioStats_tooltip": "Rádió és mesh statisztika", + "radioStats_screenTitle": "Rádió statisztika", + "radioStats_notConnected": "Csatlakozzon egy eszközhöz a rádióstatisztikák megtekintéséhez.", + "radioStats_firmwareTooOld": "A rádióstatisztikákhoz v8-as vagy újabb firmware szükséges.", + "radioStats_waiting": "Várakozás az adatokra…", + "radioStats_noiseFloor": "Zajszint: {noiseDbm} dBm", "@radioStats_noiseFloor": { "placeholders": { "noiseDbm": { @@ -2095,7 +2095,7 @@ } } }, - "radioStats_lastRssi": "Utolsó RSSI érték: {rssiDbm} dBm", + "radioStats_lastRssi": "Utolsó RSSI: {rssiDbm} dBm", "@radioStats_lastRssi": { "placeholders": { "rssiDbm": { @@ -2111,7 +2111,7 @@ } } }, - "radioStats_txAir": "TX-es idő (összesen): {seconds} másodperc", + "radioStats_txAir": "TX műsoridő (összesen): {seconds} s", "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2119,7 +2119,7 @@ } } }, - "radioStats_rxAir": "RX használat időtartama (összesen): {seconds} s", + "radioStats_rxAir": "RX műsoridő (összesen): {seconds} s", "@radioStats_rxAir": { "placeholders": { "seconds": { @@ -2127,8 +2127,8 @@ } } }, - "radioStats_chartCaption": "Háttérzaj szint (dBm) a legutóbbi minták alapján.", - "radioStats_stripNoise": "Háttérzaj szint: {noiseDbm} dBm", + "radioStats_chartCaption": "Zajszint (dBm) a legutóbbi mintákhoz képest.", + "radioStats_stripNoise": "Zajszint: {noiseDbm} dBm", "@radioStats_stripNoise": { "placeholders": { "noiseDbm": { @@ -2136,34 +2136,34 @@ } } }, - "radioStats_stripWaiting": "Rádió adatok begyűjtése…", - "radioStats_settingsTile": "Rádió statisztikák", - "radioStats_settingsSubtitle": "Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama", - "settings_denyAll": "Elutasítom", - "settings_privacySettingsDescription": "Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.", - "settings_privacySubtitle": "Ellenőrizd, hogy milyen információkat osztanak meg.", - "settings_privacy": "Adatvédelem beállítások", - "settings_allowByContact": "Lehetővé teszi a kapcsolatok kezelését", - "settings_allowAll": "Engedje meg mindent", - "settings_telemetryBaseMode": "Adatkapcsolati alapállapot", - "settings_telemetryLocationMode": "Adatkapcsolási helyszín mód", - "settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód", - "settings_advertLocation": "Reklám megjelenési hely", - "settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.", - "settings_telemetryModeUpdated": "A telemetriamód frissítve", - "contact_info": "Kapcsolattartási információk", - "contact_settings": "Kapcsolat beállítások", - "contact_telemetry": "Adatvisszaadás", - "contact_lastSeen": "Utoljára, amikor látták", - "contact_clearChat": "Tiszta beszélgetés", - "contact_teleBase": "Adatgyűjtő központ", - "contact_teleBaseSubtitle": "Engedje meg a akkumulátor töltöttségi szintjének és alapvető adatoknak megosztását.", - "contact_teleLoc": "Adatkapcsolati helyszín", - "contact_teleLocSubtitle": "Engedje meg a helyadatok megosztását", - "contact_teleEnv": "Adatkapcsolati környezet", - "contact_teleEnvSubtitle": "Engedje meg az érzékelő adatok megosztását", - "map_showOverlaps": "Az ismétlő kulcsok ütköznek", - "map_runTraceWithReturnPath": "Visszaforduljon az eredeti úton.", + "radioStats_stripWaiting": "Rádióstatisztikák lekérése…", + "radioStats_settingsTile": "Rádió statisztika", + "radioStats_settingsSubtitle": "Zajszint, RSSI, SNR és műsoridő", + "settings_denyAll": "Mindent tagadni", + "settings_privacySettingsDescription": "Válassza ki, hogy eszköze milyen információkat ossza meg másokkal.", + "settings_privacySubtitle": "Szabályozza, hogy milyen információkat osztanak meg.", + "settings_privacy": "Adatvédelmi beállítások", + "settings_allowByContact": "Engedélyezés kapcsolatjelző jelzőkkel", + "settings_allowAll": "Minden engedélyezése", + "settings_telemetryBaseMode": "Telemetriai alapmód", + "settings_telemetryLocationMode": "Telemetriás helymeghatározási mód", + "settings_telemetryEnvironmentMode": "Telemetriás környezeti mód", + "settings_advertLocation": "Hirdetés helye", + "settings_advertLocationSubtitle": "A hirdetésben szerepeljen a helyszín.", + "settings_telemetryModeUpdated": "Telemetriai mód frissítve", + "contact_info": "Elérhetőségi adatok", + "contact_settings": "Kapcsolati beállítások", + "contact_telemetry": "Telemetria", + "contact_lastSeen": "Utoljára látott", + "contact_clearChat": "Csevegés törlése", + "contact_teleBase": "Telemetriai bázis", + "contact_teleBaseSubtitle": "Az akkumulátor töltöttségi szintjének és az alapvető telemetria megosztásának engedélyezése", + "contact_teleLoc": "Telemetriás hely", + "contact_teleLocSubtitle": "Helyadatok megosztásának engedélyezése", + "contact_teleEnv": "Telemetriai környezet", + "contact_teleEnvSubtitle": "A környezetérzékelő adatainak megosztásának engedélyezése", + "map_showOverlaps": "Repeater Key átfedések", + "map_runTraceWithReturnPath": "Térj vissza ugyanazon az úton.", "@translation_downloadFailed": { "placeholders": { "error": { @@ -2172,29 +2172,29 @@ } }, "translation_title": "Fordítás", - "translation_enableTitle": "Engedje meg a fordítást", - "translation_enableSubtitle": "Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.", - "translation_composerTitle": "Fordítsa el, mielőtt elküldi", - "translation_composerSubtitle": "Ellenőrzi a zeneszerző fordítási ikon alapértékét.", - "translation_autoIncomingTitle": "Üzenetek automatikus fordítása", - "translation_autoIncomingSubtitle": "Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.", + "translation_enableTitle": "Fordítás engedélyezése", + "translation_enableSubtitle": "Fordítsa le a bejövő üzeneteket, és engedélyezze a küldés előtti fordítást.", + "translation_composerTitle": "Küldés előtt fordítsa le", + "translation_composerSubtitle": "Szabályozza a zeneszerző fordítási ikonjának alapértelmezett állapotát.", + "translation_autoIncomingTitle": "A bejövő üzenetek automatikus fordítása", + "translation_autoIncomingSubtitle": "Automatikusan lefordítja az üzeneteket értesítésekhez, csevegéshez vagy csatornához.", "translation_translateMessage": "Üzenet fordítása", "translation_targetLanguage": "Célnyelv", - "translation_useAppLanguage": "Használja az alkalmazás nyelvének beállítását.", + "translation_useAppLanguage": "Használja az alkalmazás nyelvét", "translation_downloadedModelLabel": "Letöltött modell", - "translation_presetModelLabel": "Előre definiált Hugging Face-modell", - "translation_manualUrlLabel": "Manuális modell URL", - "translation_downloadModel": "Letöltés", + "translation_presetModelLabel": "Előre beállított átölelő arc modell", + "translation_manualUrlLabel": "Kézi modell URL", + "translation_downloadModel": "Modell letöltése", "translation_downloading": "Letöltés...", - "translation_working": "Munkában vagyok...", - "translation_stop": "Halt", - "translation_mergingChunks": "A letöltött részek összeállítása a végleges fájlba...", - "translation_downloadedModels": "Letöltött modelok", - "translation_deleteModel": "Törölje a modellt", + "translation_working": "Dolgozó...", + "translation_stop": "Stop", + "translation_mergingChunks": "Letöltött darabok egyesítése a végső fájlba...", + "translation_downloadedModels": "Letöltött modellek", + "translation_deleteModel": "Modell törlése", "translation_modelDownloaded": "Fordítási modell letöltve.", "translation_downloadStopped": "A letöltés leállt.", "translation_downloadFailed": "Letöltés sikertelen: {error}", - "translation_enterUrlFirst": "Addon először egy modell URL-t.", + "translation_enterUrlFirst": "Először adja meg a modell URL-jét.", "@scanner_linuxPairingPinPrompt": { "placeholders": { "deviceName": { @@ -2202,9 +2202,9 @@ } } }, - "scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot", - "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", - "scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot", + "scanner_linuxPairingShowPin": "PIN-kód megjelenítése", + "scanner_linuxPairingPinPrompt": "Írja be a {deviceName} PIN-kódját (ha nincs, hagyja üresen).", + "scanner_linuxPairingHidePin": "PIN elrejtése", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", "@translation_translateTo": { "placeholders": { @@ -2213,102 +2213,102 @@ } } }, - "translation_translateBeforeSending": "Fordítsa el, mielőtt elküldi", - "translation_composerEnabledHint": "A üzenetek fordítását a küldés előtt elvégezzük.", - "translation_messageTranslation": "Üzenet fordítása", - "translation_composerDisabledHint": "Küldj üzeneteket az eredeti, nyomtatott nyelven.", - "translation_translateTo": "Fordítás {language}-ra", + "translation_translateBeforeSending": "Küldés előtt fordítsa le", + "translation_composerEnabledHint": "Az üzeneteket elküldés előtt lefordítják.", + "translation_messageTranslation": "Üzenet fordítás", + "translation_composerDisabledHint": "Üzeneteket küldhet az eredeti gépelt nyelven.", + "translation_translateTo": "Fordítás {language}", "translation_translationOptions": "Fordítási lehetőségek", - "translation_systemLanguage": "Rendszer nyelvé", + "translation_systemLanguage": "Rendszer nyelve", "repeater_cliQuickClockSync": "Óra szinkronizálás", - "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat", + "repeater_cliQuickDiscovery": "Fedezze fel a szomszédokat", "@repeater_clockSyncAfterLogin": { "description": "Repeater setting: auto sync device clock after successful login" }, "@repeater_clockSyncAfterLoginSubtitle": { "description": "Repeater setting subtitle: describes the clock sync after login behavior" }, - "repeater_clockSyncAfterLoginSubtitle": "Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.", + "repeater_clockSyncAfterLoginSubtitle": "„Óraszinkronizálás” automatikus küldése sikeres bejelentkezés után", "repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után", - "repeater_guestTools": "Vendégek számára elérhető eszközök", - "room_guest": "Szoba szerver információk", + "repeater_guestTools": "Vendégeszközök", + "room_guest": "Szobaszerver információ", "chat_sendMessage": "Üzenet küldése", - "repeater_guest": "Adatok a repeaterről", - "repeater_getCategory": "Szereplő értékek", - "repeater_powerMgmt": "Energiahatékonyság, energiafelhasználás optimalizálása", + "repeater_guest": "Ismétlő információ", + "repeater_getCategory": "Szerezzen értékeket", + "repeater_powerMgmt": "Energiagazdálkodás", "repeater_sensors": "Érzékelők", - "repeater_cliHelpPowerOff": "Át kapcsolja a készüléket. (nincs válasz elvárás)", - "repeater_cliHelpClkReboot": "Visszaállítja az órát egy ismert időpontra, majd újraindítja a készüléket.", - "repeater_cliHelpAdvertZeroHop": "Küld egy közvetlen szomszédoknak szóló hirdetést, amely közvetlen kapcsolatot igényel.", - "repeater_cliHelpStartOta": "Elindítja a vezeték nélküli útvonalon történő firmware frissítést a támogatott kártyákon.", - "repeater_cliHelpTime": "Beállítja a eszköz óráját a megadott Unix-időpont (Unix epoch) időpontra. Az óra nem tud visszanyúlni.", - "repeater_cliHelpBoard": "Megjeleníti a gyártó nevét/a hardver azonosítóját.", - "repeater_cliHelpDiscoverNeighbors": "Kérést küld a közeli eszközöknek, hogy azok is megtalálják egymást. (Csak egy repeater eszköz számára)", - "repeater_cliHelpPowersaving": "Megmutatja, hogy a takarékos üzemmód engedélyezve van-e vagy nem.", - "repeater_cliHelpPowersavingOnOff": "Engedélyezi vagy kikapcsolja a takarékos üzemmódot (ha támogatott).", - "repeater_cliHelpErase": "(Csak sorozatban) Formázza a eszköz fájlrendszerét. Eltávolítja az összes beállítást és a kapcsolatokat.", - "repeater_cliHelpSetDutyCycle": "Beállítja a maximális engedélyezett átviteli időtartamot százalékban (1-100). Belsőleg módosítja az időtartam-szabályozást.", - "repeater_cliHelpSetPrvKey": "(Csak sorozatban) Cseréli a eszköz egyedi kulcsát. Az alkalmazáshoz újraindítás szükséges. Új nyilvános kulcsot generál.", - "repeater_cliHelpSetRadioRxGain": "(Csak SX126x család) A növelt RX erősítést be- és kikapcsolható, így a nagyobb áramfelvétel esetén is javítható a érzékenység.", - "repeater_cliHelpSetOwnerInfo": "Megadja az üvegezésben megjelenő tulajdonos elérhetőségeinek szövegét. Használja a '|' karaktert új sorok elválasztására.", - "repeater_cliHelpSetPathHashMode": "Beállítja a hálózati útvonal-hash módot. 0 = régebbi, 1 = szabványos, 2 = szigorú. Hatással van a hálózati útvonalak megadatalására.", - "repeater_cliHelpSetLoopDetect": "Beállítja a hibaforrás-keresés érzékenységét: kikapcsolva, minimális, közepes vagy szigorú.", - "repeater_cliHelpSetFreq": "(Csak soros mód) Gyorsan beállítja a frekvenciát. A rendszer újraindítás szükséges. A teljes rádióparaméterek beállításához a \"rádió beállítás\" funkciót javaslom.", - "repeater_cliHelpSetBridgeChannel": "(Csak ESPNow híd esetén) Beállítja a híd által használt WiFi-csatornát (1-14).", + "repeater_cliHelpPowerOff": "Kikapcsolja a készüléket. (nem várható válasz)", + "repeater_cliHelpClkReboot": "Visszaállítja az órát egy ismert korszakra, és újraindítja az eszközt.", + "repeater_cliHelpAdvertZeroHop": "Zéró ugrású hirdetést küld (csak a közvetlen szomszédok számára).", + "repeater_cliHelpStartOta": "Elindít egy vezeték nélküli firmware-frissítést a támogatott kártyákon.", + "repeater_cliHelpTime": "Beállítja az eszköz óráját a megadott Unix korszak másodpercekre. Az óra nem tud visszafelé mozogni.", + "repeater_cliHelpBoard": "Megmutatja a kártya gyártóját/hardver azonosítóját.", + "repeater_cliHelpDiscoverNeighbors": "Csomópont-felderítési kérelmet küld a közeli szomszédoknak. (Csak ismétlő)", + "repeater_cliHelpPowersaving": "Megmutatja, hogy az energiatakarékos mód be vagy ki van-e kapcsolva.", + "repeater_cliHelpPowersavingOnOff": "Engedélyezi vagy letiltja az energiatakarékos módot (ahol támogatott).", + "repeater_cliHelpErase": "(Csak soros) Formázza az eszköz fájlrendszerét. Törli az összes beállítást és névjegyet.", + "repeater_cliHelpSetDutyCycle": "Beállítja a maximális megengedett átviteli munkaciklust százalékban (1-100). Belsőleg állítja be a műsoridőtényezőt.", + "repeater_cliHelpSetPrvKey": "(Csak soros) Lecseréli az eszközazonosító privát kulcsot. Újraindítás szükséges az alkalmazáshoz. Új nyilvános kulcsot generál.", + "repeater_cliHelpSetRadioRxGain": "(csak SX126x) Bekapcsolja a megnövelt RX-erősítést a jobb érzékenység érdekében nagyobb áramfelvétel mellett.", + "repeater_cliHelpSetOwnerInfo": "Beállítja a hirdetésekben szereplő tulajdonos elérhetőségi adatait. A '|' használata újsorokhoz.", + "repeater_cliHelpSetPathHashMode": "Beállítja az útvonal-kivonat módot. 0 = örökölt, 1 = szabványos, 2 = szigorú. Befolyásolja az útválasztási útvonalak egyeztetését.", + "repeater_cliHelpSetLoopDetect": "Beállítja az útválasztási hurok észlelésének érzékenységét: ki, minimális, közepes vagy szigorú.", + "repeater_cliHelpSetFreq": "(Csak soros) Gyorsan beállítja csak a frekvenciát. Újraindítás szükséges. A „rádió beállítása” előnyben részesítse a teljes rádióparamétereket.", + "repeater_cliHelpSetBridgeChannel": "(Csak ESPNow bridge) Beállítja a híd által használt WiFi csatornát (1-14).", "repeater_cliHelpGetName": "Megjeleníti a konfigurált csomópont nevét.", - "repeater_cliHelpGetRole": "Megmutatja a firmware funkcióját (repeater, szobai szerver stb.).", - "repeater_cliHelpGetPublicKey": "Megjeleníti a eszköz nyilvános kulcsát.", - "repeater_cliHelpGetPrvKey": "(Csak soros mód) Megjeleníti a eszköz privát kulcsát. Kezelje titkos információként.", - "repeater_cliHelpGetRepeat": "Megmutatja, hogy a csomagok továbbításának (repeater funkció) engedélyezve van-e vagy nem.", - "repeater_cliHelpGetTx": "Megmutatja a jelenlegi TX (átvitel) teljesítményt dBm-ben.", - "repeater_cliHelpGetFreq": "Megjeleníti a konfigurált rádiófrekvenciát MHz-ben.", - "repeater_cliHelpGetRadio": "Megjeleníti az összes rádióparamétert: frekvencia, sávszélesség, széttétező tényező, kódolási ráta.", - "repeater_cliHelpGetRadioRxGain": "(Csak SX126x családra) Megjeleníti az RX erősítés állapotát.", - "repeater_cliHelpGetAf": "Megmutatja az aktuális időadó tényezőt.", - "repeater_cliHelpGetDutyCycle": "Megmutatja az aktuális engedélyezett működési ciklust százalékban.", - "repeater_cliHelpGetIntThresh": "Megmutatja a csatornák közötti interferencia szintjét dB-ben.", - "repeater_cliHelpGetAgcResetInterval": "Megmutatja az AGC (automatikus gain-kontroll) visszaállítási időt másodpercekben.", - "repeater_cliHelpGetMultiAcks": "Megmutatja, hogy a kettős visszaigazolás (double-ACK) mód engedélyezve van-e (1), vagy kikapcsolva (0).", - "repeater_cliHelpGetAllowReadOnly": "Megmutatja, hogy a vendég csak olvasási jogosítást engedélyez-e.", - "repeater_cliHelpGetAdvertInterval": "Megmutatja a helyi hirdetés időtartamát percenként.", - "repeater_cliHelpGetFloodAdvertInterval": "Megmutatja az aktuális időzítést, amikor megjelenik a vízparti reklám, órákonként.", - "repeater_cliHelpGetGuestPassword": "Megjeleníti a konfigurált vendégjelszót.", - "repeater_cliHelpGetLat": "Megjeleníti a beállított szélességet.", - "repeater_cliHelpGetLon": "Megjeleníti a beállított hosszúságot.", - "repeater_cliHelpGetRxDelay": "Megmutatja az alapértéket a késéshez.", - "repeater_cliHelpGetTxDelay": "Megmutatja a vízszint-érzékelő jelátviteli késésének tényezőjét.", - "repeater_cliHelpGetDirectTxDelay": "Megmutatja a közvetlen módban használt késés tényezőt.", - "repeater_cliHelpGetFloodMax": "Megmutatja a maximális vízszint-emelkedés mértékét.", - "repeater_cliHelpGetOwnerInfo": "Megjeleníti az tulajdonos elérhetőségének szövegét.", - "repeater_cliHelpGetPathHashMode": "Megjeleníti a hash-alapú mód (0/1/2) beállításokat.", - "repeater_cliHelpGetLoopDetect": "Mutatja a cikkszám-azonosító érzékenységet.", - "repeater_cliHelpGetAcl": "(Csak sorozat) A repeateren található hozzáférési szabályok listája.", + "repeater_cliHelpGetRole": "Megmutatja a firmware szerepkörét (Repeater, Room Server stb.).", + "repeater_cliHelpGetPublicKey": "Megjeleníti az eszköz nyilvános kulcsát.", + "repeater_cliHelpGetPrvKey": "(Csak soros) Az eszköz privát kulcsát mutatja. Kezelje titokként.", + "repeater_cliHelpGetRepeat": "Megmutatja, hogy a csomagtovábbítás (ismétlő szerepkör) be vagy ki van-e kapcsolva.", + "repeater_cliHelpGetTx": "Az aktuális TX teljesítményt mutatja dBm-ben.", + "repeater_cliHelpGetFreq": "Megjeleníti a beállított rádiófrekvenciát MHz-ben.", + "repeater_cliHelpGetRadio": "A teljes rádióparamétereket mutatja: frekvencia, sávszélesség, szórási tényező, kódolási sebesség.", + "repeater_cliHelpGetRadioRxGain": "(csak SX126x) Megjeleníti az RX megnövelt erősítési állapotát.", + "repeater_cliHelpGetAf": "Megmutatja az aktuális műsoridőt.", + "repeater_cliHelpGetDutyCycle": "Megjeleníti az aktuális megengedett munkaciklust százalékban.", + "repeater_cliHelpGetIntThresh": "A csatorna interferencia küszöbértékét mutatja dB-ben.", + "repeater_cliHelpGetAgcResetInterval": "Az AGC reset intervallumát mutatja másodpercben.", + "repeater_cliHelpGetMultiAcks": "Megmutatja, hogy a dupla ACK mód be van-e kapcsolva (1) vagy ki (0).", + "repeater_cliHelpGetAllowReadOnly": "Megmutatja, hogy a vendég csak olvasási hozzáférés engedélyezett-e.", + "repeater_cliHelpGetAdvertInterval": "A helyi hirdetési intervallumot mutatja percben.", + "repeater_cliHelpGetFloodAdvertInterval": "Megjeleníti az árvízhirdetés intervallumát órákban.", + "repeater_cliHelpGetGuestPassword": "Megjeleníti a beállított vendégjelszót.", + "repeater_cliHelpGetLat": "Megjeleníti a beállított szélességi fokot.", + "repeater_cliHelpGetLon": "A beállított hosszúságot mutatja.", + "repeater_cliHelpGetRxDelay": "Az rxdelay alapértékét mutatja.", + "repeater_cliHelpGetTxDelay": "Megjeleníti az elárasztási mód txdelay tényezőjét.", + "repeater_cliHelpGetDirectTxDelay": "A közvetlen módú txdelay tényezőt mutatja.", + "repeater_cliHelpGetFloodMax": "Megmutatja a maximális elárasztási ugrás számát.", + "repeater_cliHelpGetOwnerInfo": "Megjeleníti a tulajdonos kapcsolatfelvételi adatait.", + "repeater_cliHelpGetPathHashMode": "Az elérési út-kivonat módot mutatja (0/1/2).", + "repeater_cliHelpGetLoopDetect": "A hurokérzékelés érzékenységét mutatja.", + "repeater_cliHelpGetAcl": "(Csak soros) Felsorolja a hozzáférés-vezérlési bejegyzéseket az átjátszón.", "repeater_cliHelpGetBridgeEnabled": "Megmutatja, hogy a híd engedélyezve van-e.", - "repeater_cliHelpGetBridgeDelay": "Megmutatja a hídon bekövetkező késést másodpercben.", - "repeater_cliHelpGetBridgeSource": "Megmutatja, hogy a híd RX vagy TX csomagokat fogad-e.", - "repeater_cliHelpGetBridgeBaud": "(Csak RS232 híd) Megjeleníti a híd sebességét.", - "repeater_cliHelpGetBridgeChannel": "(Csak ESPNow híd) Megjeleníti a híd által használt WiFi csatornát.", - "repeater_cliHelpGetBridgeSecret": "(Csak ESPNow híd esetén) Megjeleníti a híd által megosztott titkos kulcsot.", - "repeater_cliHelpGetBootloaderVer": "(Csak NRF52 esetén) Megjeleníti a bootloader verzióját.", - "repeater_cliHelpGetAdcMultiplier": "A bemeneti feszültség-átalakító (akkumulátor-feszültség-szabályozó) működését mutatja.", - "repeater_cliHelpGetPwrMgtSupport": "Megállapítja, hogy a felügyelet rendelkezik-e energiahatékonysági támogatással.", - "repeater_cliHelpGetPwrMgtSource": "Megmutatja az aktuális energiaforrást: külső vagy akkumulátor.", - "repeater_cliHelpGetPwrMgtBootReason": "Megjeleníti az utolsó újraindítás és leállítás okait.", - "repeater_cliHelpGetPwrMgtBootMv": "Megjeleníti a rendszerindításkor mért akkumulátor feszültséget millivoltban (mV).", - "repeater_cliHelpSensorGet": "Beolvas egy felhasználó által definiált szenzor beállítást kulcs segítségével.", - "repeater_cliHelpSensorSet": "Egyedi szenzorbeállítások létrehozása.", - "repeater_cliHelpSensorList": "Összesíti az összes egyedi szenzor beállításot, oldalanként, opcionális kezdő index alapján.", - "repeater_cliHelpRegionDefault": "Megmutatja a jelenlegi alapértelmezett régió határait.", - "repeater_cliHelpRegionDefaultSet": "Beállítja az alapértelmezett régió hatókörét. Használja a \"\" értéket a törléshez.", - "repeater_cliHelpRegionListAllowed": "Felhasználható területek, ahol árvíz esetén forgalmat engedélyeznek.", - "repeater_cliHelpRegionListDenied": "Felhasznál, amelyek elutasítják a árvíz okozta forgalmat.", - "repeater_cliHelpStatsPackets": "(Csak sorozat) A csomagok szintjén történő statisztikát mutat.", - "repeater_cliHelpStatsRadio": "(Csak sorozat) Mutat rádióstatisztikákat.", - "repeater_cliHelpStatsCore": "(Csak soros mód) A főfirmware-adatokat mutatja.", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "repeater_cliHelpGetBridgeDelay": "A híd késleltetését mutatja ms-ban.", + "repeater_cliHelpGetBridgeSource": "Megmutatja, hogy a híd RX vagy TX csomagokat naplóz-e.", + "repeater_cliHelpGetBridgeBaud": "(csak RS232 híd) A híd adatátviteli sebességét mutatja.", + "repeater_cliHelpGetBridgeChannel": "(csak ESPNow bridge) A híd WiFi csatornáját mutatja.", + "repeater_cliHelpGetBridgeSecret": "(Csak ESPNow bridge) Megjeleníti a híd megosztott titkát.", + "repeater_cliHelpGetBootloaderVer": "(Csak NRF52) A rendszerbetöltő verzióját mutatja.", + "repeater_cliHelpGetAdcMultiplier": "Az ADC szorzót mutatja (akkumulátor-feszültség skálázás).", + "repeater_cliHelpGetPwrMgtSupport": "Beszámol arról, hogy a testület rendelkezik-e hatalomkezelési támogatással.", + "repeater_cliHelpGetPwrMgtSource": "Az aktuális áramforrást mutatja: külső vagy akkumulátor.", + "repeater_cliHelpGetPwrMgtBootReason": "Megmutatja a legutóbbi alaphelyzetbe állítás és leállítás okait.", + "repeater_cliHelpGetPwrMgtBootMv": "Megmutatja a rendszerindítási akkumulátorfeszültséget mV-ban.", + "repeater_cliHelpSensorGet": "Kulcs segítségével beolvassa az egyéni szenzorbeállítást.", + "repeater_cliHelpSensorSet": "Egyéni szenzorbeállítást ír.", + "repeater_cliHelpSensorList": "Felsorolja az összes egyéni érzékelőbeállítást, oldalszámozással az opcionális kezdőindexből.", + "repeater_cliHelpRegionDefault": "Megjeleníti az aktuális alapértelmezett régióhatókört.", + "repeater_cliHelpRegionDefaultSet": "Beállítja az alapértelmezett régió hatókört. A törléshez használja a \"\" parancsot.", + "repeater_cliHelpRegionListAllowed": "Felsorolja azokat a régiókat, amelyek lehetővé teszik az árvízi forgalmat.", + "repeater_cliHelpRegionListDenied": "Felsorolja azokat a régiókat, amelyek megtagadják az árvízi forgalmat.", + "repeater_cliHelpStatsPackets": "(Csak soros) Csomag szintű statisztikákat jelenít meg.", + "repeater_cliHelpStatsRadio": "(Csak soros) Rádióstatisztikák megjelenítése.", + "repeater_cliHelpStatsCore": "(Csak soros) Az alapvető firmware-statisztikák megjelenítése.", + "common_done": "Kész", + "background_serviceTitle": "MeshCore fut", + "background_serviceText": "A BLE-kapcsolat fenntartása", + "appSettings_translationModelDeleted": "Törölve {name}", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2316,7 +2316,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "Nem sikerült törölni: {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2324,7 +2324,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "Nem sikerült frissíteni a csatornát: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2332,20 +2332,20 @@ } } }, - "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", - "map_publicKeyPrefixHint": "e.g. ab12", - "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", - "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", + "map_type": "Írja be", + "map_path": "Útvonal", + "map_location": "hely", + "map_estLocation": "Becsült hely", + "map_publicKey": "Nyilvános kulcs", + "map_publicKeyPrefixHint": "pl. ab12", + "contact_typeChat": "Csevegés", + "contact_typeRepeater": "Ismétlő", + "contact_typeRoom": "Szoba", + "contact_typeSensor": "Érzékelő", + "contact_typeUnknown": "Ismeretlen", + "channels_via": "ezen keresztül: {path}", "chat_score": "Score", - "settings_multiAck": "Többszörös visszaigazolások", + "settings_multiAck": "Multi-ACK", "map_sharedAt": "Megosztva", "@losBlockedSpotChip": { "placeholders": { @@ -2384,14 +2384,14 @@ }, "losSelectedObstructionTitle": "Kiválasztott akadály", "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", - "losBlockedSpotsHint": "A blokkolt területet megjelölve, hogy a térképen kiemeljük.", - "losBlockedSpotsTitle": "Foglalhatatlan területek", - "losSelectedObstructionDetails": "Elakadt a {obstruction} miatt, {heightUnit} magasságban, {distanceFromA} méterrel A-tól és {distanceFromB} méterrel B-től ({distanceUnit}).", - "chat_markAsUnread": "Merekje olvashatóként", + "losBlockedSpotsHint": "Koppintson egy blokkolt helyre, hogy kijelölje azt a térképen.", + "losBlockedSpotsTitle": "Blokkolt helyek", + "losSelectedObstructionDetails": "Letiltja: {obstruction} {heightUnit}, {distanceFromA} A-tól és {distanceFromB} B-től ({distanceUnit}).", + "chat_markAsUnread": "Megjelölés olvasatlanként", "chat_newMessages": "Új üzenetek", - "settings_companionDebugLog": "Párhuzamos hibakeresési napló", - "settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok", - "repeater_chanUtil": "Csatorna-használat", + "settings_companionDebugLog": "Companion Debug Log", + "settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és nyers adatok", + "repeater_chanUtil": "Csatornahasználat", "@routing_lastWorked": { "placeholders": { "when": { @@ -2431,58 +2431,152 @@ } }, "messageStatus_delivered": "Szállítva", - "common_undo": "Még egyszer", - "messageStatus_sent": "Elküldve", - "messageStatus_pending": "Elküldés", + "common_undo": "Visszavonás", + "messageStatus_sent": "Küldött", + "messageStatus_pending": "Küldés", "messageStatus_failed": "Nem sikerült elküldeni", - "messageStatus_repeated": "Ismételtem", + "messageStatus_repeated": "– ismételte Heard", "contacts_moreOptions": "További lehetőségek", - "contacts_searchOpen": "Keresssz kapcsolatokat", - "contacts_searchClose": "Teljesítse a keresést", - "routing_title": "Útvonal meghatározás", - "routing_modeAuto": "Autó", - "routing_modeFlood": "Áradás", - "routing_modeManual": "Használati útmutató", - "routing_modeAutoHint": "Automatikusan kiválasztja a legismertebb útvonalat, és ha egyik sem ismert, akkor \"vízzel\" tölti ki.", - "routing_modeFloodHint": "Átvisszaadások minden erősítőn keresztül. A legmegbízhatóbb megoldás, de több időt igényel.", - "routing_modeManualHint": "Mindig pontosan az útvonalat követi, amelyet megad.", + "contacts_searchOpen": "Névjegyek keresése", + "contacts_searchClose": "Keresés bezárása", + "routing_title": "Útválasztás", + "routing_modeAuto": "Auto", + "routing_modeFlood": "Árvíz", + "routing_modeManual": "Kézikönyv", + "routing_modeAutoHint": "Automatikusan kiválasztja a legismertebb utat, és elárasztja, ha nem ismert.", + "routing_modeFloodHint": "Minden átjátszón keresztül sugároz. A legmegbízhatóbb, de több műsoridőt használ.", + "routing_modeManualHint": "Mindig pontosan az Ön által megadott útvonalon küld.", "routing_currentRoute": "Jelenlegi útvonal", - "routing_directNoHops": "Közvetlen – nincs átjáró állomás", - "routing_noPathYet": "Még nincs útvonal. A következő üzenet a keresésig vár.", - "routing_floodBroadcast": "Azonnali továbbítás minden erősítőn keresztül.", + "routing_directNoHops": "Közvetlen – nincs átjátszó ugrás", + "routing_noPathYet": "Még nincs út. A következő üzenet elárasztja az útvonalat.", + "routing_floodBroadcast": "Minden átjátszón keresztül sugározd", "routing_editPath": "Útvonal szerkesztése", - "routing_forgetPath": "Felejtsd el a útvonalat", - "routing_knownPaths": "Jellegzetes útvonalak", - "routing_knownPathsHint": "Készíts egy útvonalat, hogy átválhass rá.", + "routing_forgetPath": "Felejtsd el az utat", + "routing_knownPaths": "Ismert utak", + "routing_knownPathsHint": "Koppintson egy elérési útra a váltáshoz.", "routing_inUse": "Használatban", - "routing_qualityStrong": "Erős első lépés", - "routing_qualityGood": "Jó első lépés", - "routing_qualityFair": "Jó első lépés", - "routing_qualityWorked": "Előállított", - "routing_qualityFlood": "Információt hallottam a katasztrófa miatt.", - "routing_qualityUntested": "Vizsgálatnak nem подвержен", - "routing_neverWorked": "sosem megerősítve", - "routing_floodDelivery": "Vízparti szállítás", - "pathEditor_title": "Út megépítése", - "pathEditor_hopCounter": "{count} db 64-ből", - "pathEditor_noHops": "Még nem adtam hozzá a bazsalikomot. A lent található gombokat használhatod, hogy sorrendben adjd hozzá, vagy mentheted anélkül, hogy bazsalikomot adnál hozzá, hogy közvetlenül elküldd.", - "pathEditor_addHops": "Adja hozzá a bazsaidat a megfelelő sorrendben.", - "pathEditor_searchRepeaters": "Ismétlő eszközök keresése", - "pathEditor_advancedHex": "Haladó szint: alapvető hex-út", - "pathEditor_hexLabel": "Hex előtagok", - "pathEditor_hexHelper": "Két hatjegyű szám minden lépésen, amelyek egymástól elválasztják a kommák.", + "routing_qualityStrong": "Erős első ugrás", + "routing_qualityGood": "Jó első ugrás", + "routing_qualityFair": "Tisztességes első ugrás", + "routing_qualityWorked": "Szállított", + "routing_qualityFlood": "Árvízen keresztül hallatszott", + "routing_qualityUntested": "Nem tesztelt", + "routing_neverWorked": "soha nem erősítették meg", + "routing_floodDelivery": "Árvízi szállítás", + "pathEditor_title": "Építsd meg az útvonalat", + "pathEditor_hopCounter": "{count} 64 ugrásból", + "pathEditor_noHops": "Még nincs ugrás. Koppintson az alábbi ismétlőkre, ha sorrendben szeretné felvenni őket, vagy mentse ugrás nélkül a közvetlen küldéshez.", + "pathEditor_addHops": "Sorrendben adjuk hozzá a komlót", + "pathEditor_searchRepeaters": "Ismétlő keresése", + "pathEditor_advancedHex": "Haladó: nyers hexadecimális útvonal", + "pathEditor_hexLabel": "Hexadecimális előtagok", + "pathEditor_hexHelper": "Két hexadecimális karakter ugrásonként, vesszővel elválasztva", "pathEditor_invalidTokens": "Érvénytelen: {tokens}", - "routing_lastWorked": "worked {when}", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "pathEditor_tooManyHops": "A maximális szám 64.", - "pathEditor_usePath": "Használja ezt az útvonalat.", - "pathEditor_removeHop": "Távolítsa el a bazsalikomot", - "pathEditor_unknownHop": "Tudatlan erősítő", - "map_zoomIn": "Nagyítva", - "map_zoomOut": "Kicsökkentett nézet", - "map_centerMap": "Központi tér térkép", - "chrome_bluetoothRequiresChromium": "A Web Bluetooth-hoz egy Chromium-alapú böngésző szükséges.", - "channels_communityShortId": "Az azonosító: {id}...", - "pathTrace_legendGpsConfirmed": "GPS-en megerősítve", - "pathTrace_legendInferred": "Feltehető helyzet" + "routing_lastWorked": "dolgozott {when}", + "routing_deliveryCounts": "{successes} kézbesítve, {failures} sikertelen", + "pathEditor_tooManyHops": "Maximum 64 ugrás", + "pathEditor_usePath": "Használja ezt az utat", + "pathEditor_removeHop": "Távolítsa el az ugrást", + "pathEditor_unknownHop": "Ismeretlen átjátszó", + "map_zoomIn": "Nagyítás", + "map_zoomOut": "Kicsinyítés", + "map_centerMap": "Középső térkép", + "chrome_bluetoothRequiresChromium": "A Web Bluetooth használatához Chromium böngésző szükséges", + "channels_communityShortId": "ID: {id}...", + "pathTrace_legendGpsConfirmed": "GPS megerősítve", + "pathTrace_legendInferred": "Kikövetkeztetett pozíció", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "scanner_bluetoothWebUnsupported": "A Bluetooth nem érhető el a böngészőben. Inkább csatlakoztasson USB-n keresztül.", + "map_online": "Online", + "map_searchHint": "Keresés a csomópont nevében vagy azonosítójában", + "map_activity": "Tevékenység", + "map_recent": "Legutóbbi", + "map_stale": "Állott", + "map_visible": "Látható", + "map_hidden": "Rejtett", + "map_centerOnNode": "Középre a csomóponton", + "map_details": "Részletek", + "map_noGps": "Nincs GPS", + "map_noResults": "Nincsenek megfelelő csomópontok", + "pathMap_viewSingle": "Egyetlen", + "pathMap_viewCombined": "Kombinált", + "pathMap_play": "Játék", + "pathMap_pause": "Szünet", + "pathMap_replay": "Visszajátszás", + "pathMap_stepBack": "Előző ugrás", + "pathMap_stepForward": "Következő ugrás", + "pathMap_animationOn": "Csomaganimáció megjelenítése", + "pathMap_animationOff": "Csomaganimáció elrejtése", + "pathMap_hopOf": "{current} ugrás / {total}", + "pathMap_observedPaths": "Megfigyelt útvonalak: {count}", + "pathMap_primary": "Elsődleges", + "pathMap_alternate": "Alt {index}", + "pathMap_hopCount": "{count, plural, =1{1 ugrás} other{{count} ugrás}}", + "pathMap_legendShared": "Megosztott szegmens", + "pathMap_legendEstimated": "Becsült szegmens", + "pathMap_sharedNodeCount": "{count} elérési út használja", + "pathMap_partialAnimation": "{count, plural, =1{1 ugrásnak nincs helyadata — a megjelenített útvonal részleges} other{{count} ugrásnak nincs helyadata — a megjelenített útvonal részleges}}", + "pathMap_showAllPaths": "Összes megjelenítése", + "pathMap_hidePath": "Útvonal elrejtése", + "pathMap_showPath": "Útvonal megjelenítése", + "pathMap_collapsePanel": "Panel összecsukása", + "pathMap_expandPanel": "Panel kibontása", + "pathMap_noLocation": "Nincs hely", + "pathMap_followPacket": "Nézet zárolása a csomaghoz", + "pathMap_unfollowPacket": "A nézet feloldása a csomagból", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 10fc16e4..2df230ae 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -32,7 +32,7 @@ "common_hide": "Nascondi", "common_remove": "Elimina", "common_enable": "Abilita", - "common_disable": "Disattivare", + "common_disable": "Disattiva", "common_autoRefresh": "Aggiornamento automatico", "common_interval": "Intervallo", "common_reboot": "Riavvia", @@ -77,12 +77,12 @@ } } }, - "scanner_stop": "Interrompere", + "scanner_stop": "Ferma", "scanner_scan": "Scansiona", - "device_quickSwitch": "Passa velocemente", + "device_quickSwitch": "Passa rapidamente", "device_meshcore": "MeshCore", "settings_title": "Impostazioni", - "settings_deviceInfo": "Informazioni Dispositivo", + "settings_deviceInfo": "Informazioni dispositivo", "settings_appSettings": "Impostazioni App", "settings_appSettingsSubtitle": "Notifiche, messaggi e preferenze della mappa", "settings_nodeSettings": "Impostazioni Nodo", @@ -91,13 +91,13 @@ "settings_nodeNameHint": "Inserisci nome nodo", "settings_nodeNameUpdated": "Nome aggiornato", "settings_radioSettings": "Impostazioni Radio", - "settings_radioSettingsSubtitle": "Frequenza, potenza, fattore di dispersione", + "settings_radioSettingsSubtitle": "Frequenza, potenza, fattore di diffusione", "settings_radioSettingsUpdated": "Impostazioni radio aggiornate", "settings_location": "Posizione", - "settings_locationSubtitle": "coordinate GPS", - "settings_locationUpdated": "Posizione aggiornata", + "settings_locationSubtitle": "Coordinate GPS", + "settings_locationUpdated": "Posizione e impostazioni GPS aggiornate", "settings_locationBothRequired": "Inserire sia la latitudine che la longitudine.", - "settings_locationInvalid": "Latitudine o longitudine non valida.", + "settings_locationInvalid": "Latitudine o longitudine non valide.", "settings_latitude": "Latitudine", "settings_longitude": "Longitudine", "settings_privacyMode": "Modalità Privacy", @@ -106,26 +106,26 @@ "settings_privacyModeEnabled": "Modalità privacy abilitata", "settings_privacyModeDisabled": "Modalità privacy disabilitata", "settings_actions": "Azioni", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", - "settings_sendAdvertisement": "Invia Annuncio", - "settings_sendAdvertisementSubtitle": "Presenza trasmessa ora", + "settings_deleteAllPaths": "Elimina tutti i percorsi", + "settings_deleteAllPathsSubtitle": "Cancella tutti i dati di percorso dai contatti.", + "settings_sendAdvertisement": "Invia annuncio", + "settings_sendAdvertisementSubtitle": "Trasmetti ora la presenza", "settings_advertisementSent": "Annuncio inviato", - "settings_syncTime": "Tempo di sincronizzazione", + "settings_syncTime": "Sincronizza ora", "settings_syncTimeSubtitle": "Imposta l'orologio del dispositivo sull'ora del telefono", - "settings_timeSynchronized": "Sincronizzato nel tempo", - "settings_refreshContacts": "Aggiorna Contatti", - "settings_refreshContactsSubtitle": "Ricaricare l'elenco dei contatti dal dispositivo", - "settings_rebootDevice": "Riavvia Dispositivo", - "settings_rebootDeviceSubtitle": "Riavviare il dispositivo MeshCore", + "settings_timeSynchronized": "Ora sincronizzata", + "settings_refreshContacts": "Aggiorna contatti", + "settings_refreshContactsSubtitle": "Ricarica l'elenco dei contatti dal dispositivo", + "settings_rebootDevice": "Riavvia dispositivo", + "settings_rebootDeviceSubtitle": "Riavvia il dispositivo MeshCore", "settings_rebootDeviceConfirm": "Sei sicuro di voler riavviare il dispositivo? Sarai disconnesso.", "settings_debug": "Risoluzione dei problemi", - "settings_bleDebugLog": "Log di Debug BLE", + "settings_bleDebugLog": "Registro di debug BLE", "settings_bleDebugLogSubtitle": "Comandi, risposte e dati grezzi BLE", "settings_appDebugLog": "Log di Debug dell'App", "settings_appDebugLogSubtitle": "Messaggi di debug dell'applicazione", "settings_about": "Informazioni", - "settings_aboutVersion": "MeshCore Open versione {version}", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -133,25 +133,25 @@ } } }, - "settings_aboutLegalese": "Progetto Open Source MeshCore 2024", - "settings_aboutDescription": "Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.", + "settings_aboutLegalese": "2026 Progetto open source MeshCore", + "settings_aboutDescription": "Un client Flutter open source per i dispositivi MeshCore di rete mesh LoRa.", "settings_infoName": "Nome", "settings_infoId": "ID", "settings_infoStatus": "Stato", "settings_infoBattery": "Batteria", "settings_infoPublicKey": "Chiave Pubblica", - "settings_infoContactsCount": "Numero contatti", - "settings_infoChannelCount": "Numero Canale", + "settings_infoContactsCount": "Numero di contatti", + "settings_infoChannelCount": "Numero di canali", "settings_presets": "Preset", "settings_frequency": "Frequenza (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)", "settings_bandwidth": "Larghezza di banda", - "settings_spreadingFactor": "Fattore di Spettro", + "settings_spreadingFactor": "Fattore di diffusione", "settings_codingRate": "Tasso di Codifica", - "settings_txPower": "TX Potenza (dBm)", + "settings_txPower": "Potenza TX (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)", + "settings_txPowerInvalid": "Potenza TX non valida (0-22 dBm)", "settings_error": "Errore: {message}", "@settings_error": { "placeholders": { @@ -163,11 +163,11 @@ "appSettings_title": "Impostazioni App", "appSettings_appearance": "Aspetto", "appSettings_theme": "Tema", - "appSettings_themeSystem": "Impostazione predefinita del sistema", - "appSettings_themeLight": "Luce", + "appSettings_themeSystem": "Predefinito del sistema", + "appSettings_themeLight": "Chiaro", "appSettings_themeDark": "Scuro", "appSettings_language": "Lingua", - "appSettings_languageSystem": "Predefinito di sistema", + "appSettings_languageSystem": "Predefinito del sistema", "appSettings_languageEn": "Inglese", "appSettings_languageFr": "Francese", "appSettings_languageEs": "Spagnolo", @@ -194,12 +194,12 @@ "appSettings_advertisementNotifications": "Notifiche Pubblicitarie", "appSettings_advertisementNotificationsSubtitle": "Mostra notifica quando vengono scoperti nuovi nodi", "appSettings_messaging": "Messaggi", - "appSettings_clearPathOnMaxRetry": "Cancella Percorso su Massimo Riprovo", + "appSettings_clearPathOnMaxRetry": "Cancella percorso al massimo dei tentativi", "appSettings_clearPathOnMaxRetrySubtitle": "Reimposta il percorso di contatto dopo 5 tentativi di invio falliti", - "appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.", - "appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.", + "appSettings_pathsWillBeCleared": "I percorsi verranno cancellati dopo 5 tentativi falliti.", + "appSettings_pathsWillNotBeCleared": "I percorsi non verranno cancellati automaticamente.", "appSettings_autoRouteRotation": "Rotazione Percorso Automatico", - "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione", + "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità flood", "appSettings_autoRouteRotationEnabled": "Rotazione percorso automatico abilitata", "appSettings_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata", "appSettings_battery": "Batteria", @@ -255,13 +255,68 @@ }, "appSettings_debugCard": "Risoluzione dei problemi", "appSettings_appDebugLogging": "Registrazione Debug App", - "appSettings_appDebugLoggingSubtitle": "Messaggi di debug dell'app Log per la risoluzione dei problemi", - "appSettings_appDebugLoggingEnabled": "Logging di debug dell'app abilitato", - "appSettings_appDebugLoggingDisabled": "Logging del debug dell'app disabilitato", + "appSettings_appDebugLoggingSubtitle": "Registra i messaggi di debug dell'app per la risoluzione dei problemi", + "appSettings_appDebugLoggingEnabled": "Registrazione di debug dell'app abilitata", + "appSettings_appDebugLoggingDisabled": "Registrazione di debug dell'app disabilitata", "contacts_title": "Contatti", "contacts_noContacts": "Nessun contatto ancora", - "contacts_contactsWillAppear": "I contatti appariranno quando i dispositivi pubblicizzano.", + "contacts_contactsWillAppear": "I contatti appariranno quando i dispositivi si annunciano.", "contacts_searchContacts": "Cerca contatti...", + "@contacts_searchContacts": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchContactsNoNumber": "Cerca contatti...", + "contacts_searchFavorites": "Cerca {number}{str} preferiti...", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "Cerca {number}{str} utenti...", + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Cerca {number}{str} ripetitori...", + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Cerca {number}{str} server stanza...", + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, "contacts_noUnreadContacts": "Nessun contatto non letto", "contacts_noContactsFound": "Nessun contatto o gruppo trovato.", "contacts_deleteContact": "Elimina Contatto", @@ -273,11 +328,12 @@ } } }, - "contacts_manageRepeater": "Gestisci Ripetitore", - "contacts_roomLogin": "Login Camera", - "contacts_openChat": "Apri Chat", - "contacts_editGroup": "Modifica Gruppo", - "contacts_deleteGroup": "Elimina Gruppo", + "contacts_manageRepeater": "Gestisci ripetitore", + "contacts_manageRoom": "Gestisci server stanza", + "contacts_roomLogin": "Accesso server stanza", + "contacts_openChat": "Apri chat", + "contacts_editGroup": "Modifica gruppo", + "contacts_deleteGroup": "Elimina gruppo", "contacts_deleteGroupConfirm": "Eliminare \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { @@ -286,7 +342,7 @@ } } }, - "contacts_newGroup": "Nuovo Gruppo", + "contacts_newGroup": "Nuovo gruppo", "contacts_groupName": "Nome gruppo", "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", "contacts_groupNameReserved": "Questo nome del gruppo è riservato", @@ -301,7 +357,7 @@ "contacts_filterContacts": "Filtra i contatti...", "contacts_noContactsMatchFilter": "Nessun contatto corrisponde al tuo filtro", "contacts_noMembers": "Nessun membro", - "contacts_lastSeenNow": "Ultimo avvistamento ora", + "contacts_lastSeenNow": "Poco fa", "contacts_lastSeenMinsAgo": "Ultimo visto {minutes} minuti fa", "@contacts_lastSeenMinsAgo": { "placeholders": { @@ -363,13 +419,13 @@ } } }, - "channels_addChannel": "Aggiungi Canale", - "channels_channelIndexLabel": "Indice Canale", + "channels_addChannel": "Aggiungi canale", + "channels_channelIndexLabel": "Indice canale", "channels_channelName": "Nome canale", - "channels_usePublicChannel": "Utilizza il canale pubblico", + "channels_usePublicChannel": "Usa il canale pubblico", "channels_standardPublicPsk": "PSK pubblico standard", "channels_pskHex": "PSK (esadecimale)", - "channels_generateRandomPsk": "Genera una chiave di permutazione casuale", + "channels_generateRandomPsk": "Genera una PSK casuale", "channels_enterChannelName": "Inserisci un nome per il canale", "channels_pskMustBe32Hex": "PSK deve essere composto da 32 caratteri esadecimali.", "channels_channelAdded": "Canale \"{name}\" aggiunto", @@ -396,6 +452,11 @@ "channels_cyr2latSettingsDscr": "Modifica la configurazione JSON delle sostituzioni dei caratteri", "channels_cyr2latSettingsDialogHint": "Mappa JSON delle sostituzioni", "channels_cyr2latSettingsDialogWrongJSON": "JSON non corretto: {error}", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, "settings_cyr2latProfileAdd": "Aggiungi profilo Cyr2Lat", "settings_cyr2latProfileName": "Nome profilo", "settings_cyr2latProfileNameEmpty": "Il nome del profilo non può essere vuoto", @@ -405,6 +466,13 @@ "settings_cyr2latProfileDelete": "Elimina profilo Cyr2Lat", "settings_cyr2latProfileDeleted": "Profilo eliminato con successo", "settings_cyr2latProfileDeleteDscr": "Sei sicuro di voler eliminare il profilo \"{name}\"?", + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelUpdated": "Canale \"{name}\" aggiornato", "@channels_channelUpdated": { "placeholders": { @@ -416,7 +484,7 @@ "channels_publicChannelAdded": "Canale pubblico aggiunto", "channels_sortBy": "Ordina per", "channels_sortManual": "Manuale", - "channels_sortAZ": "D-Z", + "channels_sortAZ": "A-Z", "channels_sortLatestMessages": "Ultimi messaggi", "channels_sortUnread": "Non letto", "chat_noMessages": "Nessun messaggio ancora", @@ -513,7 +581,7 @@ } }, "debugFrame_textMessageHeader": "Messaggio di testo:", - "debugFrame_destinationPubKey": "- Destinazione PubChiave: {pubKey}", + "debugFrame_destinationPubKey": "- Chiave pubblica di destinazione: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -562,11 +630,11 @@ "chat_pathManagement": "Gestione Percorsi", "chat_routingMode": "Modalità di routing", "chat_autoUseSavedPath": "Utilizza il percorso salvato", - "chat_forceFloodMode": "Modalità Inondamento Forzato", - "chat_recentAckPaths": "Percorsi ACK Recenti (tocca per usare):", - "chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.", - "chat_hopSingular": "salta", - "chat_hopPlural": "salta", + "chat_forceFloodMode": "Modalità flood forzata", + "chat_recentAckPaths": "Percorsi ACK recenti (tocca per usarli):", + "chat_pathHistoryFull": "La cronologia dei percorsi è piena. Rimuovi elementi per aggiungerne di nuovi.", + "chat_hopSingular": "salto", + "chat_hopPlural": "salto", "chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}", "@chat_hopsCount": { "placeholders": { @@ -579,16 +647,16 @@ "chat_removePath": "Rimuovi percorso", "chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.", "chat_pathActions": "Azioni Percorso:", - "chat_setCustomPath": "Imposta Percorso Personalizzato", - "chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing", - "chat_clearPath": "Cancella Percorso", - "chat_clearPathSubtitle": "Riprova la scoperta alla prossima invio", - "chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.", - "chat_floodModeSubtitle": "Utilizza l'interruttore di routing nella barra delle applicazioni", - "chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.", + "chat_setCustomPath": "Imposta percorso personalizzato", + "chat_setCustomPathSubtitle": "Specifica manualmente il percorso di instradamento", + "chat_clearPath": "Cancella percorso", + "chat_clearPathSubtitle": "Riprova la scoperta al prossimo invio", + "chat_pathCleared": "Percorso cancellato. Il prossimo messaggio riscoprirà il percorso.", + "chat_floodModeSubtitle": "Usa il selettore di routing nella barra delle applicazioni", + "chat_floodModeEnabled": "Modalità flood abilitata. Disattivala tramite l'icona di routing nella barra superiore.", "chat_fullPath": "Percorso Completo", "chat_pathDetailsNotAvailable": "I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.", - "chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{salto} other{salti}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -599,15 +667,15 @@ } } }, - "chat_pathSavedLocally": "Salvatato localmente. Connetti per sincronizzare.", + "chat_pathSavedLocally": "Salvato localmente. Connettiti per sincronizzare.", "chat_pathDeviceConfirmed": "Dispositivo confermato.", "chat_pathDeviceNotConfirmed": "Dispositivo non confermato ancora.", - "chat_type": "Digita", + "chat_type": "Tipo", "chat_path": "Percorso", - "chat_publicKey": "Chiave Pubblica", + "chat_publicKey": "Chiave pubblica", "chat_compressOutgoingMessages": "Comprimi messaggi in uscita", - "chat_floodForced": "Inondazione (forzata)", - "chat_directForced": "Riavvia (forzato)", + "chat_floodForced": "Flood (forzato)", + "chat_directForced": "Diretto (forzato)", "chat_hopsForced": "{count} salti (forzati)", "@chat_hopsForced": { "placeholders": { @@ -616,9 +684,9 @@ } } }, - "chat_floodAuto": "Inondazione (auto)", - "chat_direct": "Salva", - "chat_poiShared": "Punti di Interesse Condivisi", + "chat_floodAuto": "Flood (automatico)", + "chat_direct": "Diretto", + "chat_poiShared": "POI condiviso", "chat_unread": "Non letti: {count}", "@chat_unread": { "placeholders": { @@ -627,7 +695,7 @@ } } }, - "chat_openLink": "Aprire il link?", + "chat_openLink": "Apri il link?", "chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?", "chat_open": "Apri", "chat_couldNotOpenLink": "Impossibile aprire il link: {url}", @@ -639,7 +707,7 @@ } }, "chat_invalidLink": "Formato di link non valido", - "map_title": "Mappa Nodi", + "map_title": "Mappa nodi", "map_noNodesWithLocation": "Nessun nodo con dati di posizione", "map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa", "map_nodesCount": "Nodi: {count}", @@ -650,7 +718,7 @@ } } }, - "map_pinsCount": "Puntatori: {count}", + "map_pinsCount": "Pin: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -662,22 +730,22 @@ "map_repeater": "Ripetitore", "map_room": "Stanza", "map_sensor": "Sensore", - "map_pinDm": "Codice PIN (DM)", - "map_pinPrivate": "Blocco (Privato)", + "map_pinDm": "Pin (DM)", + "map_pinPrivate": "Pin (Privato)", "map_pinPublic": "Pin (Pubblico)", "map_lastSeen": "Ultimo visto", "map_disconnectConfirm": "Sei sicuro di voler disconnetterti da questo dispositivo?", "map_from": "Da", "map_source": "Fonte", "map_flags": "Bandiere", - "map_shareMarkerHere": "Condividi marcatore qui", - "map_pinLabel": "Etichetta PIN", + "map_shareMarkerHere": "Condividi segnaposto qui", + "map_pinLabel": "Etichetta pin", "map_label": "Etichetta", "map_pointOfInterest": "Punto di interesse", "map_sendToContact": "Invia a contatto", "map_sendToChannel": "Invia al canale", "map_noChannelsAvailable": "Nessun canale disponibile", - "map_publicLocationShare": "Condividi in una posizione pubblica", + "map_publicLocationShare": "Condivisione pubblica della posizione", "map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.", "@map_publicLocationShareConfirm": { "placeholders": { @@ -686,25 +754,25 @@ } } }, - "map_connectToShareMarkers": "Connetti a un dispositivo per condividere i segnaposti", - "map_filterNodes": "Filtra Nodi", - "map_nodeTypes": "Tipi di Nodo", - "map_chatNodes": "Nodi di Chat", + "map_connectToShareMarkers": "Connettiti a un dispositivo per condividere i segnaposti", + "map_filterNodes": "Filtra nodi", + "map_nodeTypes": "Tipi di nodo", + "map_chatNodes": "Nodi chat", "map_repeaters": "Ripetitori", - "map_otherNodes": "Altri Nodi", - "map_keyPrefix": "Prefisso Chiave", + "map_otherNodes": "Altri nodi", + "map_keyPrefix": "Prefisso chiave", "map_filterByKeyPrefix": "Filtra per prefisso chiave", "map_publicKeyPrefix": "Prefisso chiave pubblica", - "map_markers": "Segnaposto", - "map_showSharedMarkers": "Mostra i segnaposto condivisi", - "map_lastSeenTime": "Ultimo Tempo di Visualizzazione", - "map_sharedPin": "Condividi PIN", + "map_markers": "Segnaposti", + "map_showSharedMarkers": "Mostra segnaposti condivisi", + "map_lastSeenTime": "Ora dell'ultimo visto", + "map_sharedPin": "Pin condiviso", "map_joinRoom": "Unisciti alla stanza", - "map_manageRepeater": "Gestisci Ripetitore", - "mapCache_title": "Cache Mappa Offline", + "map_manageRepeater": "Gestisci ripetitore", + "mapCache_title": "Cache mappa offline", "mapCache_selectAreaFirst": "Seleziona un'area da memorizzare nella cache per prima.", "mapCache_noTilesToDownload": "Nessun tile da scaricare per questa area", - "mapCache_downloadTilesTitle": "Scarica mattoncini", + "mapCache_downloadTilesTitle": "Scarica tessere", "mapCache_downloadTilesPrompt": "Scarica {count} tile per l'uso offline?", "@mapCache_downloadTilesPrompt": { "placeholders": { @@ -714,7 +782,7 @@ } }, "mapCache_downloadAction": "Scarica", - "mapCache_cachedTiles": "Cacheggiate {count} tile", + "mapCache_cachedTiles": "Tessere memorizzate nella cache: {count}", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -722,7 +790,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Tile memorizzati {downloaded} ({failed} falliti)", + "mapCache_cachedTilesWithFailed": "Tessere memorizzate nella cache {downloaded} ({failed} fallite)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -737,10 +805,10 @@ "mapCache_clearOfflineCachePrompt": "Eliminare tutte le tile di mappa memorizzate nella cache?", "mapCache_offlineCacheCleared": "Cache offline eliminata", "mapCache_noAreaSelected": "Nessun'area selezionata", - "mapCache_cacheArea": "Area Cache", + "mapCache_cacheArea": "Area cache", "mapCache_useCurrentView": "Utilizza la visualizzazione corrente", - "mapCache_zoomRange": "Intervallo Zoom", - "mapCache_estimatedTiles": "Stima dei mattoni: {count}", + "mapCache_zoomRange": "Intervallo zoom", + "mapCache_estimatedTiles": "Tessere stimate: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -759,8 +827,8 @@ } } }, - "mapCache_downloadTilesButton": "Scarica Tessere", - "mapCache_clearCacheButton": "Svuota Cache", + "mapCache_downloadTilesButton": "Scarica tessere", + "mapCache_clearCacheButton": "Svuota cache", "mapCache_failedDownloads": "Download falliti: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -820,11 +888,11 @@ "time_month": "mese", "time_months": "mesi", "time_minutes": "minuti", - "time_allTime": "Tutto il Tempo", + "time_allTime": "Sempre", "dialog_disconnect": "Disconnetti", "dialog_disconnectConfirm": "Sei sicuro di voler disconnetterti da questo dispositivo?", - "login_repeaterLogin": "Login Ripetitore", - "login_roomLogin": "Login Camera", + "login_repeaterLogin": "Accesso ripetitore", + "login_roomLogin": "Accesso server stanza", "login_password": "Password", "login_enterPassword": "Inserisci password", "login_savePassword": "Salva password", @@ -834,8 +902,8 @@ "login_routing": "Instradamento", "login_routingMode": "Modalità di routing", "login_autoUseSavedPath": "Utilizza il percorso salvato", - "login_forceFloodMode": "Modalità Inondamento Forzato", - "login_managePaths": "Gestisci Percorsi", + "login_forceFloodMode": "Modalità flood forzata", + "login_managePaths": "Gestisci percorsi", "login_login": "Accedi", "login_attempt": "Prova {current}/{max}", "@login_attempt": { @@ -857,7 +925,7 @@ } }, "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", - "common_reload": "Ricaricare", + "common_reload": "Ricarica", "common_clear": "Cancella", "path_currentPath": "Percorso corrente: {path}", "@path_currentPath": { @@ -867,7 +935,7 @@ } } }, - "path_usingHopsPath": "Utilizzare {count} {count, plural, =1{hop} other{hops}} percorso", + "path_usingHopsPath": "Usa un percorso di {count} {count, plural, =1{salto} other{salti}}", "@path_usingHopsPath": { "placeholders": { "count": { @@ -882,7 +950,7 @@ "path_labelHexPrefixes": "Prefisso esadecimale (percorso)", "path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)", "path_selectFromContacts": "Seleziona da contatti:", - "path_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.", + "path_noRepeatersFound": "Non sono stati trovati ripetitori o server stanza.", "path_customPathsRequire": "I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.", "path_invalidHexPrefixes": "Prefissi esadecimali non validi: {prefixes}", "@path_invalidHexPrefixes": { @@ -893,9 +961,9 @@ } }, "path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.", - "path_setPath": "Imposta Percorso", - "repeater_management": "Gestione Ripetitori", - "repeater_managementTools": "Strumenti di Gestione", + "path_setPath": "Imposta percorso", + "repeater_management": "Gestione ripetitori", + "repeater_managementTools": "Strumenti di gestione", "repeater_status": "Stato", "repeater_statusSubtitle": "Visualizza lo stato, le statistiche e i vicini del ripetitore", "repeater_telemetry": "Telemetria", @@ -904,11 +972,11 @@ "repeater_cliSubtitle": "Invia comandi al ripetitore", "repeater_settings": "Impostazioni", "repeater_settingsSubtitle": "Configura i parametri del ripetitore", - "repeater_statusTitle": "Stato del Ripetitore", + "repeater_statusTitle": "Stato del ripetitore", "repeater_routingMode": "Modalità di routing", - "repeater_autoUseSavedPath": "Percorso salvato automatico", - "repeater_forceFloodMode": "Modalità Inondamento Forzato", - "repeater_pathManagement": "Gestione dei percorsi", + "repeater_autoUseSavedPath": "Usa percorso salvato automaticamente", + "repeater_forceFloodMode": "Modalità flood forzata", + "repeater_pathManagement": "Gestione percorsi", "repeater_refresh": "Aggiorna", "repeater_statusRequestTimeout": "Richiesta stato scaduta.", "repeater_errorLoadingStatus": "Errore nel caricamento dello stato: {error}", @@ -922,16 +990,16 @@ "repeater_systemInformation": "Informazioni di sistema", "repeater_battery": "Batteria", "repeater_clockAtLogin": "Orologio (all'accesso)", - "repeater_uptime": "Disponibilità", - "repeater_queueLength": "Lunghezza della coda", - "repeater_debugFlags": "Impostazioni Debug", - "repeater_radioStatistics": "Statistiche Radio", + "repeater_uptime": "Tempo di attività", + "repeater_queueLength": "Lunghezza coda", + "repeater_debugFlags": "Flag di debug", + "repeater_radioStatistics": "Statistiche radio", "repeater_lastRssi": "Ultimo RSSI", "repeater_lastSnr": "Ultimo SNR", - "repeater_noiseFloor": "Livello del Rumore", - "repeater_txAirtime": "TX Airtime", - "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Statistiche del Pacchetto", + "repeater_noiseFloor": "Rumore di fondo", + "repeater_txAirtime": "Tempo d'aria TX", + "repeater_rxAirtime": "Tempo d'aria RX", + "repeater_packetStatistics": "Statistiche pacchetti", "repeater_sent": "Inviato", "repeater_received": "Ricevuto", "repeater_duplicates": "Duplicati", @@ -952,7 +1020,7 @@ } } }, - "repeater_packetTxTotal": "Totale: {total}, Inondazione: {flood}, Diretto: {direct}", + "repeater_packetTxTotal": "Totale: {total}, Flood: {flood}, Diretto: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -966,7 +1034,7 @@ } } }, - "repeater_packetRxTotal": "Totale: {total}, Inondazione: {flood}, Diretto: {direct}", + "repeater_packetRxTotal": "Totale: {total}, Flood: {flood}, Diretto: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -980,7 +1048,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Inondazione: {flood}, Diretto: {direct}", + "repeater_duplicatesFloodDirect": "Flood: {flood}, Diretto: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -1002,33 +1070,33 @@ "repeater_settingsTitle": "Impostazioni Ripetitore", "repeater_basicSettings": "Impostazioni di Base", "repeater_repeaterName": "Nome Ripetitore", - "repeater_repeaterNameHelper": "Visualizza il nome di questo ripetitore", - "repeater_adminPassword": "Password Amministratore", - "repeater_adminPasswordHelper": "Accesso completo password", - "repeater_guestPassword": "Password Ospite", - "repeater_guestPasswordHelper": "Accesso in sola lettura con password", + "repeater_repeaterNameHelper": "Nome visualizzato per questo ripetitore", + "repeater_adminPassword": "Password amministratore", + "repeater_adminPasswordHelper": "Password per accesso completo", + "repeater_guestPassword": "Password ospite", + "repeater_guestPasswordHelper": "Password per accesso in sola lettura", "repeater_radioSettings": "Impostazioni Radio", "repeater_frequencyMhz": "Frequenza (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Potenza", + "repeater_txPower": "Potenza TX", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Larghezza di banda", - "repeater_spreadingFactor": "Fattore di propagazione", + "repeater_spreadingFactor": "Fattore di diffusione", "repeater_codingRate": "Tasso di Codifica", - "repeater_locationSettings": "Impostazioni Luogo", + "repeater_locationSettings": "Impostazioni posizione", "repeater_latitude": "Latitudine", "repeater_latitudeHelper": "Grado decimale (ad esempio, 37.7749)", "repeater_longitude": "Longitudine", "repeater_longitudeHelper": "Grado decimale (ad esempio, -122,4194)", "repeater_features": "Caratteristiche", - "repeater_packetForwarding": "Instradamento Pacchetti", + "repeater_packetForwarding": "Inoltro pacchetti", "repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti", - "repeater_guestAccess": "Accesso Ospite", + "repeater_guestAccess": "Accesso ospite", "repeater_guestAccessSubtitle": "Consenti l'accesso ospite in sola lettura", "repeater_privacyMode": "Modalità Privacy", - "repeater_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", - "repeater_advertisementSettings": "Impostazioni Annuncio", - "repeater_localAdvertInterval": "Intervallo Pubblicità Locale", + "repeater_privacyModeSubtitle": "Nascondi nome/posizione negli annunci", + "repeater_advertisementSettings": "Impostazioni annuncio", + "repeater_localAdvertInterval": "Intervallo annuncio locale", "repeater_localAdvertIntervalMinutes": "{minutes} minuti", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1037,7 +1105,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione", + "repeater_floodAdvertInterval": "Intervallo annuncio flood", "repeater_floodAdvertIntervalHours": "{hours} ore", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1046,7 +1114,7 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervallo Pubblicitario Crittografato", + "repeater_encryptedAdvertInterval": "Intervallo annuncio crittografato", "repeater_dangerZone": "Zona Pericolosa", "repeater_rebootRepeater": "Riavvia Ripetitore", "repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo ripetitore", @@ -1181,7 +1249,7 @@ } } }, - "repeater_cliTitle": "Riprova CLI", + "repeater_cliTitle": "CLI del ripetitore", "repeater_debugNextCommand": "Riavvia Comando Prossimo", "repeater_commandHelp": "Aiuto", "repeater_clearHistory": "Cancella Cronologia", @@ -1205,7 +1273,7 @@ "repeater_cliQuickGetTx": "Ottieni TX", "repeater_cliQuickNeighbors": "Vicini", "repeater_cliQuickVersion": "Versione", - "repeater_cliQuickAdvertise": "Pubblicare", + "repeater_cliQuickAdvertise": "Annuncia", "repeater_cliQuickClock": "Orologio", "repeater_cliHelpAdvert": "Invia un pacchetto pubblicitario", "repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)", @@ -1216,7 +1284,7 @@ "repeater_cliHelpSetAf": "Imposta il fattore di tempo di trasmissione.", "repeater_cliHelpSetTx": "Imposta la potenza di trasmissione LoRa in dBm (riavvia per applicare).", "repeater_cliHelpSetRepeat": "Abilita o disabilita il ruolo del ripetitore per questo nodo.", - "repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", + "repeater_cliHelpSetAllowReadOnly": "(Server stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", "repeater_cliHelpSetFloodMax": "Imposta il numero massimo di salti per i pacchetti di inondazione in entrata (se >= max, il pacchetto non viene inoltrato)", "repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.", "repeater_cliHelpSetAgcResetInterval": "Imposta l'intervallo per resettare il controllore Automatico del Guadagno. Imposta su 0 per disabilitare.", @@ -1229,17 +1297,17 @@ "repeater_cliHelpSetLon": "Imposta la longitudine della mappa pubblicitaria. (gradi decimali)", "repeater_cliHelpSetRadio": "Imposta completamente nuovi parametri radio e li salva nelle preferenze. Richiede un comando \"reboot\" per l'applicazione.", "repeater_cliHelpSetRxDelay": "Impostazioni (experimental) base (deve essere > 1 per l'effetto) per applicare un leggero ritardo ai pacchetti ricevuti, in base alla forza del segnale/punteggio. Imposta a 0 per disabilitare.", - "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).", + "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicativo sul tempo di trasmissione dei pacchetti in modalità flood e su un sistema a slot casuali, per ritardarne l'inoltro (riducendo la probabilità di collisioni).", "repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.", - "repeater_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.", + "repeater_cliHelpSetBridgeEnabled": "Abilita/disabilita il bridge.", "repeater_cliHelpSetBridgeDelay": "Imposta il ritardo prima di ritrasmettere i pacchetti.", - "repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.", + "repeater_cliHelpSetBridgeSource": "Scegli se il bridge ritrasmette i pacchetti ricevuti o trasmessi.", "repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.", "repeater_cliHelpSetBridgeSecret": "Imposta il segreto per i ponti espnow.", "repeater_cliHelpSetAdcMultiplier": "Imposta un fattore personalizzato per regolare la tensione della batteria riportata (supportato solo su schede selezionate).", "repeater_cliHelpTempRadio": "Imposta parametri radio temporanei per il numero specificato di minuti, per poi tornare ai parametri radio originali. (non salva nelle preferenze).", "repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Ottiene tipo ponte nessuno, rs232, espnow", + "repeater_cliHelpGetBridgeType": "Mostra il tipo di bridge: nessuno, rs232, espnow", "repeater_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.", "repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al file system.", "repeater_cliHelpLogErase": "Elimina i log del pacchetto dal file system.", @@ -1250,9 +1318,9 @@ "repeater_cliHelpRegionGet": "Cerca la regione con il prefisso del nome dato (o \"\" per l'ambito globale). Risponde con \"-> nome-regione (nome-genitore) 'F'\"", "repeater_cliHelpRegionPut": "Aggiunge o aggiorna una definizione di regione con il nome specificato.", "repeater_cliHelpRegionRemove": "Rimuove una definizione di regione con il dato nome. (deve corrispondere esattamente e non avere regioni figlio)", - "repeater_cliHelpRegionAllowf": "Imposta il permesso di 'F'lood per la regione specificata. ('' per lo scope globale/legacy)", - "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).", - "repeater_cliHelpRegionHome": "Risposte con la regione 'home' corrente. (Nota applicata finora, riservata per il futuro)", + "repeater_cliHelpRegionAllowf": "Imposta il permesso 'F'lood per la regione specificata. ('*' per lo scope globale/legacy)", + "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: al momento non è consigliato usarlo sullo scope globale/legacy!!).", + "repeater_cliHelpRegionHome": "Risponde con la regione 'home' corrente. (Nota non ancora applicata, riservata per il futuro)", "repeater_cliHelpRegionHomeSet": "Imposta la regione 'home'.", "repeater_cliHelpRegionSave": "Persiste l'elenco/mappa delle regioni all'archiviazione.", "repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.", @@ -1307,16 +1375,93 @@ "telemetry_switchLabel": "Interruttore", "telemetry_polylineLabel": "Polilinea", "telemetry_altitudeValue": "{meters} m", + "@telemetry_altitudeValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_frequencyValue": "{hertz} Hz", + "@telemetry_frequencyValue": { + "placeholders": { + "hertz": { + "type": "String" + } + } + }, "telemetry_pressureValue": "{hpa} hPa", + "@telemetry_pressureValue": { + "placeholders": { + "hpa": { + "type": "String" + } + } + }, "telemetry_luminosityValue": "{lux} lx", + "@telemetry_luminosityValue": { + "placeholders": { + "lux": { + "type": "String" + } + } + }, "telemetry_powerValue": "{watts} W", + "@telemetry_powerValue": { + "placeholders": { + "watts": { + "type": "String" + } + } + }, "telemetry_distanceValue": "{meters} m", + "@telemetry_distanceValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_energyValue": "{kilowattHours} kWh", + "@telemetry_energyValue": { + "placeholders": { + "kilowattHours": { + "type": "String" + } + } + }, "telemetry_directionValue": "{degrees}°", + "@telemetry_directionValue": { + "placeholders": { + "degrees": { + "type": "String" + } + } + }, "telemetry_concentrationValue": "{ppm} ppm", + "@telemetry_concentrationValue": { + "placeholders": { + "ppm": { + "type": "String" + } + } + }, "telemetry_percentageValue": "{percent}%", + "@telemetry_percentageValue": { + "placeholders": { + "percent": { + "type": "String" + } + } + }, "telemetry_analogValue": "{value}", + "@telemetry_analogValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "telemetry_autoFetchQuantity": "Numero di richieste", "telemetry_error": "Impossibile recuperare i dati", "telemetry_noData": "Nessun dato di telemetria disponibile.", @@ -1372,14 +1517,14 @@ } }, "channelPath_title": "Percorso Pacchetto", - "channelPath_viewMap": "Visualizza la mappa", - "channelPath_otherObservedPaths": "Altri Percorsi Osservati", - "channelPath_repeaterHops": "Passaggi Ripetitore", + "channelPath_viewMap": "Visualizza mappa", + "channelPath_otherObservedPaths": "Altri percorsi osservati", + "channelPath_repeaterHops": "Salti del ripetitore", "channelPath_noHopDetails": "I dettagli relativi a questo pacchetto non sono forniti.", - "channelPath_messageDetails": "Dettagli Messaggio", + "channelPath_messageDetails": "Dettagli messaggio", "channelPath_senderLabel": "Mittente", - "channelPath_timeLabel": "Tempo", - "channelPath_repeatsLabel": "Ripeti", + "channelPath_timeLabel": "Ora", + "channelPath_repeatsLabel": "Ripetizioni", "channelPath_pathLabel": "Percorso {index}", "channelPath_observedLabel": "Osservato", "channelPath_observedPathTitle": "Percorso osservato {index} • {hops}", @@ -1417,8 +1562,8 @@ } }, "channelPath_unknownPath": "Sconosciuto", - "channelPath_floodPath": "Inondazione", - "channelPath_directPath": "Salva", + "channelPath_floodPath": "Flood", + "channelPath_directPath": "Diretto", "channelPath_observedZeroOf": "0 di {total} salti", "@channelPath_observedZeroOf": { "placeholders": { @@ -1438,8 +1583,8 @@ } } }, - "channelPath_mapTitle": "Mappa del Percorso", - "channelPath_noRepeaterLocations": "Non sono disponibili posizioni per i ripetitori per questo percorso.", + "channelPath_mapTitle": "Mappa del percorso", + "channelPath_noRepeaterLocations": "Non sono disponibili posizioni dei ripetitori per questo percorso.", "channelPath_primaryPath": "Percorso {index} (Primario)", "@channelPath_primaryPath": { "placeholders": { @@ -1473,14 +1618,14 @@ "listFilter_tooltip": "Filtra e ordina", "listFilter_sortBy": "Ordina per", "listFilter_latestMessages": "Ultimi messaggi", - "listFilter_heardRecently": "Sentito di recente", - "listFilter_az": "D-Z", + "listFilter_heardRecently": "Rilevato di recente", + "listFilter_az": "A-Z", "listFilter_filters": "Filtri", "listFilter_all": "Tutti", "listFilter_users": "Utenti", "listFilter_repeaters": "Ripetitori", - "listFilter_roomServers": "Server della stanza", - "listFilter_unreadOnly": "Solo non letto", + "listFilter_roomServers": "Server stanza", + "listFilter_unreadOnly": "Solo non letti", "listFilter_newGroup": "Nuovo gruppo", "@neighbors_errorLoading": { "placeholders": { @@ -1490,12 +1635,12 @@ } }, "repeater_neighbors": "Vicini", - "repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.", - "neighbors_receivedData": "Ricevute dati vicini", - "neighbors_requestTimedOut": "I vicini richiedono un timeout.", + "repeater_neighborsSubtitle": "Visualizza vicini a salto zero.", + "neighbors_receivedData": "Dati dei vicini ricevuti", + "neighbors_requestTimedOut": "Richiesta dei vicini scaduta.", "neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}", - "neighbors_repeatersNeighbors": "Ripetitori Vicini", - "neighbors_noData": "Nessun dato sugli vicini disponibile.", + "neighbors_repeatersNeighbors": "Vicini dei ripetitori", + "neighbors_noData": "Nessun dato sui vicini disponibile.", "channels_createPrivateChannel": "Crea un Canale Privato", "channels_createPrivateChannelDesc": "Protetta con una chiave segreta.", "channels_joinPrivateChannel": "Unisciti a un Canale Privato", @@ -1528,7 +1673,7 @@ "settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.", "settings_locationIntervalSec": "Intervallo GPS (Secondi)", "settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.", - "contacts_manageRoom": "Gestisci Server Camera", + "contacts_manageRoom": "Gestisci server stanza", "room_management": "Gestione del Server di Camera", "@community_joinConfirmation": { "placeholders": { @@ -1586,7 +1731,7 @@ } } }, - "common_ok": "Va bene", + "common_ok": "OK", "community_title": "Comunità", "community_create": "Crea Comunità", "community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.", @@ -1677,7 +1822,7 @@ "pathTrace_failed": "Tracciamento del percorso fallito.", "pathTrace_you": "Tu", "pathTrace_notAvailable": "Tracciamento del percorso non disponibile.", - "pathTrace_refreshTooltip": "Aggiorna Path Trace.", + "pathTrace_refreshTooltip": "Aggiorna tracciamento percorso.", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Traccia percorso al ripetitore", "contacts_roomPathTrace": "Traccia del percorso al server della stanza", @@ -1706,9 +1851,38 @@ "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", "notification_activityTitle": "Attività MeshCore", "notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}", + "@notification_messagesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", + "@notification_newNodesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "notification_newTypeDiscovered": "Nuovo {contactType} scoperto", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": { + "type": "String" + } + } + }, + "notification_receivedNewMessage": "Nuovo messaggio ricevuto", "notification_receivedNewMessage": "Nuovo messaggio ricevuto", "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", "settings_gpxExportContacts": "Esporta compagni in GPX", @@ -1725,11 +1899,11 @@ "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", - "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", + "pathTrace_someHopsNoLocation": "Uno o più hop non hanno una posizione!", "map_removeLast": "Rimuovi ultimo", "map_pathTraceCancelled": "Tracciamento del percorso annullato.", - "pathTrace_clearTooltip": "Pulisci percorso", - "map_runTrace": "Esegui Path Trace", + "pathTrace_clearTooltip": "Cancella percorso", + "map_runTrace": "Esegui tracciamento percorso", "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", @@ -1744,7 +1918,7 @@ "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.", "settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Unità", - "appSettings_unitsMetric": "Metrico (m/km)", + "appSettings_unitsMetric": "Metrico (m / km)", "appSettings_unitsImperial": "Imperiale (ft / mi)", "map_lineOfSight": "Linea di vista", "map_losScreenTitle": "Linea di vista", @@ -1924,12 +2098,12 @@ } } }, - "contacts_searchUsers": "Cerca {number}{str} Utenti...", - "contacts_searchContactsNoNumber": "Cerca Contatti...", - "contacts_searchFavorites": "Cerca {number}{str} Preferiti...", + "contacts_searchUsers": "Cerca {number}{str} utenti...", + "contacts_searchContactsNoNumber": "Cerca contatti...", + "contacts_searchFavorites": "Cerca {number}{str} preferiti...", "contacts_unread": "Non letti", - "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", - "contacts_searchRoomServers": "Cerca {number}{str} server Room...", + "contacts_searchRepeaters": "Cerca {number}{str} ripetitori...", + "contacts_searchRoomServers": "Cerca {number}{str} server stanza...", "contactsSettings_title": "Impostazioni dei contatti", "settings_contactSettings": "Impostazioni di contatto", "contactsSettings_otherTitle": "Altre impostazioni relative ai contatti", @@ -1955,7 +2129,7 @@ "common_deleteAll": "Elimina tutto", "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti", - "map_guessedLocation": "Località indovinata", + "map_guessedLocation": "Posizione stimata", "map_showGuessedLocations": "Mostra le posizioni stimate dei nodi", "connectionChoiceBluetoothLabel": "Bluetooth", "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", @@ -2015,7 +2189,7 @@ "tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.", "tcpErrorTimedOut": "La connessione TCP è scaduta.", "tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}", - "map_showDiscoveryContacts": "Mostra Contatti di Discovery", + "map_showDiscoveryContacts": "Mostra contatti di scoperta", "map_setAsMyLocation": "Imposta come la mia posizione", "@path_routeWeight": { "placeholders": { @@ -2062,8 +2236,8 @@ "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Modalità telemetria aggiornata", "settings_multiAck": "ACK multipli", - "map_showOverlaps": "Sovrapposizioni della chiave ripetitore", - "map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso", + "map_showOverlaps": "Sovrapposizioni chiave ripetitore", + "map_runTraceWithReturnPath": "Ritorna lungo lo stesso percorso.", "@radioStats_noiseFloor": { "placeholders": { "noiseDbm": { @@ -2183,8 +2357,8 @@ "scanner_linuxPairingShowPin": "Mostra PIN", "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth", "scanner_linuxPairingHidePin": "Nascondi il PIN", - "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", - "repeater_cliQuickDiscovery": "Scopri i Vicini", + "repeater_cliQuickClockSync": "Sincronizza orologio", + "repeater_cliQuickDiscovery": "Scopri vicini", "@repeater_clockSyncAfterLogin": { "description": "Repeater setting: auto sync device clock after successful login" }, @@ -2234,14 +2408,14 @@ "repeater_cliHelpGetMultiAcks": "Indica se la modalità \"ACK doppio\" è attiva (1) o disattivata (0).", "repeater_cliHelpGetAllowReadOnly": "Indica se è consentito l'accesso in sola lettura per gli ospiti.", "repeater_cliHelpGetAdvertInterval": "Indica l'intervallo pubblicitario locale in minuti.", - "repeater_cliHelpGetFloodAdvertInterval": "Indica l'intervallo pubblicitario per la trasmissione del servizio di allerta alluvioni, espresso in ore.", + "repeater_cliHelpGetFloodAdvertInterval": "Indica l'intervallo dell'annuncio flood, espresso in ore.", "repeater_cliHelpGetGuestPassword": "Visualizza la password del guest configurata.", "repeater_cliHelpGetLat": "Mostra la latitudine configurata.", "repeater_cliHelpGetLon": "Mostra la longitudine impostata.", "repeater_cliHelpGetRxDelay": "Mostra il valore base di rxdelay.", - "repeater_cliHelpGetTxDelay": "Mostra il fattore di ritardo in modalità di allarme.", + "repeater_cliHelpGetTxDelay": "Mostra il fattore txdelay in modalità flood.", "repeater_cliHelpGetDirectTxDelay": "Mostra il fattore di ritardo in modalità diretta.", - "repeater_cliHelpGetFloodMax": "Mostra il numero massimo di salti dovuto all'inondazione.", + "repeater_cliHelpGetFloodMax": "Mostra il numero massimo di salti del flood.", "repeater_cliHelpGetOwnerInfo": "Visualizza la stringa contenente le informazioni di contatto del proprietario.", "repeater_cliHelpGetPathHashMode": "Mostra la modalità \"hash del percorso\" (0/1/2).", "repeater_cliHelpGetLoopDetect": "Indica la sensibilità alla rilevazione di loop.", @@ -2263,12 +2437,12 @@ "repeater_cliHelpSensorList": "Elenca tutte le impostazioni personalizzate dei sensori, organizzate in pagine a partire da un indice di inizio opzionale.", "repeater_cliHelpRegionDefault": "Mostra l'ambito predefinito corrente.", "repeater_cliHelpRegionDefaultSet": "Definisce l'ambito regionale predefinito. Utilizzare \"\" per cancellare.", - "repeater_cliHelpRegionListAllowed": "Elenca le regioni che consentono il transito di veicoli in caso di allagamenti.", - "repeater_cliHelpRegionListDenied": "Elenca le regioni che vietano il transito in caso di alluvioni.", + "repeater_cliHelpRegionListAllowed": "Elenca le regioni che consentono il traffico flood.", + "repeater_cliHelpRegionListDenied": "Elenca le regioni che negano il traffico flood.", "repeater_cliHelpStatsPackets": "(Solo per la visualizzazione dei dati seriali) Mostra statistiche a livello di pacchetto.", "repeater_cliHelpStatsRadio": "(Solo per serie TV) Visualizza statistiche relative alla trasmissione radiofonica.", "repeater_cliHelpStatsCore": "(Solo per serie) Visualizza le statistiche del firmware di base.", - "common_done": "Done", + "common_done": "Fatto", "background_serviceTitle": "MeshCore running", "background_serviceText": "Keeping BLE connected", "appSettings_translationModelDeleted": "Deleted {name}", @@ -2287,7 +2461,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "Impossibile aggiornare il canale: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2295,19 +2469,26 @@ } } }, - "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", - "map_publicKeyPrefixHint": "e.g. ab12", + "map_type": "Tipo", + "map_path": "Percorso", + "map_location": "Posizione", + "map_estLocation": "Posizione stimata", + "map_publicKey": "Chiave pubblica", + "map_publicKeyPrefixHint": "es. ab12", "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", - "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", - "chat_score": "Score", + "contact_typeRepeater": "Ripetitore", + "contact_typeRoom": "Stanza", + "contact_typeSensor": "Sensore", + "contact_typeUnknown": "Sconosciuto", + "channels_via": "tramite {path}", + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "chat_score": "Punteggio", "map_sharedAt": "Condiviso", "@losBlockedSpotChip": { "placeholders": { @@ -2394,57 +2575,151 @@ }, "common_undo": "Annulla", "messageStatus_delivered": "Consegnato", - "messageStatus_sent": "Invia", + "messageStatus_sent": "Inviato", "messageStatus_pending": "Invio", "messageStatus_failed": "Impossibile inviare", - "messageStatus_repeated": "Sentito ripetutamente", + "messageStatus_repeated": "Ricevuto ripetutamente", "contacts_moreOptions": "Ulteriori opzioni", - "contacts_searchOpen": "Cerca contatti", - "contacts_searchClose": "Ricerca avanzata", + "contacts_searchOpen": "Apri ricerca", + "contacts_searchClose": "Chiudi ricerca", "routing_title": "Instradamento", - "routing_modeAuto": "Auto", - "routing_modeFlood": "Inondazione", + "routing_modeAuto": "Automatico", + "routing_modeFlood": "Flood", "routing_modeManual": "Manuale", - "routing_modeAutoHint": "Seleziona automaticamente il percorso più noto, e in caso di assenza di informazioni, utilizza un percorso casuale.", - "routing_modeFloodHint": "Trasmissioni tramite ogni ripetitore. Il metodo più affidabile, ma richiede più tempo di trasmissione.", - "routing_modeManualHint": "Invia sempre esattamente il percorso che hai definito.", + "routing_modeAutoHint": "Seleziona automaticamente il percorso migliore noto, ricorrendo al flood quando non ne è noto nessuno.", + "routing_modeFloodHint": "Trasmette tramite ogni ripetitore. È il più affidabile, ma usa più airtime.", + "routing_modeManualHint": "Invia sempre lungo il percorso esatto che hai definito.", "routing_currentRoute": "Percorso attuale", - "routing_directNoHops": "Diretto — senza passaggi tramite ripetitori", - "routing_noPathYet": "Al momento non è stata individuata alcuna via. Il messaggio viene inviato ripetutamente finché non viene trovata una rotta.", - "routing_floodBroadcast": "Trasmissione attraverso ogni ripetitore", - "routing_editPath": "Percorso di modifica", - "routing_forgetPath": "Dimentica il percorso", + "routing_directNoHops": "Diretto — nessun salto tramite ripetitore", + "routing_noPathYet": "Nessun percorso ancora. Il prossimo messaggio userà il flood finché non viene scoperto un percorso.", + "routing_floodBroadcast": "Trasmissione tramite ogni ripetitore", + "routing_editPath": "Modifica percorso", + "routing_forgetPath": "Dimentica percorso", "routing_knownPaths": "Percorsi noti", - "routing_knownPathsHint": "Seleziona un percorso per accedere a questa opzione.", + "routing_knownPathsHint": "Tocca un percorso per passare a quello.", "routing_inUse": "In uso", - "routing_qualityStrong": "Primo salto molto deciso", - "routing_qualityGood": "Primo tentativo di successo", - "routing_qualityFair": "Primo salto di qualità", - "routing_qualityWorked": "È stato consegnato", - "routing_qualityFlood": "Ho sentito tramite un messaggio urgente", + "routing_qualityStrong": "Primo hop forte", + "routing_qualityGood": "Primo hop buono", + "routing_qualityFair": "Primo hop discreto", + "routing_qualityWorked": "Ha consegnato", + "routing_qualityFlood": "Ricevuto via flood", "routing_qualityUntested": "Non testato", "routing_neverWorked": "mai confermato", - "routing_floodDelivery": "Consegna in caso di alluvione", - "pathEditor_title": "Creare percorso", - "pathEditor_hopCounter": "{count} tra 64 varietà di luppolo", - "pathEditor_noHops": "Al momento non ci sono ingredienti aggiuntivi. Per aggiungerli nell'ordine desiderato, cliccate sui ripetitori sottostanti. In alternativa, potete salvare la ricetta senza ingredienti aggiuntivi per inviarla direttamente.", - "pathEditor_addHops": "Aggiungere i luppoli nell'ordine desiderato.", - "pathEditor_searchRepeaters": "Ricerca ripetitori", + "routing_floodDelivery": "Consegna flood", + "pathEditor_title": "Crea percorso", + "pathEditor_hopCounter": "{count} su 64 salti", + "pathEditor_noHops": "Nessun salto ancora. Tocca i ripetitori qui sotto per aggiungerli in ordine, oppure salva senza salti per inviare direttamente.", + "pathEditor_addHops": "Aggiungi salti in ordine", + "pathEditor_searchRepeaters": "Cerca ripetitori", "pathEditor_advancedHex": "Avanzato: percorso esadecimale grezzo", "pathEditor_hexLabel": "Prefissi esadecimali", "pathEditor_hexHelper": "Due caratteri esadecimali per ogni salto, separati da virgole.", "pathEditor_invalidTokens": "Non valido: {tokens}", - "routing_lastWorked": "worked {when}", - "pathEditor_tooManyHops": "Massimo 64 orari", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", + "routing_lastWorked": "ha funzionato {when}", + "pathEditor_tooManyHops": "Massimo 64 salti", + "routing_deliveryCounts": "{successes} consegnati, {failures} falliti", "pathEditor_usePath": "Utilizza questo percorso", - "pathEditor_removeHop": "Rimuovere il luppolo", + "pathEditor_removeHop": "Rimuovi salto", "pathEditor_unknownHop": "Ripetitore sconosciuto", "map_zoomIn": "Ingrandisci", "map_zoomOut": "Riduci la visualizzazione", - "map_centerMap": "Mappa del centro", + "map_centerMap": "Centra mappa", "channels_communityShortId": "ID: {id}...", "chrome_bluetoothRequiresChromium": "Web Bluetooth richiede un browser basato su Chromium.", "pathTrace_legendGpsConfirmed": "Il GPS conferma", - "pathTrace_legendInferred": "Posizione dedotta" + "pathTrace_legendInferred": "Posizione dedotta", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_searchHint": "Cerca il nome o l'ID del nodo", + "scanner_bluetoothWebUnsupported": "La funzionalità Bluetooth non è disponibile nel browser. Connettetevi tramite USB invece.", + "map_online": "Online", + "map_activity": "Attività", + "map_recent": "Recente", + "map_stale": "Obsoleto", + "map_visible": "Visibile", + "map_hidden": "Nascosto", + "map_centerOnNode": "Centra sul nodo", + "map_details": "Dettagli", + "map_noGps": "Senza GPS", + "map_noResults": "Nessun nodo corrispondente trovato.", + "pathMap_viewSingle": "Singola", + "pathMap_viewCombined": "Combinata", + "pathMap_play": "Riproduci", + "pathMap_pause": "Pausa", + "pathMap_replay": "Ripeti", + "pathMap_stepBack": "Salto precedente", + "pathMap_stepForward": "Salto successivo", + "pathMap_animationOn": "Mostra l'animazione del pacchetto", + "pathMap_animationOff": "Nascondi l'animazione del pacchetto", + "pathMap_hopOf": "Salto {current} di {total}", + "pathMap_observedPaths": "Percorsi osservati: {count}", + "pathMap_primary": "Primario", + "pathMap_alternate": "Alternativo {index}", + "pathMap_hopCount": "{count, plural, =1{1 salto} other{{count} salti}}", + "pathMap_legendShared": "Segmento condiviso", + "pathMap_legendEstimated": "Segmento stimato", + "pathMap_sharedNodeCount": "Utilizzato da {count} percorsi", + "pathMap_partialAnimation": "{count, plural, =1{1 salto non ha una posizione specifica — il percorso mostrato è parziale} other{{count} salti non hanno una posizione specifica — il percorso mostrato è parziale}}", + "pathMap_showAllPaths": "Mostra tutti", + "pathMap_hidePath": "Nascondi percorso", + "pathMap_showPath": "Mostra percorso", + "pathMap_collapsePanel": "Comprimi pannello", + "pathMap_expandPanel": "Espandi pannello", + "pathMap_noLocation": "Nessuna posizione", + "pathMap_followPacket": "Blocca la vista sul pacchetto", + "pathMap_unfollowPacket": "Sblocca la vista dal pacchetto", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 4afea945..1c8b728f 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -5,8 +5,8 @@ "nav_channels": "チャンネル", "nav_map": "地図", "common_cancel": "キャンセル", - "common_ok": "了解", - "common_connect": "接続する", + "common_ok": "OK", + "common_connect": "接続", "common_unknownDevice": "不明なデバイス", "common_save": "保存", "common_delete": "削除", @@ -15,18 +15,24 @@ "common_edit": "編集", "common_add": "追加", "common_settings": "設定", - "common_disconnect": "切断する", - "common_connected": "接続されている", - "common_disconnected": "切断", - "common_create": "作成する", - "common_continue": "続き", - "common_share": "共有する", + "common_disconnect": "切断", + "common_connected": "接続済み", + "common_disconnected": "切断済み", + "common_create": "作成", + "common_continue": "続行", + "common_share": "共有", "common_copy": "コピー", - "common_retry": "再試", + "common_retry": "再試行", "common_hide": "隠す", "common_remove": "削除", - "common_enable": "有効化する", - "common_disable": "無効化する", + "common_enable": "有効にする", + "common_disable": "無効にする", + "common_undo": "元に戻す", + "messageStatus_sent": "送信済み", + "messageStatus_delivered": "配信済み", + "messageStatus_pending": "送信中", + "messageStatus_failed": "送信に失敗", + "messageStatus_repeated": "重複受信", "common_autoRefresh": "自動更新", "common_interval": "間隔", "common_reboot": "再起動", @@ -48,16 +54,16 @@ } } }, - "scanner_title": "MeshCore オープン", + "scanner_title": "MeshCore Open", "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "ブルートゥース", - "connectionChoiceTcpLabel": "TCP (Transmission Control Protocol)", - "tcpScreenTitle": "TCP を使用して接続", - "tcpHostLabel": "IPアドレス", - "tcpHostHint": "192.168.40.10", - "tcpPortLabel": "港", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceTcpLabel": "TCP", + "tcpScreenTitle": "TCP で接続", + "tcpHostLabel": "接続先", + "tcpHostHint": "192.168.40.10 / example.com", + "tcpPortLabel": "ポート", "tcpPortHint": "5000", - "tcpStatus_notConnected": "エンドポイントを入力し、接続する", + "tcpStatus_notConnected": "接続先を入力して接続してください", "tcpStatus_connectingTo": "{endpoint} への接続中...", "@tcpStatus_connectingTo": { "placeholders": { @@ -66,7 +72,7 @@ } } }, - "tcpErrorHostRequired": "IPアドレスが必要です。", + "tcpErrorHostRequired": "接続先は必須です。", "tcpErrorPortInvalid": "ポート番号は1から65535の範囲で指定してください。", "tcpErrorUnsupported": "このプラットフォームでは、TCP 転送はサポートされていません。", "tcpErrorTimedOut": "TCP 接続がタイムアウトしました。", @@ -79,24 +85,24 @@ } }, "usbScreenTitle": "USB経由で接続", - "usbScreenSubtitle": "検出されたシリアルデバイスを選択し、MeshCoreノードに直接接続してください。", - "usbScreenStatus": "USBデバイスを選択する", - "usbScreenNote": "USBシリアルポートは、サポートされているAndroidデバイスおよびデスクトッププラットフォームで利用可能です。", - "usbScreenEmptyState": "USBデバイスが見つかりませんでした。「別のUSBデバイスを接続して、再度確認してください。」", + "usbScreenSubtitle": "検出されたシリアルデバイスを選択して、MeshCore ノードに直接接続します。", + "usbScreenStatus": "USB デバイスを選択してください", + "usbScreenNote": "USB シリアルは、対応する Android デバイスとデスクトップ プラットフォームで利用できます。", + "usbScreenEmptyState": "USB デバイスが見つかりません。接続してから再度更新してください。", "usbErrorPermissionDenied": "USBへのアクセス許可が拒否されました。", "usbErrorDeviceMissing": "選択されたUSBデバイスは、もう利用できません。", "usbErrorInvalidPort": "有効なUSBデバイスを選択してください。", - "usbErrorBusy": "別のUSB接続の要求がすでに処理中です。", + "usbErrorBusy": "別の USB 接続要求がすでに進行中です。", "usbErrorNotConnected": "USBデバイスは接続されていません。", "usbErrorOpenFailed": "選択したUSBデバイスを開くことができません。", "usbErrorConnectFailed": "選択したUSBデバイスへの接続に失敗しました。", "usbErrorUnsupported": "このプラットフォームでは、USBシリアル通信はサポートされていません。", - "usbErrorAlreadyActive": "USB接続はすでに確立されています。", + "usbErrorAlreadyActive": "USB 接続はすでにアクティブです。", "usbErrorNoDeviceSelected": "USBデバイスは選択されていません。", - "usbErrorPortClosed": "USB接続は確立されていません。", + "usbErrorPortClosed": "USB 接続は開かれていません。", "usbErrorConnectTimedOut": "接続がタイムアウトしました。デバイスにUSBコンパニオンファームウェアがインストールされていることを確認してください。", "usbFallbackDeviceName": "ウェブシリアルデバイス", - "usbStatus_notConnected": "USBデバイスを選択する", + "usbStatus_notConnected": "USB デバイスを選択してください", "usbStatus_connecting": "USBデバイスへの接続中...", "usbStatus_searching": "USBデバイスを検索中...", "usbConnectionFailed": "USB接続に失敗しました:{error}", @@ -111,7 +117,7 @@ "scanner_connecting": "接続中...", "scanner_disconnecting": "切断...", "scanner_notConnected": "接続されていない", - "scanner_connectedTo": "{deviceName} に接続", + "scanner_connectedTo": "{deviceName} に接続済み", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -119,8 +125,8 @@ } } }, - "scanner_searchingDevices": "MeshCoreデバイスの検索", - "scanner_tapToScan": "MeshCore デバイスを検索するには、「スキャン」ボタンをタップしてください。", + "scanner_searchingDevices": "MeshCore デバイスを検索中...", + "scanner_tapToScan": "MeshCore デバイスを見つけるには、「スキャン」をタップしてください。", "scanner_connectionFailed": "接続に失敗しました:{error}", "@scanner_connectionFailed": { "placeholders": { @@ -131,13 +137,13 @@ }, "scanner_stop": "停止", "scanner_scan": "スキャン", - "scanner_bluetoothOff": "Bluetooth はオフになっています", - "scanner_bluetoothOffMessage": "Bluetoothを有効にして、デバイスを検索してください。", + "scanner_bluetoothOff": "Bluetooth はオフです", + "scanner_bluetoothOffMessage": "Bluetooth を有効にしてデバイスを検索してください。", "scanner_chromeRequired": "Chrome ブラウザが必須です", "scanner_chromeRequiredMessage": "このWebアプリケーションは、Bluetooth機能を利用するために、Google ChromeまたはChromiumベースのブラウザが必要です。", - "scanner_enableBluetooth": "Bluetoothを有効にする", - "device_quickSwitch": "素早い切り替え", - "device_meshcore": "メッシュコア", + "scanner_enableBluetooth": "Bluetooth を有効にする", + "device_quickSwitch": "クイックスイッチ", + "device_meshcore": "MeshCore", "settings_title": "設定", "settings_deviceInfo": "デバイス情報", "settings_appSettings": "アプリ設定", @@ -146,7 +152,7 @@ "settings_nodeName": "ノード名", "settings_nodeNameNotSet": "設定されていない", "settings_nodeNameHint": "ノード名を入力してください", - "settings_nodeNameUpdated": "氏名変更", + "settings_nodeNameUpdated": "名前を更新しました", "settings_radioSettings": "ラジオ設定", "settings_radioSettingsSubtitle": "周波数、電力、スプレッドファクター", "settings_radioSettingsUpdated": "ラジオの設定が更新されました", @@ -155,40 +161,40 @@ "settings_locationUpdated": "場所とGPS設定が更新されました", "settings_locationBothRequired": "緯度と経度をそれぞれ入力してください。", "settings_locationInvalid": "無効な緯度または経度。", - "settings_locationGPSEnable": "GPS機能有効", - "settings_locationGPSEnableSubtitle": "GPSが自動的に位置情報を更新できるようにする。", - "settings_locationIntervalSec": "GPS データの取得間隔(秒)", + "settings_locationGPSEnable": "GPS を有効にする", + "settings_locationGPSEnableSubtitle": "GPS が自動的に位置情報を更新できるようにします。", + "settings_locationIntervalSec": "GPS 更新間隔(秒)", "settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。", "settings_latitude": "緯度", "settings_longitude": "経度", - "settings_contactSettings": "連絡設定", - "settings_contactSettingsSubtitle": "連絡先を追加する設定", + "settings_contactSettings": "連絡先設定", + "settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定", "settings_privacyMode": "プライバシーモード", - "settings_privacyModeSubtitle": "広告に名前/場所を記載しない", - "settings_privacyModeToggle": "プライバシーモードをオンにして、広告に表示される名前や場所を非表示にします。", + "settings_privacyModeSubtitle": "広告に名前や位置を表示しない", + "settings_privacyModeToggle": "プライバシーモードを有効にすると、広告に表示される名前と位置を非表示にします。", "settings_privacyModeEnabled": "プライバシーモードが有効になっています", - "settings_privacyModeDisabled": "プライバシーモードは無効化されています", - "settings_actions": "行動", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", - "settings_sendAdvertisement": "広告を送信する", - "settings_sendAdvertisementSubtitle": "現在、放送での活動", - "settings_advertisementSent": "広告が送信されました", - "settings_syncTime": "同期時間", - "settings_syncTimeSubtitle": "デバイスの時刻を、携帯電話の時刻に合わせる", - "settings_timeSynchronized": "時間同期", - "settings_refreshContacts": "連絡先を更新する", - "settings_refreshContactsSubtitle": "デバイスから連絡先リストを再読み込みする", - "settings_rebootDevice": "デバイスを再起動する", - "settings_rebootDeviceSubtitle": "MeshCore デバイスを再起動する", + "settings_privacyModeDisabled": "プライバシーモードは無効です", + "settings_actions": "操作", + "settings_deleteAllPaths": "すべての経路を削除", + "settings_deleteAllPathsSubtitle": "連絡先からすべての経路データを消去します。", + "settings_sendAdvertisement": "広告を送信", + "settings_sendAdvertisementSubtitle": "現在の存在を送信します", + "settings_advertisementSent": "広告を送信しました", + "settings_syncTime": "時刻を同期", + "settings_syncTimeSubtitle": "デバイスの時刻をスマートフォンに合わせます", + "settings_timeSynchronized": "時刻を同期しました", + "settings_refreshContacts": "連絡先を更新", + "settings_refreshContactsSubtitle": "デバイスから連絡先リストを再読み込みします", + "settings_rebootDevice": "デバイスを再起動", + "settings_rebootDeviceSubtitle": "MeshCore デバイスを再起動します", "settings_rebootDeviceConfirm": "本当にデバイスを再起動したいですか? その場合、接続が切断されます。", "settings_debug": "デバッグ", "settings_bleDebugLog": "BLE デバッグログ", - "settings_bleDebugLogSubtitle": "BLEコマンド、応答、および生のデータ", - "settings_appDebugLog": "アプリケーションのデバッグログ", + "settings_bleDebugLogSubtitle": "BLE コマンド、応答、および生データ", + "settings_appDebugLog": "アプリのデバッグログ", "settings_appDebugLogSubtitle": "アプリケーションのデバッグメッセージ", "settings_about": "概要", - "settings_aboutVersion": "MeshCore Open {version}版", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -196,8 +202,8 @@ } } }, - "settings_aboutLegalese": "2026年のMeshCoreオープンソースプロジェクト", - "settings_aboutDescription": "MeshCore LoRaメッシュネットワークデバイス用の、オープンソースのFlutterクライアント。", + "settings_aboutLegalese": "2026 MeshCore オープンソースプロジェクト", + "settings_aboutDescription": "MeshCore LoRa メッシュネットワークデバイス向けのオープンソース Flutter クライアント。", "settings_aboutOpenMeteoAttribution": "LOS 標高データ:Open-Meteo (CC BY 4.0)", "settings_infoName": "名前", "settings_infoId": "ID", @@ -216,10 +222,10 @@ "settings_txPower": "TX 信号電力 (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "無効な送信電力 (0-22 dBm)", - "settings_clientRepeat": "オフグリッドリピータ", - "settings_clientRepeatSubtitle": "このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。", - "settings_clientRepeatFreqWarning": "オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。", - "settings_error": "エラー:{message}", + "settings_clientRepeat": "オフグリッド中継", + "settings_clientRepeatSubtitle": "このデバイスが他のデバイス向けにメッシュパケットを中継できるようにします。", + "settings_clientRepeatFreqWarning": "オフグリッド中継には 433、869、または 918 MHz の周波数が必要です。", + "settings_error": "エラー: {message}", "@settings_error": { "placeholders": { "message": { @@ -230,11 +236,11 @@ "appSettings_title": "アプリ設定", "appSettings_appearance": "外観", "appSettings_theme": "テーマ", - "appSettings_themeSystem": "システムデフォルト", - "appSettings_themeLight": "光", - "appSettings_themeDark": "暗い", + "appSettings_themeSystem": "システム設定", + "appSettings_themeLight": "ライト", + "appSettings_themeDark": "ダーク", "appSettings_language": "言語", - "appSettings_languageSystem": "システムデフォルト", + "appSettings_languageSystem": "システム設定", "appSettings_languageEn": "英語", "appSettings_languageFr": "フランス語", "appSettings_languageEs": "スペイン語", @@ -250,11 +256,11 @@ "appSettings_languageBg": "ブルガリア語", "appSettings_languageRu": "ロシア語", "appSettings_languageUk": "ウクライナ語", - "appSettings_enableMessageTracing": "メッセージ追跡機能を有効にする", - "appSettings_enableMessageTracingSubtitle": "メッセージに関する詳細な経路およびタイミングに関するメタデータを表示する", + "appSettings_enableMessageTracing": "メッセージ追跡を有効にする", + "appSettings_enableMessageTracingSubtitle": "メッセージの詳細な経路とタイミングのメタデータを表示します", "appSettings_notifications": "通知", "appSettings_enableNotifications": "通知を有効にする", - "appSettings_enableNotificationsSubtitle": "メッセージや広告に関する通知を受け取る", + "appSettings_enableNotificationsSubtitle": "メッセージや広告の通知を受け取ります", "appSettings_notificationPermissionDenied": "通知の許可が拒否されました", "appSettings_notificationsEnabled": "通知機能が有効になっています", "appSettings_notificationsDisabled": "通知が無効化されています", @@ -265,24 +271,24 @@ "appSettings_advertisementNotifications": "広告通知", "appSettings_advertisementNotificationsSubtitle": "新しいノードが発見された場合に通知を表示する", "appSettings_messaging": "メッセージング", - "appSettings_clearPathOnMaxRetry": "マックスリトライでの明確な手順", - "appSettings_clearPathOnMaxRetrySubtitle": "5回送信に失敗した場合、連絡経路をリセットする", - "appSettings_pathsWillBeCleared": "5回失敗した後、経路が再開されます。", - "appSettings_pathsWillNotBeCleared": "パスは自動で削除されません。", - "appSettings_autoRouteRotation": "自動ルートの切り替え", - "appSettings_autoRouteRotationSubtitle": "最適なルートと、フラッドモードを切り替える", - "appSettings_autoRouteRotationEnabled": "自動ルートの切り替え機能が有効になっています", - "appSettings_autoRouteRotationDisabled": "自動ルートの変更機能が無効になっています。", - "appSettings_maxRouteWeight": "最大ルート重量", - "appSettings_maxRouteWeightSubtitle": "ある経路が、成功裏に配送された場合に、積み上げられる最大重量", - "appSettings_initialRouteWeight": "初期ルートの重み", - "appSettings_initialRouteWeightSubtitle": "新たに発見された経路の初期重量", + "appSettings_clearPathOnMaxRetry": "最大再試行時に経路を消去", + "appSettings_clearPathOnMaxRetrySubtitle": "5 回送信に失敗したら連絡先の経路をリセットします", + "appSettings_pathsWillBeCleared": "5 回失敗すると経路を消去します", + "appSettings_pathsWillNotBeCleared": "経路は自動では消去されません", + "appSettings_autoRouteRotation": "自動経路切り替え", + "appSettings_autoRouteRotationSubtitle": "最適な経路とフラッドモードを切り替えます", + "appSettings_autoRouteRotationEnabled": "自動経路切り替えが有効です", + "appSettings_autoRouteRotationDisabled": "自動経路切り替えは無効です", + "appSettings_maxRouteWeight": "最大経路重み", + "appSettings_maxRouteWeightSubtitle": "成功配送によって経路に蓄積できる最大重み", + "appSettings_initialRouteWeight": "初期経路重み", + "appSettings_initialRouteWeightSubtitle": "新しく発見された経路の初期重み", "appSettings_routeWeightSuccessIncrement": "成功時の重み増加", - "appSettings_routeWeightSuccessIncrementSubtitle": "配送が成功した場合に、経路に追加される重量", + "appSettings_routeWeightSuccessIncrementSubtitle": "成功配送後に経路へ加算する重み", "appSettings_routeWeightFailureDecrement": "失敗時の重み減少", - "appSettings_routeWeightFailureDecrementSubtitle": "配送に失敗した際に、経路から取り除かれた重量", + "appSettings_routeWeightFailureDecrementSubtitle": "失敗配送後に経路から差し引く重み", "appSettings_maxMessageRetries": "最大メッセージ再試行回数", - "appSettings_maxMessageRetriesSubtitle": "メッセージを「失敗」とマークするまでの、再試行回数", + "appSettings_maxMessageRetriesSubtitle": "メッセージを失敗として扱うまでの再試行回数", "path_routeWeight": "{weight}/{max}", "@path_routeWeight": { "placeholders": { @@ -295,8 +301,8 @@ } }, "appSettings_battery": "バッテリー", - "appSettings_batteryChemistry": "電池の化学", - "appSettings_batteryChemistryPerDevice": "{deviceName} 単位", + "appSettings_batteryChemistry": "バッテリー種別", + "appSettings_batteryChemistryPerDevice": "{deviceName} ごとに設定", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -304,20 +310,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "デバイスを選択するために接続する", + "appSettings_batteryChemistryConnectFirst": "選択するにはデバイスに接続してください", "appSettings_batteryNmc": "18650型 NMC (3.0-4.2V)", "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)", - "appSettings_mapDisplay": "地図の表示", - "appSettings_showRepeaters": "繰り返し再生機能", + "appSettings_mapDisplay": "地図表示", + "appSettings_showRepeaters": "リピータを表示", "appSettings_showRepeatersSubtitle": "地図上にリピータノードを表示する", - "appSettings_showChatNodes": "チャットノードの表示", + "appSettings_showChatNodes": "チャットノードを表示", "appSettings_showChatNodesSubtitle": "地図上にチャットノードを表示する", - "appSettings_showOtherNodes": "他のノードを表示する", + "appSettings_showOtherNodes": "その他のノードを表示", "appSettings_showOtherNodesSubtitle": "地図上に、他のノードの種類を表示する", "appSettings_timeFilter": "時間フィルター", - "appSettings_timeFilterShowAll": "すべてのノードを表示する", - "appSettings_timeFilterShowLast": "過去 {hours} 時間のノードを表示する", + "appSettings_timeFilterShowAll": "すべてのノードを表示", + "appSettings_timeFilterShowLast": "過去 {hours} 時間以内に発見されたノードを表示", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -325,19 +331,19 @@ } } }, - "appSettings_mapTimeFilter": "地図の表示期間を絞り込む", - "appSettings_showNodesDiscoveredWithin": "以下の範囲内で発見されたノードを表示する:", - "appSettings_allTime": "すべての期間", - "appSettings_lastHour": "直前の", + "appSettings_mapTimeFilter": "地図の時間フィルター", + "appSettings_showNodesDiscoveredWithin": "次の期間内に発見されたノードを表示:", + "appSettings_allTime": "全期間", + "appSettings_lastHour": "過去1時間", "appSettings_last6Hours": "過去6時間", "appSettings_last24Hours": "過去24時間", - "appSettings_lastWeek": "先週", - "appSettings_offlineMapCache": "オフライン用地図キャッシュ", + "appSettings_lastWeek": "過去1週間", + "appSettings_offlineMapCache": "オフライン地図キャッシュ", "appSettings_unitsTitle": "単位", - "appSettings_unitsMetric": "メートル (m) / キロメートル (km)", - "appSettings_unitsImperial": "帝国 (フィート / マイル)", - "appSettings_noAreaSelected": "選択されたエリアはありません", - "appSettings_areaSelectedZoom": "選択された範囲(ズームレベル:{minZoom}~{maxZoom})", + "appSettings_unitsMetric": "メートル法 (m / km)", + "appSettings_unitsImperial": "ヤード・ポンド法 (ft / mi)", + "appSettings_noAreaSelected": "エリアが選択されていません", + "appSettings_areaSelectedZoom": "エリア選択中(ズーム {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -349,16 +355,16 @@ } }, "appSettings_debugCard": "デバッグ", - "appSettings_appDebugLogging": "アプリケーションのデバッグ用ログ", - "appSettings_appDebugLoggingSubtitle": "ログアプリのデバッグメッセージ(トラブルシューティング用)", - "appSettings_appDebugLoggingEnabled": "アプリケーションのデバッグ用ログ機能が有効になっています。", - "appSettings_appDebugLoggingDisabled": "アプリケーションのデバッグログが無効化されています。", + "appSettings_appDebugLogging": "アプリのデバッグログ", + "appSettings_appDebugLoggingSubtitle": "トラブルシューティング用にアプリのデバッグメッセージを記録します", + "appSettings_appDebugLoggingEnabled": "アプリのデバッグログは有効です", + "appSettings_appDebugLoggingDisabled": "アプリのデバッグログは無効です", "contacts_title": "連絡先", - "contacts_noContacts": "現時点では、連絡先はまだありません。", - "contacts_contactsWillAppear": "デバイスが広告を行う際に、連絡先が表示されます。", + "contacts_noContacts": "まだ連絡先はありません", + "contacts_contactsWillAppear": "デバイスが広告を送信すると連絡先が表示されます", "contacts_unread": "未読", "contacts_searchContactsNoNumber": "連絡先を検索...", - "contacts_searchContacts": "{number}件の{str}に関する連絡先を検索...", + "contacts_searchContacts": "連絡先を検索... {number} 件の {str}", "@contacts_searchContacts": { "placeholders": { "number": { @@ -369,7 +375,7 @@ } } }, - "contacts_searchFavorites": "{number}件の{str}を検索...", + "contacts_searchFavorites": "お気に入りを検索... {number} 件の {str}", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -380,7 +386,7 @@ } } }, - "contacts_searchUsers": "{number}件の{str}に関するユーザーを検索する...", + "contacts_searchUsers": "ユーザーを検索... {number} 件の {str}", "@contacts_searchUsers": { "placeholders": { "number": { @@ -391,7 +397,7 @@ } } }, - "contacts_searchRepeaters": "{number} {str} までの検索...", + "contacts_searchRepeaters": "リピータを検索... {number} 件の {str}", "@contacts_searchRepeaters": { "placeholders": { "number": { @@ -402,7 +408,7 @@ } } }, - "contacts_searchRoomServers": "{number} {str} 部屋のサーバーを検索する...", + "contacts_searchRoomServers": "ルームサーバーを検索... {number} 件の {str}", "@contacts_searchRoomServers": { "placeholders": { "number": { @@ -425,10 +431,10 @@ } }, "contacts_manageRepeater": "リピータの管理", - "contacts_manageRoom": "ルームサーバーの管理", - "contacts_roomLogin": "ルームサーバーへのログイン", - "contacts_openChat": "自由な会話", - "contacts_editGroup": "編集グループ", + "contacts_manageRoom": "ルームサーバーを管理", + "contacts_roomLogin": "ルームサーバーログイン", + "contacts_openChat": "チャットを開く", + "contacts_editGroup": "グループを編集", "contacts_deleteGroup": "グループを削除", "contacts_deleteGroupConfirm": "{groupName} を削除しますか?", "@contacts_deleteGroupConfirm": { @@ -441,8 +447,8 @@ "contacts_newGroup": "新しいグループ", "contacts_groupName": "グループ名", "contacts_groupNameRequired": "グループ名が必須です", - "contacts_groupNameReserved": "このグループ名はすでに使用されています。", - "contacts_groupAlreadyExists": "グループ「{name}」はすでに存在しています", + "contacts_groupNameReserved": "このグループ名は予約済みです", + "contacts_groupAlreadyExists": "グループ「{name}」はすでに存在します", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -450,11 +456,11 @@ } } }, - "contacts_filterContacts": "連絡先をフィルタリングする…", - "contacts_noContactsMatchFilter": "指定された条件に合致する連絡先は見つかりませんでした。", - "contacts_noMembers": "メンバーはいない", + "contacts_filterContacts": "連絡先をフィルタ...", + "contacts_noContactsMatchFilter": "条件に一致する連絡先はありません", + "contacts_noMembers": "メンバーなし", "contacts_lastSeenNow": "最近", - "contacts_lastSeenMinsAgo": "~{minutes} 分", + "contacts_lastSeenMinsAgo": "約 {minutes} 分前", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -462,8 +468,8 @@ } } }, - "contacts_lastSeenHourAgo": "約1時間", - "contacts_lastSeenHoursAgo": "~ {hours} 時間", + "contacts_lastSeenHourAgo": "約 1 時間前", + "contacts_lastSeenHoursAgo": "約 {hours} 時間前", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -471,8 +477,8 @@ } } }, - "contacts_lastSeenDayAgo": "~1日", - "contacts_lastSeenDaysAgo": "~{days}日間", + "contacts_lastSeenDayAgo": "約 1 日前", + "contacts_lastSeenDaysAgo": "約 {days} 日前", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -482,8 +488,8 @@ }, "channels_title": "チャンネル", "channels_noChannelsConfigured": "設定されたチャンネルがありません", - "channels_addPublicChannel": "パブリックチャンネルを追加する", - "channels_searchChannels": "検索オプション...", + "channels_addPublicChannel": "公開チャンネルを追加", + "channels_searchChannels": "チャンネルを検索...", "channels_noChannelsFound": "チャンネルが見つかりませんでした", "channels_channelIndex": "チャンネル {index}", "@channels_channelIndex": { @@ -493,12 +499,12 @@ } } }, - "channels_public": "一般の人々", - "channels_private": "個人の", - "channels_editChannel": "チャンネルを編集する", - "channels_muteChannel": "ミュート機能", - "channels_unmuteChannel": "ミュートを解除する", - "channels_deleteChannel": "チャンネルを削除する", + "channels_public": "公開", + "channels_private": "非公開", + "channels_editChannel": "チャンネルを編集", + "channels_muteChannel": "チャンネルをミュート", + "channels_unmuteChannel": "チャンネルのミュートを解除", + "channels_deleteChannel": "チャンネルを削除", "channels_deleteChannelConfirm": "{name} を削除しますか? これは取り消すことができません。", "@channels_deleteChannelConfirm": { "placeholders": { @@ -527,12 +533,12 @@ "channels_channelIndexLabel": "チャンネルインデックス", "channels_channelName": "チャンネル名", "channels_usePublicChannel": "パブリックチャンネルを使用する", - "channels_standardPublicPsk": "標準的な公用 PSK", - "channels_pskHex": "PSK (ヘックス)", - "channels_generateRandomPsk": "ランダムなPSK(正交符号分割変調)を生成する", + "channels_standardPublicPsk": "公開用の標準 PSK", + "channels_pskHex": "PSK(16 進数)", + "channels_generateRandomPsk": "ランダムな PSK を生成", "channels_enterChannelName": "チャンネル名を入力してください", "channels_pskMustBe32Hex": "PSKは32桁の16進数で構成されている必要があります。", - "channels_channelAdded": "チャンネル「{name}」を追加", + "channels_channelAdded": "チャンネル「{name}」を追加しました", "@channels_channelAdded": { "placeholders": { "name": { @@ -551,20 +557,32 @@ "channels_smazCompression": "SMAZ 圧縮", "channels_cyr2latCompression": "Cyr2Lat 圧縮", "channels_cyr2latCompressionDscr": "送信時に一部のキリル文字をラテン文字に置き換えます。", - "channels_cyr2latSettingsHeading": "cyr2latの設定", + "channels_cyr2latSettingsHeading": "Cyr2Lat 設定", "channels_cyr2latSettingsSubheading": "置換リスト", - "channels_cyr2latSettingsDscr": "文字置換のJSON設定を編集する", - "channels_cyr2latSettingsDialogHint": "JSON置換マップ", - "channels_cyr2latSettingsDialogWrongJSON": "不正なJSON: {error}", - "settings_cyr2latProfileAdd": "Cyr2Latプロファイルの追加", + "channels_cyr2latSettingsDscr": "文字置換の JSON 設定を編集します", + "channels_cyr2latSettingsDialogHint": "JSON 置換マップ", + "channels_cyr2latSettingsDialogWrongJSON": "無効な JSON: {error}", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, + "settings_cyr2latProfileAdd": "Cyr2Lat プロファイルを追加", "settings_cyr2latProfileName": "プロファイル名", "settings_cyr2latProfileNameEmpty": "プロファイル名は空にできません", - "settings_cyr2latProfileAdded": "プロファイルが正常に追加されました", - "settings_cyr2latProfileUpdated": "プロファイルの更新に成功しました", - "settings_cyr2latProfileEdit": "Cyr2Latプロファイルを編集", - "settings_cyr2latProfileDelete": "Cyr2Latプロファイルを削除", - "settings_cyr2latProfileDeleted": "プロファイルの削除に成功しました", - "settings_cyr2latProfileDeleteDscr": "プロファイル \"{name}\" を削除してもよろしいですか?", + "settings_cyr2latProfileAdded": "プロファイルを追加しました", + "settings_cyr2latProfileUpdated": "プロファイルを更新しました", + "settings_cyr2latProfileEdit": "Cyr2Lat プロファイルを編集", + "settings_cyr2latProfileDelete": "Cyr2Lat プロファイルを削除", + "settings_cyr2latProfileDeleted": "プロファイルを削除しました", + "settings_cyr2latProfileDeleteDscr": "プロファイル「{name}」を削除しますか?", + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelUpdated": "チャンネル「{name}」が更新されました", "@channels_channelUpdated": { "placeholders": { @@ -573,18 +591,18 @@ } } }, - "channels_publicChannelAdded": "パブリックチャンネルが追加されました", + "channels_publicChannelAdded": "公開チャンネルを追加しました", "channels_sortBy": "並び替え", - "channels_sortManual": "マニュアル", + "channels_sortManual": "手動", "channels_sortAZ": "AからZ", "channels_sortLatestMessages": "最新のメッセージ", "channels_sortUnread": "未読", "channels_createPrivateChannel": "プライベートチャンネルを作成する", - "channels_createPrivateChannelDesc": "秘密鍵を使用して保護されています。", + "channels_createPrivateChannelDesc": "秘密鍵で保護されます。", "channels_joinPrivateChannel": "プライベートチャンネルに参加する", - "channels_joinPrivateChannelDesc": "手動で秘密のキーを入力する。", + "channels_joinPrivateChannelDesc": "秘密鍵を手動で入力します。", "channels_joinPublicChannel": "公開チャンネルに参加する", - "channels_joinPublicChannelDesc": "このチャンネルには、誰でも参加できます。", + "channels_joinPublicChannelDesc": "誰でもこのチャンネルに参加できます。", "channels_joinHashtagChannel": "ハッシュタグチャンネルに参加する", "channels_joinHashtagChannelDesc": "誰でもハッシュタグチャンネルに参加できます。", "channels_scanQrCode": "QRコードをスキャンする", @@ -620,7 +638,7 @@ } }, "chat_typeMessage": "メッセージを入力してください…", - "chat_messageTooLong": "メッセージが長すぎる({maxBytes} バイトを超える)。", + "chat_messageTooLong": "メッセージが長すぎます({maxBytes} バイトを超えています)", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -630,8 +648,8 @@ }, "chat_messageCopied": "メッセージがコピーされました", "chat_messageDeleted": "メッセージは削除されました", - "chat_retryingMessage": "再試行メッセージ", - "chat_retryCount": "{current} / {max} 回目", + "chat_retryingMessage": "メッセージを再試行中", + "chat_retryCount": "{current}/{max} 回目", "@chat_retryCount": { "placeholders": { "current": { @@ -642,32 +660,32 @@ } } }, - "chat_sendGif": "GIFを送信する", + "chat_sendGif": "GIF を送信", "chat_reply": "返信", - "chat_addReaction": "反応を追加", + "chat_addReaction": "リアクションを追加", "chat_me": "私", - "emojiCategorySmileys": "笑顔の絵文字", - "emojiCategoryGestures": "身振り、動作", - "emojiCategoryHearts": "心", - "emojiCategoryObjects": "対象物", - "gifPicker_title": "GIF を選択してください", - "gifPicker_searchHint": "GIFの検索...", - "gifPicker_poweredBy": "GIPHYによる提供", - "gifPicker_noGifsFound": "GIF形式のファイルは見つかりませんでした", - "gifPicker_failedLoad": "GIFファイルの読み込みに失敗しました", - "gifPicker_failedSearch": "GIFファイルの検索に失敗しました", - "gifPicker_noInternet": "インターネット接続なし", - "debugLog_appTitle": "アプリケーションのデバッグログ", + "emojiCategorySmileys": "顔文字", + "emojiCategoryGestures": "ジェスチャー", + "emojiCategoryHearts": "ハート", + "emojiCategoryObjects": "物", + "gifPicker_title": "GIF を選択", + "gifPicker_searchHint": "GIF を検索...", + "gifPicker_poweredBy": "GIPHY 提供", + "gifPicker_noGifsFound": "GIF が見つかりませんでした", + "gifPicker_failedLoad": "GIF の読み込みに失敗しました", + "gifPicker_failedSearch": "GIF の検索に失敗しました", + "gifPicker_noInternet": "インターネット接続がありません", + "debugLog_appTitle": "アプリのデバッグログ", "debugLog_bleTitle": "BLE デバッグログ", - "debugLog_copyLog": "記録", - "debugLog_clearLog": "詳細なログ", - "debugLog_copied": "デバッグログをコピー", - "debugLog_bleCopied": "BLEログのコピー", - "debugLog_noEntries": "デバッグログはまだ生成されていません", - "debugLog_enableInSettings": "アプリのデバッグログを有効にするには、設定から操作してください。", + "debugLog_copyLog": "ログをコピー", + "debugLog_clearLog": "ログをクリア", + "debugLog_copied": "デバッグログをコピーしました", + "debugLog_bleCopied": "BLE ログをコピーしました", + "debugLog_noEntries": "まだデバッグログはありません", + "debugLog_enableInSettings": "設定でアプリのデバッグログを有効にしてください", "debugLog_frames": "フレーム", - "debugLog_rawLogRx": "生のログ-RX", - "debugLog_noBleActivity": "現時点では、BLE関連の活動は行われていません。", + "debugLog_rawLogRx": "生の Log-RX", + "debugLog_noBleActivity": "まだ BLE アクティビティはありません", "debugFrame_length": "フレーム長: {count} バイト", "@debugFrame_length": { "placeholders": { @@ -684,7 +702,7 @@ } } }, - "debugFrame_textMessageHeader": "テキストメッセージ用フレーム:", + "debugFrame_textMessageHeader": "テキストメッセージフレーム:", "debugFrame_destinationPubKey": "- 宛先公開鍵: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { @@ -720,9 +738,9 @@ } } }, - "debugFrame_textTypeCli": "CLI(コマンドラインインターフェース)", - "debugFrame_textTypePlain": "シンプルな", - "debugFrame_text": "- テキスト:「{text}」", + "debugFrame_textTypeCli": "CLI", + "debugFrame_textTypePlain": "プレーン", + "debugFrame_text": "- テキスト: \"{text}\"", "@debugFrame_text": { "placeholders": { "text": { @@ -730,15 +748,15 @@ } } }, - "debugFrame_hexDump": "ヘックスダンプ:", + "debugFrame_hexDump": "16 進ダンプ:", "chat_pathManagement": "経路管理", "chat_ShowAllPaths": "すべての経路を表示", "chat_routingMode": "ルーティングモード", - "chat_autoUseSavedPath": "自動 (保存されたパスを使用)", - "chat_forceFloodMode": "強制的にフラッドモードを起動", - "chat_recentAckPaths": "最近使用したACKパス(タップして使用):", - "chat_pathHistoryFull": "パスの履歴は完全です。エントリを削除して、新しいものを追加できます。", - "chat_hopSingular": "ジャンプ", + "chat_autoUseSavedPath": "自動(保存済みの経路を使用)", + "chat_forceFloodMode": "フラッドモードを強制", + "chat_recentAckPaths": "最近使った ACK 経路(タップして使用):", + "chat_pathHistoryFull": "経路履歴がいっぱいです。既存の項目を削除すると新しい項目を追加できます。", + "chat_hopSingular": "ホップ", "chat_hopPlural": "ホップ", "chat_hopsCount": "{count} {count, plural, =1{ホップ} other{ホップ}}", "@chat_hopsCount": { @@ -748,20 +766,20 @@ } } }, - "chat_successes": "成功事例", - "chat_removePath": "パスを削除する", - "chat_noPathHistoryYet": "まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。", - "chat_pathActions": "パスの操作:", - "chat_setCustomPath": "カスタムパスを設定", - "chat_setCustomPathSubtitle": "手動で経路を指定する", - "chat_clearPath": "明確な道", - "chat_clearPathSubtitle": "次回送信時に、以前の情報を再取得する", - "chat_pathCleared": "経路が確保されました。次のメッセージでルートを再確認します。", - "chat_floodModeSubtitle": "アプリのバーにあるルーティング切り替え機能を使用する", - "chat_floodModeEnabled": "フラッドモードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。", - "chat_fullPath": "フルパス", - "chat_pathDetailsNotAvailable": "経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。", - "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_successes": "成功回数", + "chat_removePath": "経路を削除", + "chat_noPathHistoryYet": "まだ経路履歴はありません。\n経路を見つけるにはメッセージを送信してください。", + "chat_pathActions": "経路の操作:", + "chat_setCustomPath": "カスタム経路を設定", + "chat_setCustomPathSubtitle": "手動で経路を指定します", + "chat_clearPath": "経路をクリア", + "chat_clearPathSubtitle": "次回送信時に前回の経路情報を再取得します", + "chat_pathCleared": "経路をクリアしました。次のメッセージで再探索します。", + "chat_floodModeSubtitle": "アプリのバーにあるルーティング切り替えを使用します", + "chat_floodModeEnabled": "フラッドモードが有効です。アプリのメニューバーにあるルートアイコンでモードを切り替えられます。", + "chat_fullPath": "完全な経路", + "chat_pathDetailsNotAvailable": "経路の詳細はまだありません。「更新」を押してもう一度お試しください。", + "chat_pathSetHops": "経路を設定しました: {hopCount} {hopCount, plural, =1{ホップ} other{ホップ}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -772,16 +790,16 @@ } } }, - "chat_pathSavedLocally": "ローカルで保存。同期のために接続する。", - "chat_pathDeviceConfirmed": "デバイスの確認済み。", - "chat_pathDeviceNotConfirmed": "デバイスの確認はまだできていません。", + "chat_pathSavedLocally": "ローカルに保存しました。同期するには接続してください。", + "chat_pathDeviceConfirmed": "デバイスで確認済み", + "chat_pathDeviceNotConfirmed": "デバイスではまだ確認されていません", "chat_type": "種類", "chat_path": "道", "chat_publicKey": "公開鍵", - "chat_compressOutgoingMessages": "送信されるメッセージを圧縮する", - "chat_floodForced": "フラッド(強制的な)", - "chat_directForced": "直接的な(強制的な)", - "chat_hopsForced": "{count} 本のホップ(強制的に採取)", + "chat_compressOutgoingMessages": "送信メッセージを圧縮する", + "chat_floodForced": "フラッド(強制)", + "chat_directForced": "ダイレクト(強制)", + "chat_hopsForced": "{count} ホップ(強制)", "@chat_hopsForced": { "placeholders": { "count": { @@ -791,7 +809,7 @@ }, "chat_floodAuto": "フラッド (自動)", "chat_direct": "直接", - "chat_poiShared": "共有されたPOI", + "chat_poiShared": "共有された POI", "chat_unread": "未読: {count}", "@chat_unread": { "placeholders": { @@ -800,10 +818,10 @@ } } }, - "chat_openLink": "リンクを開く?", - "chat_openLinkConfirmation": "このリンクをブラウザで開くことはご希望ですか?", + "chat_openLink": "リンクを開きますか?", + "chat_openLinkConfirmation": "このリンクをブラウザで開きますか?", "chat_open": "開く", - "chat_couldNotOpenLink": "リンクを開けられませんでした: {url}", + "chat_couldNotOpenLink": "リンクを開けませんでした: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -813,11 +831,11 @@ }, "chat_invalidLink": "無効なリンク形式", "map_title": "ノードマップ", - "map_lineOfSight": "視界", - "map_losScreenTitle": "視界", - "map_noNodesWithLocation": "位置情報データを持つノードは存在しません", - "map_nodesNeedGps": "ノードは、地図上に表示されるために、GPS座標を共有する必要があります。", - "map_nodesCount": "ノード:{count}", + "map_lineOfSight": "見通し", + "map_losScreenTitle": "見通し", + "map_noNodesWithLocation": "位置情報を持つノードはありません", + "map_nodesNeedGps": "ノードを地図に表示するには GPS 座標の共有が必要です", + "map_nodesCount": "ノード: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -825,7 +843,7 @@ } } }, - "map_pinsCount": "ピン:{count}個", + "map_pinsCount": "ピン: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -838,23 +856,23 @@ "map_room": "部屋", "map_sensor": "センサー", "map_pinDm": "ピン(DM)", - "map_pinPrivate": "プライベート(非公開)", - "map_pinPublic": "公開 (一般公開)", - "map_lastSeen": "最後に確認された場所", + "map_pinPrivate": "ピン(非公開)", + "map_pinPublic": "ピン(公開)", + "map_lastSeen": "最後に確認", "map_disconnectConfirm": "本当にこのデバイスとの接続を解除したいですか?", - "map_from": "~から", + "map_from": "送信元", "map_source": "出典", - "map_flags": "旗", - "map_shareMarkerHere": "この場所でシェア", + "map_flags": "フラグ", + "map_shareMarkerHere": "ここにマーカーを共有", "map_setAsMyLocation": "現在地として設定", "map_pinLabel": "ピンラベル", "map_label": "ラベル", - "map_pointOfInterest": "注目すべき点", + "map_pointOfInterest": "興味地点", "map_sendToContact": "連絡先へ送信", - "map_sendToChannel": "特定のチャンネルに送信する", + "map_sendToChannel": "チャンネルへ送信", "map_noChannelsAvailable": "利用可能なチャンネルはありません", - "map_publicLocationShare": "公共スペースの共有", - "map_publicLocationShareConfirm": "現在、{channelLabel} で位置情報を共有する準備をしています。このチャンネルは公開されており、PSK を持つ誰でも閲覧できます。", + "map_publicLocationShare": "公開位置情報の共有", + "map_publicLocationShareConfirm": "{channelLabel} で位置情報を共有しようとしています。このチャンネルは公開されており、PSK を持つ誰でも閲覧できます。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -862,28 +880,28 @@ } } }, - "map_connectToShareMarkers": "他のデバイスと接続して、マーカーを共有する", - "map_filterNodes": "フィルタノード", + "map_connectToShareMarkers": "マーカーを共有するにはデバイスに接続してください", + "map_filterNodes": "ノードを絞り込む", "map_nodeTypes": "ノードの種類", "map_chatNodes": "チャットノード", "map_repeaters": "リピータ", "map_otherNodes": "その他のノード", - "map_keyPrefix": "主要なプレフィックス", - "map_filterByKeyPrefix": "主要なプレフィックスでフィルタリングする", + "map_keyPrefix": "キー接頭辞", + "map_filterByKeyPrefix": "キー接頭辞で絞り込む", "map_publicKeyPrefix": "公開鍵のプレフィックス", "map_markers": "マーカー", "map_showSharedMarkers": "共有のマーカーを表示する", "map_showGuessedLocations": "推測されたノードの位置を表示する", - "map_showDiscoveryContacts": "Discovery社の連絡先を表示する", + "map_showDiscoveryContacts": "Discovery 連絡先を表示", "map_guessedLocation": "推測された場所", "map_lastSeenTime": "最後に確認された時間", - "map_sharedPin": "共有パスワード", + "map_sharedPin": "共有ピン", "map_joinRoom": "部屋に参加する", "map_manageRepeater": "リピータの管理", - "map_tapToAdd": "ノードをクリックして、パスに追加します。", - "map_runTrace": "パスの追跡を実行", - "map_removeLast": "最後のものを削除", - "map_pathTraceCancelled": "パスの追跡は中止。", + "map_tapToAdd": "ノードをタップして経路に追加します。", + "map_runTrace": "経路トレースを実行", + "map_removeLast": "最後を削除", + "map_pathTraceCancelled": "経路トレースはキャンセルされました", "mapCache_title": "オフライン用地図キャッシュ", "mapCache_selectAreaFirst": "最初にキャッシュする領域を選択してください", "mapCache_noTilesToDownload": "この地域にはダウンロードできるタイルは存在しません。", @@ -897,7 +915,7 @@ } }, "mapCache_downloadAction": "ダウンロード", - "mapCache_cachedTiles": "{count} 個のタイルをキャッシュ", + "mapCache_cachedTiles": "{count} 枚のタイルをキャッシュしました", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -905,7 +923,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", + "mapCache_cachedTilesWithFailed": "{downloaded} 枚をキャッシュ済み({failed} 件失敗)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -931,7 +949,7 @@ } } }, - "mapCache_downloadedTiles": "Downloaded {completed} / {total}", + "mapCache_downloadedTiles": "ダウンロード済み {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -1041,7 +1059,7 @@ }, "login_failedMessage": "ログインに失敗しました。パスワードが間違っているか、または接続が確立されていません。", "common_reload": "再読み込み", - "common_clear": "明確", + "common_clear": "クリア", "path_currentPath": "現在のパス: {path}", "@path_currentPath": { "placeholders": { @@ -1108,18 +1126,18 @@ "repeater_systemInformation": "システム情報", "repeater_battery": "バッテリー", "repeater_clockAtLogin": "ログイン時の時刻表示", - "repeater_uptime": "稼働率", + "repeater_uptime": "稼働時間", "repeater_queueLength": "待ち行列の長さ", "repeater_debugFlags": "デバッグフラグ", "repeater_radioStatistics": "ラジオに関する統計", "repeater_lastRssi": "最後のRSSI", "repeater_lastSnr": "最後のSNR", "repeater_noiseFloor": "ノイズレベル", - "repeater_txAirtime": "TXの放送時間", - "repeater_rxAirtime": "RX 空き時間", + "repeater_txAirtime": "TX 送信時間", + "repeater_rxAirtime": "RX 受信時間", "repeater_packetStatistics": "パケット統計", - "repeater_sent": "送信", - "repeater_received": "受領", + "repeater_sent": "送信済み", + "repeater_received": "受信済み", "repeater_duplicates": "重複", "repeater_daysHoursMinsSecs": "{days}日 {hours}時間 {minutes}分 {seconds}秒", "@repeater_daysHoursMinsSecs": { @@ -1302,13 +1320,13 @@ "repeater_intThreshHelper": "ラジオのノイズレベルを基準とする閾値を設定し、このレベルを超えるノイズを抑制します。 0 を設定すると、ノイズの多い帯域で RX エラーが発生した場合のみ、この値を上げることができます。", "repeater_agcResetInterval": "AGCのリセット間隔", "repeater_agcResetIntervalHelper": "ラジオの自動ゲイン制御をリセットする頻度について:ゲインが固定状態になった場合に、回復するために、何度リセットするかを設定します。4の倍数でリセットする場合、0を設定すると、定期的なリセットは停止します。", - "repeater_actionsTitle": "行動", - "repeater_sendAdvert": "フラッドに関する広告を送信", - "repeater_sendAdvertSubtitle": "ネットワークを通じて、フラッドに関する広告を放送する", - "repeater_sendAdvertZeroHop": "ゼロホップ形式の広告を送信する", - "repeater_sendAdvertZeroHopSubtitle": "ワンホップでの広告放送(リピータなし)", - "repeater_clockSync": "現在、時刻を同期する", - "repeater_clockSyncSubtitle": "スマートフォンの時刻をルーターに設定する", + "repeater_actionsTitle": "操作", + "repeater_sendAdvert": "フラッド広告を送信", + "repeater_sendAdvertSubtitle": "ネットワーク全体にフラッド広告をブロードキャストします", + "repeater_sendAdvertZeroHop": "ゼロホップ広告を送信", + "repeater_sendAdvertZeroHopSubtitle": "1 ホップ広告を送信します(リピータなし)", + "repeater_clockSync": "時刻を同期", + "repeater_clockSyncSubtitle": "スマートフォンの時刻をデバイスに設定します", "repeater_actionSucceeded": "{action} が成功しました", "@repeater_actionSucceeded": { "placeholders": { @@ -1367,10 +1385,10 @@ } } }, - "repeater_cliTitle": "リピータのコマンドラインインターフェース", + "repeater_cliTitle": "リピータ CLI", "repeater_debugNextCommand": "次のコマンドのデバッグ", "repeater_commandHelp": "コマンドヘルプ", - "repeater_clearHistory": "明確な歴史", + "repeater_clearHistory": "履歴をクリア", "repeater_noCommandsSent": "まだコマンドは送信されていません", "repeater_typeCommandOrUseQuick": "以下のコマンドを入力するか、クイックコマンドを使用してください。", "repeater_enterCommandHint": "コマンドを入力してください...", @@ -1388,11 +1406,11 @@ }, "repeater_cliQuickGetName": "名前を取得する", "repeater_cliQuickGetRadio": "ラジオを聴く", - "repeater_cliQuickGetTx": "TXを入手する", - "repeater_cliQuickNeighbors": "近隣住民", + "repeater_cliQuickGetTx": "TX を取得", + "repeater_cliQuickNeighbors": "近隣", "repeater_cliQuickVersion": "バージョン", - "repeater_cliQuickAdvertise": "広告", - "repeater_cliQuickClock": "時計", + "repeater_cliQuickAdvertise": "広告送信", + "repeater_cliQuickClock": "時刻", "repeater_cliHelpAdvert": "広告用資料を送る", "repeater_cliHelpReboot": "デバイスを再起動します。(注:通常は「タイムアウト」が表示されますが、これは正常です)", "repeater_cliHelpClock": "各デバイスの時計で現在の時刻を表示します。", @@ -1493,16 +1511,93 @@ "telemetry_switchLabel": "スイッチ", "telemetry_polylineLabel": "ポリライン", "telemetry_altitudeValue": "{meters} m", + "@telemetry_altitudeValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_frequencyValue": "{hertz} Hz", + "@telemetry_frequencyValue": { + "placeholders": { + "hertz": { + "type": "String" + } + } + }, "telemetry_pressureValue": "{hpa} hPa", + "@telemetry_pressureValue": { + "placeholders": { + "hpa": { + "type": "String" + } + } + }, "telemetry_luminosityValue": "{lux} lx", + "@telemetry_luminosityValue": { + "placeholders": { + "lux": { + "type": "String" + } + } + }, "telemetry_powerValue": "{watts} W", + "@telemetry_powerValue": { + "placeholders": { + "watts": { + "type": "String" + } + } + }, "telemetry_distanceValue": "{meters} m", + "@telemetry_distanceValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, "telemetry_energyValue": "{kilowattHours} kWh", + "@telemetry_energyValue": { + "placeholders": { + "kilowattHours": { + "type": "String" + } + } + }, "telemetry_directionValue": "{degrees}°", + "@telemetry_directionValue": { + "placeholders": { + "degrees": { + "type": "String" + } + } + }, "telemetry_concentrationValue": "{ppm} ppm", + "@telemetry_concentrationValue": { + "placeholders": { + "ppm": { + "type": "String" + } + } + }, "telemetry_percentageValue": "{percent}%", + "@telemetry_percentageValue": { + "placeholders": { + "percent": { + "type": "String" + } + } + }, "telemetry_analogValue": "{value}", + "@telemetry_analogValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "telemetry_autoFetchQuantity": "リクエスト数", "telemetry_error": "データを取得できません", "telemetry_noData": "テレメトリデータは利用できません。", @@ -1641,7 +1736,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} of {total} hops", + "channelPath_observedSomeOf": "{observed}/{total} ホップ", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1684,12 +1779,12 @@ }, "channelPath_noHopDetailsAvailable": "このパッケージに関する詳細な配送情報は利用できません。", "channelPath_unknownRepeater": "不明な増幅機", - "community_title": "地域", - "community_create": "コミュニティを構築する", - "community_createDesc": "新しいコミュニティを作成し、QRコードで共有する。", - "community_join": "参加する", - "community_joinTitle": "コミュニティに参加する", - "community_joinConfirmation": "{name}さんのようなコミュニティに参加したいですか?", + "community_title": "コミュニティ", + "community_create": "コミュニティを作成", + "community_createDesc": "新しいコミュニティを作成して QR コードで共有します。", + "community_join": "参加", + "community_joinTitle": "コミュニティに参加", + "community_joinConfirmation": "{name} のコミュニティに参加しますか?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1697,14 +1792,14 @@ } } }, - "community_scanQr": "コミュニティのQRコードをスキャン", - "community_scanInstructions": "カメラを、地域のQRコードを向けて", - "community_showQr": "QRコードを表示する", - "community_publicChannel": "地域住民向け", + "community_scanQr": "コミュニティの QR コードをスキャン", + "community_scanInstructions": "カメラをコミュニティの QR コードに向けてください", + "community_showQr": "QR コードを表示", + "community_publicChannel": "公開チャンネル", "community_hashtagChannel": "コミュニティ用ハッシュタグ", "community_name": "コミュニティ名", "community_enterName": "コミュニティ名を入力してください", - "community_created": "コミュニティ「{name}」が作成されました", + "community_created": "コミュニティ「{name}」を作成しました", "@community_created": { "placeholders": { "name": { @@ -1712,7 +1807,7 @@ } } }, - "community_joined": "{name} のコミュニティに参加", + "community_joined": "{name} のコミュニティに参加しました", "@community_joined": { "placeholders": { "name": { @@ -1721,7 +1816,7 @@ } }, "community_qrTitle": "コミュニティ共有", - "community_qrInstructions": "このQRコードをスキャンして、{name}に参加してください。", + "community_qrInstructions": "この QR コードをスキャンして {name} に参加してください。", "@community_qrInstructions": { "placeholders": { "name": { @@ -1729,10 +1824,10 @@ } } }, - "community_hashtagPrivacyHint": "コミュニティハッシュタグのチャンネルは、コミュニティのメンバーのみが参加できます。", + "community_hashtagPrivacyHint": "コミュニティのハッシュタグチャンネルには、コミュニティのメンバーだけが参加できます。", "community_invalidQrCode": "無効なコミュニティQRコード", - "community_alreadyMember": "すでに会員である", - "community_alreadyMemberMessage": "あなたはすでに {name} の会員です。", + "community_alreadyMember": "すでにメンバーです", + "community_alreadyMemberMessage": "あなたはすでに {name} のメンバーです。", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1740,13 +1835,13 @@ } } }, - "community_addPublicChannel": "コミュニティ用の公開チャンネルを追加", - "community_addPublicChannelHint": "このコミュニティの公開チャンネルを自動的に追加する", - "community_noCommunities": "まだコミュニティは形成されていません。", - "community_scanOrCreate": "QRコードをスキャンするか、コミュニティを作成して開始してください。", - "community_manageCommunities": "コミュニティの管理", - "community_delete": "コミュニティからの離脱", - "community_deleteConfirm": "{name}を辞める?", + "community_addPublicChannel": "公開チャンネルを追加", + "community_addPublicChannelHint": "このコミュニティの公開チャンネルを自動で追加します", + "community_noCommunities": "まだコミュニティはありません", + "community_scanOrCreate": "QR コードをスキャンするか、コミュニティを作成して始めてください。", + "community_manageCommunities": "コミュニティを管理", + "community_delete": "コミュニティを離脱", + "community_deleteConfirm": "{name} から離脱しますか?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1762,7 +1857,7 @@ } } }, - "community_deleted": "コミュニティ「{name}」を離れる", + "community_deleted": "コミュニティ「{name}」から離脱しました", "@community_deleted": { "placeholders": { "name": { @@ -1770,8 +1865,8 @@ } } }, - "community_regenerateSecret": "秘密の復元", - "community_regenerateSecretConfirm": "{name} の秘密鍵を再生成しますか? 継続的に通信するため、すべてのメンバーは新しいQRコードをスキャンする必要があります。", + "community_regenerateSecret": "秘密鍵を再生成", + "community_regenerateSecretConfirm": "{name} の秘密鍵を再生成しますか? 継続して通信するには、すべてのメンバーが新しい QR コードをスキャンする必要があります。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1779,8 +1874,8 @@ } } }, - "community_regenerate": "再生", - "community_secretRegenerated": "{name} への秘密が再設定されました", + "community_regenerate": "再生成", + "community_secretRegenerated": "{name} の秘密鍵を再生成しました", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1788,8 +1883,8 @@ } } }, - "community_updateSecret": "秘密情報の更新", - "community_secretUpdated": "{name} 向けの秘密設定を更新", + "community_updateSecret": "秘密鍵を更新", + "community_secretUpdated": "{name} の秘密設定を更新しました", "@community_secretUpdated": { "placeholders": { "name": { @@ -1797,7 +1892,7 @@ } } }, - "community_scanToUpdateSecret": "新しいQRコードをスキャンして、{name}の秘密情報を更新してください。", + "community_scanToUpdateSecret": "新しい QR コードをスキャンして {name} の秘密設定を更新してください。", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1805,14 +1900,14 @@ } } }, - "community_addHashtagChannel": "コミュニティのハッシュタグを追加", - "community_addHashtagChannelDesc": "このコミュニティ用のハッシュタグチャンネルを追加する", + "community_addHashtagChannel": "ハッシュタグチャンネルを追加", + "community_addHashtagChannelDesc": "このコミュニティ用のハッシュタグチャンネルを追加します", "community_selectCommunity": "コミュニティを選択", - "community_regularHashtag": "定期的なハッシュタグ", - "community_regularHashtagDesc": "一般のハッシュタグ(誰でも参加可能)", + "community_regularHashtag": "通常のハッシュタグ", + "community_regularHashtagDesc": "公開ハッシュタグ(誰でも参加可能)", "community_communityHashtag": "コミュニティ用ハッシュタグ", - "community_communityHashtagDesc": "コミュニティメンバーのみへの限定", - "community_forCommunity": "{name} 様", + "community_communityHashtagDesc": "コミュニティメンバーのみ利用できます", + "community_forCommunity": "{name} 向け", "@community_forCommunity": { "placeholders": { "name": { @@ -2220,7 +2315,7 @@ "scanner_linuxPairingHidePin": "PINを非表示", "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", - "repeater_cliQuickClockSync": "クロック同期", + "repeater_cliQuickClockSync": "時刻同期", "repeater_cliQuickDiscovery": "近隣を発見する", "@repeater_clockSyncAfterLogin": { "description": "Repeater setting: auto sync device clock after successful login" @@ -2228,8 +2323,8 @@ "@repeater_clockSyncAfterLoginSubtitle": { "description": "Repeater setting subtitle: describes the clock sync after login behavior" }, - "repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する", - "repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。", + "repeater_clockSyncAfterLogin": "ログイン後に時刻を同期", + "repeater_clockSyncAfterLoginSubtitle": "ログイン成功時に自動で時刻同期を送信します", "room_guest": "ルームサーバーに関する情報", "chat_sendMessage": "メッセージを送信する", "repeater_guest": "リピータに関する情報", @@ -2305,10 +2400,10 @@ "repeater_cliHelpStatsPackets": "(シリアルのみ)パケットレベルの統計情報を表示します。", "repeater_cliHelpStatsRadio": "(シリーズのみ)ラジオの統計情報を表示します。", "repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "common_done": "完了", + "background_serviceTitle": "MeshCore 実行中", + "background_serviceText": "BLE 接続を維持しています", + "appSettings_translationModelDeleted": "{name} を削除しました", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2316,7 +2411,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "削除に失敗しました: {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2324,7 +2419,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "チャンネルの更新に失敗しました: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2332,20 +2427,27 @@ } } }, - "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", + "map_type": "種類", + "map_path": "経路", + "map_location": "位置", + "map_estLocation": "推定位置", + "map_publicKey": "公開鍵", "map_publicKeyPrefixHint": "e.g. ab12", - "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", - "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", - "chat_score": "Score", - "settings_multiAck": "複数のACK(応答)", + "contact_typeChat": "チャット", + "contact_typeRepeater": "リピータ", + "contact_typeRoom": "ルーム", + "contact_typeSensor": "センサー", + "contact_typeUnknown": "不明", + "channels_via": "経由: {path}", + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "chat_score": "スコア", + "settings_multiAck": "マルチ ACK", "map_sharedAt": "共有済み", "@losBlockedSpotChip": { "placeholders": { @@ -2439,50 +2541,144 @@ "messageStatus_repeated": "何度も聞いた", "contacts_searchOpen": "連絡先を検索する", "contacts_searchClose": "検索を終了", - "routing_modeFlood": "洪水", + "routing_modeFlood": "フラッド", "routing_title": "経路設定", - "routing_modeAuto": "自動車", - "routing_modeManual": "マニュアル", - "routing_modeAutoHint": "最も一般的な経路を自動的に選択し、経路が不明な場合は、水没状態にします。", - "routing_modeFloodHint": "すべてのリピーターを通じて放送。最も信頼性が高いですが、より多くの時間を使用します。", - "routing_modeManualHint": "常に、あなたが設定した正確な経路を辿って移動します。", - "routing_currentRoute": "現在までのルート", - "routing_directNoHops": "直接接続—中継装置を経由しない", - "routing_noPathYet": "まだ経路は確立されていません。「次のメッセージを送信し、経路が特定されるまで待ちます」。", - "routing_floodBroadcast": "すべてのリピーターを通じて放送", - "routing_editPath": "パスの編集", - "routing_forgetPath": "道にこだわらない", + "routing_modeAuto": "自動", + "routing_modeManual": "手動", + "routing_modeAutoHint": "既知の最良経路を自動で選び、経路がないときはフラッドします。", + "routing_modeFloodHint": "すべてのリピータを通じてブロードキャストします。最も信頼性は高いですが、空中時間を多く使います。", + "routing_modeManualHint": "常に、設定した正確な経路を使います。", + "routing_currentRoute": "現在の経路", + "routing_directNoHops": "直接 - 中継ホップなし", + "routing_noPathYet": "まだ経路はありません。次のメッセージは経路が見つかるまでフラッドされます。", + "routing_floodBroadcast": "すべてのリピータにブロードキャスト", + "routing_editPath": "経路を編集", + "routing_forgetPath": "経路を忘れる", "routing_knownPaths": "既知の経路", - "routing_knownPathsHint": "そのアプリケーションに切り替えるためのショートカットを作成します。", + "routing_knownPathsHint": "経路をタップして切り替えます。", "routing_inUse": "使用中", - "routing_qualityStrong": "最初の段階で大きな成果を上げる", - "routing_qualityGood": "最初の成功", - "routing_qualityFair": "最初の試みは成功を収めた", - "routing_qualityWorked": "完了しました", - "routing_qualityFlood": "氾濫によって伝聞", + "routing_qualityStrong": "強い最初のホップ", + "routing_qualityGood": "良好な最初のホップ", + "routing_qualityFair": "まずまずの最初のホップ", + "routing_qualityWorked": "配信済み", + "routing_qualityFlood": "フラッドで受信", "routing_qualityUntested": "未検証", - "routing_lastWorked": "{when}に勤務", - "routing_neverWorked": "確認されていない", - "routing_floodDelivery": "洪水による配送", - "pathEditor_title": "経路の作成", - "pathEditor_hopCounter": "64個のホップのうち、{count}個", - "pathEditor_noHops": "まだホップは追加されていません。ホップを順番に追加するには、以下の「タップ」ボタンをクリックしてください。または、ホップを一切追加せずに直接送信するには、「保存」ボタンをクリックしてください。", - "pathEditor_addHops": "ホップを、指定された順番に加える", - "pathEditor_searchRepeaters": "繰り返し検索", - "pathEditor_advancedHex": "高度なレベル:生のヘックスパス", - "pathEditor_hexLabel": "ヘックスプレフィックス", - "pathEditor_hexHelper": "各ホップごとに2つのハッシュ文字を、カンマで区切って記述", + "routing_lastWorked": "{when} に確認", + "routing_neverWorked": "未確認", + "routing_floodDelivery": "フラッド配送", + "pathEditor_title": "経路を作成", + "pathEditor_hopCounter": "64 ホップ中 {count}", + "pathEditor_noHops": "まだホップはありません。下のリピータをタップして順番に追加するか、ホップなしで保存して直接送信します。", + "pathEditor_addHops": "順番にホップを追加", + "pathEditor_searchRepeaters": "リピータを検索", + "pathEditor_advancedHex": "詳細: 生の 16 進経路", + "pathEditor_hexLabel": "16 進接頭辞", + "pathEditor_hexHelper": "ホップごとに 16 進数 2 文字をカンマ区切りで入力します", "pathEditor_invalidTokens": "無効: {tokens}", - "pathEditor_tooManyHops": "最大64段階", - "pathEditor_usePath": "この経路を使用してください", - "pathEditor_removeHop": "ホップを取り除く", - "pathEditor_unknownHop": "不明な増幅器", + "pathEditor_tooManyHops": "最大 64 ホップ", + "pathEditor_usePath": "この経路を使用", + "pathEditor_removeHop": "ホップを削除", + "pathEditor_unknownHop": "不明なリピータ", "map_zoomIn": "ズームイン", "map_zoomOut": "ズームアウト", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "map_centerMap": "中心地図", - "chrome_bluetoothRequiresChromium": "Web Bluetooth は、Chromium ブラウザが必要です。", + "routing_deliveryCounts": "{successes} 件配信済み、{failures} 件失敗", + "map_centerMap": "地図を中央に移動", + "chrome_bluetoothRequiresChromium": "Web Bluetooth には Chromium ベースのブラウザが必要です。", "channels_communityShortId": "ID: {id}…", "pathTrace_legendGpsConfirmed": "GPSによる確認", - "pathTrace_legendInferred": "推測される位置" + "pathTrace_legendInferred": "推測される位置", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "scanner_bluetoothWebUnsupported": "ブラウザでは Bluetooth は利用できません。代わりに USB で接続してください。", + "map_online": "オンライン", + "map_activity": "アクティビティ", + "map_searchHint": "ノード名またはIDで検索", + "map_recent": "最近", + "map_stale": "古い", + "map_visible": "表示中", + "map_hidden": "非表示", + "map_centerOnNode": "ノードを中央に表示", + "map_details": "詳細", + "map_noGps": "GPS なし", + "map_noResults": "一致するノードなし", + "pathMap_viewSingle": "単独表示", + "pathMap_viewCombined": "統合表示", + "pathMap_play": "再生", + "pathMap_pause": "一時停止", + "pathMap_replay": "リプレイ", + "pathMap_stepBack": "前のホップ", + "pathMap_stepForward": "次のホップ", + "pathMap_animationOn": "パケットアニメーションを表示", + "pathMap_animationOff": "パケットアニメーションを非表示", + "pathMap_hopOf": "{current}/{total} ホップ目", + "pathMap_observedPaths": "観測された経路: {count}", + "pathMap_primary": "主要", + "pathMap_alternate": "代替 {index}", + "pathMap_hopCount": "{count, plural, =1{1 ホップ} other{{count} ホップ}}", + "pathMap_legendShared": "共有セグメント", + "pathMap_legendEstimated": "概算のセグメント", + "pathMap_sharedNodeCount": "{count} 経路で使用されています", + "pathMap_partialAnimation": "{count, plural, =1{1 つのホップに位置情報がありません - 表示中の経路は一部です} other{{count} つのホップに位置情報がありません - 表示中の経路は一部です}}", + "pathMap_showAllPaths": "すべて表示", + "pathMap_hidePath": "経路を非表示", + "pathMap_showPath": "経路を表示", + "pathMap_collapsePanel": "パネルを折りたたむ", + "pathMap_expandPanel": "パネルを展開", + "pathMap_noLocation": "位置情報なし", + "pathMap_followPacket": "パケットを追跡", + "pathMap_unfollowPacket": "パケットの追跡を解除", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index f6f804e4..6ed6f9a4 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -5,8 +5,8 @@ "nav_channels": "채널", "nav_map": "지도", "common_cancel": "취소", - "common_ok": "알겠습니다", - "common_connect": "연결", + "common_ok": "확인", + "common_connect": "연결하기", "common_unknownDevice": "알 수 없는 장치", "common_save": "저장", "common_delete": "삭제", @@ -16,21 +16,21 @@ "common_add": "추가", "common_settings": "설정", "common_disconnect": "연결 해제", - "common_connected": "연결된", - "common_disconnected": "단절", - "common_create": "만들다", + "common_connected": "연결됨", + "common_disconnected": "연결 해제됨", + "common_create": "만들기", "common_continue": "계속", "common_share": "공유", "common_copy": "복사", "common_retry": "다시 시도", - "common_hide": "숨기다", + "common_hide": "숨기기", "common_remove": "제거", - "common_enable": "활성화", - "common_disable": "비활성화", + "common_enable": "사용", + "common_disable": "사용 안 함", "common_autoRefresh": "자동 새로고침", "common_interval": "간격", "common_reboot": "재부팅", - "common_loading": "로딩 중...", + "common_loading": "불러오는 중...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { @@ -48,16 +48,16 @@ } } }, - "scanner_title": "MeshCore 공개", + "scanner_title": "MeshCore Open", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "블루투스", "connectionChoiceTcpLabel": "TCP", "tcpScreenTitle": "TCP를 통해 연결", "tcpHostLabel": "IP 주소", - "tcpHostHint": "192.168.40.10", - "tcpPortLabel": "항", + "tcpHostHint": "192.168.40.10 / example.com", + "tcpPortLabel": "포트", "tcpPortHint": "5000", - "tcpStatus_notConnected": "목적지 주소 입력 후 연결", + "tcpStatus_notConnected": "엔드포인트를 입력한 뒤 연결하세요.", "tcpStatus_connectingTo": "{endpoint}에 연결 중...", "@tcpStatus_connectingTo": { "placeholders": { @@ -80,21 +80,21 @@ }, "usbScreenTitle": "USB를 통해 연결", "usbScreenSubtitle": "감지된 시리얼 장치를 선택하고 MeshCore 노드에 직접 연결하십시오.", - "usbScreenStatus": "USB 장치를 선택합니다.", - "usbScreenNote": "USB 직렬 통신은 지원되는 안드로이드 장치 및 데스크톱 플랫폼에서 활성화됩니다.", - "usbScreenEmptyState": "USB 장치가 탐지되지 않았습니다. USB 장치를 연결하고 다시 시도해 보세요.", + "usbScreenStatus": "USB 장치를 선택하세요.", + "usbScreenNote": "USB 직렬 통신은 지원되는 Android 기기 및 데스크톱 플랫폼에서 사용할 수 있습니다.", + "usbScreenEmptyState": "USB 장치가 없습니다. 하나 연결한 뒤 새로고침하세요.", "usbErrorPermissionDenied": "USB 접근 권한이 거부되었습니다.", - "usbErrorDeviceMissing": "선택한 USB 장치는 더 이상 사용 불가능합니다.", + "usbErrorDeviceMissing": "선택한 USB 장치를 더 이상 사용할 수 없습니다.", "usbErrorInvalidPort": "유효한 USB 장치를 선택하세요.", - "usbErrorBusy": "또 다른 USB 연결 요청이 이미 진행 중입니다.", + "usbErrorBusy": "다른 USB 연결 요청이 이미 진행 중입니다.", "usbErrorNotConnected": "USB 장치가 연결되지 않았습니다.", "usbErrorOpenFailed": "선택한 USB 장치를 열 수 없습니다.", - "usbErrorConnectFailed": "선택한 USB 장치에 연결에 실패했습니다.", + "usbErrorConnectFailed": "선택한 USB 장치에 연결하지 못했습니다.", "usbErrorUnsupported": "이 플랫폼에서는 USB 직렬 통신을 지원하지 않습니다.", - "usbErrorAlreadyActive": "USB 연결이 이미 활성화되어 있습니다.", + "usbErrorAlreadyActive": "USB 연결이 이미 활성 상태입니다.", "usbErrorNoDeviceSelected": "USB 장치가 선택되지 않았습니다.", - "usbErrorPortClosed": "USB 연결이 활성화되지 않았습니다.", - "usbErrorConnectTimedOut": "연결이 시간 초과되었습니다. 장치가 USB Companion 펌웨어를 가지고 있는지 확인해 주세요.", + "usbErrorPortClosed": "USB 연결이 열려 있지 않습니다.", + "usbErrorConnectTimedOut": "연결 시간이 초과되었습니다. 장치에 USB Companion 펌웨어가 있는지 확인하세요.", "usbFallbackDeviceName": "웹 시리얼 장치", "usbStatus_notConnected": "USB 장치를 선택합니다.", "usbStatus_connecting": "USB 장치에 연결 중...", @@ -131,13 +131,13 @@ }, "scanner_stop": "멈춰", "scanner_scan": "스캔", - "scanner_bluetoothOff": "블루투스는 꺼져 있습니다.", - "scanner_bluetoothOffMessage": "블루투스를 켜서 장치를 검색해주세요.", - "scanner_chromeRequired": "크롬 브라우저 필요", - "scanner_chromeRequiredMessage": "이 웹 애플리케이션은 블루투드 지원을 위해 Google Chrome 또는 Chromium 기반 브라우저가 필요합니다.", - "scanner_enableBluetooth": "블루투스 활성화", + "scanner_bluetoothOff": "블루투스가 꺼져 있습니다.", + "scanner_bluetoothOffMessage": "기기를 검색하려면 블루투스를 켜세요.", + "scanner_chromeRequired": "Chrome 브라우저 필요", + "scanner_chromeRequiredMessage": "이 웹 앱은 블루투스 지원을 위해 Google Chrome 또는 Chromium 기반 브라우저가 필요합니다.", + "scanner_enableBluetooth": "블루투스 켜기", "device_quickSwitch": "빠른 전환", - "device_meshcore": "메쉬코어", + "device_meshcore": "MeshCore", "settings_title": "설정", "settings_deviceInfo": "장치 정보", "settings_appSettings": "앱 설정", @@ -148,7 +148,7 @@ "settings_nodeNameHint": "노드 이름을 입력하세요", "settings_nodeNameUpdated": "이름 변경", "settings_radioSettings": "라디오 설정", - "settings_radioSettingsSubtitle": "주파수, 전력, 스펙트럼", + "settings_radioSettingsSubtitle": "주파수, 전력, 확산 계수", "settings_radioSettingsUpdated": "라디오 설정이 업데이트되었습니다.", "settings_location": "위치", "settings_locationSubtitle": "GPS 좌표", @@ -169,26 +169,26 @@ "settings_privacyModeEnabled": "개인 정보 보호 모드 활성화", "settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화", "settings_actions": "행동", - "settings_deleteAllPaths": "Delete All Paths", - "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", + "settings_deleteAllPaths": "모든 경로 삭제", + "settings_deleteAllPathsSubtitle": "연락처의 모든 경로 데이터를 지웁니다.", "settings_sendAdvertisement": "광고 전송", - "settings_sendAdvertisementSubtitle": "방송 활동", - "settings_advertisementSent": "광고 전송", - "settings_syncTime": "동기화 시간", + "settings_sendAdvertisementSubtitle": "현재 존재를 방송합니다.", + "settings_advertisementSent": "광고가 전송되었습니다.", + "settings_syncTime": "시간 동기화", "settings_syncTimeSubtitle": "장치 시계를 휴대폰 시간으로 설정", - "settings_timeSynchronized": "시간 동기화", - "settings_refreshContacts": "연락처 갱신", + "settings_timeSynchronized": "시간이 동기화되었습니다.", + "settings_refreshContacts": "연락처 새로고침", "settings_refreshContactsSubtitle": "장치에서 연락처 목록을 다시 불러오기", "settings_rebootDevice": "장치 재부팅", - "settings_rebootDeviceSubtitle": "MeshCore 장치를 재부팅하세요.", - "settings_rebootDeviceConfirm": "정말로 장치를 재부팅하시겠습니까? 이 경우 연결이 끊어집니다.", - "settings_debug": "디버깅", + "settings_rebootDeviceSubtitle": "MeshCore 장치를 재부팅합니다.", + "settings_rebootDeviceConfirm": "정말 장치를 재부팅하시겠습니까? 연결이 끊어집니다.", + "settings_debug": "디버그", "settings_bleDebugLog": "BLE 디버그 로그", - "settings_bleDebugLogSubtitle": "BLE 명령어, 응답 및 원시 데이터", - "settings_appDebugLog": "앱 디버깅 로그", - "settings_appDebugLogSubtitle": "애플리케이션 디버깅 메시지", - "settings_about": "소개", - "settings_aboutVersion": "MeshCore Open {version} 버전", + "settings_bleDebugLogSubtitle": "BLE 명령, 응답 및 원시 데이터", + "settings_appDebugLog": "앱 디버그 로그", + "settings_appDebugLogSubtitle": "애플리케이션 디버그 메시지", + "settings_about": "정보", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -196,8 +196,8 @@ } } }, - "settings_aboutLegalese": "2026년 MeshCore 오픈 소스 프로젝트", - "settings_aboutDescription": "MeshCore LoRa 메시 네트워크 장치를 위한 오픈 소스 Flutter 클라이언트.", + "settings_aboutLegalese": "2026 MeshCore 오픈 소스 프로젝트", + "settings_aboutDescription": "MeshCore LoRa 메시 네트워크 장치를 위한 오픈소스 Flutter 클라이언트.", "settings_aboutOpenMeteoAttribution": "LOS 고도 데이터: Open-Meteo (CC BY 4.0)", "settings_infoName": "이름", "settings_infoId": "ID", @@ -206,19 +206,19 @@ "settings_infoPublicKey": "공개 키", "settings_infoContactsCount": "연락처 수", "settings_infoChannelCount": "채널 수", - "settings_presets": "기본 설정", + "settings_presets": "프리셋", "settings_frequency": "주파수 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "유효하지 않은 주파수 (300-2500 MHz)", "settings_bandwidth": "대역폭", "settings_spreadingFactor": "분산 계수", "settings_codingRate": "코딩 속도", - "settings_txPower": "TX 전력 (dBm)", + "settings_txPower": "송신 전력 (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "유효하지 않은 TX 전력 (0-22 dBm)", + "settings_txPowerInvalid": "유효하지 않은 송신 전력 (0-22 dBm)", "settings_clientRepeat": "오프그리드 반복", - "settings_clientRepeatSubtitle": "이 장치가 다른 사람들을 위해 메시 패킷을 반복하도록 허용합니다.", - "settings_clientRepeatFreqWarning": "오프그리드(무전력) 시스템 재연결에는 433MHz, 869MHz, 또는 918MHz 주파수가 필요합니다.", + "settings_clientRepeatSubtitle": "이 장치가 다른 장치의 메시 패킷을 반복하도록 허용합니다.", + "settings_clientRepeatFreqWarning": "오프그리드 반복에는 433MHz, 869MHz 또는 918MHz 주파수가 필요합니다.", "settings_error": "오류: {message}", "@settings_error": { "placeholders": { @@ -228,36 +228,36 @@ } }, "appSettings_title": "앱 설정", - "appSettings_appearance": "외관", - "appSettings_theme": "주제", - "appSettings_themeSystem": "기본 설정", - "appSettings_themeLight": "빛", - "appSettings_themeDark": "어둡다", + "appSettings_appearance": "모양", + "appSettings_theme": "테마", + "appSettings_themeSystem": "시스템 기본값", + "appSettings_themeLight": "밝음", + "appSettings_themeDark": "어두움", "appSettings_language": "언어", "appSettings_languageSystem": "기본 설정", "appSettings_languageEn": "영어", "appSettings_languageFr": "프랑스어", "appSettings_languageEs": "스페인어", "appSettings_languageDe": "독일어", - "appSettings_languagePl": "폴란드", + "appSettings_languagePl": "폴란드어", "appSettings_languageSl": "슬로베니아어", "appSettings_languagePt": "포르투갈어", "appSettings_languageIt": "이탈리아어", "appSettings_languageZh": "중국어", "appSettings_languageSv": "스웨덴어", "appSettings_languageNl": "네덜란드어", - "appSettings_languageSk": "슬로베니아어", - "appSettings_languageBg": "불가리", + "appSettings_languageSk": "슬로바키아어", + "appSettings_languageBg": "불가리아어", "appSettings_languageRu": "러시아어", - "appSettings_languageUk": "우크라이나", + "appSettings_languageUk": "우크라이나어", "appSettings_enableMessageTracing": "메시지 추적 기능 활성화", "appSettings_enableMessageTracingSubtitle": "메시지에 대한 상세한 경로 및 시간 정보를 표시", "appSettings_notifications": "알림", "appSettings_enableNotifications": "알림 활성화", "appSettings_enableNotificationsSubtitle": "메시지와 광고에 대한 알림을 받으세요.", "appSettings_notificationPermissionDenied": "알림 권한 거부", - "appSettings_notificationsEnabled": "알림 기능 활성화", - "appSettings_notificationsDisabled": "알림 기능 끄기", + "appSettings_notificationsEnabled": "알림 사용", + "appSettings_notificationsDisabled": "알림 사용 안 함", "appSettings_messageNotifications": "메시지 알림", "appSettings_messageNotificationsSubtitle": "새로운 메시지를 받을 때 알림 표시", "appSettings_channelMessageNotifications": "채널 메시지 알림", @@ -265,22 +265,22 @@ "appSettings_advertisementNotifications": "광고 알림", "appSettings_advertisementNotificationsSubtitle": "새 노드가 발견되었을 때 알림 표시", "appSettings_messaging": "메시징", - "appSettings_clearPathOnMaxRetry": "Max 재시도 시 경로 명확하게 설정", - "appSettings_clearPathOnMaxRetrySubtitle": "5번의 전송 시도가 실패하면 연락 경로를 재설정", - "appSettings_pathsWillBeCleared": "5번의 시도 실패 후, 해당 경로가 확보될 것입니다.", - "appSettings_pathsWillNotBeCleared": "경로는 자동으로 정리되지 않습니다.", + "appSettings_clearPathOnMaxRetry": "최대 재시도 시 경로 지우기", + "appSettings_clearPathOnMaxRetrySubtitle": "전송 시도가 5번 실패하면 연락 경로를 재설정합니다.", + "appSettings_pathsWillBeCleared": "5번 실패하면 해당 경로를 지웁니다.", + "appSettings_pathsWillNotBeCleared": "경로를 자동으로 지우지 않습니다.", "appSettings_autoRouteRotation": "자동 경로 순환", - "appSettings_autoRouteRotationSubtitle": "최적 경로와 방수 모드 사이를 전환", + "appSettings_autoRouteRotationSubtitle": "최적 경로와 플러드 모드 사이를 전환합니다.", "appSettings_autoRouteRotationEnabled": "자동 경로 순환 기능 활성화", "appSettings_autoRouteRotationDisabled": "자동 경로 순환 기능 비활성화", - "appSettings_maxRouteWeight": "최대 경로 무게", - "appSettings_maxRouteWeightSubtitle": "한 경로가 성공적인 배송을 통해 누적할 수 있는 최대 무게", + "appSettings_maxRouteWeight": "최대 경로 가중치", + "appSettings_maxRouteWeightSubtitle": "한 경로가 성공적인 전송을 통해 누적할 수 있는 최대 가중치", "appSettings_initialRouteWeight": "초기 경로 가중치", - "appSettings_initialRouteWeightSubtitle": "새롭게 발견된 경로의 초기 무게", - "appSettings_routeWeightSuccessIncrement": "성공 횟수 증가", - "appSettings_routeWeightSuccessIncrementSubtitle": "성공적으로 배송된 경로에 추가된 무게", - "appSettings_routeWeightFailureDecrement": "오류 가중치 감소", - "appSettings_routeWeightFailureDecrementSubtitle": "배송 실패 후 경로에서 제거된 무게", + "appSettings_initialRouteWeightSubtitle": "새로 발견된 경로의 초기 가중치", + "appSettings_routeWeightSuccessIncrement": "성공 시 증가", + "appSettings_routeWeightSuccessIncrementSubtitle": "성공적으로 전송된 경로에 추가되는 가중치", + "appSettings_routeWeightFailureDecrement": "실패 시 감소", + "appSettings_routeWeightFailureDecrementSubtitle": "전송 실패 후 경로에서 제거되는 가중치", "appSettings_maxMessageRetries": "최대 메시지 재시도 횟수", "appSettings_maxMessageRetriesSubtitle": "메시지를 실패로 처리하기 전 시도 횟수", "path_routeWeight": "{weight}/{max}", @@ -295,8 +295,8 @@ } }, "appSettings_battery": "배터리", - "appSettings_batteryChemistry": "배터리 화학", - "appSettings_batteryChemistryPerDevice": "{deviceName} 당분간", + "appSettings_batteryChemistry": "배터리 종류", + "appSettings_batteryChemistryPerDevice": "{deviceName}별", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -304,20 +304,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "장치를 선택하기 위해 연결", + "appSettings_batteryChemistryConnectFirst": "배터리 종류를 선택하려면 먼저 장치를 연결하세요.", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", "appSettings_batteryLipo": "리튬 폴리머 (3.0-4.2V)", "appSettings_mapDisplay": "지도 표시", - "appSettings_showRepeaters": "반복 기능 표시", - "appSettings_showRepeatersSubtitle": "지도에 반복자 노드를 표시", + "appSettings_showRepeaters": "리피터 표시", + "appSettings_showRepeatersSubtitle": "지도에 리피터 노드를 표시", "appSettings_showChatNodes": "채팅 노드 표시", "appSettings_showChatNodesSubtitle": "지도에 채팅 노드를 표시", "appSettings_showOtherNodes": "다른 노드 표시", - "appSettings_showOtherNodesSubtitle": "지도에서 다른 노드 유형을 표시", + "appSettings_showOtherNodesSubtitle": "지도에 다른 노드 유형을 표시", "appSettings_timeFilter": "시간 필터", "appSettings_timeFilterShowAll": "모든 노드 표시", - "appSettings_timeFilterShowLast": "지난 {hours} 시간 동안의 노드 표시", + "appSettings_timeFilterShowLast": "최근 {hours}시간 동안의 노드 표시", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -325,17 +325,17 @@ } } }, - "appSettings_mapTimeFilter": "지도 필터", - "appSettings_showNodesDiscoveredWithin": "다음 내역에서 발견된 노드 표시:", - "appSettings_allTime": "모든 시간", - "appSettings_lastHour": "지난 시간", + "appSettings_mapTimeFilter": "지도 시간 필터", + "appSettings_showNodesDiscoveredWithin": "다음 기간 내에 발견된 노드 표시:", + "appSettings_allTime": "전체 기간", + "appSettings_lastHour": "지난 1시간", "appSettings_last6Hours": "지난 6시간", "appSettings_last24Hours": "지난 24시간", "appSettings_lastWeek": "지난 주", "appSettings_offlineMapCache": "오프라인 지도 캐시", "appSettings_unitsTitle": "단위", - "appSettings_unitsMetric": "단위 (m / km)", - "appSettings_unitsImperial": "제국 (피트/마일)", + "appSettings_unitsMetric": "미터법 (m / km)", + "appSettings_unitsImperial": "영국식 (ft / mi)", "appSettings_noAreaSelected": "선택된 영역 없음", "appSettings_areaSelectedZoom": "선택된 영역 (줌 레벨: {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { @@ -348,9 +348,9 @@ } } }, - "appSettings_debugCard": "디버깅", - "appSettings_appDebugLogging": "앱 디버깅 로깅", - "appSettings_appDebugLoggingSubtitle": "로그 앱 디버깅 메시지 (문제 해결을 위한)", + "appSettings_debugCard": "디버그", + "appSettings_appDebugLogging": "앱 디버그 로깅", + "appSettings_appDebugLoggingSubtitle": "문제 해결을 위한 앱 디버그 메시지를 기록합니다.", "appSettings_appDebugLoggingEnabled": "앱 디버깅 로깅 활성화", "appSettings_appDebugLoggingDisabled": "앱 디버깅 로깅 비활성화", "contacts_title": "연락처", @@ -454,7 +454,7 @@ "contacts_noContactsMatchFilter": "입력하신 검색 조건과 일치하는 연락처가 없습니다.", "contacts_noMembers": "회원 없음", "contacts_lastSeenNow": "최근", - "contacts_lastSeenMinsAgo": "~ {minutes} min.", + "contacts_lastSeenMinsAgo": "~ {minutes}분", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -463,7 +463,7 @@ } }, "contacts_lastSeenHourAgo": "약 1시간", - "contacts_lastSeenHoursAgo": "~ {hours} hours", + "contacts_lastSeenHoursAgo": "~ {hours}시간", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -721,7 +721,7 @@ } }, "debugFrame_textTypeCli": "명령줄 인터페이스 (CLI)", - "debugFrame_textTypePlain": "단순한", + "debugFrame_textTypePlain": "일반 텍스트", "debugFrame_text": "- 텍스트: \"{text}\"", "@debugFrame_text": { "placeholders": { @@ -735,7 +735,7 @@ "chat_ShowAllPaths": "모든 경로 표시", "chat_routingMode": "라우팅 방식", "chat_autoUseSavedPath": "자동 (저장된 경로 사용)", - "chat_forceFloodMode": "강수 모드 활성화", + "chat_forceFloodMode": "플러드 모드 활성화", "chat_recentAckPaths": "최근 사용한 ACK 경로 (사용하려면 탭):", "chat_pathHistoryFull": "이력 기록은 이미 가득 차 있습니다. 항목을 삭제하여 새로운 항목을 추가할 수 있습니다.", "chat_hopSingular": "점프", @@ -748,20 +748,20 @@ } } }, - "chat_successes": "성공 사례", + "chat_successes": "성공", "chat_removePath": "경로 제거", "chat_noPathHistoryYet": "아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.", "chat_pathActions": "경로 작업:", "chat_setCustomPath": "사용자 지정 경로 설정", "chat_setCustomPathSubtitle": "수동으로 경로를 지정", - "chat_clearPath": "명확한 길", - "chat_clearPathSubtitle": "다음 전송 시, 강제 재전송 설정", + "chat_clearPath": "경로 지우기", + "chat_clearPathSubtitle": "다음 전송 시 강제로 새 경로를 찾습니다.", "chat_pathCleared": "경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.", - "chat_floodModeSubtitle": "앱 바에서 라우팅 스위치를 사용", - "chat_floodModeEnabled": "홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.", + "chat_floodModeSubtitle": "앱 바의 라우팅 스위치를 사용하세요.", + "chat_floodModeEnabled": "플러드 모드가 활성화되었습니다. 앱 바의 경로 아이콘으로 다시 전환할 수 있습니다.", "chat_fullPath": "전체 경로", "chat_pathDetailsNotAvailable": "경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.", - "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathSetHops": "경로 설정: {hopCount} {hopCount, plural, =1{홉} other{홉}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -772,16 +772,16 @@ } } }, - "chat_pathSavedLocally": "로컬에 저장. 동기화 연결", - "chat_pathDeviceConfirmed": "장치 확인 완료.", + "chat_pathSavedLocally": "로컬에 저장되었습니다. 동기화할 장치에 연결하세요.", + "chat_pathDeviceConfirmed": "장치가 확인되었습니다.", "chat_pathDeviceNotConfirmed": "기기가 아직 확인되지 않았습니다.", - "chat_type": "종류", + "chat_type": "유형", "chat_path": "경로", "chat_publicKey": "공개 키", "chat_compressOutgoingMessages": "전송되는 메시지 압축", - "chat_floodForced": "홍수 (강제)", - "chat_directForced": "직접적인 (강제적인)", - "chat_hopsForced": "{count}번 띄우기 (강제)", + "chat_floodForced": "플러드 (강제)", + "chat_directForced": "직접 (강제)", + "chat_hopsForced": "{count}홉 (강제)", "@chat_hopsForced": { "placeholders": { "count": { @@ -789,7 +789,7 @@ } } }, - "chat_floodAuto": "홍수 (자동)", + "chat_floodAuto": "플러드 (자동)", "chat_direct": "직접", "chat_poiShared": "공유된 POI", "chat_unread": "읽지 않음: {count}", @@ -905,7 +905,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", + "mapCache_cachedTilesWithFailed": "캐시된 타일 {downloaded}개 ({failed}개 실패)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -931,7 +931,7 @@ } } }, - "mapCache_downloadedTiles": "Downloaded {completed} / {total}", + "mapCache_downloadedTiles": "다운로드됨 {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -978,7 +978,7 @@ } } }, - "time_hoursAgo": "{hours}h ago", + "time_hoursAgo": "{hours}시간 전", "@time_hoursAgo": { "placeholders": { "hours": { @@ -1040,8 +1040,8 @@ } }, "login_failedMessage": "로그인에 실패했습니다. 비밀번호가 잘못되었거나, 연결이 되지 않는 것 같습니다.", - "common_reload": "다시 로드", - "common_clear": "명확하게", + "common_reload": "다시 불러오기", + "common_clear": "지우기", "path_currentPath": "현재 경로: {path}", "@path_currentPath": { "placeholders": { @@ -1050,7 +1050,7 @@ } } }, - "path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path", + "path_usingHopsPath": "{count} {count, plural, =1{홉} other{홉}} 경로 사용 중", "@path_usingHopsPath": { "placeholders": { "count": { @@ -1577,7 +1577,7 @@ } } }, - "neighbors_heardAgo": "Heard: {time} ago", + "neighbors_heardAgo": "수신: {time} 전", "@neighbors_heardAgo": { "placeholders": { "time": { @@ -1641,7 +1641,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} of {total} hops", + "channelPath_observedSomeOf": "{observed}/{total} 홉 관찰됨", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1877,7 +1877,7 @@ } } }, - "losAntennaB": "Antenna B: {value} {unit}", + "losAntennaB": "안테나 B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1890,7 +1890,7 @@ }, "losRun": "LOS (Loss of Signal) 상태로 전환", "losNoElevationData": "고도 정보 없음", - "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}", + "losProfileClear": "{distance} {distanceUnit}, LOS 확보, 최소 여유 {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1907,7 +1907,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, {obstruction} {heightUnit}에 의해 차단됨", "@losProfileBlocked": { "placeholders": { "distance": { @@ -2305,10 +2305,10 @@ "repeater_cliHelpStatsPackets": "(전송 속도만 표시) 패킷 수준의 통계 정보를 보여줍니다.", "repeater_cliHelpStatsRadio": "(특정 시리즈만 해당) 라디오 통계 정보를 표시합니다.", "repeater_cliHelpStatsCore": "(시리얼 번호만 표시) 핵심 펌웨어 통계 정보를 보여줍니다.", - "common_done": "Done", - "background_serviceTitle": "MeshCore running", - "background_serviceText": "Keeping BLE connected", - "appSettings_translationModelDeleted": "Deleted {name}", + "common_done": "완료", + "background_serviceTitle": "MeshCore 실행 중", + "background_serviceText": "BLE 연결 유지 중", + "appSettings_translationModelDeleted": "{name} 삭제됨", "@appSettings_translationModelDeleted": { "placeholders": { "name": { @@ -2316,7 +2316,7 @@ } } }, - "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "appSettings_translationModelDeleteFailed": "삭제 실패: {error}", "@appSettings_translationModelDeleteFailed": { "placeholders": { "error": { @@ -2324,7 +2324,7 @@ } } }, - "channels_channelUpdateFailed": "Failed to update channel: {error}", + "channels_channelUpdateFailed": "채널 업데이트 실패: {error}", "@channels_channelUpdateFailed": { "placeholders": { "error": { @@ -2332,19 +2332,19 @@ } } }, - "map_type": "Type", - "map_path": "Path", - "map_location": "Location", - "map_estLocation": "Est. Location", - "map_publicKey": "Public Key", - "map_publicKeyPrefixHint": "e.g. ab12", - "contact_typeChat": "Chat", - "contact_typeRepeater": "Repeater", - "contact_typeRoom": "Room", - "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown", - "channels_via": "via {path}", - "chat_score": "Score", + "map_type": "유형", + "map_path": "경로", + "map_location": "위치", + "map_estLocation": "추정 위치", + "map_publicKey": "공개 키", + "map_publicKeyPrefixHint": "예: ab12", + "contact_typeChat": "채팅", + "contact_typeRepeater": "리피터", + "contact_typeRoom": "룸", + "contact_typeSensor": "센서", + "contact_typeUnknown": "알 수 없음", + "channels_via": "{path} 경유", + "chat_score": "점수", "settings_multiAck": "다중 ACK", "map_sharedAt": "공유됨", "@losBlockedSpotChip": { @@ -2386,7 +2386,7 @@ "losBlockedSpotsTitle": "차단된 공간", "losSelectedObstructionTitle": "선택된 장애물", "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", - "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).", + "losSelectedObstructionDetails": "{obstruction} {heightUnit}에 의해 차단됨, A에서 {distanceFromA}, B에서 {distanceFromB} ({distanceUnit})", "settings_companionDebugLog": "동반 디버깅 로그", "chat_newMessages": "새로운 메시지", "settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터", @@ -2433,56 +2433,249 @@ "messageStatus_pending": "발송", "messageStatus_sent": "발송", "messageStatus_delivered": "배송 완료", - "common_undo": "취소", - "messageStatus_failed": "발송 실패", - "messageStatus_repeated": "반복적으로 들었습니다", + "common_undo": "되돌리기", + "messageStatus_pending": "전송 중", + "messageStatus_sent": "전송됨", + "messageStatus_delivered": "전달됨", + "messageStatus_failed": "전송 실패", + "messageStatus_repeated": "반복 수신됨", "contacts_searchOpen": "연락처 검색", "contacts_moreOptions": "더 많은 옵션", "contacts_searchClose": "검색 닫기", "routing_title": "라우팅", "routing_modeAuto": "자동", - "routing_modeFlood": "홍수", - "routing_modeManual": "사용 설명서", - "routing_modeAutoHint": "가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없을 경우에는 무작위로 경로를 선택합니다.", - "routing_modeFloodHint": "모든 증폭기를 통해 방송됩니다. 가장 안정적이지만, 더 많은 송출 시간을 사용합니다.", - "routing_modeManualHint": "항상 설정하신 정확한 경로를 따라 이동합니다.", + "routing_modeFlood": "플러드", + "routing_modeManual": "수동", + "routing_modeAutoHint": "가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없으면 플러드로 전환합니다.", + "routing_modeFloodHint": "모든 중계기를 통해 방송합니다. 가장 안정적이지만 송신 시간을 더 많이 사용합니다.", + "routing_modeManualHint": "항상 지정한 정확한 경로를 따릅니다.", "routing_currentRoute": "현재 경로", - "routing_directNoHops": "직접 연결 – 중계 장치 사용 없이", + "routing_directNoHops": "직접 연결 - 중계 없음", "routing_noPathYet": "아직 경로가 없습니다. 다음 메시지가 도착할 때까지 계속 탐색합니다.", - "routing_floodBroadcast": "모든 증폭기를 통해 방송", + "routing_floodBroadcast": "모든 중계기를 통해 방송", "routing_editPath": "경로 편집", - "routing_forgetPath": "길을 잊어라", + "routing_forgetPath": "경로 지우기", "routing_knownPaths": "알려진 경로", - "routing_knownPathsHint": "해당 항목으로 전환하기 위한 경로를 선택합니다.", + "routing_knownPathsHint": "전환할 경로를 선택하세요.", "routing_inUse": "사용 중", - "routing_qualityStrong": "강력한 첫 번째 단계", - "routing_qualityGood": "좋은 첫 시작", - "routing_qualityFair": "처음 시도", - "routing_qualityWorked": "완료됨", - "routing_qualityFlood": "홍수 피해 상황을 통해 들었습니다.", - "routing_qualityUntested": "검증되지 않음", - "routing_lastWorked": "{when}에 일했습니다", - "routing_neverWorked": "확인되지 않음", - "routing_floodDelivery": "홍수 피해 지역 배송", + "routing_qualityStrong": "매우 좋음", + "routing_qualityGood": "좋음", + "routing_qualityFair": "보통", + "routing_qualityWorked": "작동함", + "routing_qualityFlood": "플러드로 수신됨", + "routing_qualityUntested": "미검증", + "routing_lastWorked": "{when}에 작동", + "routing_neverWorked": "아직 작동한 적 없음", + "routing_floodDelivery": "플러드 전송", "pathEditor_title": "경로 만들기", - "pathEditor_hopCounter": "64개의 홉 중 {count}", - "pathEditor_noHops": "현재 홉은 추가되지 않았습니다. 아래의 탭을 사용하여 순서대로 추가하거나, 홉 없이 바로 전송하려면 \"홉 없음\"으로 저장하십시오.", - "pathEditor_addHops": "홉을 순서대로 첨가해주세요.", - "pathEditor_searchRepeaters": "반복 검색", - "pathEditor_advancedHex": "고급: 원시 헥스 경로", - "pathEditor_hexLabel": "헥스 접두사", - "pathEditor_hexHelper": "각 홉마다 2개의 6자리 숫자, 쉼표로 구분", + "pathEditor_hopCounter": "64개 중 {count} 홉", + "pathEditor_noHops": "아직 홉이 추가되지 않았습니다. 아래 탭을 사용해 순서대로 추가하거나, 홉 없이 바로 보내려면 \"홉 없음\"으로 저장하세요.", + "pathEditor_addHops": "홉을 순서대로 추가하세요.", + "pathEditor_searchRepeaters": "리피터 검색", + "pathEditor_advancedHex": "고급: 원시 HEX 경로", + "pathEditor_hexLabel": "HEX 접두사", + "pathEditor_hexHelper": "각 홉마다 2개의 16진수 바이트, 쉼표로 구분", "pathEditor_invalidTokens": "유효하지 않음: {tokens}", "pathEditor_tooManyHops": "최대 64개의 홉", - "pathEditor_usePath": "이 경로를 사용하세요", + "pathEditor_usePath": "이 경로 사용", "pathEditor_removeHop": "홉 제거", "pathEditor_unknownHop": "알 수 없는 중계기", - "map_zoomIn": "줌 인", - "routing_deliveryCounts": "{successes} delivered, {failures} failed", - "map_zoomOut": "줌 아웃", - "map_centerMap": "중심 지도", - "chrome_bluetoothRequiresChromium": "웹 블루투스는 크롬 브라우저가 필요합니다.", + "map_zoomIn": "확대", + "routing_deliveryCounts": "{successes}건 성공, {failures}건 실패", + "map_zoomOut": "축소", + "map_centerMap": "지도 중앙 맞추기", + "chrome_bluetoothRequiresChromium": "웹 블루투스는 Chromium 기반 브라우저가 필요합니다.", "channels_communityShortId": "ID: {id}...", - "pathTrace_legendGpsConfirmed": "GPS 확인 완료", - "pathTrace_legendInferred": "추론된 위치" + "pathTrace_legendGpsConfirmed": "GPS로 확인됨", + "pathTrace_legendInferred": "추정된 위치", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_activity": "활동", + "map_searchHint": "노드 이름 또는 ID 검색", + "map_online": "온라인", + "scanner_bluetoothWebUnsupported": "브라우저에서는 블루투스를 사용할 수 없습니다. 대신 USB로 연결하세요.", + "map_recent": "최근", + "map_stale": "오래됨", + "map_visible": "보임", + "map_hidden": "숨김", + "map_centerOnNode": "노드 중심으로 보기", + "map_details": "세부 정보", + "map_noGps": "GPS 없음", + "map_noResults": "일치하는 노드가 없습니다.", + "pathMap_viewSingle": "단일", + "pathMap_viewCombined": "결합", + "pathMap_play": "재생", + "pathMap_pause": "일시 정지", + "pathMap_replay": "다시 재생", + "pathMap_stepBack": "이전 홉", + "pathMap_stepForward": "다음 홉", + "pathMap_animationOn": "패킷 애니메이션 표시", + "pathMap_animationOff": "패킷 애니메이션 숨기기", + "pathMap_hopOf": "{current}/{total} 홉", + "pathMap_observedPaths": "관찰된 경로: {count}", + "pathMap_primary": "주 경로", + "pathMap_alternate": "대체 {index}", + "pathMap_hopCount": "{count, plural, =1{1 홉} other{{count} 홉}}", + "pathMap_legendShared": "공유 구간", + "pathMap_legendEstimated": "추정 구간", + "pathMap_sharedNodeCount": "{count}개의 경로에서 사용됨", + "pathMap_partialAnimation": "{count, plural, =1{1 홉은 위치가 없어 표시된 경로가 일부입니다} other{{count} 홉은 위치가 없어 표시된 경로가 일부입니다}}", + "pathMap_showAllPaths": "모두 보기", + "pathMap_hidePath": "경로 숨기기", + "pathMap_showPath": "경로 표시", + "pathMap_collapsePanel": "패널 접기", + "pathMap_expandPanel": "패널 펼치기", + "pathMap_noLocation": "위치 없음", + "pathMap_followPacket": "패킷 고정", + "pathMap_unfollowPacket": "패킷 고정 해제", + "pathMap_gpsCount": "{confirmed}/{total} GPS", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@telemetry_altitudeValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, + "@telemetry_analogValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "@telemetry_concentrationValue": { + "placeholders": { + "ppm": { + "type": "String" + } + } + }, + "@telemetry_directionValue": { + "placeholders": { + "degrees": { + "type": "String" + } + } + }, + "@telemetry_distanceValue": { + "placeholders": { + "meters": { + "type": "String" + } + } + }, + "@telemetry_energyValue": { + "placeholders": { + "kilowattHours": { + "type": "String" + } + } + }, + "@telemetry_frequencyValue": { + "placeholders": { + "hertz": { + "type": "String" + } + } + }, + "@telemetry_luminosityValue": { + "placeholders": { + "lux": { + "type": "String" + } + } + }, + "@telemetry_percentageValue": { + "placeholders": { + "percent": { + "type": "String" + } + } + }, + "@telemetry_powerValue": { + "placeholders": { + "watts": { + "type": "String" + } + } + }, + "@telemetry_pressureValue": { + "placeholders": { + "hpa": { + "type": "String" + } + } + } } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 69e165ec..e0621f98 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -45,7 +45,7 @@ class AppLocalizationsBg extends AppLocalizations { String get common_close => 'Затвори'; @override - String get common_done => 'Done'; + String get common_done => 'Готово'; @override String get common_edit => 'Редактирай'; @@ -63,7 +63,7 @@ class AppLocalizationsBg extends AppLocalizations { String get common_connected => 'Свързано'; @override - String get common_disconnected => 'Откъснато'; + String get common_disconnected => 'Прекъснато'; @override String get common_create => 'Създай'; @@ -81,7 +81,7 @@ class AppLocalizationsBg extends AppLocalizations { String get common_retry => 'Опитай отново'; @override - String get common_hide => 'Скриване'; + String get common_hide => 'Скрий'; @override String get common_remove => 'Изтрий'; @@ -93,22 +93,22 @@ class AppLocalizationsBg extends AppLocalizations { String get common_disable => 'Деактивирай'; @override - String get common_undo => 'Отмяни'; + String get common_undo => 'Отмени'; @override String get messageStatus_sent => 'Изпратено'; @override - String get messageStatus_delivered => 'Доставен'; + String get messageStatus_delivered => 'Доставено'; @override - String get messageStatus_pending => 'Изпращане'; + String get messageStatus_pending => 'Изпраща се'; @override - String get messageStatus_failed => 'Не успях да изпратя'; + String get messageStatus_failed => 'Неуспешно изпращане'; @override - String get messageStatus_repeated => 'Слушах го многократно'; + String get messageStatus_repeated => 'Повторно чуто'; @override String get common_reboot => 'Рестартирай'; @@ -288,7 +288,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_tapToScan => - 'Натиснете Сканиране, за да намерите устройства MeshCore.'; + 'Докоснете „Сканирай“, за да намерите устройства MeshCore.'; @override String scanner_connectionFailed(String error) { @@ -320,7 +320,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Функцията Bluetooth не е налична в браузъра. Моля, свържете се чрез USB вместо това.'; @override String get device_quickSwitch => 'Бързо превключване'; @@ -371,7 +371,7 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_location => 'Местоположение'; @override - String get settings_locationSubtitle => 'Координати на GPS'; + String get settings_locationSubtitle => 'GPS координати'; @override String get settings_locationUpdated => 'Местоположението е актуализирано'; @@ -468,7 +468,7 @@ class AppLocalizationsBg extends AppLocalizations { 'Включи местоположение в обявата'; @override - String get settings_multiAck => 'Множество потвърждения'; + String get settings_multiAck => 'Множество ACK'; @override String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен'; @@ -477,20 +477,20 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_actions => 'Действия'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'Изтрий всички пътища'; @override String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + 'Изчисти всички данни за пътищата от контактите.'; @override - String get settings_sendAdvertisement => 'Изпрати Реклама'; + String get settings_sendAdvertisement => 'Изпрати реклама'; @override - String get settings_sendAdvertisementSubtitle => 'Сега присъствие в ефир'; + String get settings_sendAdvertisementSubtitle => 'Излъчи присъствието сега'; @override - String get settings_advertisementSent => 'Реклама изпратена'; + String get settings_advertisementSent => 'Рекламата е изпратена'; @override String get settings_syncTime => 'Време за синхронизация'; @@ -510,22 +510,22 @@ class AppLocalizationsBg extends AppLocalizations { 'Презареди списъка с контакти от устройството'; @override - String get settings_rebootDevice => 'Рестартирайте устройството'; + String get settings_rebootDevice => 'Рестартирай устройството'; @override String get settings_rebootDeviceSubtitle => - 'Рестартирайте устройството MeshCore'; + 'Рестартирай устройството MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.'; + 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете прекъснати.'; @override String get settings_debug => 'Отстрани'; @override String get settings_companionDebugLog => - 'Лог за отстраняване на грешки (за съпътстваща програма)'; + 'Дневник за отстраняване на грешки на придружаващото приложение'; @override String get settings_companionDebugLogSubtitle => @@ -552,7 +552,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_aboutDescription => - 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; + 'Отворен Flutter клиент за MeshCore LoRa мрежови устройства.'; @override String get settings_aboutOpenMeteoAttribution => @@ -571,7 +571,7 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_infoBattery => 'Батерия'; @override - String get settings_infoPublicKey => 'Общ публичен ключ'; + String get settings_infoPublicKey => 'Публичен ключ'; @override String get settings_infoContactsCount => 'Брой контакти'; @@ -592,16 +592,16 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_frequencyInvalid => 'Невалидна честота (300-2500 MHz)'; @override - String get settings_bandwidth => 'Ширина на честотния спектър'; + String get settings_bandwidth => 'Ширина на честотната лента'; @override String get settings_spreadingFactor => 'Фактор на разпространение'; @override - String get settings_codingRate => 'Такса за кодиране'; + String get settings_codingRate => 'Скорост на кодиране'; @override - String get settings_txPower => 'TX Мощност (dBm)'; + String get settings_txPower => 'TX мощност (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @@ -610,7 +610,7 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)'; @override - String get settings_clientRepeat => 'Без електричество – повторение'; + String get settings_clientRepeat => 'Клиентско повторение'; @override String get settings_clientRepeatSubtitle => @@ -638,10 +638,10 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_themeSystem => 'Система по подразбиране'; @override - String get appSettings_themeLight => 'Ярка'; + String get appSettings_themeLight => 'Светла'; @override - String get appSettings_themeDark => 'Тъмно'; + String get appSettings_themeDark => 'Тъмна'; @override String get appSettings_language => 'Език'; @@ -665,7 +665,7 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languagePl => 'Полски'; @override - String get appSettings_languageSl => 'Словенски език'; + String get appSettings_languageSl => 'Словенски'; @override String get appSettings_languagePt => 'Португалски'; @@ -680,10 +680,10 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languageSv => 'Шведски'; @override - String get appSettings_languageNl => 'Хололандски'; + String get appSettings_languageNl => 'Нидерландски'; @override - String get appSettings_languageSk => 'Словенски'; + String get appSettings_languageSk => 'Словашки'; @override String get appSettings_languageBg => 'Български'; @@ -706,36 +706,36 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_notifications => 'Уведомления'; @override - String get appSettings_enableNotifications => 'Активирай Известия'; + String get appSettings_enableNotifications => 'Включи известията'; @override String get appSettings_enableNotificationsSubtitle => - 'Получете известия за съобщения и реклами'; + 'Получавайте известия за съобщения и реклами'; @override String get appSettings_notificationPermissionDenied => 'Отказвано е разрешение за известия'; @override - String get appSettings_notificationsEnabled => 'Уведомителни са активирани'; + String get appSettings_notificationsEnabled => 'Известията са включени'; @override - String get appSettings_notificationsDisabled => 'Известия са изключени'; + String get appSettings_notificationsDisabled => 'Известията са изключени'; @override - String get appSettings_messageNotifications => 'Уведомления'; + String get appSettings_messageNotifications => 'Известия за съобщения'; @override String get appSettings_messageNotificationsSubtitle => - 'Покажи известие при получаване на нови съобщения'; + 'Показвай известие при получаване на нови съобщения'; @override String get appSettings_channelMessageNotifications => - 'Уведомления за съобщения от канал'; + 'Известия за канални съобщения'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Покажи известие при получаване на съобщения от канали'; + 'Показвай известие при получаване на съобщения от канали'; @override String get appSettings_advertisementNotifications => 'Уведомления за реклами'; @@ -748,11 +748,12 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_messaging => 'Съобщения'; @override - String get appSettings_clearPathOnMaxRetry => 'Изчисти Път на Макс Опит'; + String get appSettings_clearPathOnMaxRetry => + 'Изчисти пътя при максимален брой опити'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Възстанови контактния път след 5 неуспешни опита за изпращане'; + 'Възстанови пътя към контакта след 5 неуспешни опита за изпращане'; @override String get appSettings_pathsWillBeCleared => @@ -764,19 +765,19 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_autoRouteRotation => - 'Автоматично маршрутизиране на завъртания'; + 'Автоматична ротация на маршрутите'; @override String get appSettings_autoRouteRotationSubtitle => - 'Превключете между най-добрите пътища и режим на наводняване'; + 'Превключвайте между най-добрите пътища и режим на наводняване'; @override String get appSettings_autoRouteRotationEnabled => - 'Автоматично маршрутизиране вкл.'; + 'Автоматичната ротация на маршрутите е включена'; @override String get appSettings_autoRouteRotationDisabled => - 'Автоматично маршрутизирането е деактивирано'; + 'Автоматичната ротация на маршрутите е изключена'; @override String get appSettings_maxRouteWeight => @@ -924,19 +925,19 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_appDebugLogging => - 'Логване за отстраняване на грешки на приложението'; + 'Дневник за отстраняване на грешки на приложението'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.'; + 'Записвай съобщенията за отстраняване на грешки на приложението.'; @override String get appSettings_appDebugLoggingEnabled => - 'Режимът за отстраняване на грешки в приложението е активиран.'; + 'Дневникът за отстраняване на грешки на приложението е включен.'; @override String get appSettings_appDebugLoggingDisabled => - 'Логването за отстраняване на грешки в приложението е изключено.'; + 'Дневникът за отстраняване на грешки на приложението е изключен.'; @override String get contacts_title => 'Контакти'; @@ -994,7 +995,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; + String get contacts_manageRepeater => 'Управление на повторителя'; @override String get contacts_manageRoom => 'Управление на сървър за стая'; @@ -1029,7 +1030,7 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_searchClose => 'Затвори търсене'; @override - String get contacts_groupName => 'Група'; + String get contacts_groupName => 'Име на групата'; @override String get contacts_groupNameRequired => 'Името на групата е задължително.'; @@ -1043,7 +1044,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get contacts_filterContacts => 'Филтрирайте контактите...'; + String get contacts_filterContacts => 'Филтрирай контактите...'; @override String get contacts_noContactsMatchFilter => @@ -1053,27 +1054,27 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_noMembers => 'Няма членове'; @override - String get contacts_lastSeenNow => 'Последно видяно сега'; + String get contacts_lastSeenNow => 'Видян току-що'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Последна активност $minutes минути преди'; + return 'Преди $minutes минути'; } @override - String get contacts_lastSeenHourAgo => 'Последно видяно преди час'; + String get contacts_lastSeenHourAgo => 'Преди час'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Последно видян $hours часа преди.'; + return 'Преди $hours часа'; } @override - String get contacts_lastSeenDayAgo => 'Последно видяно преди 1 ден'; + String get contacts_lastSeenDayAgo => 'Преди 1 ден'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Последно видян $days дни преди.'; + return 'Преди $days дни'; } @override @@ -1137,11 +1138,11 @@ class AppLocalizationsBg extends AppLocalizations { @override String channels_via(String path) { - return 'via $path'; + return 'чрез $path'; } @override - String get channels_private => 'Личен'; + String get channels_private => 'Частен'; @override String get channels_editChannel => 'Редактирай канал'; @@ -1180,7 +1181,7 @@ class AppLocalizationsBg extends AppLocalizations { String get channels_channelName => 'Име на канала'; @override - String get channels_usePublicChannel => 'Използвайте публичен канал'; + String get channels_usePublicChannel => 'Използвай публичен канал'; @override String get channels_standardPublicPsk => 'Стандартен публичен PSK'; @@ -1196,7 +1197,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channels_pskMustBe32Hex => - 'PSK трябва да бъде 32 шестнаредни знака.'; + 'PSK трябва да бъде 32 шестнадесетични знака.'; @override String channels_channelAdded(String name) { @@ -1282,7 +1283,7 @@ class AppLocalizationsBg extends AppLocalizations { String get channels_sortManual => 'Ръчно'; @override - String get channels_sortAZ => 'От A до Я'; + String get channels_sortAZ => 'От А до Я'; @override String get channels_sortLatestMessages => 'Последни съобщения'; @@ -1513,8 +1514,8 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'скача', - one: 'скача', + other: 'скока', + one: 'скок', ); return '$count $_temp0'; } @@ -1537,21 +1538,21 @@ class AppLocalizationsBg extends AppLocalizations { String get routing_title => 'Маршрутизиране'; @override - String get routing_modeAuto => 'Автомобил'; + String get routing_modeAuto => 'Автоматично'; @override - String get routing_modeFlood => 'Наводнение'; + String get routing_modeFlood => 'Наводняване'; @override - String get routing_modeManual => 'Ръководство'; + String get routing_modeManual => 'Ръчно'; @override String get routing_modeAutoHint => - 'Автоматично избира най-известния път, като при липса на информация, използва стратегия за \"запълване\" на празните пространства.'; + 'Автоматично избира най-добрия известен път, а при липса на информация използва стратегия за наводняване.'; @override String get routing_modeFloodHint => - 'Излъчване през всички ретранслатори. Най-надежният начин, но изисква повече време на въздуха.'; + 'Излъчва през всички повторители. Най-надеждният начин, но изисква повече време в ефира.'; @override String get routing_modeManualHint => @@ -1561,30 +1562,30 @@ class AppLocalizationsBg extends AppLocalizations { String get routing_currentRoute => 'Текущ маршрут'; @override - String get routing_directNoHops => 'Директ – без превключватели'; + String get routing_directNoHops => 'Директно - без скокове'; @override String get routing_noPathYet => 'Все още няма път. Съобщението продължава да се изпраща, докато не бъде открит маршрут.'; @override - String get routing_floodBroadcast => 'Предаване през всички ретранслатори'; + String get routing_floodBroadcast => 'Предаване през всички повторители'; @override - String get routing_editPath => 'Редактиране на пътя'; + String get routing_editPath => 'Редактирай пътя'; @override - String get routing_forgetPath => 'Забравете за пътя'; + String get routing_forgetPath => 'Забрави пътя'; @override - String get routing_knownPaths => 'Известни маршрути'; + String get routing_knownPaths => 'Известни пътища'; @override String get routing_knownPathsHint => - 'Натиснете бутона, за да превключите към него.'; + 'Докоснете бутона, за да превключите към него.'; @override - String get routing_inUse => 'В експлоатация'; + String get routing_inUse => 'В употреба'; @override String get routing_qualityStrong => 'Силен първи скок'; @@ -1593,78 +1594,77 @@ class AppLocalizationsBg extends AppLocalizations { String get routing_qualityGood => 'Добър първи опит'; @override - String get routing_qualityFair => 'Първият добър скок'; + String get routing_qualityFair => 'Приемлив първи скок'; @override - String get routing_qualityWorked => 'Беше изпълнено/Доведено до край'; + String get routing_qualityWorked => 'Работил'; @override - String get routing_qualityFlood => - 'Получено чрез информация, разпространена в резултат на навод.'; + String get routing_qualityFlood => 'Получено чрез наводняване'; @override - String get routing_qualityUntested => 'Не тестван'; + String get routing_qualityUntested => 'Нетестирано'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'последно работил $when'; } @override - String get routing_neverWorked => 'никога не е потвърдено'; + String get routing_neverWorked => 'Никога не е потвърдено'; @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes доставени, $failures неуспешни'; } @override - String get routing_floodDelivery => 'Доставка при навод'; + String get routing_floodDelivery => 'Доставка при наводняване'; @override String get pathEditor_title => 'Създаване на път'; @override String pathEditor_hopCounter(int count) { - return '$count от 64 различни вида малц'; + return '$count от 64 скока'; } @override String get pathEditor_noHops => - 'Все още няма добавени хмел. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите рецептата без хмел, за да я изпратите директно.'; + 'Все още няма добавени скокове. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите пътя без скокове, за да го изпратите директно.'; @override - String get pathEditor_addHops => 'Добавете хмела в реда, в който е посочено.'; + String get pathEditor_addHops => 'Добавете скоковете в посочения ред.'; @override - String get pathEditor_searchRepeaters => 'Търсене на повтори'; + String get pathEditor_searchRepeaters => 'Търсене на повторители'; @override - String get pathEditor_advancedHex => 'Разширено: необработен шестничен път'; + String get pathEditor_advancedHex => 'Разширено: суров шестнадесетичен път'; @override - String get pathEditor_hexLabel => 'Префикси на шестнадесетична система'; + String get pathEditor_hexLabel => 'Шестнадесетични префикси'; @override String get pathEditor_hexHelper => - 'Два шест-символни идентификатора на скок, разделени със запетаи'; + 'Два шестнадесетични идентификатора на скок, разделени със запетаи'; @override String pathEditor_invalidTokens(String tokens) { - return 'Невалидно: $tokens'; + return 'Невалидни: $tokens'; } @override - String get pathEditor_tooManyHops => 'Максимум 64 крачета'; + String get pathEditor_tooManyHops => 'Максимум 64 скока'; @override - String get pathEditor_usePath => 'Използвайте този маршрут.'; + String get pathEditor_usePath => 'Използвай този маршрут.'; @override - String get pathEditor_removeHop => 'Премахнете хмела'; + String get pathEditor_removeHop => 'Премахни скока'; @override - String get pathEditor_unknownHop => 'Неизвестен репитер'; + String get pathEditor_unknownHop => 'Неизвестен повторител'; @override String get chat_pathSavedLocally => @@ -1681,7 +1681,7 @@ class AppLocalizationsBg extends AppLocalizations { String get chat_type => 'Въведете'; @override - String get chat_path => 'Пътекино'; + String get chat_path => 'Път'; @override String get chat_publicKey => 'Публичен ключ'; @@ -1691,7 +1691,7 @@ class AppLocalizationsBg extends AppLocalizations { 'Компресиране на изходящи съобщения'; @override - String get chat_floodForced => 'Потоп (принуден)'; + String get chat_floodForced => 'Наводняване (принудително)'; @override String get chat_directForced => 'Директно (принудително)'; @@ -1702,13 +1702,13 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get chat_floodAuto => 'Потоп (автоматично)'; + String get chat_floodAuto => 'Наводняване (автоматично)'; @override String get chat_direct => 'Директно'; @override - String get chat_poiShared => 'Споделено място от интерес'; + String get chat_poiShared => 'Споделена точка на интерес'; @override String chat_unread(int count) { @@ -1722,7 +1722,7 @@ class AppLocalizationsBg extends AppLocalizations { String get chat_newMessages => 'Нови съобщения'; @override - String get chat_openLink => 'Отваряне на връзката?'; + String get chat_openLink => 'Отворете връзката?'; @override String get chat_openLinkConfirmation => @@ -1743,37 +1743,37 @@ class AppLocalizationsBg extends AppLocalizations { String get map_title => 'Карта на възлите'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Търсене по име или идентификатор на възел'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Дейност'; @override - String get map_online => 'Online'; + String get map_online => 'Онлайн'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Скорошни'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Остарял'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Видими'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Скрит'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Центрирай върху възела'; @override - String get map_details => 'Details'; + String get map_details => 'Подробности'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Без GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Няма съвпадащи възли'; @override String get map_lineOfSight => 'Линия на видимост'; @@ -1786,45 +1786,45 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_nodesNeedGps => - 'Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.'; + 'Възлите трябва да споделят GPS координатите си,\nза да се появят на картата.'; @override String map_nodesCount(int count) { - return 'Нодове: $count'; + return 'Възли: $count'; } @override String map_pinsCount(int count) { - return 'Ключове: $count'; + return 'Пинове: $count'; } @override String get map_chat => 'Чат'; @override - String get map_repeater => 'Повтарящ се'; + String get map_repeater => 'Повторител'; @override String get map_room => 'Стая'; @override - String get map_sensor => 'Датчик'; + String get map_sensor => 'Сензор'; @override - String get map_pinDm => 'Задържане (DM)'; + String get map_pinDm => 'Пин (DM)'; @override - String get map_pinPrivate => 'Задържане (Приватно)'; + String get map_pinPrivate => 'Пин (личен)'; @override - String get map_pinPublic => 'Публичен ключ'; + String get map_pinPublic => 'Публичен пин'; @override - String get map_lastSeen => 'Последна видяна'; + String get map_lastSeen => 'Последно видян'; @override String get map_disconnectConfirm => - 'Сигурни ли сте, че искате да се откъснете от това устройство?'; + 'Сигурни ли сте, че искате да прекъснете връзката с това устройство?'; @override String get map_from => 'От'; @@ -1833,25 +1833,25 @@ class AppLocalizationsBg extends AppLocalizations { String get map_source => 'Източник'; @override - String get map_flags => 'Флаг'; + String get map_flags => 'Флагове'; @override - String get map_type => 'Type'; + String get map_type => 'Тип'; @override - String get map_path => 'Path'; + String get map_path => 'Път'; @override - String get map_location => 'Location'; + String get map_location => 'Местоположение'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => 'Прибл. местоположение'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => 'Публичен ключ'; @override - String get map_publicKeyPrefixHint => 'e.g. ab12'; + String get map_publicKeyPrefixHint => 'напр. ab12'; @override String get map_shareMarkerHere => 'Споделете маркер тук'; @@ -1860,7 +1860,7 @@ class AppLocalizationsBg extends AppLocalizations { String get map_setAsMyLocation => 'Задайте като моя местоположение'; @override - String get map_pinLabel => 'Етикетиране на пин'; + String get map_pinLabel => 'Етикет на пина'; @override String get map_label => 'Етикет'; @@ -1878,7 +1878,7 @@ class AppLocalizationsBg extends AppLocalizations { String get map_noChannelsAvailable => 'Няма налични канали'; @override - String get map_publicLocationShare => 'Споделяне на публично място'; + String get map_publicLocationShare => 'Споделяне на публично местоположение'; @override String map_publicLocationShareConfirm(String channelLabel) { @@ -1887,10 +1887,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_connectToShareMarkers => - 'Свържете се с устройство, за да споделите маркери.'; + 'Свържете се с устройство, за да споделяте маркери.'; @override - String get map_filterNodes => 'Филтрирайте възли'; + String get map_filterNodes => 'Филтрирай възлите'; @override String get map_nodeTypes => 'Типове възли'; @@ -1914,13 +1914,13 @@ class AppLocalizationsBg extends AppLocalizations { String get map_filterByKeyPrefix => 'Филтрирайте по префикс на ключ'; @override - String get map_publicKeyPrefix => 'Префикс на публичен ключ'; + String get map_publicKeyPrefix => 'Префикс на публичния ключ'; @override String get map_markers => 'Маркери'; @override - String get map_showSharedMarkers => 'Покажи споделени маркери'; + String get map_showSharedMarkers => 'Показвай споделените маркери'; @override String get map_showGuessedLocations => @@ -1933,10 +1933,10 @@ class AppLocalizationsBg extends AppLocalizations { String get map_guessedLocation => 'Предполагано местоположение'; @override - String get map_lastSeenTime => 'Последна видяна дата'; + String get map_lastSeenTime => 'Последно видян'; @override - String get map_sharedPin => 'Споделено копие'; + String get map_sharedPin => 'Споделен пин'; @override String get map_sharedAt => 'Споделено'; @@ -1945,20 +1945,19 @@ class AppLocalizationsBg extends AppLocalizations { String get map_joinRoom => 'Присъедини се към стаята'; @override - String get map_manageRepeater => 'Управление на Повтарящ се Елемент'; + String get map_manageRepeater => 'Управление на повторителя'; @override - String get map_tapToAdd => - 'Натиснете върху възлите, за да ги добавите към пътя.'; + String get map_tapToAdd => 'Докоснете възлите, за да ги добавите към пътя.'; @override - String get map_runTrace => 'Изпълни Път на Следване'; + String get map_runTrace => 'Стартирай проследяването на пътя'; @override String get map_runTraceWithReturnPath => 'Върни се по същия път.'; @override - String get map_removeLast => 'Премахни Последно'; + String get map_removeLast => 'Премахни последното'; @override String get map_pathTraceCancelled => 'Отменен е следването на пътя.'; @@ -2028,7 +2027,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get mapCache_downloadTilesButton => 'Изтегли Плочки'; + String get mapCache_downloadTilesButton => 'Изтегли плочките'; @override String get mapCache_clearCacheButton => 'Изчисти кеша'; @@ -2049,21 +2048,21 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get time_justNow => 'Сега'; + String get time_justNow => 'Току-що'; @override String time_minutesAgo(int minutes) { - return '$minutes минути преди'; + return 'Преди $minutes минути'; } @override String time_hoursAgo(int hours) { - return '$hours часа преди'; + return 'Преди $hours часа'; } @override String time_daysAgo(int days) { - return '$days дни преди'; + return 'Преди $days дни'; } @override @@ -2082,7 +2081,7 @@ class AppLocalizationsBg extends AppLocalizations { String get time_week => 'седмица'; @override - String get time_weeks => 'секти'; + String get time_weeks => 'седмици'; @override String get time_month => 'месец'; @@ -2094,17 +2093,17 @@ class AppLocalizationsBg extends AppLocalizations { String get time_minutes => 'минути'; @override - String get time_allTime => 'Всичко време'; + String get time_allTime => 'За цялото време'; @override String get dialog_disconnect => 'Прекъсни'; @override String get dialog_disconnectConfirm => - 'Сигурни ли сте, че искате да се откъснете от това устройство?'; + 'Сигурни ли сте, че искате да прекъснете връзката с това устройство?'; @override - String get login_repeaterLogin => 'Повторител Вход'; + String get login_repeaterLogin => 'Вход за повторител'; @override String get login_roomLogin => 'Вход в стаята'; @@ -2116,7 +2115,7 @@ class AppLocalizationsBg extends AppLocalizations { String get login_enterPassword => 'Въведете парола'; @override - String get login_savePassword => 'Запази парола'; + String get login_savePassword => 'Запази паролата'; @override String get login_savePasswordSubtitle => @@ -2124,7 +2123,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get login_repeaterDescription => - 'Въведете паролата на репитера, за да получите достъп до настройките и статуса.'; + 'Въведете паролата на повторителя, за да получите достъп до настройките и статуса.'; @override String get login_roomDescription => @@ -2143,7 +2142,7 @@ class AppLocalizationsBg extends AppLocalizations { String get login_forceFloodMode => 'Принуди режим на наводняване'; @override - String get login_managePaths => 'Управление на пътища'; + String get login_managePaths => 'Управление на пътищата'; @override String get login_login => 'Вход'; @@ -2225,7 +2224,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_settingsSubtitle => - 'Конфигурирайте параметрите на репитера'; + 'Конфигурирайте параметрите на повторителя'; @override String get repeater_clockSyncAfterLogin => @@ -2284,10 +2283,10 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_noiseFloor => 'Ниво на шум'; @override - String get repeater_txAirtime => 'TX Airtime'; + String get repeater_txAirtime => 'TX време в ефир'; @override - String get repeater_rxAirtime => 'RX Airtime'; + String get repeater_rxAirtime => 'RX време в ефир'; @override String get repeater_chanUtil => 'Използване на канала'; @@ -2345,7 +2344,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_repeaterNameHelper => - 'Показване на името на този репитер'; + 'Показване на името на този повторител'; @override String get repeater_adminPassword => 'Парола на администратора'; @@ -2369,7 +2368,7 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX Power'; + String get repeater_txPower => 'TX мощност'; @override String get repeater_txPowerHelper => '1-30 dBm'; @@ -2381,7 +2380,7 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_spreadingFactor => 'Фактор на разпространение'; @override - String get repeater_codingRate => 'Такса за кодиране'; + String get repeater_codingRate => 'Скорост на кодиране'; @override String get repeater_locationSettings => 'Настройки на местоположението'; @@ -2407,13 +2406,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_packetForwardingSubtitle => - 'Активирайте репитера, за да препращате пакети.'; + 'Активирайте повторителя, за да препраща пакети.'; @override - String get repeater_guestAccess => 'Достъп за Гост'; + String get repeater_guestAccess => 'Достъп за гости'; @override - String get repeater_guestAccessSubtitle => 'Разрешете самочетене за гости'; + String get repeater_guestAccessSubtitle => 'Разрешете само четене за гости'; @override String get repeater_privacyMode => 'Режим на поверителност'; @@ -2426,7 +2425,7 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_advertisementSettings => 'Настройки на рекламите'; @override - String get repeater_localAdvertInterval => 'Местен Рекламен Интервал'; + String get repeater_localAdvertInterval => 'Интервал на местната реклама'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2435,7 +2434,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Интервал на рекламата за наводнения'; + 'Интервал на рекламата за наводняване'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2443,44 +2442,44 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get repeater_encryptedAdvertInterval => 'Криптиран Рекламен Интервал'; + String get repeater_encryptedAdvertInterval => + 'Криптиран интервал на рекламата'; @override - String get repeater_dangerZone => - 'Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно'; + String get repeater_dangerZone => 'Опасна зона'; @override - String get repeater_rebootRepeater => 'БеРестартирай Репитер'; + String get repeater_rebootRepeater => 'Рестартирай повторителя'; @override - String get repeater_rebootRepeaterSubtitle => 'Рестартирайте ретранслатора.'; + String get repeater_rebootRepeaterSubtitle => 'Рестартира повторителя.'; @override String get repeater_rebootRepeaterConfirm => - 'Сигурни ли сте, че искате да рестартирате този репитер?'; + 'Сигурни ли сте, че искате да рестартирате този повторител?'; @override String get repeater_regenerateIdentityKey => - 'Генериране на Ключ за Идентичност'; + 'Генерирай нов идентификационен ключ'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Генериране на нова двойка публичен/частен ключ'; + 'Генерирай нова двойка публичен/частен ключ'; @override String get repeater_regenerateIdentityKeyConfirm => - 'БеТова ще генерира нова идентичност за репитера. Продължете?'; + 'Това ще генерира нова идентичност за повторителя. Продължете?'; @override - String get repeater_eraseFileSystem => 'Изтрий Файлова Система'; + String get repeater_eraseFileSystem => 'Изтрий файловата система'; @override String get repeater_eraseFileSystemSubtitle => - 'Форматирайте файла на репитера'; + 'Форматирай файловата система на повторителя'; @override String get repeater_eraseFileSystemConfirm => - 'ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!'; + 'ВНИМАНИЕ: Това ще изтрие всички данни от повторителя. Това не може да бъде отменено!'; @override String get repeater_eraseSerialOnly => @@ -2497,31 +2496,30 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get repeater_confirm => 'БеПотвърди'; + String get repeater_confirm => 'Потвърди'; @override String get repeater_settingsSaved => 'Настройките са запазени успешно.'; @override - String get repeater_rxGain => 'Увеличен коефициент на възвръщаемост (RX)'; + String get repeater_rxGain => 'RX усилване'; @override String get repeater_rxGainHelper => 'По-висока чувствителност, по-голям ток (само за SX1262/SX1268)'; @override - String get repeater_refreshRxGain => - 'Възстановете повишената ефективност на RX'; + String get repeater_refreshRxGain => 'Обнови RX усилването'; @override String get repeater_multiAcks => 'Множество потвърждения'; @override String get repeater_multiAcksSubtitle => - 'Признавайте съобщения по множество канали за по-добро доставяне.'; + 'Потвърждавай съобщенията по множество канали за по-добро доставяне.'; @override - String get repeater_refreshMultiAcks => 'Обновете множество потвърждения'; + String get repeater_refreshMultiAcks => 'Обнови множествените ACK'; @override String get repeater_networkHealth => 'Състояние на мрежата'; @@ -2546,11 +2544,11 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_loopDetectStrict => 'Строг'; @override - String get repeater_dutyCycle => 'Цикъл на работа/почивка'; + String get repeater_dutyCycle => 'Работен цикъл'; @override String get repeater_dutyCycleHelper => - 'Максимален процент на използване на времето на въздуха'; + 'Максимален процент на използване на времето в ефир'; @override String repeater_dutyCyclePercent(int percent) { @@ -2561,55 +2559,56 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_ownerInfo => 'Информация за оператора'; @override - String get repeater_ownerInfoHelper => 'Публични метаданни за този репитер'; + String get repeater_ownerInfoHelper => + 'Публични метаданни за този повторител'; @override String get repeater_refreshOwnerInfo => 'Обновете информацията за оператора'; @override - String get repeater_floodMax => 'Максимален брой скачания при наводнение'; + String get repeater_floodMax => 'Максимален брой хопове при наводняване'; @override String get repeater_floodMaxHelper => - 'Максималният брой пакети, които един поток може да пренесе (0-64)'; + 'Максималният брой хопове, които един пакет може да премине (0-64)'; @override - String get repeater_advancedSettings => 'Напреднал'; + String get repeater_advancedSettings => 'Разширени настройки'; @override String get repeater_advancedSettingsSubtitle => - 'Регулаторни копчета за опитни оператори'; + 'Експериментални настройки за опитни оператори'; @override String get repeater_pathHashMode => 'Режим за хеширане на пътища'; @override String get repeater_pathHashModeHelper => - 'Байтовете, използвани за кодиране на идентификатора на този репитер в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скача), 1=2 байта (65 000 идентификатора, до 32 скача), 2=3 байта (16 милиона идентификатора, до 21 скача). Версии 1.13 и по-стари версии на фърмуера използват многобайтови пътища – само след като мрежата е актуализирана до версия 1.14 или по-нова.'; + 'Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.'; @override - String get repeater_txDelay => 'Забавяне на проекта \"Flood TX\"'; + String get repeater_txDelay => 'Забавяне на Flood TX'; @override String get repeater_txDelayHelper => - 'Предавайте разстоянието между пакетите за трафик при наводнения, като множител на времето за пренос на пакета (0-2, по подразбиране 0.5). По-висока стойност означава по-малко сблъсъци, но по-бавно предаване.'; + 'Разстоянието между пакетите при наводняване като множител на времето за пренос на пакета (0-2, по подразбиране 0.5). По-висока стойност означава по-малко сблъсъци, но по-бавно предаване.'; @override - String get repeater_directTxDelay => 'Директно забавяне на сигнала'; + String get repeater_directTxDelay => 'Забавяне на директното предаване'; @override String get repeater_directTxDelayHelper => - 'Предаване на интервали за директен (не-масивен) трафик, като множител на времето за пренос на пакета (0-2, по подразбиране 0.3).'; + 'Интервал за директен (не-масов) трафик, като множител на времето за пренос на пакета (0-2, по подразбиране 0.3).'; @override - String get repeater_intThresh => 'Праг на интерференция'; + String get repeater_intThresh => 'Праг на смущенията'; @override String get repeater_intThreshHelper => 'Прагът е зададен на нивото на шума на радиото, така че да отхвърля смущения, които са над този праг. 0 – изключва; активирайте само, ако забележите грешки в шумна честотна лента.'; @override - String get repeater_agcResetInterval => 'Интервал за рестартиране на AGC'; + String get repeater_agcResetInterval => 'Интервал за нулиране на AGC'; @override String get repeater_agcResetIntervalHelper => @@ -2619,40 +2618,39 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_actionsTitle => 'Действия'; @override - String get repeater_sendAdvert => 'Изпратете реклама за навод'; + String get repeater_sendAdvert => 'Изпрати реклама за наводняване'; @override String get repeater_sendAdvertSubtitle => - 'Публикувайте реклама за навод в мрежата.'; + 'Публикувай реклама за наводняване в мрежата.'; @override - String get repeater_sendAdvertZeroHop => - 'Изпратете реклама без преминаване през други системи'; + String get repeater_sendAdvertZeroHop => 'Изпрати реклама без хопове'; @override String get repeater_sendAdvertZeroHopSubtitle => - 'Публикувайте реклама, която достига до целевата аудитория само чрез директно разпространение (без използване на посредници).'; + 'Публикувай реклама, която достига до целевата аудитория само чрез директно разпространение.'; @override - String get repeater_clockSync => 'Синхронизиране на часовника сега'; + String get repeater_clockSync => 'Синхронизирай часовника сега'; @override String get repeater_clockSyncSubtitle => - 'Настройте времето на телефона си да съвпада с времето на репитера.'; + 'Настройте времето на телефона си да съвпада с времето на повторителя.'; @override String repeater_actionSucceeded(String action) { - return '$action succeeded'; + return '$action успешно'; } @override String repeater_actionFailed(String action, String error) { - return '$action failed: $error'; + return '$action не успя: $error'; } @override String get repeater_settingsSavedRebootNeeded => - 'Настройки запаметени – рестартирайте ретранслатора, за да ги приложите.'; + 'Настройките са запазени - рестартирайте повторителя, за да ги приложите.'; @override String repeater_settingsPartialFailure(String failures) { @@ -2665,23 +2663,23 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get repeater_refreshBasicSettings => 'Обнови Основни Настройки'; + String get repeater_refreshBasicSettings => 'Обнови основните настройки'; @override - String get repeater_refreshRadioSettings => - 'Обнови настройките на радиопредавателите'; + String get repeater_refreshRadioSettings => 'Обнови настройките на радиото'; @override - String get repeater_refreshTxPower => 'Обнови TX захранване'; + String get repeater_refreshTxPower => 'Обнови TX мощността'; @override - String get repeater_refreshPacketForwarding => 'Обнови пакетно пренасочване'; + String get repeater_refreshPacketForwarding => + 'Обнови препращането на пакети'; @override - String get repeater_refreshGuestAccess => 'Обнови достъп за гости'; + String get repeater_refreshGuestAccess => 'Обнови достъпа за гости'; @override - String get repeater_refreshPrivacyMode => 'Обнови Режим на поверителност'; + String get repeater_refreshPrivacyMode => 'Обнови режима на поверителност'; @override String repeater_refreshed(String label) { @@ -2694,16 +2692,16 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get repeater_cliTitle => 'Повторител CLI'; + String get repeater_cliTitle => 'CLI на повторителя'; @override - String get repeater_debugNextCommand => 'Поправи Следваща Команда'; + String get repeater_debugNextCommand => 'Отстрани следващата команда'; @override String get repeater_commandHelp => 'Помощ'; @override - String get repeater_clearHistory => 'Изчисти История'; + String get repeater_clearHistory => 'Изчисти историята'; @override String get repeater_noCommandsSent => 'Няма изпратени команди засега.'; @@ -2716,7 +2714,7 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_enterCommandHint => 'Въведете команда...'; @override - String get repeater_previousCommand => 'Предходна команда'; + String get repeater_previousCommand => 'Предишна команда'; @override String get repeater_nextCommand => 'Следваща команда'; @@ -2725,7 +2723,7 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_enterCommandFirst => 'Въведете първо команда.'; @override - String get repeater_cliCommandFrameTitle => 'Рамка за команда CLI'; + String get repeater_cliCommandFrameTitle => 'Рамка на CLI команда'; @override String repeater_cliCommandError(String error) { @@ -2733,13 +2731,13 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Получи име'; + String get repeater_cliQuickGetName => 'Вземи име'; @override - String get repeater_cliQuickGetRadio => 'Получи радио'; + String get repeater_cliQuickGetRadio => 'Вземи радио'; @override - String get repeater_cliQuickGetTx => 'Получи TX'; + String get repeater_cliQuickGetTx => 'Вземи TX'; @override String get repeater_cliQuickNeighbors => 'Съседи'; @@ -2886,7 +2884,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliHelpGetBridgeType => - 'Получава тип мост none, rs232, espnow'; + 'Получава типа на моста: none, rs232, espnow'; @override String get repeater_cliHelpLogStart => @@ -2930,11 +2928,11 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliHelpRegionAllowf => - 'Задава \'Потоп\' разрешение за посочената област. (\'\' за глобалния/стария обхват)'; + 'Задава разрешение \'Flood\' за посочената област. (\'\' за глобалния/стария обхват)'; @override String get repeater_cliHelpRegionDenyf => - 'Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )'; + 'Премахва разрешението \"F\" за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобално/старо ниво!!)'; @override String get repeater_cliHelpRegionHome => @@ -3093,7 +3091,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliHelpGetRole => - 'Показва ролята на фърмуера (например, репитер, сървър за стая и т.н.).'; + 'Показва ролята на фърмуера (например повторител, сървър на стая и т.н.).'; @override String get repeater_cliHelpGetPublicKey => @@ -3105,7 +3103,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliHelpGetRepeat => - 'Показва дали функцията за пренасочване на пакети (ролята на репитер) е активирана или деактивирана.'; + 'Показва дали функцията за пренасочване на пакети (ролята на повторителя) е активирана или деактивирана.'; @override String get repeater_cliHelpGetTx => 'Показва текущата мощност на TX в dBm.'; @@ -3193,7 +3191,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliHelpGetAcl => - '(Само за серийни номера) Изброява настройките за контрол на достъпа в репитера.'; + '(Само за серийни устройства) Изброява настройките за контрол на достъпа в повторителя.'; @override String get repeater_cliHelpGetBridgeEnabled => @@ -3583,7 +3581,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get channelPath_pathLabelTitle => 'Пътекино'; + String get channelPath_pathLabelTitle => 'Пътеки'; @override String get channelPath_observedPathHeader => 'Наблюдаван път'; @@ -3823,7 +3821,7 @@ class AppLocalizationsBg extends AppLocalizations { String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.'; @override - String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; + String get pathTrace_refreshTooltip => 'Обнови проследяването на пътя.'; @override String get pathTrace_someHopsNoLocation => @@ -3979,7 +3977,7 @@ class AppLocalizationsBg extends AppLocalizations { String distanceUnit, String distanceFromB, ) { - return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + return 'Блокирано от $obstruction $heightUnit, $distanceFromA от A и $distanceFromB от B ($distanceUnit).'; } @override @@ -4044,7 +4042,7 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_zeroHopAdvert => 'Реклама без скок'; @override - String get contacts_floodAdvert => 'Потопна реклама'; + String get contacts_floodAdvert => 'Реклама за наводняване'; @override String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда'; @@ -4172,7 +4170,7 @@ class AppLocalizationsBg extends AppLocalizations { 'meshcore-open износ на данни за карта в формат GPX'; @override - String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства'; + String get snrIndicator_nearByRepeaters => 'Близки повторители'; @override String get snrIndicator_lastSeen => 'Последно видян'; @@ -4458,49 +4456,49 @@ class AppLocalizationsBg extends AppLocalizations { String get translation_systemLanguage => 'Език на системата'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore работи'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'Поддържа BLE връзката активна'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return 'Изтрит $name'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return 'Неуспешно изтриване: $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'Неуспешно обновяване на канала: $error'; } @override - String get contact_typeChat => 'Chat'; + String get contact_typeChat => 'Чат'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => 'Повторител'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => 'Стая'; @override - String get contact_typeSensor => 'Sensor'; + String get contact_typeSensor => 'Сензор'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => 'Неизвестен'; @override String get map_zoomIn => 'Увеличи'; @override - String get map_zoomOut => 'Приближете се по-малко'; + String get map_zoomOut => 'Намали мащаба'; @override - String get map_centerMap => 'Карта на центъра'; + String get map_centerMap => 'Центрирай картата'; @override String get chrome_bluetoothRequiresChromium => @@ -4518,48 +4516,48 @@ class AppLocalizationsBg extends AppLocalizations { String get pathTrace_legendInferred => 'Извлечена позиция'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Самостоятелен'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Комбиниран'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Пусни'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Пауза'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Повторение'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Предишна стъпка'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Следваща стъпка'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Показвай анимацията на пакета'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Скрий анимацията на пакета'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Стъпка $current от $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Наблюдавани пътища: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Основен'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Алтернативен $index'; } @override @@ -4567,8 +4565,8 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count скока', + one: '1 скок', ); return '$_temp0'; } @@ -4579,14 +4577,14 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Споделена секция'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Очаквана стойност на сегмента'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Използвани от $count пътища'; } @override @@ -4594,33 +4592,34 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count скока нямат определено местоположение — показаният път е непълен', + one: '1 скок няма определено местоположение — показаният път е непълен', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Покажи всички пътища'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Скрий пътя'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Покажи пътя'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Сгъни панела'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Разгъни панела'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Без посочено местоположение'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Проследи пакета'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Спри проследяването на пакета'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 0b6e5833..4efda5d8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -24,7 +24,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_cancel => 'Abbrechen'; @override - String get common_ok => 'Alles klar'; + String get common_ok => 'OK'; @override String get common_connect => 'Verbinden'; @@ -45,7 +45,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_close => 'Schließen'; @override - String get common_done => 'Done'; + String get common_done => 'Fertig'; @override String get common_edit => 'Bearbeiten'; @@ -78,13 +78,13 @@ class AppLocalizationsDe extends AppLocalizations { String get common_copy => 'Kopieren'; @override - String get common_retry => 'Versuchen'; + String get common_retry => 'Wiederholen'; @override String get common_hide => 'Ausblenden'; @override - String get common_remove => 'Löschen'; + String get common_remove => 'Entfernen'; @override String get common_enable => 'Aktivieren'; @@ -99,22 +99,22 @@ class AppLocalizationsDe extends AppLocalizations { String get messageStatus_sent => 'Gesendet'; @override - String get messageStatus_delivered => 'Geliefert'; + String get messageStatus_delivered => 'Zugestellt'; @override - String get messageStatus_pending => 'Versenden'; + String get messageStatus_pending => 'Wird gesendet'; @override - String get messageStatus_failed => 'Nicht gesendet'; + String get messageStatus_failed => 'Senden fehlgeschlagen'; @override - String get messageStatus_repeated => 'Wiederholt gehört'; + String get messageStatus_repeated => 'Mehrfach gehört'; @override String get common_reboot => 'Neustart'; @override - String get common_loading => 'Laden...'; + String get common_loading => 'Lädt...'; @override String get common_notAvailable => '—'; @@ -136,7 +136,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_interval => 'Intervall'; @override - String get scanner_title => 'MeshCore – Open-Version'; + String get scanner_title => 'MeshCore Open'; @override String get connectionChoiceUsbLabel => 'USB'; @@ -151,20 +151,19 @@ class AppLocalizationsDe extends AppLocalizations { String get tcpScreenTitle => 'Verbinden über TCP'; @override - String get tcpHostLabel => 'IP-Adresse'; + String get tcpHostLabel => 'Endpunkt'; @override - String get tcpHostHint => '192.168.40.10'; + String get tcpHostHint => '192.168.40.10 / example.com'; @override - String get tcpPortLabel => 'Hafen'; + String get tcpPortLabel => 'Port'; @override String get tcpPortHint => '5000'; @override - String get tcpStatus_notConnected => - 'Geben Sie den Endpunkt ein und verbinden Sie sich.'; + String get tcpStatus_notConnected => 'Endpunkt eingeben und verbinden'; @override String tcpStatus_connectingTo(String endpoint) { @@ -172,7 +171,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.'; + String get tcpErrorHostRequired => 'Ein Endpunkt ist erforderlich.'; @override String get tcpErrorPortInvalid => @@ -180,29 +179,29 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tcpErrorUnsupported => - 'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.'; + 'TCP wird auf dieser Plattform nicht unterstützt.'; @override String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.'; @override String tcpConnectionFailed(String error) { - return 'Fehler beim TCP-Verbindungsaufbau: $error'; + return 'TCP-Verbindung fehlgeschlagen: $error'; } @override - String get usbScreenTitle => 'Verbinden über USB'; + String get usbScreenTitle => 'Über USB verbinden'; @override String get usbScreenSubtitle => - 'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; + 'Wählen Sie ein erkanntes serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; @override String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus'; @override String get usbScreenNote => - 'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; + 'USB-Seriell ist auf unterstützten Android-Geräten und Desktop-Plattformen verfügbar.'; @override String get usbScreenEmptyState => @@ -228,15 +227,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbErrorOpenFailed => - 'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.'; + 'Das ausgewählte USB-Gerät konnte nicht geöffnet werden.'; @override String get usbErrorConnectFailed => - 'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.'; + 'Mit dem ausgewählten USB-Gerät konnte keine Verbindung hergestellt werden.'; @override String get usbErrorUnsupported => - 'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.'; + 'USB-Seriell wird auf dieser Plattform nicht unterstützt.'; @override String get usbErrorAlreadyActive => @@ -250,30 +249,31 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.'; + 'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät über USB-Companion-Firmware verfügt.'; @override - String get usbFallbackDeviceName => 'Web-Serielle Geräte'; + String get usbFallbackDeviceName => 'Web-Serial-Gerät'; @override String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus'; @override - String get usbStatus_connecting => 'Verbindung zum USB-Gerät...'; + String get usbStatus_connecting => + 'Verbindung zum USB-Gerät wird hergestellt...'; @override String get usbStatus_searching => 'Suche nach USB-Geräten...'; @override String usbConnectionFailed(String error) { - return 'Fehler beim USB-Verbindungsaufbau: $error'; + return 'USB-Verbindung fehlgeschlagen: $error'; } @override - String get scanner_scanning => 'Scannen nach Geräten...'; + String get scanner_scanning => 'Suche nach Geräten...'; @override - String get scanner_connecting => 'Verbunden...'; + String get scanner_connecting => 'Verbinde...'; @override String get scanner_disconnecting => 'Trenne...'; @@ -291,11 +291,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_tapToScan => - 'Tippen Sie auf Scan, um MeshCore-Geräte zu finden.'; + 'Tippen Sie auf Scannen, um MeshCore-Geräte zu finden.'; @override String scanner_connectionFailed(String error) { - return 'Verbindungsfehler: $error'; + return 'Verbindung fehlgeschlagen: $error'; } @override @@ -323,10 +323,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Bluetooth ist im Browser nicht verfügbar. Verwenden Sie stattdessen eine USB-Verbindung.'; @override - String get device_quickSwitch => 'Schnelles Umschalten'; + String get device_quickSwitch => 'Schnellwechsel'; @override String get device_meshcore => 'MeshCore'; @@ -342,10 +342,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_appSettingsSubtitle => - 'Benachrichtigungen, Messaging und Kartenwahrnehmung'; + 'Benachrichtigungen, Nachrichten und Karten-Einstellungen'; @override - String get settings_nodeSettings => 'Knoten-Einstellungen'; + String get settings_nodeSettings => 'Knoteneinstellungen'; @override String get settings_nodeName => 'Knotenname'; @@ -354,17 +354,17 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_nodeNameNotSet => 'Nicht festgelegt'; @override - String get settings_nodeNameHint => 'Gebe den Knotenamen ein'; + String get settings_nodeNameHint => 'Geben Sie den Knotennamen ein'; @override String get settings_nodeNameUpdated => 'Name aktualisiert'; @override - String get settings_radioSettings => 'Funk Einstellungen'; + String get settings_radioSettings => 'Funk-Einstellungen'; @override String get settings_radioSettingsSubtitle => - 'Frequenz, Leistung, Verbreitungsfaktor'; + 'Frequenz, Leistung, Spreading-Faktor'; @override String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert'; @@ -376,24 +376,25 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_locationSubtitle => 'GPS-Koordinaten'; @override - String get settings_locationUpdated => 'Ort aktualisiert'; + String get settings_locationUpdated => + 'Standort und GPS-Einstellungen aktualisiert'; @override String get settings_locationBothRequired => - 'Bitte geben Sie sowohl Breite als auch Längengrad ein.'; + 'Bitte geben Sie sowohl Breiten- als auch Längengrad ein.'; @override - String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; + String get settings_locationInvalid => 'Ungültiger Breiten- oder Längengrad.'; @override String get settings_locationGPSEnable => 'GPS aktivieren'; @override String get settings_locationGPSEnableSubtitle => - 'Aktiviert GPS zur automatischen Aktualisierung des Standorts.'; + 'Ermöglicht GPS, den Standort automatisch zu aktualisieren.'; @override - String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)'; + String get settings_locationIntervalSec => 'GPS-Intervall (Sekunden)'; @override String get settings_locationIntervalInvalid => @@ -413,38 +414,38 @@ class AppLocalizationsDe extends AppLocalizations { 'Einstellungen für das Hinzufügen von Kontakten'; @override - String get settings_privacyMode => 'Privatsphäreeinstellung'; + String get settings_privacyMode => 'Privatsphärenmodus'; @override String get settings_privacyModeSubtitle => - 'Verstecken Sie Name/Ort in Ankündigungen'; + 'Name und Standort in Ankündigungen verbergen'; @override String get settings_privacyModeToggle => - 'Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.'; + 'Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.'; @override - String get settings_privacyModeEnabled => 'Datenschutzmodus aktiviert'; + String get settings_privacyModeEnabled => 'Privatsphärenmodus aktiviert'; @override - String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert'; + String get settings_privacyModeDisabled => 'Privatsphärenmodus deaktiviert'; @override String get settings_privacy => 'Datenschutzeinstellungen'; @override String get settings_privacySubtitle => - 'Steuern Sie die Informationen, die freigegeben werden.'; + 'Steuern Sie, welche Informationen freigegeben werden.'; @override String get settings_privacySettingsDescription => - 'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.'; + 'Wählen Sie aus, welche Informationen Ihr Gerät mit anderen teilt.'; @override - String get settings_denyAll => 'Alle ablehnen'; + String get settings_denyAll => 'Alles verweigern'; @override - String get settings_allowByContact => 'Zulassen durch Kontaktflaggen'; + String get settings_allowByContact => 'Nach Kontakt-Flags zulassen'; @override String get settings_allowAll => 'Alles zulassen'; @@ -453,20 +454,20 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_telemetryBaseMode => 'Telemetrie-Basismodus'; @override - String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus'; + String get settings_telemetryLocationMode => 'Telemetrie-Standortmodus'; @override String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus'; @override - String get settings_advertLocation => 'Anzeigenort'; + String get settings_advertLocation => 'Standort in Ankündigung'; @override String get settings_advertLocationSubtitle => - 'Ort in der Anzeige einbeziehen'; + 'Standort in die Ankündigung einschließen.'; @override - String get settings_multiAck => 'Mehrere Bestätigungen'; + String get settings_multiAck => 'Mehrfach-ACKs'; @override String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert'; @@ -475,27 +476,27 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_actions => 'Aktionen'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'Alle Pfade löschen'; @override String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + 'Alle Pfaddaten aus den Kontakten entfernen.'; @override - String get settings_sendAdvertisement => 'Sende Ankündigung'; + String get settings_sendAdvertisement => 'Ankündigung senden'; @override - String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; + String get settings_sendAdvertisementSubtitle => 'Präsenz jetzt senden'; @override String get settings_advertisementSent => 'Ankündigung gesendet'; @override - String get settings_syncTime => 'Zeitsynchronisierung'; + String get settings_syncTime => 'Zeit synchronisieren'; @override String get settings_syncTimeSubtitle => - 'Stelle die Gerätezeit auf die Uhrzeit des Telefons ein'; + 'Geräteuhr auf die Zeit des Telefons setzen'; @override String get settings_timeSynchronized => 'Zeit synchronisiert'; @@ -505,7 +506,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Kontakt-Liste vom Gerät neu laden'; + 'Kontaktliste vom Gerät neu laden'; @override String get settings_rebootDevice => 'Gerät neu starten'; @@ -532,18 +533,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_appDebugLog => 'App-Debug-Protokoll'; @override - String get settings_appDebugLogSubtitle => 'Anwendung Debug-Nachrichten'; + String get settings_appDebugLogSubtitle => 'App-Debug-Nachrichten'; @override String get settings_about => 'Über'; @override String settings_aboutVersion(String version) { - return 'MeshCore Open, Version $version'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => 'MeshCore Open Source Projekt 2026'; + String get settings_aboutLegalese => '2026 MeshCore Open-Source-Projekt'; @override String get settings_aboutDescription => @@ -569,10 +570,10 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_infoPublicKey => 'Öffentlicher Schlüssel'; @override - String get settings_infoContactsCount => 'Anzahl Kontakte'; + String get settings_infoContactsCount => 'Kontakte'; @override - String get settings_infoChannelCount => 'Anzahl Kanäle'; + String get settings_infoChannelCount => 'Kanäle'; @override String get settings_presets => 'Voreinstellungen'; @@ -581,7 +582,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_frequency => 'Frequenz (MHz)'; @override - String get settings_frequencyHelper => '300,00 - 2.500,00'; + String get settings_frequencyHelper => '300,0 - 2500,0'; @override String get settings_frequencyInvalid => 'Ungültige Frequenz (300-2500 MHz)'; @@ -590,30 +591,30 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_bandwidth => 'Bandbreite'; @override - String get settings_spreadingFactor => 'Verteilungsfaktor'; + String get settings_spreadingFactor => 'Spreading-Faktor'; @override String get settings_codingRate => 'Kodierungsrate'; @override - String get settings_txPower => 'TX-Leistung (dBm)'; + String get settings_txPower => 'Sendeleistung (dBm)'; @override String get settings_txPowerHelper => '0 – 22'; @override - String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; + String get settings_txPowerInvalid => 'Ungültige Sendeleistung (0-22 dBm)'; @override - String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss'; + String get settings_clientRepeat => 'Weiterleitung ohne Netzstrom'; @override String get settings_clientRepeatSubtitle => - 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; + 'Dieses Gerät kann Mesh-Pakete für andere weiterleiten'; @override String get settings_clientRepeatFreqWarning => - 'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.'; + 'Weiterleitung ohne Netzstrom erfordert 433, 869 oder 918 MHz'; @override String settings_error(String message) { @@ -624,10 +625,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_title => 'App-Einstellungen'; @override - String get appSettings_appearance => 'Aussehen'; + String get appSettings_appearance => 'Erscheinungsbild'; @override - String get appSettings_theme => 'Thema'; + String get appSettings_theme => 'Design'; @override String get appSettings_themeSystem => 'Systemstandard'; @@ -678,7 +679,7 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageNl => 'Niederländisch'; @override - String get appSettings_languageSk => 'Slowenisch'; + String get appSettings_languageSk => 'Slowakisch'; @override String get appSettings_languageBg => 'Bulgarisch'; @@ -695,7 +696,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_enableMessageTracingSubtitle => - 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + 'Detaillierte Routing- und Zeitmetadaten für Nachrichten anzeigen'; @override String get appSettings_notifications => 'Benachrichtigungen'; @@ -705,11 +706,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_enableNotificationsSubtitle => - 'Erhalte Benachrichtigungen für Nachrichten und Ankündigungen'; + 'Benachrichtigungen für Nachrichten und Ankündigungen erhalten'; @override String get appSettings_notificationPermissionDenied => - 'Erlaubnis zur Benachrichtigung verweigert'; + 'Benachrichtigungsberechtigung verweigert'; @override String get appSettings_notificationsEnabled => 'Benachrichtigungen aktiviert'; @@ -720,19 +721,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_messageNotifications => - 'Direktnachrichten Benachrichtigungen'; + 'Direktnachrichten-Benachrichtigungen'; @override String get appSettings_messageNotificationsSubtitle => - 'Zeige Benachrichtigung beim Empfang neuer Direktnachrichten'; + 'Benachrichtigung anzeigen, wenn neue Direktnachrichten eingehen'; @override String get appSettings_channelMessageNotifications => - 'Kanalnachrichten Benachrichtigungen'; + 'Kanalnachrichten-Benachrichtigungen'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Zeige Benachrichtigung beim Empfangen von Kanalnachrichten'; + 'Benachrichtigung anzeigen, wenn neue Kanalnachrichten eingehen'; @override String get appSettings_advertisementNotifications => @@ -740,33 +741,33 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Zeige Benachrichtigung, wenn neue Knoten entdeckt werden.'; + 'Benachrichtigung anzeigen, wenn neue Knoten entdeckt werden'; @override String get appSettings_messaging => 'Nachrichten'; @override String get appSettings_clearPathOnMaxRetry => - 'Lösche Pfade bei Max Wiederholungsversuchen'; + 'Pfad bei maximalen Wiederholungsversuchen löschen'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen'; + 'Kontaktpfade nach 5 fehlgeschlagenen Sendeversuchen zurücksetzen'; @override String get appSettings_pathsWillBeCleared => - 'Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.'; + 'Pfade werden nach 5 fehlgeschlagenen Wiederholungen gelöscht.'; @override String get appSettings_pathsWillNotBeCleared => - 'Die Pfade werden nicht automatisch gelöscht.'; + 'Pfade werden nicht automatisch gelöscht.'; @override String get appSettings_autoRouteRotation => 'Automatische Routenrotation'; @override String get appSettings_autoRouteRotationSubtitle => - 'Wechseln zwischen den besten Pfaden und dem Fluten'; + 'Zwischen den besten Pfaden und dem Flood-Modus wechseln'; @override String get appSettings_autoRouteRotationEnabled => @@ -822,12 +823,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Konfiguriert pro Gerät ($deviceName)'; + return 'Pro Gerät festgelegt ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Verbinde ein Gerät, um zu wählen'; + 'Zum Auswählen mit einem Gerät verbinden'; @override String get appSettings_batteryNmc => '18650 NMC (3,0–4,2 V)'; @@ -842,21 +843,21 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_mapDisplay => 'Kartendarstellung'; @override - String get appSettings_showRepeaters => 'Zeige Repeater'; + String get appSettings_showRepeaters => 'Repeater anzeigen'; @override String get appSettings_showRepeatersSubtitle => - 'Zeige Repeater-Knoten auf der Karte an'; + 'Repeater-Knoten auf der Karte anzeigen'; @override - String get appSettings_showChatNodes => 'Zeige Chat-Knoten'; + String get appSettings_showChatNodes => 'Chat-Knoten anzeigen'; @override String get appSettings_showChatNodesSubtitle => 'Chat-Knoten auf der Karte anzeigen'; @override - String get appSettings_showOtherNodes => 'Zeige andere Knoten'; + String get appSettings_showOtherNodes => 'Andere Knoten anzeigen'; @override String get appSettings_showOtherNodesSubtitle => @@ -870,18 +871,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String appSettings_timeFilterShowLast(int hours) { - return 'Zeige Knoten der letzten $hours Stunden an'; + return 'Knoten der letzten $hours Stunden anzeigen'; } @override - String get appSettings_mapTimeFilter => 'Karten Zeitfilter'; + String get appSettings_mapTimeFilter => 'Karten-Zeitfilter'; @override String get appSettings_showNodesDiscoveredWithin => 'Zeige Knoten, die innerhalb von:'; @override - String get appSettings_allTime => 'Ganzer Zeitverlauf'; + String get appSettings_allTime => 'Gesamter Zeitraum'; @override String get appSettings_lastHour => 'Letzte Stunde'; @@ -912,18 +913,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Ausgewählte Fläche (Zoom $minZoom-$maxZoom)'; + return 'Bereich ausgewählt (Zoom $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Fehlerbehebung'; + String get appSettings_debugCard => 'Debug'; @override String get appSettings_appDebugLogging => 'App-Debug-Protokollierung'; @override String get appSettings_appDebugLoggingSubtitle => - 'Protokolliere App-Debug-Nachrichten zur Fehlerbehebung'; + 'App-Debug-Nachrichten zur Fehlerbehebung protokollieren'; @override String get appSettings_appDebugLoggingEnabled => @@ -937,11 +938,11 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_title => 'Kontakte'; @override - String get contacts_noContacts => 'Noch keine Kontakte vorhanden.'; + String get contacts_noContacts => 'Noch keine Kontakte'; @override String get contacts_contactsWillAppear => - 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; + 'Kontakte werden angezeigt, wenn Geräte Ankündigungen senden.'; @override String get contacts_unread => 'Ungelesen'; @@ -951,7 +952,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String contacts_searchContacts(int number, String str) { - return 'Suche Kontakte...'; + return 'Kontakte suchen...'; } @override @@ -975,14 +976,14 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte'; + String get contacts_noUnreadContacts => 'Keine ungelesenen Kontakte'; @override String get contacts_noContactsFound => 'Keine Kontakte oder Gruppen gefunden.'; @override - String get contacts_deleteContact => 'Lösche den Kontakt'; + String get contacts_deleteContact => 'Kontakt löschen'; @override String contacts_removeConfirm(String contactName) { @@ -993,23 +994,23 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_manageRepeater => 'Repeater verwalten'; @override - String get contacts_manageRoom => 'Raum-Server verwalten'; + String get contacts_manageRoom => 'Raumserver verwalten'; @override - String get contacts_roomLogin => 'Raum-Login'; + String get contacts_roomLogin => 'Raumserver-Login'; @override - String get contacts_openChat => 'Öffne Chat'; + String get contacts_openChat => 'Chat öffnen'; @override String get contacts_editGroup => 'Gruppe bearbeiten'; @override - String get contacts_deleteGroup => 'Löschen Gruppe'; + String get contacts_deleteGroup => 'Gruppe löschen'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Löschen von \"$groupName\"?'; + return 'Gruppe \"$groupName\" löschen?'; } @override @@ -1019,16 +1020,16 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_moreOptions => 'Weitere Optionen'; @override - String get contacts_searchOpen => 'Kontakte suchen'; + String get contacts_searchOpen => 'Suche öffnen'; @override - String get contacts_searchClose => 'Erweiterte Suche'; + String get contacts_searchClose => 'Suche schließen'; @override String get contacts_groupName => 'Gruppenname'; @override - String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.'; + String get contacts_groupNameRequired => 'Der Gruppenname ist erforderlich.'; @override String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert'; @@ -1039,7 +1040,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get contacts_filterContacts => 'Filtert Kontakte...'; + String get contacts_filterContacts => 'Kontakte filtern...'; @override String get contacts_noContactsMatchFilter => @@ -1049,7 +1050,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_noMembers => 'Keine Mitglieder'; @override - String get contacts_lastSeenNow => 'kürzlich'; + String get contacts_lastSeenNow => 'gerade eben'; @override String contacts_lastSeenMinsAgo(int minutes) { @@ -1117,7 +1118,7 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_addPublicChannel => 'Öffentlichen Kanal hinzufügen'; @override - String get channels_searchChannels => 'Suche Kanäle...'; + String get channels_searchChannels => 'Kanäle suchen...'; @override String get channels_noChannelsFound => 'Keine Kanäle gefunden'; @@ -1132,7 +1133,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channels_via(String path) { - return 'via $path'; + return 'über $path'; } @override @@ -1148,11 +1149,11 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben'; @override - String get channels_deleteChannel => 'Lösche den Kanal'; + String get channels_deleteChannel => 'Kanal löschen'; @override String channels_deleteChannelConfirm(String name) { - return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; + return 'Kanal \"$name\" löschen? Dies kann nicht rückgängig gemacht werden.'; } @override @@ -1175,24 +1176,24 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_channelName => 'Kanalname'; @override - String get channels_usePublicChannel => 'Verwende öffentlichen Kanal'; + String get channels_usePublicChannel => 'Öffentlichen Kanal verwenden'; @override - String get channels_standardPublicPsk => 'Öffentliche Standard PSK'; + String get channels_standardPublicPsk => 'Öffentlicher Standard-PSK'; @override String get channels_pskHex => 'PSK (Hexadezimal)'; @override - String get channels_generateRandomPsk => 'Zufällige PSK generieren'; + String get channels_generateRandomPsk => 'Zufälligen PSK generieren'; @override String get channels_enterChannelName => - 'Bitte geben Sie einen Kanalnamen ein.'; + 'Bitte geben Sie einen Kanalnamen ein'; @override String get channels_pskMustBe32Hex => - 'Die PSK muss 32 hexadezimale Zeichen haben.'; + 'Der PSK muss 32 hexadezimale Zeichen haben.'; @override String channels_channelAdded(String name) { @@ -1201,7 +1202,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channels_editChannelTitle(int index) { - return 'Bearbeiteter Kanal $index'; + return 'Kanal $index bearbeiten'; } @override @@ -1212,7 +1213,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_cyr2latCompressionDscr => - 'Ersetzt einige kyrillische Zeichen durch lateinische Zeichen, wenn sie gesendet werden.'; + 'Ersetzt beim Senden einige kyrillische Zeichen durch lateinische Zeichen.'; @override String get channels_cyr2latSettingsHeading => 'Cyr2Lat-Einstellungen'; @@ -1222,7 +1223,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_cyr2latSettingsDscr => - 'JSON-Konfiguration für die Zeichenersetzung bearbeiten'; + 'JSON-Konfiguration für Zeichenersetzungen bearbeiten'; @override String get channels_cyr2latSettingsDialogHint => 'JSON-Ersetzungstabelle'; @@ -1272,7 +1273,7 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_publicChannelAdded => 'Öffentlicher Kanal hinzugefügt'; @override - String get channels_sortBy => 'Sortiere nach'; + String get channels_sortBy => 'Sortieren nach'; @override String get channels_sortManual => 'Manuell'; @@ -1281,7 +1282,7 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_sortAZ => 'A bis Z'; @override - String get channels_sortLatestMessages => 'Letzte Nachrichten'; + String get channels_sortLatestMessages => 'Neueste Nachrichten'; @override String get channels_sortUnread => 'Ungelesen'; @@ -1336,14 +1337,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'Sende eine Nachricht an $contactName'; + return 'Nachricht an $contactName senden'; } @override - String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.'; + String get chat_sendMessageToStart => + 'Senden Sie eine Nachricht, um zu beginnen.'; @override - String get chat_originalMessageNotFound => 'Originalmeldung nicht gefunden'; + String get chat_originalMessageNotFound => 'Originalnachricht nicht gefunden'; @override String chat_replyingTo(String name) { @@ -1359,11 +1361,11 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_location => 'Ort'; @override - String get chat_typeMessage => 'Eine Nachricht eingeben...'; + String get chat_typeMessage => 'Nachricht eingeben...'; @override String chat_messageTooLong(int maxBytes) { - return 'Nachricht ist zu lang (max $maxBytes Bytes).'; + return 'Nachricht zu lang (max. $maxBytes Bytes).'; } @override @@ -1373,18 +1375,18 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_messageDeleted => 'Nachricht gelöscht'; @override - String get chat_retryingMessage => 'Versuche es erneut.'; + String get chat_retryingMessage => 'Nachricht wird erneut gesendet.'; @override String chat_retryCount(int current, int max) { - return 'Versuche $current/$max'; + return 'Wiederholen $current/$max'; } @override String get chat_sendGif => 'GIF senden'; @override - String get chat_reply => 'Beantworten'; + String get chat_reply => 'Antworten'; @override String get chat_addReaction => 'Reaktion hinzufügen'; @@ -1444,11 +1446,11 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_bleCopied => 'BLE-Protokoll kopiert'; @override - String get debugLog_noEntries => 'No Debug-Protokolle noch verfügbar'; + String get debugLog_noEntries => 'Noch keine Debug-Protokolle vorhanden'; @override String get debugLog_enableInSettings => - 'Aktivieren Sie das App-Debug-Logging in den Einstellungen'; + 'App-Debug-Protokollierung in den Einstellungen aktivieren'; @override String get debugLog_frames => 'Rahmen'; @@ -1535,77 +1537,76 @@ class AppLocalizationsDe extends AppLocalizations { String get routing_title => 'Routenplanung'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Automatisch'; @override - String get routing_modeFlood => 'Überschwemmung'; + String get routing_modeFlood => 'Flut'; @override - String get routing_modeManual => 'Handbuch'; + String get routing_modeManual => 'Manuell'; @override String get routing_modeAutoHint => - 'Wählt automatisch den bekanntesten Pfad aus und verwendet eine Flutungsmethode, wenn kein Pfad bekannt ist.'; + 'Wählt automatisch den besten bekannten Pfad und wechselt auf Flut, wenn keiner bekannt ist.'; @override String get routing_modeFloodHint => - 'Übertragung über alle Repeater. Die zuverlässigste Methode, jedoch mit höherem Datenverbrauch.'; + 'Über alle Repeater senden. Am zuverlässigsten, aber mit höherem Funkzeitbedarf.'; @override String get routing_modeManualHint => - 'Sendet immer genau den von Ihnen festgelegten Weg.'; + 'Sendet immer genau den von Ihnen festgelegten Pfad.'; @override String get routing_currentRoute => 'Aktuelle Route'; @override - String get routing_directNoHops => 'Direkt – ohne Zwischenverstärkung'; + String get routing_directNoHops => 'Direkt - keine Repeater-Sprünge'; @override String get routing_noPathYet => - 'Noch kein Pfad gefunden. Die Nachricht wird gesendet, bis ein Weg entdeckt wurde.'; + 'Noch kein Pfad gefunden. Die nächste Nachricht wird geflutet, bis eine Route entdeckt ist.'; @override - String get routing_floodBroadcast => 'Übertragung über jeden Repeater'; + String get routing_floodBroadcast => 'Über alle Repeater senden'; @override String get routing_editPath => 'Pfad bearbeiten'; @override - String get routing_forgetPath => 'Vergiss den Weg'; + String get routing_forgetPath => 'Pfad vergessen'; @override - String get routing_knownPaths => 'Bekannte Routen'; + String get routing_knownPaths => 'Bekannte Pfade'; @override String get routing_knownPathsHint => - 'Wählen Sie den Pfad, um zu diesem zu wechseln.'; + 'Tippen Sie auf einen Pfad, um zu ihm zu wechseln.'; @override - String get routing_inUse => 'Im Gebrauch'; + String get routing_inUse => 'In Verwendung'; @override - String get routing_qualityStrong => 'Ein starker erster Sprung'; + String get routing_qualityStrong => 'Starker erster Hop'; @override - String get routing_qualityGood => 'Ein guter erster Schritt'; + String get routing_qualityGood => 'Guter erster Hop'; @override - String get routing_qualityFair => 'Erster erfolgreicher Schritt'; + String get routing_qualityFair => 'Ausreichender erster Hop'; @override - String get routing_qualityWorked => 'Hat erfolgreich geliefert'; + String get routing_qualityWorked => 'Hat zugestellt'; @override - String get routing_qualityFlood => - 'Information erhalten durch Nachrichten über die Überschwemmung'; + String get routing_qualityFlood => 'Per Flood gehört'; @override String get routing_qualityUntested => 'Nicht getestet'; @override String routing_lastWorked(String when) { - return 'war beschäftigt $when'; + return 'funktionierte $when'; } @override @@ -1613,41 +1614,39 @@ class AppLocalizationsDe extends AppLocalizations { @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes zugestellt, $failures fehlgeschlagen'; } @override - String get routing_floodDelivery => 'Lieferung bei Überschwemmung'; + String get routing_floodDelivery => 'Flood-Zustellung'; @override String get pathEditor_title => 'Pfad erstellen'; @override String pathEditor_hopCounter(int count) { - return '$count von 64 Hopfengewächsen'; + return '$count von 64 Sprüngen'; } @override String get pathEditor_noHops => - 'Noch keine Hopfen hinzugefügt. Klicken Sie auf die Schaltflächen unten, um sie nacheinander hinzuzufügen, oder speichern Sie die Rezepter ohne Hopfen, um sie direkt zu versenden.'; + 'Noch keine Sprünge hinzugefügt. Tippen Sie unten auf Repeater, um sie in Reihenfolge hinzuzufügen, oder speichern Sie ohne Sprünge, um direkt zu senden.'; @override - String get pathEditor_addHops => - 'Fügen Sie die Hopfen in der richtigen Reihenfolge hinzu.'; + String get pathEditor_addHops => 'Sprünge in Reihenfolge hinzufügen'; @override - String get pathEditor_searchRepeaters => - 'Suche nach wiederholten Nachrichten'; + String get pathEditor_searchRepeaters => 'Repeater suchen'; @override - String get pathEditor_advancedHex => 'Fortgeschritten: Roh-Hex-Pfad'; + String get pathEditor_advancedHex => 'Erweitert: roher Hex-Pfad'; @override String get pathEditor_hexLabel => 'Hex-Präfixe'; @override String get pathEditor_hexHelper => - 'Zwei Hexadezimalzeichen pro Sprung, getrennt durch Kommas'; + 'Zwei Hex-Zeichen pro Sprung, durch Kommas getrennt'; @override String pathEditor_invalidTokens(String tokens) { @@ -1655,13 +1654,13 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get pathEditor_tooManyHops => 'Maximal 64 Hopfengreifer'; + String get pathEditor_tooManyHops => 'Maximal 64 Sprünge'; @override - String get pathEditor_usePath => 'Verwenden Sie diesen Pfad.'; + String get pathEditor_usePath => 'Diesen Pfad verwenden'; @override - String get pathEditor_removeHop => 'Hop entfernen'; + String get pathEditor_removeHop => 'Sprung entfernen'; @override String get pathEditor_unknownHop => 'Unbekannter Repeater'; @@ -1715,7 +1714,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get chat_markAsUnread => 'Als nicht gelesen markieren'; + String get chat_markAsUnread => 'Als ungelesen markieren'; @override String get chat_newMessages => 'Neue Nachrichten'; @@ -1742,37 +1741,37 @@ class AppLocalizationsDe extends AppLocalizations { String get map_title => 'Karte'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Nach Knotenname oder ID suchen'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Aktivität'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Kürzlich'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Veraltet'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Sichtbar'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Versteckt'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Auf Knoten zentrieren'; @override String get map_details => 'Details'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Kein GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Keine passenden Knoten gefunden'; @override String get map_lineOfSight => 'Sichtlinie'; @@ -1798,13 +1797,13 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get map_chat => 'Benutzer'; + String get map_chat => 'Chat'; @override - String get map_repeater => 'Wiederholungseinheit'; + String get map_repeater => 'Repeater'; @override - String get map_room => 'Raum'; + String get map_room => 'Raumserver'; @override String get map_sensor => 'Sensor'; @@ -1819,7 +1818,7 @@ class AppLocalizationsDe extends AppLocalizations { String get map_pinPublic => 'Kennzeichnung (Öffentlich)'; @override - String get map_lastSeen => 'Letzte Sichtung'; + String get map_lastSeen => 'Zuletzt gesehen'; @override String get map_disconnectConfirm => @@ -1835,43 +1834,43 @@ class AppLocalizationsDe extends AppLocalizations { String get map_flags => 'Flaggen'; @override - String get map_type => 'Type'; + String get map_type => 'Typ'; @override - String get map_path => 'Path'; + String get map_path => 'Pfad'; @override - String get map_location => 'Location'; + String get map_location => 'Standort'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => 'Geschätzter Standort'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => 'Öffentlicher Schlüssel'; @override - String get map_publicKeyPrefixHint => 'e.g. ab12'; + String get map_publicKeyPrefixHint => 'z. B. ab12'; @override - String get map_shareMarkerHere => 'Teilen Sie den Marker hier.'; + String get map_shareMarkerHere => 'Marker hier teilen'; @override String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen'; @override - String get map_pinLabel => 'Pin Name'; + String get map_pinLabel => 'Pin-Beschriftung'; @override - String get map_label => 'Etikett'; + String get map_label => 'Beschriftung'; @override String get map_pointOfInterest => 'Punkt von Interesse'; @override - String get map_sendToContact => 'Senden an Kontakt'; + String get map_sendToContact => 'An Kontakt senden'; @override - String get map_sendToChannel => 'Senden an Kanal'; + String get map_sendToChannel => 'An Kanal senden'; @override String get map_noChannelsAvailable => 'Keine Kanäle verfügbar'; @@ -1881,15 +1880,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Sie werden kurz darauf einen Ort in $channelLabel teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.'; + return 'Sie sind dabei, einen Standort in $channelLabel zu teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.'; } @override String get map_connectToShareMarkers => - 'Verbinde ein Gerät, um Marker zu teilen'; + 'Mit einem Gerät verbinden, um Marker zu teilen'; @override - String get map_filterNodes => 'Knotenfilter'; + String get map_filterNodes => 'Knoten filtern'; @override String get map_nodeTypes => 'Knotentypen'; @@ -1904,44 +1903,43 @@ class AppLocalizationsDe extends AppLocalizations { String get map_otherNodes => 'Andere Knoten'; @override - String get map_showOverlaps => 'Überlappungen der Repeater-Taste'; + String get map_showOverlaps => 'Repeater-Schlüsselüberlappungen'; @override String get map_keyPrefix => 'Schlüsselpräfix'; @override - String get map_filterByKeyPrefix => 'Filter nach Schlüsselpräfix'; + String get map_filterByKeyPrefix => 'Nach Schlüsselpräfix filtern'; @override - String get map_publicKeyPrefix => 'Schlüsselpräfix'; + String get map_publicKeyPrefix => 'Präfix des öffentlichen Schlüssels'; @override String get map_markers => 'Marker'; @override - String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker'; + String get map_showSharedMarkers => 'Gemeinsam genutzte Marker anzeigen'; @override - String get map_showGuessedLocations => - 'Zeige die vermuteten Knotenpositionen'; + String get map_showGuessedLocations => 'Vermutete Knotenstandorte anzeigen'; @override - String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen'; + String get map_showDiscoveryContacts => 'Entdeckte Kontakte anzeigen'; @override - String get map_guessedLocation => 'Geschätzter Ort'; + String get map_guessedLocation => 'Vermuteter Standort'; @override String get map_lastSeenTime => 'Letzte Sichtung'; @override - String get map_sharedPin => 'Gemeinsames Passwort'; + String get map_sharedPin => 'Gemeinsamer Pin'; @override - String get map_sharedAt => 'Geteilt'; + String get map_sharedAt => 'Geteilt am'; @override - String get map_joinRoom => 'Beitreten Sie dem Raum'; + String get map_joinRoom => 'Raum beitreten'; @override String get map_manageRepeater => 'Repeater verwalten'; @@ -1958,7 +1956,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Auf dem gleichen Pfad zurückkehren.'; @override - String get map_removeLast => 'Letztes Entfernen'; + String get map_removeLast => 'Letztes entfernen'; @override String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.'; @@ -1975,11 +1973,11 @@ class AppLocalizationsDe extends AppLocalizations { 'Keine Kacheln für diese Region zum Herunterladen verfügbar.'; @override - String get mapCache_downloadTilesTitle => 'Herunterladen von Kacheln'; + String get mapCache_downloadTilesTitle => 'Kacheln herunterladen'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Laden $count Kacheln für den Offline-Bereich herunter?'; + return '$count Kacheln für die Offline-Nutzung herunterladen?'; } @override @@ -1987,20 +1985,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'Zwischengespeicherte $count Kacheln'; + return '$count Kacheln zwischengespeichert'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Zwischengespeicherte $downloaded Kacheln ($failed fehlgeschlagen)'; + return '$downloaded Kacheln zwischengespeichert ($failed fehlgeschlagen)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Leere Offline-Cache'; + String get mapCache_clearOfflineCacheTitle => 'Offline-Cache leeren'; @override String get mapCache_clearOfflineCachePrompt => - 'Alle zwischengespeicherten Kartenraster entfernen?'; + 'Alle zwischengespeicherten Kartenkacheln entfernen?'; @override String get mapCache_offlineCacheCleared => 'Offline-Cache gelöscht'; @@ -2009,13 +2007,13 @@ class AppLocalizationsDe extends AppLocalizations { String get mapCache_noAreaSelected => 'Kein Bereich ausgewählt'; @override - String get mapCache_cacheArea => 'Zwischenspeicherbereich'; + String get mapCache_cacheArea => 'Bereich zwischenspeichern'; @override String get mapCache_useCurrentView => 'Aktuelle Ansicht verwenden'; @override - String get mapCache_zoomRange => 'Zoom Bereich'; + String get mapCache_zoomRange => 'Zoombereich'; @override String mapCache_estimatedTiles(int count) { @@ -2028,7 +2026,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get mapCache_downloadTilesButton => 'Herunterladen von Kacheln'; + String get mapCache_downloadTilesButton => 'Kacheln herunterladen'; @override String get mapCache_clearCacheButton => 'Cache leeren'; @@ -2053,17 +2051,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String time_minutesAgo(int minutes) { - return '$minutes Minuten her'; + return 'vor $minutes Min.'; } @override String time_hoursAgo(int hours) { - return '$hours Stunden her'; + return 'vor $hours Std.'; } @override String time_daysAgo(int days) { - return '$days Tage/Tage zuvor'; + return 'vor $days Tagen'; } @override @@ -2167,7 +2165,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_reload => 'Neu laden'; @override - String get common_clear => 'Löschen'; + String get common_clear => 'Leeren'; @override String get path_currentPathLabel => 'Aktueller Pfad'; @@ -2189,7 +2187,7 @@ class AppLocalizationsDe extends AppLocalizations { String get room_guest => 'Informationen zum Room Server'; @override - String get repeater_managementTools => 'Verwaltungs-Tools'; + String get repeater_managementTools => 'Verwaltungstools'; @override String get repeater_guestTools => 'Gastwerkzeuge'; @@ -2199,20 +2197,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Status, Statistiken und Nachbarn anzeigen'; + 'Repeater-Status, Statistiken und Nachbarn anzeigen'; @override String get repeater_telemetry => 'Telemetrie'; @override String get repeater_telemetrySubtitle => - 'Sensordaten und Systemwerte anzeigen'; + 'Telemetriedaten und Systemstatistiken anzeigen'; @override - String get repeater_cli => 'Befehlszeilen-Schnittstelle'; + String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Sende Befehle an den Repeater'; + String get repeater_cliSubtitle => 'Befehle an den Repeater senden'; @override String get repeater_neighbors => 'Nachbarn'; @@ -2224,7 +2222,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_settings => 'Einstellungen'; @override - String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren'; + String get repeater_settingsSubtitle => 'Repeater-Parameter konfigurieren'; @override String get repeater_clockSyncAfterLogin => @@ -2235,17 +2233,16 @@ class AppLocalizationsDe extends AppLocalizations { 'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.'; @override - String get repeater_statusTitle => 'Repeaterstatus'; + String get repeater_statusTitle => 'Repeater-Status'; @override - String get repeater_routingMode => 'Routenmodus'; + String get repeater_routingMode => 'Routing-Modus'; @override String get repeater_refresh => 'Aktualisieren'; @override - String get repeater_statusRequestTimeout => - 'Statusanfrage durch Timeout fehlgeschlagen.'; + String get repeater_statusRequestTimeout => 'Statusanfrage abgelaufen.'; @override String repeater_errorLoadingStatus(String error) { @@ -2253,7 +2250,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_systemInformation => 'Systeminformation'; + String get repeater_systemInformation => 'Systeminformationen'; @override String get repeater_battery => 'Akku'; @@ -2262,16 +2259,16 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_clockAtLogin => 'Uhr (bei Anmeldung)'; @override - String get repeater_uptime => 'Verfügbarkeit'; + String get repeater_uptime => 'Betriebszeit'; @override String get repeater_queueLength => 'Warteschlangenlänge'; @override - String get repeater_debugFlags => 'Fehlerbehebungsoptionen'; + String get repeater_debugFlags => 'Debug-Flags'; @override - String get repeater_radioStatistics => 'Funk-Statistik'; + String get repeater_radioStatistics => 'Funkstatistiken'; @override String get repeater_lastRssi => 'Letzter RSSI'; @@ -2315,17 +2312,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Gesamt: $total, Flut: $flood, Direkt: $direct'; + return 'Gesamt: $total, Flood: $flood, Direkt: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Gesamt: $total, Flut: $flood, Direkt: $direct'; + return 'Gesamt: $total, Flood: $flood, Direkt: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Flut: $flood, Direkt: $direct'; + return 'Flood: $flood, Direkt: $direct'; } @override @@ -2334,13 +2331,13 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Repeater Einstellungen'; + String get repeater_settingsTitle => 'Repeater-Einstellungen'; @override - String get repeater_basicSettings => 'Grundlegende Einstellungen'; + String get repeater_basicSettings => 'Grundeinstellungen'; @override - String get repeater_repeaterName => 'Name des Repeater'; + String get repeater_repeaterName => 'Repeatername'; @override String get repeater_repeaterNameHelper => 'Anzeigename für diesen Repeater'; @@ -2349,17 +2346,16 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_adminPassword => 'Admin-Passwort'; @override - String get repeater_adminPasswordHelper => 'Vollzugriffspasswort'; + String get repeater_adminPasswordHelper => 'Passwort für Vollzugriff'; @override String get repeater_guestPassword => 'Gast-Passwort'; @override - String get repeater_guestPasswordHelper => - 'Schreibgeschütztes Zugriffspasswort'; + String get repeater_guestPasswordHelper => 'Passwort für Lesezugriff'; @override - String get repeater_radioSettings => 'Funk Einstellungen'; + String get repeater_radioSettings => 'Funk-Einstellungen'; @override String get repeater_frequencyMhz => 'Frequenz (MHz)'; @@ -2368,7 +2364,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_frequencyHelper => '300–2500 MHz'; @override - String get repeater_txPower => 'TX Power'; + String get repeater_txPower => 'Sendeleistung'; @override String get repeater_txPowerHelper => '1-30 dBm'; @@ -2377,13 +2373,13 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_bandwidth => 'Bandbreite'; @override - String get repeater_spreadingFactor => 'Verteilungsfaktor'; + String get repeater_spreadingFactor => 'Spreading-Faktor'; @override String get repeater_codingRate => 'Kodierungsrate'; @override - String get repeater_locationSettings => 'Standort Einstellungen'; + String get repeater_locationSettings => 'Standorteinstellungen'; @override String get repeater_latitude => 'Breitengrad'; @@ -2405,28 +2401,26 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_packetForwardingSubtitle => - 'Aktivieren Sie den Repeater, um Pakete weiterzuleiten.'; + 'Den Repeater aktivieren, um Pakete weiterzuleiten'; @override String get repeater_guestAccess => 'Gastzugriff'; @override - String get repeater_guestAccessSubtitle => - 'Gast-Zugriff mit beschränkten Rechten zulassen'; + String get repeater_guestAccessSubtitle => 'Nur-Lese-Gastzugriff erlauben'; @override - String get repeater_privacyMode => 'Privatsphäreeinstellung'; + String get repeater_privacyMode => 'Privatsphärenmodus'; @override String get repeater_privacyModeSubtitle => - 'Verstecken Sie Name/Ort in Ankündigungen'; + 'Name und Standort in Ankündigungen verbergen'; @override String get repeater_advertisementSettings => 'Ankündigungseinstellungen'; @override - String get repeater_localAdvertInterval => - 'Intervall der lokalen Ankündigungen'; + String get repeater_localAdvertInterval => 'Lokales Ankündigungsintervall'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2434,8 +2428,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => - 'Intervall der gefluteten Ankündigungen'; + String get repeater_floodAdvertInterval => 'Flood-Ankündigungsintervall'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2444,16 +2437,16 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervall der verschlüsselten Ankündigung'; + 'Verschlüsseltes Ankündigungsintervall'; @override String get repeater_dangerZone => 'Gefahrenzone'; @override - String get repeater_rebootRepeater => 'Neustart Repeater'; + String get repeater_rebootRepeater => 'Repeater neu starten'; @override - String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.'; + String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten'; @override String get repeater_rebootRepeaterConfirm => @@ -2461,22 +2454,22 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_regenerateIdentityKey => - 'Schlüssel für die Identitätswiederherstellung'; + 'Identitätsschlüssel neu erzeugen'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Neuen öffentlichen/privaten Schlüsselpaar generieren'; + 'Neues öffentlich-privates Schlüsselpaar erzeugen'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Dies generiert eine neue Identität für den Repeater. Fortfahren?'; + 'Dies erzeugt eine neue Identität für den Repeater. Fortfahren?'; @override String get repeater_eraseFileSystem => 'Dateisystem löschen'; @override String get repeater_eraseFileSystemSubtitle => - 'Formatiere die Repeater-Dateisystemdatei'; + 'Das Dateisystem des Repeaters formatieren'; @override String get repeater_eraseFileSystemConfirm => @@ -2484,7 +2477,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_eraseSerialOnly => - 'Löschen ist nur über die serielle Konsole möglich.'; + 'Löschen ist nur über die serielle Konsole verfügbar.'; @override String repeater_commandSent(String command) { @@ -2503,35 +2496,34 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_settingsSaved => 'Einstellungen erfolgreich gespeichert'; @override - String get repeater_rxGain => 'Erhöhter RX-Gewinn'; + String get repeater_rxGain => 'Verstärkter RX-Gewinn'; @override String get repeater_rxGainHelper => - 'Höhere Empfindlichkeit, höherer Stromverbrauch (nur für SX1262/SX1268)'; + 'Höhere Empfindlichkeit, höherer Stromverbrauch (nur SX1262/SX1268)'; @override - String get repeater_refreshRxGain => 'Erneuerung des verstärkten RX-Effekts'; + String get repeater_refreshRxGain => 'Verstärkten RX-Gewinn aktualisieren'; @override - String get repeater_multiAcks => 'Mehrere Bestätigungen'; + String get repeater_multiAcks => 'Mehrfach-ACKs'; @override String get repeater_multiAcksSubtitle => - 'Nachrichten über verschiedene Pfade senden, um die Zustellbarkeit zu verbessern.'; + 'Nachrichten über mehrere Pfade bestätigen, um die Zustellung zu verbessern'; @override - String get repeater_refreshMultiAcks => - 'Mehrere Bestätigungen neu senden/aktualisieren'; + String get repeater_refreshMultiAcks => 'Mehrfach-ACKs aktualisieren'; @override - String get repeater_networkHealth => 'Netzwerkgesundheit'; + String get repeater_networkHealth => 'Netzwerkzustand'; @override - String get repeater_loopDetect => 'Erkennung von Schleifen'; + String get repeater_loopDetect => 'Schleifenerkennung'; @override String get repeater_loopDetectHelper => - 'Erstellen Sie \"Flood\"-Pakete, die so aussehen, als ob sie Schleifen erzeugen.'; + 'Flood-Pakete verwerfen, die wie Routing-Schleifen aussehen'; @override String get repeater_loopDetectOff => 'Aus'; @@ -2543,10 +2535,10 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_loopDetectModerate => 'mäßig'; @override - String get repeater_loopDetectStrict => 'streng'; + String get repeater_loopDetectStrict => 'Streng'; @override - String get repeater_dutyCycle => 'Betriebsdauer'; + String get repeater_dutyCycle => 'Duty-Cycle'; @override String get repeater_dutyCycleHelper => @@ -2558,29 +2550,29 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_ownerInfo => 'Information zum Betreiber'; + String get repeater_ownerInfo => 'Betreiberinformationen'; @override String get repeater_ownerInfoHelper => - 'Öffentliche Metadaten für dieses Gerät'; + 'Öffentliche Metadaten für diesen Repeater'; @override String get repeater_refreshOwnerInfo => - 'Aktualisieren Sie die Informationen zum Betreiber'; + 'Betreiberinformationen aktualisieren'; @override - String get repeater_floodMax => 'Max-Hops-Flut'; + String get repeater_floodMax => 'Maximale Flood-Sprünge'; @override String get repeater_floodMaxHelper => - 'Maximale Anzahl an Hop-Paketen, die ein einzelnes Paket durchlaufen kann (0-64)'; + 'Maximale Anzahl von Sprüngen, die ein Flood-Paket zurücklegen darf (0-64)'; @override - String get repeater_advancedSettings => 'Fortgeschritten'; + String get repeater_advancedSettings => 'Erweitert'; @override String get repeater_advancedSettingsSubtitle => - 'Regler für erfahrene Bediener'; + 'Feinabstimmung für erfahrene Betreiber'; @override String get repeater_pathHashMode => 'Hash-Modus für Pfade'; @@ -2590,62 +2582,60 @@ class AppLocalizationsDe extends AppLocalizations { 'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade – ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.'; @override - String get repeater_txDelay => 'Verzögerung bei Flood TX'; + String get repeater_txDelay => 'Flood-TX-Verzögerung'; @override String get repeater_txDelayHelper => - 'Wiederholung des Abstands für Hochwasser-Verkehr, als Multiplikator der Übertragungszeit des Pakets (0-2, Standardwert 0,5). Höherer Wert = weniger Kollisionen, aber langsamere Übertragung.'; + 'Abstand für Flood-Verkehr als Faktor der Paket-Sendezeit (0-2, Standard 0,5). Höher = weniger Kollisionen, aber langsamere Zustellung.'; @override - String get repeater_directTxDelay => 'Direkter TX-Verzögerung'; + String get repeater_directTxDelay => 'Direkte TX-Verzögerung'; @override String get repeater_directTxDelayHelper => - 'Die Übertragungsrate für direkten (nicht-fluten) Datenverkehr wird als Vielfaches der Übertragungszeit des Pakets festgelegt (0-2, Standardwert 0,3).'; + 'Abstand für direkten (Nicht-Flood-)Verkehr als Faktor der Paket-Sendezeit (0-2, Standard 0,3).'; @override - String get repeater_intThresh => 'Grenzwert für Störungen'; + String get repeater_intThresh => 'Interferenzschwelle'; @override String get repeater_intThreshHelper => - 'Der Schwellenwert wird an die Rauschpegel-Kalibrierung des Radios angepasst, sodass Störungen über diesem Wert abgefangen werden. 0 deaktiviert – erhöhen Sie diesen Wert nur, wenn Sie in einem verrauschten Frequenzbereich RX-Fehler feststellen.'; + 'Schwelle für die Rauschboden-Kalibrierung des Radios; ignoriert Interferenzen oberhalb dieses Werts. 0 deaktiviert - nur erhöhen, wenn in einem lauten Band RX-Fehler auftreten.'; @override - String get repeater_agcResetInterval => - 'Intervall für die Rücksetzung von AGC'; + String get repeater_agcResetInterval => 'AGC-Reset-Intervall'; @override String get repeater_agcResetIntervalHelper => - 'Wie oft sollte die automatische Verstärkungskontrolle des Radios zurückgesetzt werden, um von einem Zustand mit zu hoher Verstärkung wieder in einen normalen Zustand zu gelangen? Die Einstellung „Sekunden“ ermöglicht eine Rücksetzung alle 4 Sekunden. Die Einstellung „0“ deaktiviert die periodische Rücksetzung.'; + 'Wie oft die automatische Verstärkungsregelung zurückgesetzt werden soll, um aus einem festgefahrenen Verstärkungszustand zu kommen. Sekunden, auf ein Vielfaches von 4 abgerundet. 0 deaktiviert periodische Resets.'; @override String get repeater_actionsTitle => 'Aktionen'; @override - String get repeater_sendAdvert => 'Flood-Werbung versenden'; + String get repeater_sendAdvert => 'Flood-Ankündigung senden'; @override String get repeater_sendAdvertSubtitle => - 'Eine Werbekampagne für Überschwemmungen über das Netzwerk verbreiten.'; + 'Eine Flood-Ankündigung über das Netzwerk senden'; @override - String get repeater_sendAdvertZeroHop => - 'Versenden Sie eine Anzeige ohne Zwischenvermittler.'; + String get repeater_sendAdvertZeroHop => 'Zero-Hop-Ankündigung senden'; @override String get repeater_sendAdvertZeroHopSubtitle => - 'Eine Werbekampagne mit einem einzigen Sender (ohne Weiterleitung) senden.'; + 'Eine Ein-Hop-Ankündigung ohne Weiterleiter senden'; @override String get repeater_clockSync => 'Uhr jetzt synchronisieren'; @override String get repeater_clockSyncSubtitle => - 'Übertragen Sie die Uhrzeit Ihres Telefons an den Repeater.'; + 'Die Zeit Ihres Telefons an den Repeater übertragen'; @override String repeater_actionSucceeded(String action) { - return '$action war erfolgreich'; + return '$action erfolgreich'; } @override @@ -2655,7 +2645,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_settingsSavedRebootNeeded => - 'Einstellungen gespeichert – Repeater neu starten, um die Änderungen anzuwenden.'; + 'Einstellungen gespeichert - starten Sie den Repeater neu, um sie anzuwenden'; @override String repeater_settingsPartialFailure(String failures) { @@ -2669,29 +2659,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_refreshBasicSettings => - 'Grundlegende Einstellungen aktualisieren'; + 'Grundeinstellungen aktualisieren'; @override String get repeater_refreshRadioSettings => - 'Radio-Einstellungen aktualisieren'; + 'Funk-Einstellungen aktualisieren'; @override String get repeater_refreshTxPower => 'Sendeleistung aktualisieren'; @override String get repeater_refreshPacketForwarding => - 'Aktualisieren Paketweiterleitung'; + 'Paketweiterleitung aktualisieren'; @override - String get repeater_refreshGuestAccess => 'Aktualisieren Sie den Gastzugriff'; + String get repeater_refreshGuestAccess => 'Gastzugriff aktualisieren'; @override - String get repeater_refreshPrivacyMode => - 'Wiederherstellen des Datenschutzzustands'; + String get repeater_refreshPrivacyMode => 'Privatsphärenmodus aktualisieren'; @override String repeater_refreshed(String label) { - return '$label wurde aktualisiert'; + return '$label aktualisiert'; } @override @@ -2700,39 +2689,38 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_cliTitle => - 'Befehlszeilen-Schnittstelle (CLI) für Repeater'; + String get repeater_cliTitle => 'Repeater-CLI'; @override - String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; + String get repeater_debugNextCommand => 'Nächsten Befehl debuggen'; @override - String get repeater_commandHelp => 'Hilfe'; + String get repeater_commandHelp => 'Befehls-Hilfe'; @override - String get repeater_clearHistory => 'Löschen der Historie'; + String get repeater_clearHistory => 'Verlauf löschen'; @override - String get repeater_noCommandsSent => 'Noch keine Befehle gesendet.'; + String get repeater_noCommandsSent => 'Noch keine Befehle gesendet'; @override String get repeater_typeCommandOrUseQuick => 'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle'; @override - String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...'; + String get repeater_enterCommandHint => 'Befehl eingeben...'; @override - String get repeater_previousCommand => 'Vorhergehende Aktion'; + String get repeater_previousCommand => 'Vorheriger Befehl'; @override - String get repeater_nextCommand => 'Nächste Aktion'; + String get repeater_nextCommand => 'Nächster Befehl'; @override String get repeater_enterCommandFirst => 'Geben Sie zuerst einen Befehl ein'; @override - String get repeater_cliCommandFrameTitle => 'CLI-Befehlsfenster'; + String get repeater_cliCommandFrameTitle => 'CLI-Befehlsframe'; @override String repeater_cliCommandError(String error) { @@ -2740,13 +2728,13 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Name erhalten'; + String get repeater_cliQuickGetName => 'Name abrufen'; @override - String get repeater_cliQuickGetRadio => 'Radio empfangen'; + String get repeater_cliQuickGetRadio => 'Funkdaten abrufen'; @override - String get repeater_cliQuickGetTx => 'Erhalte TX'; + String get repeater_cliQuickGetTx => 'TX abrufen'; @override String get repeater_cliQuickNeighbors => 'Nachbarn'; @@ -2755,16 +2743,16 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliQuickVersion => 'Version'; @override - String get repeater_cliQuickAdvertise => 'Ankündigungen'; + String get repeater_cliQuickAdvertise => 'Ankündigen'; @override String get repeater_cliQuickClock => 'Uhr'; @override - String get repeater_cliQuickClockSync => 'Uhr Synchronisieren'; + String get repeater_cliQuickClockSync => 'Uhr synchronisieren'; @override - String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn'; + String get repeater_cliQuickDiscovery => 'Nachbarn entdecken'; @override String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; @@ -2794,7 +2782,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetTx => - 'Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)'; + 'Legt die LoRa-Sendeleistung in dBm fest. (Neustart erforderlich, um die Änderungen anzuwenden)'; @override String get repeater_cliHelpSetRepeat => @@ -2806,7 +2794,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetFloodMax => - 'Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)'; + 'Legt die maximale Anzahl an Sprüngen für eingehende Flood-Pakete fest (wenn >= max, wird das Paket nicht weitergeleitet)'; @override String get repeater_cliHelpSetIntThresh => @@ -2853,7 +2841,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetTxDelay => - 'Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).'; + 'Legt einen Faktor fest, der mit der Sendezeit eines Flood-Mode-Pakets und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).'; @override String get repeater_cliHelpSetDirectTxDelay => @@ -3185,7 +3173,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpGetFloodMax => - 'Zeigt die maximale Anzahl von Überschwemmungsphasen an.'; + 'Zeigt die maximale Anzahl von Sprüngen für Flood-Pakete an.'; @override String get repeater_cliHelpGetOwnerInfo => @@ -3273,11 +3261,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpRegionListAllowed => - 'Nennt die Regionen, die Überschwemmungsverkehr zulassen.'; + 'Nennt die Regionen, die Flood-Verkehr zulassen.'; @override String get repeater_cliHelpRegionListDenied => - 'Auflistung von Regionen, die den Verkehr aufgrund von Überschwemmungen verbieten.'; + 'Nennt die Regionen, die Flood-Verkehr verbieten.'; @override String get repeater_cliHelpStatsPackets => @@ -4303,7 +4291,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get radioStats_notConnected => - 'Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.'; + 'Verbinden Sie ein Gerät, um Radiostatistiken anzuzeigen.'; @override String get radioStats_firmwareTooOld => @@ -4479,24 +4467,24 @@ class AppLocalizationsDe extends AppLocalizations { String get translation_systemLanguage => 'Sprache des Systems'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore läuft'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'BLE-Verbindung bleibt aktiv'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return 'Übersetzungsmodell $name gelöscht'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return 'Löschen fehlgeschlagen: $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'Kanal konnte nicht aktualisiert werden: $error'; } @override @@ -4506,22 +4494,22 @@ class AppLocalizationsDe extends AppLocalizations { String get contact_typeRepeater => 'Repeater'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => 'Raumserver'; @override String get contact_typeSensor => 'Sensor'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => 'Unbekannt'; @override - String get map_zoomIn => 'Zoomen'; + String get map_zoomIn => 'Vergrößern'; @override - String get map_zoomOut => 'Auszoomen'; + String get map_zoomOut => 'Verkleinern'; @override - String get map_centerMap => 'Zentralkarte'; + String get map_centerMap => 'Karte zentrieren'; @override String get chrome_bluetoothRequiresChromium => @@ -4533,54 +4521,54 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS-Bestätigung'; + String get pathTrace_legendGpsConfirmed => 'GPS bestätigt'; @override String get pathTrace_legendInferred => 'Abgeleitete Position'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Einzeln'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Kombiniert'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Abspielen'; @override String get pathMap_pause => 'Pause'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Erneut abspielen'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Vorheriger Sprung'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Nächster Sprung'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Paketanimation anzeigen'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Paketanimation ausblenden'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return '$current von $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Beobachtete Pfade: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primär'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternative $index'; } @override @@ -4588,8 +4576,8 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count Sprünge', + one: '1 Sprung', ); return '$_temp0'; } @@ -4600,14 +4588,14 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Gemeinsamer Abschnitt'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Geschätzter Abschnitt'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Verwendet von $count Pfaden'; } @override @@ -4615,33 +4603,35 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count Sprünge haben keinen Standort - der angezeigte Pfad ist unvollständig', + one: + '1 Sprung hat keinen Standort - der angezeigte Pfad ist unvollständig', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Alle anzeigen'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Pfad ausblenden'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Pfad anzeigen'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Panel einklappen'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Panel ausklappen'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Keine Standortdaten'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Ansicht auf Paket fixieren'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Fixierung aufheben'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ef47ff3f..db655fa7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -24,7 +24,7 @@ class AppLocalizationsEs extends AppLocalizations { String get common_cancel => 'Cancelar'; @override - String get common_ok => 'De acuerdo'; + String get common_ok => 'Aceptar'; @override String get common_connect => 'Conectar'; @@ -45,7 +45,7 @@ class AppLocalizationsEs extends AppLocalizations { String get common_close => 'Cerrar'; @override - String get common_done => 'Done'; + String get common_done => 'Hecho'; @override String get common_edit => 'Editar'; @@ -78,7 +78,7 @@ class AppLocalizationsEs extends AppLocalizations { String get common_copy => 'Copiar'; @override - String get common_retry => 'Intentar'; + String get common_retry => 'Reintentar'; @override String get common_hide => 'Ocultar'; @@ -96,19 +96,19 @@ class AppLocalizationsEs extends AppLocalizations { String get common_undo => 'Deshacer'; @override - String get messageStatus_sent => 'Sentido'; + String get messageStatus_sent => 'Enviado'; @override String get messageStatus_delivered => 'Entregado'; @override - String get messageStatus_pending => 'Enviar'; + String get messageStatus_pending => 'Enviando'; @override String get messageStatus_failed => 'No se pudo enviar'; @override - String get messageStatus_repeated => 'Escuché repetidamente'; + String get messageStatus_repeated => 'Escuchado repetidamente'; @override String get common_reboot => 'Reiniciar'; @@ -136,7 +136,7 @@ class AppLocalizationsEs extends AppLocalizations { String get common_interval => 'Intervalo'; @override - String get scanner_title => 'MeshCore: Versión abierta'; + String get scanner_title => 'MeshCore Open'; @override String get connectionChoiceUsbLabel => 'USB'; @@ -277,7 +277,7 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_disconnecting => 'Desconectando...'; @override - String get scanner_notConnected => 'No está conectado'; + String get scanner_notConnected => 'No conectado'; @override String scanner_connectedTo(String deviceName) { @@ -289,7 +289,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_tapToScan => - 'Toca Escanear para encontrar dispositivos MeshCore'; + 'Pulsa Escanear para encontrar dispositivos MeshCore'; @override String scanner_connectionFailed(String error) { @@ -300,7 +300,7 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_stop => 'Detener'; @override - String get scanner_scan => 'Escanea'; + String get scanner_scan => 'Escanear'; @override String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; @@ -321,7 +321,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'La funcionalidad Bluetooth no está disponible en el navegador. Conéctese mediante USB en su lugar.'; @override String get device_quickSwitch => 'Cambiar rápidamente'; @@ -411,7 +411,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Configuración de cómo se agregan los contactos.'; @override - String get settings_privacyMode => 'Modo Privacidad'; + String get settings_privacyMode => 'Modo de privacidad'; @override String get settings_privacyModeSubtitle => @@ -474,41 +474,41 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_actions => 'Acciones'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'Eliminar todas las rutas'; @override String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + 'Borrar todos los datos de ruta de los contactos.'; @override - String get settings_sendAdvertisement => 'Enviar Anuncio'; + String get settings_sendAdvertisement => 'Enviar anuncio'; @override String get settings_sendAdvertisementSubtitle => - 'Presencia de transmisión ahora'; + 'Difundir la presencia ahora'; @override String get settings_advertisementSent => 'Anuncio enviado'; @override - String get settings_syncTime => 'Tiempo de Sincronización'; + String get settings_syncTime => 'Sincronizar hora'; @override String get settings_syncTimeSubtitle => - 'Establecer la hora del dispositivo al tiempo del teléfono'; + 'Establecer la hora del dispositivo con la del teléfono'; @override - String get settings_timeSynchronized => 'Sincronizado en el tiempo'; + String get settings_timeSynchronized => 'Hora sincronizada'; @override - String get settings_refreshContacts => 'Actualizar Contactos'; + String get settings_refreshContacts => 'Actualizar contactos'; @override String get settings_refreshContactsSubtitle => 'Recargar lista de contactos del dispositivo'; @override - String get settings_rebootDevice => 'Reiniciar Dispositivo'; + String get settings_rebootDevice => 'Reiniciar dispositivo'; @override String get settings_rebootDeviceSubtitle => @@ -540,15 +540,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String settings_aboutVersion(String version) { - return 'MeshCore Open versión $version'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => '2026 Proyecto Open Source MeshCore'; + String get settings_aboutLegalese => 'Proyecto MeshCore Open Source 2026'; @override String get settings_aboutDescription => - 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; + 'Un cliente Flutter de código abierto para dispositivos MeshCore de malla LoRa.'; @override String get settings_aboutOpenMeteoAttribution => @@ -567,7 +567,7 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_infoBattery => 'Batería'; @override - String get settings_infoPublicKey => 'Clave Pública'; + String get settings_infoPublicKey => 'Clave pública'; @override String get settings_infoContactsCount => 'Número de contactos'; @@ -594,10 +594,10 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_spreadingFactor => 'Factor de propagación'; @override - String get settings_codingRate => 'Tasa de Programación'; + String get settings_codingRate => 'Tasa de codificación'; @override - String get settings_txPower => 'TX Potencia (dBm)'; + String get settings_txPower => 'Potencia TX (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @@ -634,7 +634,7 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_themeSystem => 'Valor predeterminado del sistema'; @override - String get appSettings_themeLight => 'Luz'; + String get appSettings_themeLight => 'Claro'; @override String get appSettings_themeDark => 'Oscuro'; @@ -679,10 +679,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageNl => 'Neerlandés'; @override - String get appSettings_languageSk => 'Esloveno'; + String get appSettings_languageSk => 'Eslovaco'; @override - String get appSettings_languageBg => 'Bulgaro'; + String get appSettings_languageBg => 'Búlgaro'; @override String get appSettings_languageRu => 'Ruso'; @@ -702,7 +702,7 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_notifications => 'Notificaciones'; @override - String get appSettings_enableNotifications => 'Habilitar Notificaciones'; + String get appSettings_enableNotifications => 'Habilitar notificaciones'; @override String get appSettings_enableNotificationsSubtitle => @@ -719,7 +719,7 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_notificationsDisabled => 'Notificaciones desactivadas'; @override - String get appSettings_messageNotifications => 'Notificaciones de Mensaje'; + String get appSettings_messageNotifications => 'Notificaciones de mensajes'; @override String get appSettings_messageNotificationsSubtitle => @@ -727,7 +727,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_channelMessageNotifications => - 'Notificaciones de Mensajes del Canal'; + 'Notificaciones de mensajes del canal'; @override String get appSettings_channelMessageNotificationsSubtitle => @@ -735,7 +735,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_advertisementNotifications => - 'Notificaciones de Anuncios'; + 'Notificaciones de anuncios'; @override String get appSettings_advertisementNotificationsSubtitle => @@ -746,7 +746,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetry => - 'Borrar Camino en Max Reintentos'; + 'Borrar ruta al máximo de reintentos'; @override String get appSettings_clearPathOnMaxRetrySubtitle => @@ -754,14 +754,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_pathsWillBeCleared => - 'Los caminos se limpiarán después de 5 intentos fallidos.'; + 'Las rutas se borrarán después de 5 intentos fallidos.'; @override String get appSettings_pathsWillNotBeCleared => 'Las rutas no se eliminarán automáticamente.'; @override - String get appSettings_autoRouteRotation => 'Rotación de Ruta Automática'; + String get appSettings_autoRouteRotation => 'Rotación automática de rutas'; @override String get appSettings_autoRouteRotationSubtitle => @@ -803,7 +803,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_routeWeightFailureDecrementSubtitle => - 'Peso retirado de un camino después de un intento de entrega fallido.'; + 'Peso retirado de una ruta después de un intento de entrega fallido.'; @override String get appSettings_maxMessageRetries => @@ -821,34 +821,34 @@ class AppLocalizationsEs extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Configuración por dispositivo ($deviceName)'; + return 'Configurar por dispositivo ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Conéctate a un dispositivo para elegir'; + 'Conéctate a un dispositivo para elegirlo'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2 V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65 V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65 V)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; + String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)'; @override - String get appSettings_mapDisplay => 'Visualización del Mapa'; + String get appSettings_mapDisplay => 'Visualización del mapa'; @override - String get appSettings_showRepeaters => 'Mostrar Repetidores'; + String get appSettings_showRepeaters => 'Mostrar repetidores'; @override String get appSettings_showRepeatersSubtitle => 'Mostrar nodos de repetidor en el mapa'; @override - String get appSettings_showChatNodes => 'Mostrar Nodos de Chat'; + String get appSettings_showChatNodes => 'Mostrar nodos de chat'; @override String get appSettings_showChatNodesSubtitle => @@ -862,7 +862,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Mostrar otros tipos de nodo en el mapa'; @override - String get appSettings_timeFilter => 'Filtro de Tiempo'; + String get appSettings_timeFilter => 'Filtro de tiempo'; @override String get appSettings_timeFilterShowAll => 'Mostrar todos los nodos'; @@ -873,7 +873,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get appSettings_mapTimeFilter => 'Filtro de Tiempo del Mapa'; + String get appSettings_mapTimeFilter => 'Filtro de tiempo del mapa'; @override String get appSettings_showNodesDiscoveredWithin => @@ -895,7 +895,7 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_lastWeek => 'La semana pasada'; @override - String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; + String get appSettings_offlineMapCache => 'Caché de mapa sin conexión'; @override String get appSettings_unitsTitle => 'Unidades'; @@ -922,7 +922,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_appDebugLoggingSubtitle => - 'Registrar mensajes de depuración de la app de registro para solucionar problemas'; + 'Registrar mensajes de depuración de la app para solucionar problemas'; @override String get appSettings_appDebugLoggingEnabled => @@ -950,22 +950,22 @@ class AppLocalizationsEs extends AppLocalizations { @override String contacts_searchContacts(int number, String str) { - return 'Buscar contactos...'; + return 'Buscar $number$str contactos...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Buscar $number$str Favoritos...'; + return 'Buscar $number$str favoritos...'; } @override String contacts_searchUsers(int number, String str) { - return 'Buscar $number$str Usuarios...'; + return 'Buscar $number$str usuarios...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Buscar $number$str Repetidores...'; + return 'Buscar $number$str repetidores...'; } @override @@ -974,37 +974,37 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get contacts_noUnreadContacts => 'No contactos sin leer'; + String get contacts_noUnreadContacts => 'No hay contactos sin leer'; @override String get contacts_noContactsFound => 'No se encontraron contactos ni grupos.'; @override - String get contacts_deleteContact => 'Eliminar Contacto'; + String get contacts_deleteContact => 'Eliminar contacto'; @override String contacts_removeConfirm(String contactName) { - return 'Eliminar $contactName de los contactos?'; + return '¿Eliminar $contactName de los contactos?'; } @override - String get contacts_manageRepeater => 'Gestionar Repetidor'; + String get contacts_manageRepeater => 'Gestionar repetidor'; @override - String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; + String get contacts_manageRoom => 'Gestionar servidor de sala'; @override - String get contacts_roomLogin => 'Inicio de Sala'; + String get contacts_roomLogin => 'Inicio de sesión en sala'; @override - String get contacts_openChat => 'Abrir Chat'; + String get contacts_openChat => 'Abrir chat'; @override - String get contacts_editGroup => 'Editar Grupo'; + String get contacts_editGroup => 'Editar grupo'; @override - String get contacts_deleteGroup => 'Eliminar Grupo'; + String get contacts_deleteGroup => 'Eliminar grupo'; @override String contacts_deleteGroupConfirm(String groupName) { @@ -1021,7 +1021,7 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_searchOpen => 'Buscar contactos'; @override - String get contacts_searchClose => 'Búsqueda avanzada'; + String get contacts_searchClose => 'Cerrar búsqueda'; @override String get contacts_groupName => 'Nombre del grupo'; @@ -1133,7 +1133,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channels_via(String path) { - return 'via $path'; + return 'vía $path'; } @override @@ -1453,7 +1453,7 @@ class AppLocalizationsEs extends AppLocalizations { String get debugLog_frames => 'Marcos'; @override - String get debugLog_rawLogRx => 'Registro Crudo-RX'; + String get debugLog_rawLogRx => 'Registro bruto RX'; @override String get debugLog_noBleActivity => 'Aún no hay actividad BLE'; @@ -1492,10 +1492,10 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get debugFrame_textTypeCli => 'Interfaz de línea de comandos'; + String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Sencillo'; + String get debugFrame_textTypePlain => 'Plano'; @override String debugFrame_text(String text) { @@ -1503,7 +1503,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Mapeo Hexadecimal:'; + String get debugFrame_hexDump => 'Volcado hexadecimal:'; @override String chat_hopsCount(int count) { @@ -1525,16 +1525,16 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_pathCleared => - 'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; + 'Ruta borrada. El siguiente mensaje redescubrirá la ruta.'; @override String get chat_fullPath => 'Ruta completa'; @override - String get routing_title => 'Ruteo'; + String get routing_title => 'Enrutamiento'; @override - String get routing_modeAuto => 'Coche'; + String get routing_modeAuto => 'Automático'; @override String get routing_modeFlood => 'Inundación'; @@ -1544,15 +1544,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get routing_modeAutoHint => - 'Selecciona automáticamente la ruta más conocida, y si no hay ninguna ruta conocida, utiliza la ruta más directa.'; + 'Selecciona automáticamente la mejor ruta conocida y, si no hay ninguna, usa el modo de inundación.'; @override String get routing_modeFloodHint => - 'Transmisiones a través de todos los repetidores. Es la opción más fiable, pero utiliza más tiempo de transmisión.'; + 'Transmite por todos los repetidores. Es la opción más fiable, pero usa más tiempo de aire.'; @override String get routing_modeManualHint => - 'Siempre sigue exactamente la ruta que usted ha definido.'; + 'Siempre sigue exactamente la ruta que has definido.'; @override String get routing_currentRoute => 'Ruta actual'; @@ -1562,49 +1562,47 @@ class AppLocalizationsEs extends AppLocalizations { @override String get routing_noPathYet => - 'Aún no hay un camino definido. El mensaje se envía continuamente hasta que se encuentre una ruta.'; + 'Aún no hay una ruta. El siguiente mensaje se enviará por inundación hasta que se descubra una ruta.'; @override - String get routing_floodBroadcast => - 'Transmisión a través de todos los repetidores.'; + String get routing_floodBroadcast => 'Transmisión por todos los repetidores'; @override String get routing_editPath => 'Editar ruta'; @override - String get routing_forgetPath => 'Olvídate del camino'; + String get routing_forgetPath => 'Olvidar ruta'; @override String get routing_knownPaths => 'Rutas conocidas'; @override - String get routing_knownPathsHint => - 'Seleccione una opción para cambiar a esa.'; + String get routing_knownPathsHint => 'Toca una ruta para cambiar a ella.'; @override String get routing_inUse => 'En uso'; @override - String get routing_qualityStrong => 'Primer salto exitoso'; + String get routing_qualityStrong => 'Primer salto fuerte'; @override - String get routing_qualityGood => 'Primer paso exitoso'; + String get routing_qualityGood => 'Primer salto bueno'; @override - String get routing_qualityFair => 'Primer salto de calidad'; + String get routing_qualityFair => 'Primer salto aceptable'; @override - String get routing_qualityWorked => 'Ha cumplido'; + String get routing_qualityWorked => 'Ha entregado'; @override - String get routing_qualityFlood => 'Se ha escuchado a través de rumores.'; + String get routing_qualityFlood => 'Escuchado por inundación'; @override String get routing_qualityUntested => 'Sin probar'; @override String routing_lastWorked(String when) { - return 'trabajó $when'; + return 'funcionó $when'; } @override @@ -1612,7 +1610,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes entregados, $failures fallidos'; } @override @@ -1623,15 +1621,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String pathEditor_hopCounter(int count) { - return '$count de 64 granos de lúpulo'; + return '$count de 64 saltos'; } @override String get pathEditor_noHops => - 'Aún no se han añadido lúpulos. Haga clic en los repetidores para añadirlos en el orden deseado, o guarde la receta sin lúpulos para enviarla directamente.'; + 'Aún no se han añadido saltos. Toca los repetidores de abajo para añadirlos en orden, o guarda la ruta sin saltos para enviarla directamente.'; @override - String get pathEditor_addHops => 'Añadir los lúpulos en el orden adecuado.'; + String get pathEditor_addHops => 'Añadir los saltos en orden'; @override String get pathEditor_searchRepeaters => 'Buscar repetidores'; @@ -1656,10 +1654,10 @@ class AppLocalizationsEs extends AppLocalizations { String get pathEditor_tooManyHops => 'Máximo 64 saltos'; @override - String get pathEditor_usePath => 'Utilice esta ruta.'; + String get pathEditor_usePath => 'Usar esta ruta'; @override - String get pathEditor_removeHop => 'Eliminar el lúpulo'; + String get pathEditor_removeHop => 'Eliminar salto'; @override String get pathEditor_unknownHop => 'Repetidor desconocido'; @@ -1675,13 +1673,13 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_pathDeviceNotConfirmed => 'Dispositivo aún no confirmado.'; @override - String get chat_type => 'Escribe'; + String get chat_type => 'Tipo'; @override String get chat_path => 'Ruta'; @override - String get chat_publicKey => 'Clave Pública'; + String get chat_publicKey => 'Clave pública'; @override String get chat_compressOutgoingMessages => 'Comprimir mensajes salientes'; @@ -1701,10 +1699,10 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_floodAuto => 'Inundación (automática)'; @override - String get chat_direct => 'Guardar'; + String get chat_direct => 'Directo'; @override - String get chat_poiShared => 'Punto de Interés Compartido'; + String get chat_poiShared => 'Punto de interés compartido'; @override String chat_unread(int count) { @@ -1722,7 +1720,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_openLinkConfirmation => - '¿Quiere abrir este enlace en su navegador?'; + '¿Quieres abrir este enlace en tu navegador?'; @override String get chat_open => 'Abrir'; @@ -1736,40 +1734,40 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_invalidLink => 'Formato de enlace no válido'; @override - String get map_title => 'Mapa de Nodos'; + String get map_title => 'Mapa de nodos'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Buscar por nombre o ID del nodo'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Actividad'; @override - String get map_online => 'Online'; + String get map_online => 'En línea'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Reciente'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Antiguo; pasado de fecha'; @override String get map_visible => 'Visible'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Oculto'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Enfocar en el nodo'; @override - String get map_details => 'Details'; + String get map_details => 'Detalles'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Sin GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'No se encontraron nodos coincidentes.'; @override String get map_lineOfSight => 'Línea de visión'; @@ -1791,7 +1789,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Ganchos: $count'; + return 'Pines: $count'; } @override @@ -1801,22 +1799,22 @@ class AppLocalizationsEs extends AppLocalizations { String get map_repeater => 'Repetidor'; @override - String get map_room => 'Habitación'; + String get map_room => 'Sala'; @override String get map_sensor => 'Sensor'; @override - String get map_pinDm => 'Etiqueta (DM)'; + String get map_pinDm => 'Pin (DM)'; @override - String get map_pinPrivate => 'Bloqueo (Privado)'; + String get map_pinPrivate => 'Pin (privado)'; @override - String get map_pinPublic => 'Clave (Pública)'; + String get map_pinPublic => 'Pin (público)'; @override - String get map_lastSeen => 'Última vez que se vio'; + String get map_lastSeen => 'Última vez visto'; @override String get map_disconnectConfirm => @@ -1856,7 +1854,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_setAsMyLocation => 'Establecer mi ubicación'; @override - String get map_pinLabel => 'Etiqueta de marcador'; + String get map_pinLabel => 'Etiqueta del pin'; @override String get map_label => 'Etiqueta'; @@ -1878,7 +1876,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Estás a punto de compartir una ubicación en $channelLabel. Este canal es público y cualquiera con la PSK puede verlo.'; + return 'Estás a punto de compartir una ubicación en $channelLabel. Este canal es público y cualquiera con la PSK puede verla.'; } @override @@ -1886,19 +1884,19 @@ class AppLocalizationsEs extends AppLocalizations { 'Conéctate a un dispositivo para compartir marcadores'; @override - String get map_filterNodes => 'Filtrar Nodos'; + String get map_filterNodes => 'Filtrar nodos'; @override String get map_nodeTypes => 'Tipos de nodo'; @override - String get map_chatNodes => 'Nodos de Chat'; + String get map_chatNodes => 'Nodos de chat'; @override String get map_repeaters => 'Repetidores'; @override - String get map_otherNodes => 'Otros Nodos'; + String get map_otherNodes => 'Otros nodos'; @override String get map_showOverlaps => 'Superposiciones de tecla repetidora'; @@ -1923,13 +1921,13 @@ class AppLocalizationsEs extends AppLocalizations { 'Mostrar las ubicaciones estimadas de los nodos.'; @override - String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento'; + String get map_showDiscoveryContacts => 'Mostrar contactos de descubrimiento'; @override String get map_guessedLocation => 'Ubicación estimada'; @override - String get map_lastSeenTime => 'Última vez que se vio'; + String get map_lastSeenTime => 'Hora de última vez visto'; @override String get map_sharedPin => 'Pin compartido'; @@ -1938,43 +1936,43 @@ class AppLocalizationsEs extends AppLocalizations { String get map_sharedAt => 'Compartido'; @override - String get map_joinRoom => 'Únete a la sala'; + String get map_joinRoom => 'Unirse a la sala'; @override - String get map_manageRepeater => 'Gestionar Repetidor'; + String get map_manageRepeater => 'Gestionar repetidor'; @override - String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.'; + String get map_tapToAdd => 'Toque los nodos para añadirlos a la ruta.'; @override - String get map_runTrace => 'Ejecutar Rastreo de Ruta'; + String get map_runTrace => 'Ejecutar traza de ruta'; @override - String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.'; + String get map_runTraceWithReturnPath => 'Volver por la misma ruta.'; @override String get map_removeLast => 'Eliminar último'; @override - String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.'; + String get map_pathTraceCancelled => 'Traza de ruta cancelada.'; @override - String get mapCache_title => 'Caché de Mapa Offline'; + String get mapCache_title => 'Caché de mapa sin conexión'; @override String get mapCache_selectAreaFirst => - 'Seleccionar un área para cachear primero'; + 'Selecciona primero un área para almacenar en caché'; @override String get mapCache_noTilesToDownload => - 'No hay azulejos para descargar para este área.'; + 'No hay teselas para descargar para esta área.'; @override - String get mapCache_downloadTilesTitle => 'Descargar ficheros'; + String get mapCache_downloadTilesTitle => 'Descargar teselas'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Descargar $count ficheros para usar sin conexión?'; + return '¿Descargar $count teselas para uso sin conexión?'; } @override @@ -1982,40 +1980,39 @@ class AppLocalizationsEs extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'Almacenados $count azulejos'; + return 'Teselas almacenadas: $count'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Archivados $downloaded azulejos ($failed fallidos)'; + return 'Teselas almacenadas: $downloaded ($failed fallidas)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Borrar caché offline'; + String get mapCache_clearOfflineCacheTitle => 'Borrar caché sin conexión'; @override String get mapCache_clearOfflineCachePrompt => - 'Eliminar todas las baldosas en caché del mapa?'; + '¿Eliminar todas las teselas del mapa almacenadas en caché?'; @override - String get mapCache_offlineCacheCleared => - 'Almacén en caché sin conexión eliminado'; + String get mapCache_offlineCacheCleared => 'Caché sin conexión borrada'; @override String get mapCache_noAreaSelected => 'No se ha seleccionado ningún área'; @override - String get mapCache_cacheArea => 'Área de Caché'; + String get mapCache_cacheArea => 'Área de caché'; @override - String get mapCache_useCurrentView => 'Usar Vista Actual'; + String get mapCache_useCurrentView => 'Usar vista actual'; @override - String get mapCache_zoomRange => 'Rango de Zoom'; + String get mapCache_zoomRange => 'Rango de zoom'; @override String mapCache_estimatedTiles(int count) { - return 'Tiles estimados: $count'; + return 'Teselas estimadas: $count'; } @override @@ -2024,10 +2021,10 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get mapCache_downloadTilesButton => 'Descargar Mosaicos'; + String get mapCache_downloadTilesButton => 'Descargar teselas'; @override - String get mapCache_clearCacheButton => 'Borrar Caché'; + String get mapCache_clearCacheButton => 'Borrar caché'; @override String mapCache_failedDownloads(int count) { @@ -2045,21 +2042,21 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get time_justNow => 'Hace un momento'; + String get time_justNow => 'Justo ahora'; @override String time_minutesAgo(int minutes) { - return '$minutes minutos hace.'; + return 'hace $minutes min.'; } @override String time_hoursAgo(int hours) { - return '${hours}h hace'; + return 'hace $hours h'; } @override String time_daysAgo(int days) { - return '$days días hace'; + return 'hace $days días'; } @override @@ -2090,7 +2087,7 @@ class AppLocalizationsEs extends AppLocalizations { String get time_minutes => 'minutos'; @override - String get time_allTime => 'Todas las veces'; + String get time_allTime => 'Todo el tiempo'; @override String get dialog_disconnect => 'Desconectar'; @@ -2100,16 +2097,16 @@ class AppLocalizationsEs extends AppLocalizations { '¿Está seguro de que desea desconectarse de este dispositivo?'; @override - String get login_repeaterLogin => 'Iniciar sesión en el Repetidor'; + String get login_repeaterLogin => 'Inicio de sesión del repetidor'; @override - String get login_roomLogin => 'Inicio de Sala'; + String get login_roomLogin => 'Inicio de sesión en la sala'; @override String get login_password => 'Contraseña'; @override - String get login_enterPassword => 'Introducir contraseña'; + String get login_enterPassword => 'Introduce la contraseña'; @override String get login_savePassword => 'Guardar contraseña'; @@ -2120,11 +2117,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get login_repeaterDescription => - 'Ingrese la contraseña del repetidor para acceder a la configuración y el estado.'; + 'Introduce la contraseña del repetidor para acceder como invitado o administrador.'; @override String get login_roomDescription => - 'Ingrese la contraseña de la sala para acceder a la configuración y el estado.'; + 'Introduce la contraseña de la sala para acceder como invitado o administrador.'; @override String get login_routing => 'Enrutamiento'; @@ -2136,27 +2133,27 @@ class AppLocalizationsEs extends AppLocalizations { String get login_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get login_forceFloodMode => 'Activar Modo Inundación Forzada'; + String get login_forceFloodMode => 'Forzar modo inundación'; @override - String get login_managePaths => 'Gestionar Rutas'; + String get login_managePaths => 'Gestionar rutas'; @override String get login_login => 'Iniciar sesión'; @override String login_attempt(int current, int max) { - return 'Intentar $current/$max'; + return 'Intento $current/$max'; } @override String login_failed(String error) { - return 'Inicio fallido: $error'; + return 'Error de inicio de sesión: $error'; } @override String get login_failedMessage => - 'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.'; + 'El inicio de sesión ha fallado. La contraseña es incorrecta o el repetidor no está disponible.'; @override String get common_reload => 'Recargar'; @@ -2172,10 +2169,10 @@ class AppLocalizationsEs extends AppLocalizations { 'No se encontraron repetidores ni servidores de sala.'; @override - String get repeater_management => 'Gestión de Repetidores'; + String get repeater_management => 'Gestión de repetidores'; @override - String get room_management => 'Administración del Servidor de Habitación'; + String get room_management => 'Administración del servidor de sala'; @override String get repeater_guest => 'Información sobre repetidores'; @@ -2184,7 +2181,7 @@ class AppLocalizationsEs extends AppLocalizations { String get room_guest => 'Información del servidor'; @override - String get repeater_managementTools => 'Herramientas de Gestión'; + String get repeater_managementTools => 'Herramientas de gestión'; @override String get repeater_guestTools => 'Herramientas para invitados'; @@ -2204,7 +2201,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Ver la telemetría de los sensores y las estadísticas del sistema'; @override - String get repeater_cli => 'Interfaz de línea de comandos'; + String get repeater_cli => 'CLI'; @override String get repeater_cliSubtitle => 'Enviar comandos al repetidor'; @@ -2239,7 +2236,8 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_refresh => 'Actualizar'; @override - String get repeater_statusRequestTimeout => 'Solicitud de estado caducó.'; + String get repeater_statusRequestTimeout => + 'Se agotó el tiempo de espera de la solicitud de estado.'; @override String repeater_errorLoadingStatus(String error) { @@ -2262,10 +2260,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_queueLength => 'Longitud de la cola'; @override - String get repeater_debugFlags => 'Marcadores de Depuración'; + String get repeater_debugFlags => 'Banderas de depuración'; @override - String get repeater_radioStatistics => 'Estadísticas de Radio'; + String get repeater_radioStatistics => 'Estadísticas de radio'; @override String get repeater_lastRssi => 'Último RSSI'; @@ -2274,25 +2272,25 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_lastSnr => 'Último SNR'; @override - String get repeater_noiseFloor => 'Nivel de Ruido'; + String get repeater_noiseFloor => 'Nivel de ruido'; @override - String get repeater_txAirtime => 'TX Airtime'; + String get repeater_txAirtime => 'Tiempo de aire TX'; @override - String get repeater_rxAirtime => 'RX Airtime'; + String get repeater_rxAirtime => 'Tiempo de aire RX'; @override String get repeater_chanUtil => 'Utilización del canal'; @override - String get repeater_packetStatistics => 'Estadísticas del Paquete'; + String get repeater_packetStatistics => 'Estadísticas de paquetes'; @override - String get repeater_sent => 'Enviado'; + String get repeater_sent => 'Enviados'; @override - String get repeater_received => 'Recibido'; + String get repeater_received => 'Recibidos'; @override String get repeater_duplicates => 'Duplicados'; @@ -2304,7 +2302,7 @@ class AppLocalizationsEs extends AppLocalizations { int minutes, int seconds, ) { - return '$days días ${hours}h ${minutes}m ${seconds}s'; + return '$days días ${hours}h $minutes min $seconds s'; } @override @@ -2328,20 +2326,20 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Configuración del Repetidor'; + String get repeater_settingsTitle => 'Ajustes del repetidor'; @override - String get repeater_basicSettings => 'Configuración Básica'; + String get repeater_basicSettings => 'Ajustes básicos'; @override - String get repeater_repeaterName => 'Nombre del Repetidor'; + String get repeater_repeaterName => 'Nombre del repetidor'; @override String get repeater_repeaterNameHelper => - 'Mostrar nombre para este repetidor'; + 'Nombre visible para este repetidor'; @override - String get repeater_adminPassword => 'Contraseña de Administrador'; + String get repeater_adminPassword => 'Contraseña de administrador'; @override String get repeater_adminPasswordHelper => 'Contraseña de acceso completo'; @@ -2351,10 +2349,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_guestPasswordHelper => - 'Acceso de solo lectura con contraseña'; + 'Contraseña de acceso de solo lectura'; @override - String get repeater_radioSettings => 'Configuración de Radio'; + String get repeater_radioSettings => 'Ajustes de radio'; @override String get repeater_frequencyMhz => 'Frecuencia (MHz)'; @@ -2363,7 +2361,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX Potencia'; + String get repeater_txPower => 'Potencia TX'; @override String get repeater_txPowerHelper => '1-30 dBm'; @@ -2375,10 +2373,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_spreadingFactor => 'Factor de propagación'; @override - String get repeater_codingRate => 'Tasa de Programación'; + String get repeater_codingRate => 'Tasa de codificación'; @override - String get repeater_locationSettings => 'Configuración de Ubicación'; + String get repeater_locationSettings => 'Ajustes de ubicación'; @override String get repeater_latitude => 'Latitud'; @@ -2395,34 +2393,34 @@ class AppLocalizationsEs extends AppLocalizations { 'Grados decimales (por ejemplo, -122.4194)'; @override - String get repeater_features => 'Características'; + String get repeater_features => 'Funciones'; @override - String get repeater_packetForwarding => 'Enrutamiento de Paquetes'; + String get repeater_packetForwarding => 'Reenvío de paquetes'; @override String get repeater_packetForwardingSubtitle => - 'Habilitar el repetidor para reenviar paquetes'; + 'Permitir que el repetidor reenvíe paquetes'; @override - String get repeater_guestAccess => 'Acceso de Invitados'; + String get repeater_guestAccess => 'Acceso de invitado'; @override String get repeater_guestAccessSubtitle => - 'Permitir acceso de invitado en solo lectura'; + 'Permitir acceso de invitado de solo lectura'; @override - String get repeater_privacyMode => 'Modo Privacidad'; + String get repeater_privacyMode => 'Modo de privacidad'; @override String get repeater_privacyModeSubtitle => 'Ocultar nombre/ubicación en anuncios'; @override - String get repeater_advertisementSettings => 'Configuración de Anuncios'; + String get repeater_advertisementSettings => 'Ajustes de anuncios'; @override - String get repeater_localAdvertInterval => 'Intervalo de Anuncio Local'; + String get repeater_localAdvertInterval => 'Intervalo de anuncio local'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2431,7 +2429,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalo de Anuncio de Inundación'; + 'Intervalo de anuncio por inundación'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2439,13 +2437,13 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_encryptedAdvertInterval => 'Intervalo de Anuncio Cifrado'; + String get repeater_encryptedAdvertInterval => 'Intervalo de anuncio cifrado'; @override - String get repeater_dangerZone => 'Zona de Peligro'; + String get repeater_dangerZone => 'Zona de peligro'; @override - String get repeater_rebootRepeater => 'Reiniciar Repetidor'; + String get repeater_rebootRepeater => 'Reiniciar repetidor'; @override String get repeater_rebootRepeaterSubtitle => @@ -2456,18 +2454,18 @@ class AppLocalizationsEs extends AppLocalizations { '¿Está seguro de que desea reiniciar este repetidor?'; @override - String get repeater_regenerateIdentityKey => 'Regenerar Clave de Identidad'; + String get repeater_regenerateIdentityKey => 'Regenerar clave de identidad'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Generar nueva pareja de clave pública/privada'; + 'Generar una nueva pareja de claves pública/privada'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Esto generará una nueva identidad para el repetidor. Continuar?'; + 'Esto generará una nueva identidad para el repetidor. ¿Continuar?'; @override - String get repeater_eraseFileSystem => 'Borrar Sistema de Archivos'; + String get repeater_eraseFileSystem => 'Borrar sistema de archivos'; @override String get repeater_eraseFileSystemSubtitle => @@ -2479,7 +2477,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_eraseSerialOnly => - 'Borrar solo está disponible a través de la consola serial.'; + 'Borrar solo está disponible a través de la consola serie.'; @override String repeater_commandSent(String command) { @@ -2495,10 +2493,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_confirm => 'Confirmar'; @override - String get repeater_settingsSaved => 'Guardado de ajustes exitoso'; + String get repeater_settingsSaved => 'Ajustes guardados correctamente'; @override - String get repeater_rxGain => 'Aumento en la ganancia de RX'; + String get repeater_rxGain => 'Ganancia RX'; @override String get repeater_rxGainHelper => @@ -2506,17 +2504,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_refreshRxGain => - 'Aumenta el rendimiento de RX con la nueva versión.'; + 'Mejora el rendimiento de RX con la nueva versión.'; @override - String get repeater_multiAcks => 'Múltiples respuestas de confirmación'; + String get repeater_multiAcks => 'ACK múltiples'; @override String get repeater_multiAcksSubtitle => - 'Reconocer mensajes a través de múltiples vías para una mejor entrega.'; + 'Reconocer mensajes a través de múltiples rutas para una mejor entrega.'; @override - String get repeater_refreshMultiAcks => 'Reenviar múltiples confirmaciones'; + String get repeater_refreshMultiAcks => 'Actualizar ACK múltiples'; @override String get repeater_networkHealth => 'Salud de la red'; @@ -2529,7 +2527,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Crea paquetes de \"flujo\" que parezcan bucles de enrutamiento.'; @override - String get repeater_loopDetectOff => 'Fuera'; + String get repeater_loopDetectOff => 'Desactivado'; @override String get repeater_loopDetectMinimal => 'Mínimo'; @@ -2538,14 +2536,13 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_loopDetectModerate => 'Moderado'; @override - String get repeater_loopDetectStrict => 'Estrictos'; + String get repeater_loopDetectStrict => 'Estricto'; @override String get repeater_dutyCycle => 'Ciclo de trabajo'; @override - String get repeater_dutyCycleHelper => - 'Porcentaje máximo de tiempo de antena'; + String get repeater_dutyCycleHelper => 'Porcentaje máximo de tiempo de aire'; @override String repeater_dutyCyclePercent(int percent) { @@ -2553,29 +2550,29 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_ownerInfo => 'Información del operador'; + String get repeater_ownerInfo => 'Información del propietario'; @override String get repeater_ownerInfoHelper => 'Metadatos públicos para este repetidor'; @override - String get repeater_refreshOwnerInfo => 'Actualizar información del operador'; + String get repeater_refreshOwnerInfo => + 'Actualizar información del propietario'; @override - String get repeater_floodMax => - 'Máximo número de saltos en caso de inundación'; + String get repeater_floodMax => 'Máximo de saltos por inundación'; @override String get repeater_floodMaxHelper => - 'Número máximo de paquetes de flujo que un nodo puede enviar (0-64)'; + 'Número máximo de paquetes de inundación que un nodo puede enviar (0-64)'; @override - String get repeater_advancedSettings => 'Avanzado'; + String get repeater_advancedSettings => 'Ajustes avanzados'; @override String get repeater_advancedSettingsSubtitle => - 'Perillas de ajuste para operadores experimentados'; + 'Controles de ajuste para operadores experimentados'; @override String get repeater_pathHashMode => 'Modo de hash de ruta'; @@ -2585,14 +2582,14 @@ class AppLocalizationsEs extends AppLocalizations { 'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.'; @override - String get repeater_txDelay => 'Retraso en Flood, TX'; + String get repeater_txDelay => 'Retraso TX por inundación'; @override String get repeater_txDelayHelper => 'Ajuste de retransmisión para el tráfico de inundación, como un multiplicador del tiempo de transmisión del paquete (0-2, valor predeterminado 0.5). Un valor más alto significa menos colisiones, pero una entrega más lenta.'; @override - String get repeater_directTxDelay => 'Retraso directo en TX'; + String get repeater_directTxDelay => 'Retraso TX directo'; @override String get repeater_directTxDelayHelper => @@ -2603,27 +2600,27 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_intThreshHelper => - 'Se establece un umbral para la calibración del nivel de ruido de la radio, de modo que rechaza las interferencias que superen este nivel. 0 deshabilita — solo aumente este valor si observa errores en una banda de frecuencia con mucho ruido.'; + 'Se establece un umbral para la calibración del nivel de ruido de la radio, de modo que rechaza las interferencias que superen este nivel. 0 deshabilita: solo aumente este valor si observa errores en una banda de frecuencia con mucho ruido.'; @override String get repeater_agcResetInterval => 'Intervalo de reinicio de AGC'; @override String get repeater_agcResetIntervalHelper => - '¿Con qué frecuencia se debe restablecer el control automático de ganancia del radio para recuperarse de un estado de ganancia bloqueada? Se puede restablecer cada pocos segundos, o cada 4 segundos. Desactivar la función de restablecimiento periódico.'; + '¿Con qué frecuencia se debe restablecer el control automático de ganancia del radio para recuperarse de un estado de ganancia bloqueada? Se puede restablecer cada pocos segundos o cada 4 segundos. Desactiva la función de restablecimiento periódico.'; @override String get repeater_actionsTitle => 'Acciones'; @override - String get repeater_sendAdvert => 'Enviar anuncio sobre inundaciones'; + String get repeater_sendAdvert => 'Enviar anuncio por inundación'; @override String get repeater_sendAdvertSubtitle => - 'Transmite un anuncio sobre inundaciones a través de la red.'; + 'Transmitir un anuncio por inundación a través de la red.'; @override - String get repeater_sendAdvertZeroHop => 'Enviar anuncio sin intermediarios'; + String get repeater_sendAdvertZeroHop => 'Enviar anuncio sin saltos'; @override String get repeater_sendAdvertZeroHopSubtitle => @@ -2638,12 +2635,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String repeater_actionSucceeded(String action) { - return '$action succeeded'; + return '$action completado correctamente'; } @override String repeater_actionFailed(String action, String error) { - return '$action failed: $error'; + return 'Error en $action: $error'; } @override @@ -3311,7 +3308,7 @@ class AppLocalizationsEs extends AppLocalizations { String get telemetry_temperatureLabel => 'Temperatura'; @override - String get telemetry_currentLabel => 'Actual'; + String get telemetry_currentLabel => 'Corriente'; @override String telemetry_batteryValue(int percent, String volts) { @@ -3391,7 +3388,7 @@ class AppLocalizationsEs extends AppLocalizations { String get telemetry_timeLabel => 'Hora'; @override - String get telemetry_gyrometerLabel => 'Girómetro'; + String get telemetry_gyrometerLabel => 'Giroscopio'; @override String get telemetry_colourLabel => 'Color'; @@ -3491,20 +3488,20 @@ class AppLocalizationsEs extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Escuchado: $time hace atrás'; + return 'Escuchado hace $time'; } @override - String get channelPath_title => 'Ruta del Paquete'; + String get channelPath_title => 'Ruta del paquete'; @override String get channelPath_viewMap => 'Ver mapa'; @override - String get channelPath_otherObservedPaths => 'Otros caminos observados'; + String get channelPath_otherObservedPaths => 'Otras rutas observadas'; @override - String get channelPath_repeaterHops => 'Saltos del Repetidor'; + String get channelPath_repeaterHops => 'Saltos del repetidor'; @override String get channelPath_noHopDetails => @@ -3517,7 +3514,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channelPath_senderLabel => 'Remitente'; @override - String get channelPath_timeLabel => 'Tiempo'; + String get channelPath_timeLabel => 'Hora'; @override String get channelPath_repeatsLabel => 'Repetir'; @@ -3822,11 +3819,11 @@ class AppLocalizationsEs extends AppLocalizations { String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.'; @override - String get pathTrace_refreshTooltip => 'Actualizar Path Trace'; + String get pathTrace_refreshTooltip => 'Actualizar trazado de ruta'; @override String get pathTrace_someHopsNoLocation => - 'Uno o más de los lúpulos carecen de una ubicación'; + 'Uno o más de los saltos carecen de una ubicación'; @override String get pathTrace_clearTooltip => 'Borrar ruta'; @@ -4003,37 +4000,36 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get contacts_pathTrace => 'Rastreo de caminos'; + String get contacts_pathTrace => 'Traza de ruta'; @override String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'Rastrear ruta al repetidor'; + String get contacts_repeaterPathTrace => 'Traza de ruta al repetidor'; @override - String get contacts_repeaterPing => 'Pingar repetidor'; + String get contacts_repeaterPing => 'Hacer ping al repetidor'; @override - String get contacts_roomPathTrace => - 'Rastreo de ruta al servidor de la habitación'; + String get contacts_roomPathTrace => 'Traza de ruta al servidor de sala'; @override - String get contacts_roomPing => 'Pingar servidor de sala'; + String get contacts_roomPing => 'Hacer ping al servidor de sala'; @override String get contacts_chatTraceRoute => 'Ruta de trazado'; @override String contacts_pathTraceTo(String name) { - return 'Rastrear ruta a $name'; + return 'Traza de ruta a $name'; } @override String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; @override - String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; + String get contacts_invalidAdvertFormat => 'Formato de anuncio no válido'; @override String get contacts_contactImported => 'El contacto ha sido importado.'; @@ -4043,7 +4039,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Contacto no se importó correctamente.'; @override - String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop'; + String get contacts_zeroHopAdvert => 'Anuncio de un solo salto'; @override String get contacts_floodAdvert => 'Anuncio de inundación'; @@ -4056,24 +4052,26 @@ class AppLocalizationsEs extends AppLocalizations { 'Agregar contacto desde el portapapeles'; @override - String get contacts_ShareContact => 'Copiar contacto al Portapapeles'; + String get contacts_ShareContact => 'Copiar contacto al portapapeles'; @override - String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio'; + String get contacts_ShareContactZeroHop => + 'Compartir contacto por anuncio de un solo salto'; @override - String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.'; + String get contacts_zeroHopContactAdvertSent => + 'Contacto enviado por anuncio.'; @override String get contacts_zeroHopContactAdvertFailed => 'No se pudo enviar el contacto.'; @override - String get contacts_contactAdvertCopied => 'Anuncio copiado al Portapapeles.'; + String get contacts_contactAdvertCopied => 'Anuncio copiado al portapapeles.'; @override String get contacts_contactAdvertCopyFailed => - 'Copiar anuncio al Portapapeles ha fallado.'; + 'No se pudo copiar el anuncio al portapapeles.'; @override String get notification_activityTitle => 'Actividad de MeshCore'; @@ -4121,18 +4119,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportRepeaters => - 'Exportar repetidores / servidor de sala a GPX'; + 'Exportar repetidores / servidores de sala a GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; + 'Exporta repetidores o servidores de sala con una ubicación a un archivo GPX.'; @override String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporta compañeros con una ubicación a archivo GPX.'; + 'Exporta compañeros con una ubicación a un archivo GPX.'; @override String get settings_gpxExportAll => 'Exportar todos los contactos a GPX'; @@ -4149,17 +4147,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportNotAvailable => - 'No compatible con tu dispositivo/SO'; + 'No compatible con tu dispositivo o sistema operativo'; @override String get settings_gpxExportError => 'Hubo un error al exportar.'; @override String get settings_gpxExportRepeatersRoom => - 'Ubicaciones del servidor de repetidor y sala'; + 'Ubicaciones de repetidores y servidores de sala'; @override - String get settings_gpxExportChat => 'Ubicaciones de compañero'; + String get settings_gpxExportChat => 'Ubicaciones de compañeros'; @override String get settings_gpxExportAllContacts => @@ -4171,7 +4169,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportShareSubject => - 'meshcore-open exportación de datos de mapa GPX'; + 'Exportación GPX de datos de mapa de meshcore-open'; @override String get snrIndicator_nearByRepeaters => 'Repetidores cercanos'; @@ -4465,49 +4463,49 @@ class AppLocalizationsEs extends AppLocalizations { String get translation_systemLanguage => 'Idioma del sistema'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore en ejecución'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'Manteniendo BLE conectado'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return 'Eliminado $name'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return 'No se pudo eliminar: $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'No se pudo actualizar el canal: $error'; } @override String get contact_typeChat => 'Chat'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => 'Repetidor'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => 'Sala'; @override String get contact_typeSensor => 'Sensor'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => 'Desconocido'; @override String get map_zoomIn => 'Acercar'; @override - String get map_zoomOut => 'Acercar'; + String get map_zoomOut => 'Alejar'; @override - String get map_centerMap => 'Mapa del centro'; + String get map_centerMap => 'Centrar mapa'; @override String get chrome_bluetoothRequiresChromium => @@ -4522,51 +4520,51 @@ class AppLocalizationsEs extends AppLocalizations { String get pathTrace_legendGpsConfirmed => 'Confirmado mediante GPS'; @override - String get pathTrace_legendInferred => 'Posición inferida'; + String get pathTrace_legendInferred => 'Posición estimada'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Individual'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Combinado'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Reproducir'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Pausa'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Repetir'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Salto anterior'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Siguiente salto'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Mostrar animación del paquete'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Ocultar la animación del paquete'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Saltar $current de $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Rutas observadas: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Principal'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternativo $index'; } @override @@ -4574,8 +4572,8 @@ class AppLocalizationsEs extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count saltos', + one: '1 salto', ); return '$_temp0'; } @@ -4586,14 +4584,14 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Segmento compartido'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Segmento estimado'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Utilizado en $count rutas.'; } @override @@ -4601,33 +4599,33 @@ class AppLocalizationsEs extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count saltos no tienen ubicación — la ruta mostrada es parcial', + one: '1 salto no tiene ubicación — la ruta mostrada es parcial', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Mostrar todas'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Ocultar ruta'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Mostrar ruta'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Cerrar panel'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Ampliar panel'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Sin ubicación'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Seguir paquete'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Dejar de seguir el paquete'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 46f8cf2f..5dc20ea9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -12,7 +12,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Coordonnées'; + String get nav_contacts => 'Contacts'; @override String get nav_channels => 'Canaux'; @@ -39,13 +39,13 @@ class AppLocalizationsFr extends AppLocalizations { String get common_delete => 'Supprimer'; @override - String get common_deleteAll => 'Supprimer tout'; + String get common_deleteAll => 'Tout supprimer'; @override String get common_close => 'Fermer'; @override - String get common_done => 'Done'; + String get common_done => 'Terminé'; @override String get common_edit => 'Modifier'; @@ -96,19 +96,19 @@ class AppLocalizationsFr extends AppLocalizations { String get common_undo => 'Annuler'; @override - String get messageStatus_sent => 'Envoyer'; + String get messageStatus_sent => 'Envoyé'; @override String get messageStatus_delivered => 'Livré'; @override - String get messageStatus_pending => 'Envoyer'; + String get messageStatus_pending => 'Envoi en cours'; @override String get messageStatus_failed => 'Échec de l\'envoi'; @override - String get messageStatus_repeated => 'Répété plusieurs fois'; + String get messageStatus_repeated => 'Reçu plusieurs fois'; @override String get common_reboot => 'Redémarrer'; @@ -323,7 +323,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'La fonctionnalité Bluetooth n\'est pas disponible dans le navigateur. Veuillez vous connecter via USB à la place.'; @override String get device_quickSwitch => 'Basculement rapide'; @@ -390,11 +390,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Activer la mise à jour automatique de la position via GPS'; + 'Activer la mise à jour automatique de la localisation via GPS'; @override String get settings_locationIntervalSec => - 'Intervalle de mise-à-jour du GPS (Secondes)'; + 'Intervalle de mise à jour du GPS (secondes)'; @override String get settings_locationIntervalInvalid => @@ -418,11 +418,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_privacyModeSubtitle => - 'Cacher le nom/l\'emplacement dans les annonces'; + 'Masquer le nom et la localisation dans les annonces'; @override String get settings_privacyModeToggle => - 'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.'; + 'Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.'; @override String get settings_privacyModeEnabled => 'Mode de confidentialité activé'; @@ -455,18 +455,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_telemetryLocationMode => - 'Mode d\'emplacement de télémétrie'; + 'Mode de localisation de la télémétrie'; @override String get settings_telemetryEnvironmentMode => - 'Mode d\'environnement de télémétrie'; + 'Mode d\'environnement de la télémétrie'; @override - String get settings_advertLocation => 'Emplacement de l\'annonce'; + String get settings_advertLocation => 'Localisation de l\'annonce'; @override String get settings_advertLocationSubtitle => - 'Inclure l\'emplacement dans l\'annonce'; + 'Inclure la localisation dans l\'annonce'; @override String get settings_multiAck => 'Plusieurs accusés de réception'; @@ -479,34 +479,34 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_actions => 'Actions'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'Supprimer tous les chemins'; @override String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + 'Effacer toutes les données de chemin des contacts.'; @override String get settings_sendAdvertisement => 'S\'annoncer'; @override String get settings_sendAdvertisementSubtitle => - 'Présence diffusée maintenant'; + 'Diffuser la présence maintenant'; @override String get settings_advertisementSent => 'Annonce envoyée'; @override - String get settings_syncTime => 'Temps de synchronisation'; + String get settings_syncTime => 'Synchroniser l\'heure'; @override String get settings_syncTimeSubtitle => - 'Définir l\'heure de l\'appareil sur l\'heure du téléphone.'; + 'Régler l\'heure de l\'appareil sur celle du téléphone.'; @override - String get settings_timeSynchronized => 'Synchronisation temporelle'; + String get settings_timeSynchronized => 'Heure synchronisée'; @override - String get settings_refreshContacts => 'Rafraîchir les Contacts'; + String get settings_refreshContacts => 'Actualiser les contacts'; @override String get settings_refreshContactsSubtitle => @@ -523,7 +523,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Êtes-vous sûr de vouloir redémarrer l\'appareil ? Vous serez déconnecté.'; @override - String get settings_debug => 'Déboguer'; + String get settings_debug => 'Débogage'; @override String get settings_companionDebugLog => 'Journal de débogage associé'; @@ -544,15 +544,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String settings_aboutVersion(String version) { - return 'MeshCore Open $version'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => 'Projet MeshCore Open Source 2026'; + String get settings_aboutLegalese => 'Projet open source MeshCore 2026'; @override String get settings_aboutDescription => - 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; + 'Client Flutter open source pour les appareils de réseau maillé LoRa MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => @@ -562,7 +562,7 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_infoName => 'Nom'; @override - String get settings_infoId => 'Numéro d\'identification'; + String get settings_infoId => 'ID'; @override String get settings_infoStatus => 'État'; @@ -571,7 +571,7 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_infoBattery => 'Batterie'; @override - String get settings_infoPublicKey => 'Clé Publique'; + String get settings_infoPublicKey => 'Clé publique'; @override String get settings_infoContactsCount => 'Nombre de contacts'; @@ -595,13 +595,13 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_bandwidth => 'Bande passante'; @override - String get settings_spreadingFactor => 'Facteur de répartition (SF)'; + String get settings_spreadingFactor => 'Facteur d\'étalement (SF)'; @override String get settings_codingRate => 'Taux de codage (CR)'; @override - String get settings_txPower => 'TX Puissance (dBm)'; + String get settings_txPower => 'Puissance TX (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @@ -614,7 +614,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_clientRepeatSubtitle => - 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; + 'Permettre à cet appareil de relayer les paquets du maillage pour les autres.'; @override String get settings_clientRepeatFreqWarning => @@ -638,7 +638,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_themeSystem => 'Défaut système'; @override - String get appSettings_themeLight => 'Lumière'; + String get appSettings_themeLight => 'Clair'; @override String get appSettings_themeDark => 'Sombre'; @@ -665,7 +665,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languagePl => 'Polonais'; @override - String get appSettings_languageSl => 'Sloveno'; + String get appSettings_languageSl => 'Slovène'; @override String get appSettings_languagePt => 'Portugais'; @@ -683,7 +683,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languageNl => 'Néerlandais'; @override - String get appSettings_languageSk => 'Slovène'; + String get appSettings_languageSk => 'Slovaque'; @override String get appSettings_languageBg => 'Bulgare'; @@ -706,7 +706,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_notifications => 'Notifications'; @override - String get appSettings_enableNotifications => 'Activer les Notifications'; + String get appSettings_enableNotifications => 'Activer les notifications'; @override String get appSettings_enableNotificationsSubtitle => @@ -723,7 +723,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_notificationsDisabled => 'Notifications désactivées'; @override - String get appSettings_messageNotifications => 'Notifications de Messages'; + String get appSettings_messageNotifications => 'Notifications de messages'; @override String get appSettings_messageNotificationsSubtitle => @@ -731,7 +731,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_channelMessageNotifications => - 'Notifications des Messages de Canal'; + 'Notifications des messages de canal'; @override String get appSettings_channelMessageNotificationsSubtitle => @@ -750,7 +750,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetry => - 'Effacer le chemin sur Max Retry'; + 'Effacer le chemin après le nombre maximal de tentatives'; @override String get appSettings_clearPathOnMaxRetrySubtitle => @@ -758,35 +758,33 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_pathsWillBeCleared => - 'Les chemins seront effacés après 5 tentatives infructueuses.'; + 'Les chemins seront effacés après 5 échecs.'; @override String get appSettings_pathsWillNotBeCleared => 'Les chemins ne seront pas effacés automatiquement.'; @override - String get appSettings_autoRouteRotation => - 'Rotation de l\'itinéraire automatique'; + String get appSettings_autoRouteRotation => 'Rotation automatique du routage'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)'; + 'Alterner entre les meilleurs chemins et le mode flood'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotation du routage automatique activée'; + 'Rotation automatique du routage activée'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotation de l\'itinéraire automatique désactivée'; + 'Rotation automatique du routage désactivée'; @override - String get appSettings_maxRouteWeight => - 'Poids maximal autorisé pour le trajet'; + String get appSettings_maxRouteWeight => 'Poids maximal du chemin'; @override String get appSettings_maxRouteWeightSubtitle => - 'Poids maximal qu\'un itinéraire peut accumuler grâce à des livraisons réussies.'; + 'Poids maximal qu\'un chemin peut accumuler grâce aux livraisons réussies.'; @override String get appSettings_initialRouteWeight => 'Poids initial de l\'itinéraire'; @@ -797,27 +795,27 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_routeWeightSuccessIncrement => - 'Augmentation du poids de réussite'; + 'Incrément du poids de succès'; @override String get appSettings_routeWeightSuccessIncrementSubtitle => - 'Poids ajouté à un itinéraire après une livraison réussie.'; + 'Poids ajouté à un chemin après une livraison réussie.'; @override String get appSettings_routeWeightFailureDecrement => - 'Réduction du poids de pénalité'; + 'Décrément du poids d\'échec'; @override String get appSettings_routeWeightFailureDecrementSubtitle => - 'Poids retiré d\'un itinéraire après une tentative de livraison infructueuse.'; + 'Poids retiré d\'un chemin après une tentative de livraison infructueuse.'; @override String get appSettings_maxMessageRetries => - 'Nombre maximal de tentatives de récupération de messages'; + 'Nombre maximal de tentatives d\'envoi'; @override String get appSettings_maxMessageRetriesSubtitle => - 'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.'; + 'Nombre de tentatives avant de marquer un message comme ayant échoué.'; @override String get appSettings_battery => 'Batterie'; @@ -879,11 +877,11 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get appSettings_mapTimeFilter => 'Filtre du Temps de la Carte'; + String get appSettings_mapTimeFilter => 'Filtre temporel de la carte'; @override String get appSettings_showNodesDiscoveredWithin => - 'Afficher les nœuds découverts dans :'; + 'Afficher les nœuds découverts au cours de :'; @override String get appSettings_allTime => 'Tout le temps'; @@ -901,7 +899,7 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_lastWeek => 'La semaine dernière'; @override - String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne'; + String get appSettings_offlineMapCache => 'Cache de carte hors ligne'; @override String get appSettings_unitsTitle => 'Unités'; @@ -921,15 +919,15 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get appSettings_debugCard => 'Déboguer'; + String get appSettings_debugCard => 'Débogage'; @override String get appSettings_appDebugLogging => - 'Journalisation de débogage de l\'application'; + 'Journalisation des débogages de l\'application'; @override String get appSettings_appDebugLoggingSubtitle => - 'Enregistrez les messages de débogage de l\'application Log pour le dépannage.'; + 'Journaliser les messages de débogage de l\'application pour le dépannage.'; @override String get appSettings_appDebugLoggingEnabled => @@ -937,17 +935,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_appDebugLoggingDisabled => - 'Le débogage de l\'application est désactivé.'; + 'Journalisation de débogage de l\'application désactivée'; @override - String get contacts_title => 'Coordonnées'; + String get contacts_title => 'Contacts'; @override String get contacts_noContacts => 'Aucun contact trouvé.'; @override String get contacts_contactsWillAppear => - 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; + 'Les contacts apparaîtront lorsque les appareils diffuseront leur annonce.'; @override String get contacts_unread => 'Non lu'; @@ -957,7 +955,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_searchContacts(int number, String str) { - return 'Rechercher des contacts...'; + return 'Rechercher $number$str contacts...'; } @override @@ -998,13 +996,13 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_manageRepeater => 'Gérer le répéteur'; @override - String get contacts_manageRoom => 'Gérer le Room Server'; + String get contacts_manageRoom => 'Gérer le room server'; @override - String get contacts_roomLogin => 'Connexion Room Server'; + String get contacts_roomLogin => 'Connexion au room server'; @override - String get contacts_openChat => 'Ouverture du Chat'; + String get contacts_openChat => 'Ouvrir le chat'; @override String get contacts_editGroup => 'Modifier le groupe'; @@ -1014,11 +1012,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_deleteGroupConfirm(String groupName) { - return 'Supprimer $groupName?'; + return 'Supprimer le groupe \"$groupName\" ?'; } @override - String get contacts_newGroup => 'Nouveau Groupe'; + String get contacts_newGroup => 'Nouveau groupe'; @override String get contacts_moreOptions => 'Plus d\'options'; @@ -1054,11 +1052,11 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_noMembers => 'Aucun membre'; @override - String get contacts_lastSeenNow => 'Vu maintenant'; + String get contacts_lastSeenNow => 'À l\'instant'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '~ $minutes minutes.'; + return '~ $minutes min'; } @override @@ -1087,7 +1085,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contact_telemetry => 'Télémétrie'; @override - String get contact_lastSeen => 'Dernière fois vu'; + String get contact_lastSeen => 'Vu pour la dernière fois'; @override String get contact_clearChat => 'Effacer la conversation'; @@ -1158,24 +1156,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return 'Supprimer $name? Cela ne peut pas être annulé.'; + return 'Supprimer \"$name\" ? Cette action est irréversible.'; } @override String channels_channelDeleteFailed(String name) { - return 'Échec de la suppression de la chaîne \"$name\"'; + return 'Échec de la suppression du canal \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Le canal \"$name\" a été supprimé'; + return 'Canal \"$name\" supprimé'; } @override - String get channels_addChannel => 'Ajouter un Canal'; + String get channels_addChannel => 'Ajouter un canal'; @override - String get channels_channelIndexLabel => 'Index de canal'; + String get channels_channelIndexLabel => 'Index du canal'; @override String get channels_channelName => 'Nom du canal'; @@ -1190,15 +1188,14 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_pskHex => 'PSK (Hexadécimal)'; @override - String get channels_generateRandomPsk => - 'Générer une clé de modulation PSK aléatoire'; + String get channels_generateRandomPsk => 'Générer une PSK aléatoire'; @override String get channels_enterChannelName => 'Veuillez entrer un nom de canal'; @override String get channels_pskMustBe32Hex => - 'Le PKS doit être composé de 32 caractères hexadécimaux.'; + 'La PSK doit contenir 32 caractères hexadécimaux.'; @override String channels_channelAdded(String name) { @@ -1271,11 +1268,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String settings_cyr2latProfileDeleteDscr(String name) { - return 'Êtes-vous sûr de vouloir supprimer le profil \"$name\"?'; + return 'Êtes-vous sûr de vouloir supprimer le profil \"$name\" ?'; } @override - String get channels_publicChannelAdded => 'Le canal public a été ajouté'; + String get channels_publicChannelAdded => 'Canal public ajouté'; @override String get channels_sortBy => 'Trier par'; @@ -1290,7 +1287,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_sortLatestMessages => 'Derniers messages'; @override - String get channels_sortUnread => 'Non lu'; + String get channels_sortUnread => 'Non lus'; @override String get channels_createPrivateChannel => 'Créer un Canal Privé'; @@ -1333,7 +1330,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_hashtagHint => 'ex. #equipe'; @override - String get chat_noMessages => 'Aucun message pour le moment.'; + String get chat_noMessages => 'Aucun message pour le moment'; @override String get chat_sendMessage => 'Envoyer un message'; @@ -1377,7 +1374,7 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_messageDeleted => 'Message supprimé'; @override - String get chat_retryingMessage => 'Tentative de récupération.'; + String get chat_retryingMessage => 'Nouvelle tentative d\'envoi du message'; @override String chat_retryCount(int current, int max) { @@ -1385,13 +1382,13 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get chat_sendGif => 'Envoyer GIF'; + String get chat_sendGif => 'Envoyer un GIF'; @override String get chat_reply => 'Répondre'; @override - String get chat_addReaction => 'Ajouter une Réaction'; + String get chat_addReaction => 'Ajouter une réaction'; @override String get chat_me => 'Moi'; @@ -1501,7 +1498,7 @@ class AppLocalizationsFr extends AppLocalizations { String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Simple'; + String get debugFrame_textTypePlain => 'Texte brut'; @override String debugFrame_text(String text) { @@ -1509,7 +1506,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; + String get debugFrame_hexDump => 'Dump hexadécimal :'; @override String chat_hopsCount(int count) { @@ -1527,30 +1524,30 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; + 'Aucun historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; @override String get chat_pathCleared => - 'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; + 'Chemin effacé. Le prochain message redécouvrira la route.'; @override String get chat_fullPath => 'Chemin complet'; @override - String get routing_title => 'Planification des itinéraires'; + String get routing_title => 'Routage'; @override - String get routing_modeAuto => 'Voiture'; + String get routing_modeAuto => 'Auto'; @override - String get routing_modeFlood => 'Inondation'; + String get routing_modeFlood => 'Flood'; @override String get routing_modeManual => 'Manuel'; @override String get routing_modeAutoHint => - 'Sélectionne automatiquement le chemin le plus connu, et utilise la méthode de \"inondation\" si aucun chemin n\'est connu.'; + 'Sélectionne automatiquement le meilleur chemin connu, et utilise le flood si aucun chemin n\'est connu.'; @override String get routing_modeFloodHint => @@ -1558,17 +1555,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String get routing_modeManualHint => - 'Il suit toujours le chemin précis que vous avez défini.'; + 'Envoie toujours selon le chemin exact que vous avez défini.'; @override - String get routing_currentRoute => 'Itinéraire actuel'; + String get routing_currentRoute => 'Route actuelle'; @override String get routing_directNoHops => 'Direct — sans relais'; @override String get routing_noPathYet => - 'Aucune voie encore trouvée. Le message suivant est envoyé jusqu\'à ce qu\'une route soit découverte.'; + 'Aucun chemin pour le moment. Le prochain message passera en flood jusqu\'à ce qu\'une route soit découverte.'; @override String get routing_floodBroadcast => 'Diffusion via tous les répéteurs'; @@ -1577,39 +1574,38 @@ class AppLocalizationsFr extends AppLocalizations { String get routing_editPath => 'Modifier le chemin'; @override - String get routing_forgetPath => 'Oubliez le chemin'; + String get routing_forgetPath => 'Oublier le chemin'; @override String get routing_knownPaths => 'Chemins connus'; @override - String get routing_knownPathsHint => 'Créez un raccourci pour y accéder.'; + String get routing_knownPathsHint => 'Touchez un chemin pour l\'utiliser.'; @override String get routing_inUse => 'En cours d\'utilisation'; @override - String get routing_qualityStrong => 'Première étape réussie'; + String get routing_qualityStrong => 'Premier saut fort'; @override - String get routing_qualityGood => 'Première étape réussie'; + String get routing_qualityGood => 'Premier saut correct'; @override - String get routing_qualityFair => 'Première étape réussie'; + String get routing_qualityFair => 'Premier saut moyen'; @override - String get routing_qualityWorked => 'A livré'; + String get routing_qualityWorked => 'A déjà fonctionné'; @override - String get routing_qualityFlood => - 'Rapporté par des informations provenant de plusieurs sources.'; + String get routing_qualityFlood => 'Entendu via flood'; @override String get routing_qualityUntested => 'Non testé'; @override String routing_lastWorked(String when) { - return 'a travaillé $when'; + return 'a fonctionné $when'; } @override @@ -1617,27 +1613,26 @@ class AppLocalizationsFr extends AppLocalizations { @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes livrés, $failures échoués'; } @override - String get routing_floodDelivery => 'Livraison en cas de inondation'; + String get routing_floodDelivery => 'Livraison flood'; @override String get pathEditor_title => 'Créer un chemin'; @override String pathEditor_hopCounter(int count) { - return '$count parmi 64 houblons'; + return '$count sur 64 sauts'; } @override String get pathEditor_noHops => - 'Aucun houblon ajouté pour le moment. Cliquez sur les répétiteurs ci-dessous pour les ajouter dans l\'ordre souhaité, ou enregistrez sans houblon pour envoyer directement.'; + 'Aucun saut pour le moment. Touchez les répéteurs ci-dessous pour les ajouter dans l\'ordre, ou enregistrez sans saut pour envoyer directement.'; @override - String get pathEditor_addHops => - 'Ajoutez les houblons dans l\'ordre souhaité.'; + String get pathEditor_addHops => 'Ajouter les sauts dans l\'ordre'; @override String get pathEditor_searchRepeaters => 'Rechercher des répétiteurs'; @@ -1654,17 +1649,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String pathEditor_invalidTokens(String tokens) { - return 'Incorrect : $tokens'; + return 'Invalide : $tokens'; } @override String get pathEditor_tooManyHops => 'Maximum 64 sauts'; @override - String get pathEditor_usePath => 'Utilisez ce chemin.'; + String get pathEditor_usePath => 'Utiliser ce chemin'; @override - String get pathEditor_removeHop => 'Éliminer le haricot'; + String get pathEditor_removeHop => 'Supprimer le saut'; @override String get pathEditor_unknownHop => 'Répéteur non identifié'; @@ -1681,20 +1676,20 @@ class AppLocalizationsFr extends AppLocalizations { 'L\'appareil n\'a pas encore été confirmé.'; @override - String get chat_type => 'Saisir'; + String get chat_type => 'Type'; @override String get chat_path => 'Chemin'; @override - String get chat_publicKey => 'Clé Publique'; + String get chat_publicKey => 'Clé publique'; @override String get chat_compressOutgoingMessages => 'Compresser les messages sortants'; @override - String get chat_floodForced => 'Tout le réseau (forcée)'; + String get chat_floodForced => 'Flood (forcé)'; @override String get chat_directForced => 'Direct (forcé)'; @@ -1705,13 +1700,13 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get chat_floodAuto => 'Tout le réseau (auto)'; + String get chat_floodAuto => 'Flood (auto)'; @override - String get chat_direct => 'Afficher'; + String get chat_direct => 'Direct'; @override - String get chat_poiShared => 'Point d\'intérêt Partagé'; + String get chat_poiShared => 'Point d\'intérêt partagé'; @override String chat_unread(int count) { @@ -1746,37 +1741,37 @@ class AppLocalizationsFr extends AppLocalizations { String get map_title => 'Carte des nœuds'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Rechercher par nom ou ID de nœud'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Activité'; @override - String get map_online => 'Online'; + String get map_online => 'En ligne'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Récents'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Rancide'; @override String get map_visible => 'Visible'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Masqué'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Centrer sur le nœud'; @override - String get map_details => 'Details'; + String get map_details => 'Détails'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Aucun GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Aucun nœud correspondant'; @override String get map_lineOfSight => 'Ligne de vue'; @@ -1799,7 +1794,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Epingles: $count'; + return 'Épingles : $count'; } @override @@ -1809,29 +1804,29 @@ class AppLocalizationsFr extends AppLocalizations { String get map_repeater => 'Répéteur'; @override - String get map_room => 'Room Server'; + String get map_room => 'Room server'; @override String get map_sensor => 'Capteur'; @override - String get map_pinDm => 'Clé (DM)'; + String get map_pinDm => 'Épingle (DM)'; @override - String get map_pinPrivate => 'Verrouiller (Privé)'; + String get map_pinPrivate => 'Épingle (privé)'; @override - String get map_pinPublic => 'Clé (Public)'; + String get map_pinPublic => 'Épingle (public)'; @override - String get map_lastSeen => 'Dernière fois vu'; + String get map_lastSeen => 'Vu pour la dernière fois'; @override String get map_disconnectConfirm => 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get map_from => 'À partir de'; + String get map_from => 'Depuis'; @override String get map_source => 'Source'; @@ -1843,19 +1838,19 @@ class AppLocalizationsFr extends AppLocalizations { String get map_type => 'Type'; @override - String get map_path => 'Path'; + String get map_path => 'Chemin'; @override - String get map_location => 'Location'; + String get map_location => 'Localisation'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => 'Localisation estimée'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => 'Clé publique'; @override - String get map_publicKeyPrefixHint => 'e.g. ab12'; + String get map_publicKeyPrefixHint => 'p. ex. ab12'; @override String get map_shareMarkerHere => 'Partager le marqueur ici'; @@ -1864,7 +1859,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_setAsMyLocation => 'Définir comme ma localisation'; @override - String get map_pinLabel => 'Étiquete de repin'; + String get map_pinLabel => 'Libellé de l\'épingle'; @override String get map_label => 'Étiquette'; @@ -1882,11 +1877,11 @@ class AppLocalizationsFr extends AppLocalizations { String get map_noChannelsAvailable => 'Aucun canal disponible'; @override - String get map_publicLocationShare => 'Partager dans un lieu public'; + String get map_publicLocationShare => 'Partage public de localisation'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Vous êtes sur le point de partager un emplacement dans $channelLabel. Ce canal est public et toute personne disposant de la clé PSK peut le voir.'; + return 'Vous êtes sur le point de partager une localisation dans $channelLabel. Ce canal est public et toute personne disposant de la PSK peut la voir.'; } @override @@ -1900,7 +1895,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_nodeTypes => 'Types de nœuds'; @override - String get map_chatNodes => 'Nœuds de Chat'; + String get map_chatNodes => 'Nœuds de chat'; @override String get map_repeaters => 'Répéteurs'; @@ -1909,10 +1904,10 @@ class AppLocalizationsFr extends AppLocalizations { String get map_otherNodes => 'Autres nœuds'; @override - String get map_showOverlaps => 'Chevauchement de la touche répétitive'; + String get map_showOverlaps => 'Chevauchements de clés de répéteur'; @override - String get map_keyPrefix => 'Préfixe clé'; + String get map_keyPrefix => 'Préfixe de clé'; @override String get map_filterByKeyPrefix => 'Filtrer par préfixe de clé'; @@ -1928,19 +1923,19 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_showGuessedLocations => - 'Afficher les emplacements des nœuds estimés'; + 'Afficher les emplacements estimés des nœuds'; @override String get map_showDiscoveryContacts => 'Afficher les contacts de découverte'; @override - String get map_guessedLocation => 'Lieu deviné'; + String get map_guessedLocation => 'Localisation estimée'; @override - String get map_lastSeenTime => 'Dernière fois vu'; + String get map_lastSeenTime => 'Vu pour la dernière fois'; @override - String get map_sharedPin => 'Clé partagée'; + String get map_sharedPin => 'Épingle partagée'; @override String get map_sharedAt => 'Partagé'; @@ -1968,22 +1963,22 @@ class AppLocalizationsFr extends AppLocalizations { String get map_pathTraceCancelled => 'Traçage de chemin annulé'; @override - String get mapCache_title => 'Cache de Carte Hors Ligne'; + String get mapCache_title => 'Cache de carte hors ligne'; @override String get mapCache_selectAreaFirst => - 'Sélectionner une zone pour la mise en cache en premier'; + 'Sélectionnez d\'abord une zone à mettre en cache'; @override String get mapCache_noTilesToDownload => - 'Aucun tuilage à télécharger pour cette zone.'; + 'Aucune tuile à télécharger pour cette zone.'; @override String get mapCache_downloadTilesTitle => 'Télécharger les tuiles'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Télécharger $count tuiles pour un usage hors ligne ?'; + return 'Télécharger $count tuiles pour une utilisation hors ligne ?'; } @override @@ -1991,16 +1986,16 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'Cachez $count tuiles'; + return '$count tuiles mises en cache'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Tuiles mis en cache ($downloaded) ($failed ratés)'; + return '$downloaded tuiles mises en cache ($failed en échec)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Vider le cache hors ligne'; + String get mapCache_clearOfflineCacheTitle => 'Effacer le cache hors ligne'; @override String get mapCache_clearOfflineCachePrompt => @@ -2014,10 +2009,10 @@ class AppLocalizationsFr extends AppLocalizations { String get mapCache_noAreaSelected => 'Aucune zone sélectionnée'; @override - String get mapCache_cacheArea => 'Zone de cache'; + String get mapCache_cacheArea => 'Zone à mettre en cache'; @override - String get mapCache_useCurrentView => 'Utiliser la Vue Actuelle'; + String get mapCache_useCurrentView => 'Utiliser la vue actuelle'; @override String get mapCache_zoomRange => 'Plage de zoom'; @@ -2029,14 +2024,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_downloadedTiles(int completed, int total) { - return 'Téléchargé $completed / $total'; + return '$completed / $total téléchargées'; } @override String get mapCache_downloadTilesButton => 'Télécharger les tuiles'; @override - String get mapCache_clearCacheButton => 'Vider le Cache'; + String get mapCache_clearCacheButton => 'Vider le cache'; @override String mapCache_failedDownloads(int count) { @@ -2112,7 +2107,7 @@ class AppLocalizationsFr extends AppLocalizations { String get login_repeaterLogin => 'Connexion au répéteur'; @override - String get login_roomLogin => 'Connexion Room Server'; + String get login_roomLogin => 'Connexion au room server'; @override String get login_password => 'Mot de passe'; @@ -2177,14 +2172,13 @@ class AppLocalizationsFr extends AppLocalizations { String get path_currentPathLabel => 'Chemin actuel'; @override - String get path_noRepeatersFound => - 'Aucun répéteur ou room server n\'a été trouvé.'; + String get path_noRepeatersFound => 'Aucun répéteur ou room server trouvé.'; @override String get repeater_management => 'Gestion des répéteurs'; @override - String get room_management => 'Administrattion Room Server'; + String get room_management => 'Gestion du room server'; @override String get repeater_guest => 'Informations sur les répéteurs'; @@ -2426,7 +2420,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Cacher le nom/l\'emplacement dans les annonces'; + 'Masquer le nom et la localisation dans les annonces'; @override String get repeater_advertisementSettings => 'Paramètres d\'annonces'; @@ -2543,7 +2537,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Envoyer des paquets de données qui semblent former des boucles de routage.'; @override - String get repeater_loopDetectOff => 'Prix'; + String get repeater_loopDetectOff => 'Désactivé'; @override String get repeater_loopDetectMinimal => 'Minimal'; @@ -2567,19 +2561,18 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_ownerInfo => 'Informations sur l\'opérateur'; + String get repeater_ownerInfo => 'Informations sur le propriétaire'; @override String get repeater_ownerInfoHelper => - 'Métadonnées publiques pour cet émetteur'; + 'Métadonnées publiques pour cet appareil'; @override String get repeater_refreshOwnerInfo => - 'Rafraîchir les informations sur l\'opérateur'; + 'Rafraîchir les informations du propriétaire'; @override - String get repeater_floodMax => - 'Nombre maximal de sauts lors des inondations'; + String get repeater_floodMax => 'Nombre maximal de sauts en flood'; @override String get repeater_floodMaxHelper => @@ -2590,7 +2583,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_advancedSettingsSubtitle => - 'Molettes de réglage pour les opérateurs expérimentés'; + 'Réglages avancés pour les opérateurs expérimentés'; @override String get repeater_pathHashMode => 'Mode de hachage de chemin'; @@ -2600,18 +2593,18 @@ class AppLocalizationsFr extends AppLocalizations { 'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.'; @override - String get repeater_txDelay => 'Retard dû aux inondations à Texas'; + String get repeater_txDelay => 'Délai de transmission en flood'; @override String get repeater_txDelayHelper => - 'Rétransmettre l\'espacement pour le trafic de secours en cas de inondation, en multipliant le temps d\'émission du paquet (0-2, valeur par défaut : 0,5). Une valeur plus élevée signifie moins de collisions, mais une vitesse de transmission plus lente.'; + 'Intervalle de retransmission pour le trafic flood, en multipliant le temps d\'émission des paquets (0-2, valeur par défaut : 0,5). Une valeur plus élevée réduit les collisions, mais ralentit la transmission.'; @override - String get repeater_directTxDelay => 'Retard de transmission direct'; + String get repeater_directTxDelay => 'Délai de transmission direct'; @override String get repeater_directTxDelayHelper => - 'Rétransmettre l\'espacement pour le trafic direct (non-inondation), en multipliant le temps de transmission des paquets (0-2, valeur par défaut : 0,3).'; + 'Intervalle de retransmission pour le trafic direct (non flood), en multipliant le temps de transmission des paquets (0-2, valeur par défaut : 0,3).'; @override String get repeater_intThresh => 'Seuil de perturbation'; @@ -2622,7 +2615,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_agcResetInterval => - 'Interval de réinitialisation de l\'AGC'; + 'Intervalle de réinitialisation de l\'AGC'; @override String get repeater_agcResetIntervalHelper => @@ -2632,19 +2625,18 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_actionsTitle => 'Actions'; @override - String get repeater_sendAdvert => 'Envoyer une publicité sur les inondations'; + String get repeater_sendAdvert => 'Envoyer une annonce flood'; @override String get repeater_sendAdvertSubtitle => - 'Diffuser une publicité sur les inondations via le réseau.'; + 'Diffuser une annonce flood via le réseau.'; @override - String get repeater_sendAdvertZeroHop => - 'Envoyer une publicité sans intermédiaire'; + String get repeater_sendAdvertZeroHop => 'Envoyer une annonce sans relais'; @override String get repeater_sendAdvertZeroHopSubtitle => - 'Diffuser une publicité d\'un seul saut (sans relais)'; + 'Diffuser une annonce d\'un seul saut (sans relais)'; @override String get repeater_clockSync => 'Synchroniser l\'heure maintenant'; @@ -2685,7 +2677,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_refreshRadioSettings => 'Rafraîchir les paramètres Radio'; @override - String get repeater_refreshTxPower => 'Rafraîchir la tension TX'; + String get repeater_refreshTxPower => 'Rafraîchir la puissance TX'; @override String get repeater_refreshPacketForwarding => @@ -2709,10 +2701,10 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_cliTitle => 'Répéteur CLI'; + String get repeater_cliTitle => 'CLI du répéteur'; @override - String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; + String get repeater_debugNextCommand => 'Déboguer la prochaine commande'; @override String get repeater_commandHelp => 'Aide'; @@ -2752,7 +2744,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cliQuickGetName => 'Obtenir le nom'; @override - String get repeater_cliQuickGetRadio => 'Obtenir la Radio'; + String get repeater_cliQuickGetRadio => 'Obtenir la radio'; @override String get repeater_cliQuickGetTx => 'Obtenir TX'; @@ -2764,7 +2756,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cliQuickVersion => 'Version'; @override - String get repeater_cliQuickAdvertise => 'Publier'; + String get repeater_cliQuickAdvertise => 'Diffuser'; @override String get repeater_cliQuickClock => 'Horloge'; @@ -3185,7 +3177,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpGetTxDelay => - 'Indique le facteur de délai de transmission en mode inondation.'; + 'Indique le facteur de délai de transmission en mode flood.'; @override String get repeater_cliHelpGetDirectTxDelay => @@ -3281,11 +3273,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpRegionListAllowed => - 'Énumère les régions autorisant la circulation des véhicules en cas de inondation.'; + 'Énumère les régions autorisant la circulation des véhicules en cas de flood.'; @override String get repeater_cliHelpRegionListDenied => - 'Liste des régions qui interdisent la circulation en cas de inondation.'; + 'Liste les régions qui interdisent la circulation en cas de flood.'; @override String get repeater_cliHelpStatsPackets => @@ -3837,13 +3829,13 @@ class AppLocalizationsFr extends AppLocalizations { String get pathTrace_you => 'Vous'; @override - String get pathTrace_failed => 'Traçage du chemin échoué.'; + String get pathTrace_failed => 'Échec du traçage du chemin.'; @override - String get pathTrace_notAvailable => 'Tracé de chemin non disponible.'; + String get pathTrace_notAvailable => 'Le traçage du chemin est indisponible.'; @override - String get pathTrace_refreshTooltip => 'Actualiser Path Trace'; + String get pathTrace_refreshTooltip => 'Actualiser le traçage du chemin'; @override String get pathTrace_someHopsNoLocation => @@ -4203,7 +4195,7 @@ class AppLocalizationsFr extends AppLocalizations { String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; @override - String get snrIndicator_lastSeen => 'Dernière fois vu'; + String get snrIndicator_lastSeen => 'Vu pour la dernière fois'; @override String get contactsSettings_title => 'Paramètres des contacts'; @@ -4341,12 +4333,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String radioStats_txAir(int seconds) { - return 'Temps d\'antenne à la télévision du Texas (total) : $seconds s'; + return 'Temps d\'antenne TX (total) : $seconds s'; } @override String radioStats_rxAir(int seconds) { - return 'Temps d\'utilisation de l\'appareil RX (total) : $seconds s'; + return 'Temps d\'antenne RX (total) : $seconds s'; } @override @@ -4491,49 +4483,49 @@ class AppLocalizationsFr extends AppLocalizations { String get translation_systemLanguage => 'Langue du système'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore en cours d\'exécution'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'Maintien de la connexion BLE'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return 'Supprimé $name'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return 'Échec de la suppression : $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'Échec de la mise à jour du canal : $error'; } @override String get contact_typeChat => 'Chat'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => 'Répéteur'; @override String get contact_typeRoom => 'Room'; @override - String get contact_typeSensor => 'Sensor'; + String get contact_typeSensor => 'Capteur'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => 'Inconnu'; @override - String get map_zoomIn => 'Zoomez'; + String get map_zoomIn => 'Zoom avant'; @override - String get map_zoomOut => 'Zoomez'; + String get map_zoomOut => 'Zoom arrière'; @override - String get map_centerMap => 'Carte du centre'; + String get map_centerMap => 'Centrer la carte'; @override String get chrome_bluetoothRequiresChromium => @@ -4545,54 +4537,54 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'Le GPS a confirmé.'; + String get pathTrace_legendGpsConfirmed => 'GPS confirmé'; @override String get pathTrace_legendInferred => 'Position déduite'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Simple'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Combiné'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Lecture'; @override String get pathMap_pause => 'Pause'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Rejouer'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Saut précédent'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Saut suivant'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Afficher l\'animation des paquets'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Masquer l\'animation des paquets'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Saut $current sur $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Chemins observés : $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Principal'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternative $index'; } @override @@ -4600,8 +4592,8 @@ class AppLocalizationsFr extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count sauts', + one: '1 saut', ); return '$_temp0'; } @@ -4612,14 +4604,14 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Segment partagé'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Segment estimé'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Utilisé par $count chemins'; } @override @@ -4627,33 +4619,34 @@ class AppLocalizationsFr extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count sauts n\'ont pas de localisation — le chemin indiqué est partiel', + one: 'Un saut n\'a pas de localisation — le chemin indiqué est partiel', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Afficher tout'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Masquer le chemin'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Afficher le chemin'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Réduire le panneau'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Développer le panneau'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Aucune localisation'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Verrouiller la vue sur le paquet'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Déverrouiller la vue du paquet'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 01c06a91..5d1a9a1f 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -21,100 +21,100 @@ class AppLocalizationsHu extends AppLocalizations { String get nav_map => 'Térkép'; @override - String get common_cancel => 'Át kell venni'; + String get common_cancel => 'Mégsem'; @override - String get common_ok => 'Rendben'; + String get common_ok => 'RENDBEN'; @override - String get common_connect => 'Kapcsolódj'; + String get common_connect => 'Csatlakozás'; @override - String get common_unknownDevice => 'Tudatlan eszköz'; + String get common_unknownDevice => 'Ismeretlen eszköz'; @override - String get common_save => 'Mentés'; + String get common_save => 'Megtakarítás'; @override String get common_delete => 'Töröl'; @override - String get common_deleteAll => 'Minden törlés'; + String get common_deleteAll => 'Összes törlése'; @override - String get common_close => 'Bezárás'; + String get common_close => 'Közeli'; @override - String get common_done => 'Done'; + String get common_done => 'Kész'; @override String get common_edit => 'Szerkesztés'; @override - String get common_add => 'Hozzáad'; + String get common_add => 'Hozzáadás'; @override - String get common_settings => 'Beállítások'; + String get common_settings => 'Beállítások elemre'; @override - String get common_disconnect => 'Csatlakozást megszakasztani'; + String get common_disconnect => 'Leválasztás'; @override - String get common_connected => 'Kapcsolódó'; + String get common_connected => 'Csatlakozva'; @override - String get common_disconnected => 'Elválasztva'; + String get common_disconnected => 'Szétkapcsolt'; @override - String get common_create => 'Készítsd'; + String get common_create => 'Teremt'; @override String get common_continue => 'Folytatás'; @override - String get common_share => 'Ossza meg'; + String get common_share => 'Részesedés'; @override String get common_copy => 'Másolat'; @override - String get common_retry => 'Újrapróbálja'; + String get common_retry => 'Próbálja újra'; @override String get common_hide => 'Elrejt'; @override - String get common_remove => 'Eltávolít'; + String get common_remove => 'Távolítsa el'; @override String get common_enable => 'Engedélyezés'; @override - String get common_disable => 'Leteteszt'; + String get common_disable => 'Letiltás'; @override - String get common_undo => 'Még egyszer'; + String get common_undo => 'Visszavonás'; @override - String get messageStatus_sent => 'Elküldve'; + String get messageStatus_sent => 'Küldött'; @override String get messageStatus_delivered => 'Szállítva'; @override - String get messageStatus_pending => 'Elküldés'; + String get messageStatus_pending => 'Küldés'; @override String get messageStatus_failed => 'Nem sikerült elküldeni'; @override - String get messageStatus_repeated => 'Ismételtem'; + String get messageStatus_repeated => '– ismételte Heard'; @override - String get common_reboot => 'Újraindítás'; + String get common_reboot => 'Indítsa újra'; @override - String get common_loading => 'Betöltés...'; + String get common_loading => 'Terhelés...'; @override String get common_notAvailable => '—'; @@ -136,7 +136,7 @@ class AppLocalizationsHu extends AppLocalizations { String get common_interval => 'Intervallum'; @override - String get scanner_title => 'MeshCore nyitott'; + String get scanner_title => 'MeshCore Open'; @override String get connectionChoiceUsbLabel => 'USB'; @@ -148,41 +148,40 @@ class AppLocalizationsHu extends AppLocalizations { String get connectionChoiceTcpLabel => 'TCP'; @override - String get tcpScreenTitle => 'TCP-n keresztül kapcsolódjon'; + String get tcpScreenTitle => 'Csatlakozzon TCP-n keresztül'; @override - String get tcpHostLabel => 'IP-cím'; + String get tcpHostLabel => 'Végpont'; @override - String get tcpHostHint => '192.168.40.10'; + String get tcpHostHint => '192.168.40.10 / example.com'; @override - String get tcpPortLabel => 'Múzeum'; + String get tcpPortLabel => 'Kikötő'; @override String get tcpPortHint => '5000'; @override - String get tcpStatus_notConnected => - 'Adja meg a célpontot, majd kapcsolja össze.'; + String get tcpStatus_notConnected => 'Adja meg a végpontot, és csatlakozzon'; @override String tcpStatus_connectingTo(String endpoint) { - return 'Kapcsolat a $endpoint-hez...'; + return 'Csatlakozás a következőhöz: $endpoint...'; } @override - String get tcpErrorHostRequired => 'Az IP-címet meg kell adni.'; + String get tcpErrorHostRequired => 'Házigazda szükséges.'; @override - String get tcpErrorPortInvalid => 'Az érték 1 és 65535 között kell lennie.'; + String get tcpErrorPortInvalid => 'A portnak 1 és 65535 között kell lennie.'; @override String get tcpErrorUnsupported => - 'A TCP-protokoll nem támogatott ez a platformon.'; + 'A TCP-átvitel nem támogatott ezen a platformon.'; @override - String get tcpErrorTimedOut => 'A TCP-kapcsolat időtúllépett.'; + String get tcpErrorTimedOut => 'A TCP-kapcsolat időtúllépése lejárt.'; @override String tcpConnectionFailed(String error) { @@ -190,97 +189,97 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get usbScreenTitle => 'USB-en keresztül csatlakoztassuk'; + String get usbScreenTitle => 'Csatlakoztassa USB-n keresztül'; @override String get usbScreenSubtitle => - 'Válasszon egy azonosított soros eszközt, és közvetlenül csatlakoztassa a MeshCore-hoz.'; + 'Válasszon egy észlelt soros eszközt, és csatlakozzon közvetlenül a MeshCore csomóponthoz.'; @override - String get usbScreenStatus => 'Válasszon egy USB-es eszközt'; + String get usbScreenStatus => 'Válasszon ki egy USB-eszközt'; @override String get usbScreenNote => - 'Az USB-es soros kommunikáció a támogatott Android eszközökön és asztali rendszereken is elérhető.'; + 'Az USB-soros aktív a támogatott Android-eszközökön és asztali platformokon.'; @override String get usbScreenEmptyState => - 'Nincs USB eszköz megtalálva. Csatlakoztasson egyet, majd frissítse a rendszert.'; + 'Nem található USB-eszköz. Csatlakoztasson egyet, és frissítse.'; @override - String get usbErrorPermissionDenied => 'A USB-es hozzáférés megtagadva.'; + String get usbErrorPermissionDenied => 'Az USB-engedély megtagadva.'; @override String get usbErrorDeviceMissing => - 'Az kiválasztott USB eszköz már nem elérhető.'; + 'A kiválasztott USB-eszköz már nem érhető el.'; @override - String get usbErrorInvalidPort => 'Válasszon egy érvényes USB-eszközt.'; + String get usbErrorInvalidPort => 'Válasszon ki egy érvényes USB-eszközt.'; @override String get usbErrorBusy => - 'Egy másik USB-csatlakozás kérése már folyamatban van.'; + 'Egy másik USB-csatlakozási kérelem már folyamatban van.'; @override - String get usbErrorNotConnected => 'Nincs csatlakoztatott USB eszköz.'; + String get usbErrorNotConnected => 'Nincs USB-eszköz csatlakoztatva.'; @override String get usbErrorOpenFailed => - 'Nem sikerült megnyitni a kiválasztott USB-eszközöt.'; + 'Nem sikerült megnyitni a kiválasztott USB-eszközt.'; @override String get usbErrorConnectFailed => - 'Nem sikerült kapcsolatot létesíteni a kiválasztott USB-eszközhöz.'; + 'Nem sikerült csatlakozni a kiválasztott USB-eszközhöz.'; @override String get usbErrorUnsupported => - 'Ez a platform nem támogat USB-es soros kommunikációt.'; + 'Az USB-soros nem támogatott ezen a platformon.'; @override - String get usbErrorAlreadyActive => 'Az USB-kapcsolat már be van állítva.'; + String get usbErrorAlreadyActive => 'Az USB-kapcsolat már aktív.'; @override - String get usbErrorNoDeviceSelected => 'Nincs kiválasztva USB eszköz.'; + String get usbErrorNoDeviceSelected => 'Nem lett kiválasztva USB-eszköz.'; @override - String get usbErrorPortClosed => 'Az USB-kapcsolat nem aktív.'; + String get usbErrorPortClosed => 'Az USB-csatlakozás nincs nyitva.'; @override String get usbErrorConnectTimedOut => - 'Kapcsolódás sikertelen. Ellenőrizze, hogy a eszköz rendelkezik-e USB-hez tartozó firmware-rel.'; + 'A kapcsolat időtúllépése lejárt. Győződjön meg arról, hogy az eszköz rendelkezik USB Companion firmware-rel.'; @override - String get usbFallbackDeviceName => 'Web-szériás eszköz'; + String get usbFallbackDeviceName => 'Web soros eszköz'; @override - String get usbStatus_notConnected => 'Válasszon egy USB-es eszközt'; + String get usbStatus_notConnected => 'Válasszon ki egy USB-eszközt'; @override - String get usbStatus_connecting => 'USB eszközhez való csatlakozás...'; + String get usbStatus_connecting => 'Csatlakozás USB-eszközhöz...'; @override - String get usbStatus_searching => 'USB eszközök keresése...'; + String get usbStatus_searching => 'USB-eszközök keresése...'; @override String usbConnectionFailed(String error) { - return 'USB-kapcsolat sikertelen: $error'; + return 'USB csatlakozás sikertelen: $error'; } @override - String get scanner_scanning => 'Készülékek keresése...'; + String get scanner_scanning => 'Eszközök keresése...'; @override - String get scanner_connecting => 'Kapcsolódás...'; + String get scanner_connecting => 'Csatlakozás...'; @override - String get scanner_disconnecting => 'Kapcsolat megszakad...'; + String get scanner_disconnecting => 'Leválasztás...'; @override - String get scanner_notConnected => 'Nem csatlakozva'; + String get scanner_notConnected => 'Nincs csatlakoztatva'; @override String scanner_connectedTo(String deviceName) { - return 'Kapcsolódik a $deviceName-hez'; + return 'Csatlakozva a következőhöz: $deviceName'; } @override @@ -288,39 +287,39 @@ class AppLocalizationsHu extends AppLocalizations { @override String get scanner_tapToScan => - 'A Tap Scan funkció segítségével kereshet MeshCore eszközöket.'; + 'Koppintson a Scan elemre a MeshCore-eszközök megkereséséhez'; @override String scanner_connectionFailed(String error) { - return 'Kapcsolódás sikertelen: $error'; + return 'A csatlakozás sikertelen: $error'; } @override - String get scanner_stop => 'Megállj'; + String get scanner_stop => 'Stop'; @override - String get scanner_scan => 'Szkenálás'; + String get scanner_scan => 'Letapogatás'; @override - String get scanner_bluetoothOff => 'A Bluetooth kikapcsolva'; + String get scanner_bluetoothOff => 'Bluetooth ki van kapcsolva'; @override String get scanner_bluetoothOffMessage => - 'Kérjük, kapcsolja be a Bluetooth-ot, hogy eszközök keresése lehessen.'; + 'Kérjük, kapcsolja be a Bluetooth-t az eszközök kereséséhez'; @override String get scanner_chromeRequired => 'Chrome böngésző szükséges'; @override String get scanner_chromeRequiredMessage => - 'Ez az alkalmazás a Bluetooth funkcióhoz Google Chrome-ot vagy Chromium alapú böngészőt igényel.'; + 'Ehhez a webes alkalmazáshoz Google Chrome vagy Chromium-alapú böngésző szükséges a Bluetooth támogatásához.'; @override - String get scanner_enableBluetooth => 'Engedje be a Bluetooth funkciót'; + String get scanner_enableBluetooth => 'Bluetooth engedélyezése'; @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'A Bluetooth nem érhető el a böngészőben. Inkább csatlakoztasson USB-n keresztül.'; @override String get device_quickSwitch => 'Gyors váltás'; @@ -329,228 +328,224 @@ class AppLocalizationsHu extends AppLocalizations { String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Beállítások'; + String get settings_title => 'Beállítások elemre'; @override - String get settings_deviceInfo => 'A készülék információi'; + String get settings_deviceInfo => 'Készülék Info'; @override - String get settings_appSettings => 'Alkalmazási beállítások'; + String get settings_appSettings => 'Alkalmazásbeállítások'; @override String get settings_appSettingsSubtitle => - 'Értesítések, üzenetküldés és térképi beállítások'; + 'Értesítések, üzenetküldés és térképbeállítások'; @override - String get settings_nodeSettings => 'Műközép beállítások'; + String get settings_nodeSettings => 'Csomópont beállításai'; @override - String get settings_nodeName => 'Vonal neve'; + String get settings_nodeName => 'Csomópont neve'; @override - String get settings_nodeNameNotSet => 'Nem megállapított'; + String get settings_nodeNameNotSet => 'Nincs beállítva'; @override - String get settings_nodeNameHint => 'Adja meg a csomópont nevét'; + String get settings_nodeNameHint => 'Írja be a csomópont nevét'; @override - String get settings_nodeNameUpdated => 'Neve frissítve'; + String get settings_nodeNameUpdated => 'Név frissítve'; @override String get settings_radioSettings => 'Rádióbeállítások'; @override String get settings_radioSettingsSubtitle => - 'Frekvencia, teljesítmény, szélesítési tényező'; + 'Frekvencia, teljesítmény, szórási tényező'; @override - String get settings_radioSettingsUpdated => 'A rádió beállítások frissítve'; + String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve'; @override - String get settings_location => 'Helyszín'; + String get settings_location => 'hely'; @override String get settings_locationSubtitle => 'GPS koordináták'; @override - String get settings_locationUpdated => - 'A helyzet és a GPS beállítások frissítve'; + String get settings_locationUpdated => 'A hely- és GPS-beállítások frissítve'; @override String get settings_locationBothRequired => - 'Kérjük, adja meg a földrajzi szélességet és hosszúságot.'; + 'Adja meg a szélességi és hosszúsági fokokat is.'; @override String get settings_locationInvalid => - 'Érvénytelen szélesszög vagy hosszszög.'; + 'Érvénytelen szélesség vagy hosszúság.'; @override - String get settings_locationGPSEnable => 'GPS engedélyezve'; + String get settings_locationGPSEnable => 'GPS engedélyezése'; @override String get settings_locationGPSEnableSubtitle => - 'Lehetővé teszi, hogy a GPS automatikusan frissítse a helyzetet.'; + 'Lehetővé teszi a GPS számára a hely automatikus frissítését.'; @override - String get settings_locationIntervalSec => - 'GPS-számolási intervallum (másodpercek)'; + String get settings_locationIntervalSec => 'GPS intervallum (másodperc)'; @override String get settings_locationIntervalInvalid => - 'Az intervallum legalább 60 másodpercnek, de legfeljebb 86400 másodpercnak kell lennie.'; + 'Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.'; @override - String get settings_latitude => 'Nyugat-–––––––––––––––––––––––––––––––'; + String get settings_latitude => 'Szélesség'; @override - String get settings_longitude => 'hosszúság'; + String get settings_longitude => 'Hosszúság'; @override - String get settings_contactSettings => 'Kapcsolat beállítások'; + String get settings_contactSettings => 'Kapcsolati beállítások'; @override String get settings_contactSettingsSubtitle => - 'Beállítások, amelyek meghatározzák, hogyan lehet új kapcsolatokat hozzáadni.'; + 'A névjegyek hozzáadásának beállításai.'; @override - String get settings_privacyMode => 'Adatvédelem mód'; + String get settings_privacyMode => 'Adatvédelmi mód'; @override String get settings_privacyModeSubtitle => - 'Elrejtsük a nevét/a helyszínt az űrianyagokban'; + 'Név/hely elrejtése a hirdetésekben'; @override String get settings_privacyModeToggle => - 'Engedje be a privát üzemmódot, hogy elrejtse a nevét és a helyét az online hirdetésekben.'; + 'Kapcsolja be az adatvédelmi módot, hogy elrejtse nevét és tartózkodási helyét a hirdetésekben.'; @override - String get settings_privacyModeEnabled => 'Adatvédelem mód beállítva'; + String get settings_privacyModeEnabled => 'Az adatvédelmi mód engedélyezve'; @override - String get settings_privacyModeDisabled => 'Adatvédelem mód kikapcsolva'; + String get settings_privacyModeDisabled => 'Az adatvédelmi mód letiltva'; @override - String get settings_privacy => 'Adatvédelem beállítások'; + String get settings_privacy => 'Adatvédelmi beállítások'; @override String get settings_privacySubtitle => - 'Ellenőrizd, hogy milyen információkat osztanak meg.'; + 'Szabályozza, hogy milyen információkat osztanak meg.'; @override String get settings_privacySettingsDescription => - 'Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.'; + 'Válassza ki, hogy eszköze milyen információkat ossza meg másokkal.'; @override - String get settings_denyAll => 'Elutasítom'; + String get settings_denyAll => 'Mindent tagadni'; @override - String get settings_allowByContact => - 'Lehetővé teszi a kapcsolatok kezelését'; + String get settings_allowByContact => 'Engedélyezés kapcsolatjelző jelzőkkel'; @override - String get settings_allowAll => 'Engedje meg mindent'; + String get settings_allowAll => 'Minden engedélyezése'; @override - String get settings_telemetryBaseMode => 'Adatkapcsolati alapállapot'; + String get settings_telemetryBaseMode => 'Telemetriai alapmód'; @override - String get settings_telemetryLocationMode => 'Adatkapcsolási helyszín mód'; + String get settings_telemetryLocationMode => + 'Telemetriás helymeghatározási mód'; @override - String get settings_telemetryEnvironmentMode => - 'Adatkapcsolati környezeti mód'; + String get settings_telemetryEnvironmentMode => 'Telemetriás környezeti mód'; @override - String get settings_advertLocation => 'Reklám megjelenési hely'; + String get settings_advertLocation => 'Hirdetés helye'; @override String get settings_advertLocationSubtitle => - 'A hirdetés tartalmazza a helyszínt.'; + 'A hirdetésben szerepeljen a helyszín.'; @override - String get settings_multiAck => 'Többszörös visszaigazolások'; + String get settings_multiAck => 'Multi-ACK'; @override - String get settings_telemetryModeUpdated => 'A telemetriamód frissítve'; + String get settings_telemetryModeUpdated => 'Telemetriai mód frissítve'; @override - String get settings_actions => 'Tevékenységek'; + String get settings_actions => 'Akciók'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'Az összes elérési út törlése'; @override String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + 'Törölje az összes elérési utat a névjegyekből.'; @override String get settings_sendAdvertisement => 'Hirdetés küldése'; @override - String get settings_sendAdvertisementSubtitle => 'A nyilvános megjelenés'; + String get settings_sendAdvertisementSubtitle => + 'Jelenlét a közvetítésben most'; @override - String get settings_advertisementSent => 'Hirdetés elküldve'; + String get settings_advertisementSent => 'Reklám elküldve'; @override - String get settings_syncTime => 'Szinkronizációs idő'; + String get settings_syncTime => 'Szinkronizálási idő'; @override String get settings_syncTimeSubtitle => - 'Állítsa a készülék időzítését a telefon időjére'; + 'Az eszköz órájának beállítása a telefon idejére'; @override - String get settings_timeSynchronized => 'Időben szinkronizált'; + String get settings_timeSynchronized => 'Idő szinkronizálva'; @override - String get settings_refreshContacts => 'Újraindítsd a kapcsolatok listát'; + String get settings_refreshContacts => 'Névjegyek frissítése'; @override String get settings_refreshContactsSubtitle => - 'Újra töltse a kontaktlista-adatokat a készülékről'; + 'Névjegylista újratöltése az eszközről'; @override - String get settings_rebootDevice => 'Újraindítás'; + String get settings_rebootDevice => 'Eszköz újraindítása'; @override - String get settings_rebootDeviceSubtitle => - 'Indítsa újra a MeshCore eszközt.'; + String get settings_rebootDeviceSubtitle => 'Indítsa újra a MeshCore eszközt'; @override String get settings_rebootDeviceConfirm => - 'Biztosan szeretné újraindítani a készüléket? Ebben az esetben a kapcsolat megszűnik.'; + 'Biztosan újra akarja indítani az eszközt? Megszakad a kapcsolat.'; @override String get settings_debug => 'Hibakeresés'; @override - String get settings_companionDebugLog => 'Párhuzamos hibakeresési napló'; + String get settings_companionDebugLog => 'Companion Debug Log'; @override String get settings_companionDebugLogSubtitle => - 'BLE/TCP/USB parancsok, válaszok és alapvető adatok'; + 'BLE/TCP/USB parancsok, válaszok és nyers adatok'; @override - String get settings_appDebugLog => 'App-debug log'; + String get settings_appDebugLog => 'Alkalmazás hibakeresési naplója'; @override - String get settings_appDebugLogSubtitle => 'Programozási hibajelzések'; + String get settings_appDebugLogSubtitle => 'Alkalmazás hibakeresési üzenetei'; @override - String get settings_about => 'Ról'; + String get settings_about => 'Körülbelül'; @override String settings_aboutVersion(String version) { - return 'MeshCore Open $version verzió'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => - '2026-os MeshCore nyílt forráskódú projekt'; + String get settings_aboutLegalese => '2026 MeshCore nyílt forráskódú projekt'; @override String get settings_aboutDescription => - 'Egy nyílt forráskódú Flutter kliens a MeshCore LoRa hálózati eszközök számára.'; + 'Nyílt forráskódú Flutter kliens MeshCore LoRa mesh hálózati eszközökhöz.'; @override String get settings_aboutOpenMeteoAttribution => @@ -560,41 +555,41 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_infoName => 'Név'; @override - String get settings_infoId => 'Az azonosító'; + String get settings_infoId => 'ID'; @override String get settings_infoStatus => 'Állapot'; @override - String get settings_infoBattery => 'Akku'; + String get settings_infoBattery => 'Akkumulátor'; @override - String get settings_infoPublicKey => 'Nyelvkönyv'; + String get settings_infoPublicKey => 'Nyilvános kulcs'; @override - String get settings_infoContactsCount => 'Kapcsolatok száma'; + String get settings_infoContactsCount => 'Névjegyek száma'; @override - String get settings_infoChannelCount => 'Csatorna száma'; + String get settings_infoChannelCount => 'Csatornaszám'; @override - String get settings_presets => 'Előre beállított beállítások'; + String get settings_presets => 'Előbeállítások'; @override String get settings_frequency => 'Frekvencia (MHz)'; @override - String get settings_frequencyHelper => '300,0 – 2500,0'; + String get settings_frequencyHelper => '300,0 - 2500,0'; @override String get settings_frequencyInvalid => 'Érvénytelen frekvencia (300-2500 MHz)'; @override - String get settings_bandwidth => 'Kapacitás'; + String get settings_bandwidth => 'Sávszélesség'; @override - String get settings_spreadingFactor => 'Terjesztési tényező'; + String get settings_spreadingFactor => 'Spreading Factor'; @override String get settings_codingRate => 'Kódolási sebesség'; @@ -603,22 +598,22 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_txPower => 'TX teljesítmény (dBm)'; @override - String get settings_txPowerHelper => '0 – 22'; + String get settings_txPowerHelper => '0-22'; @override String get settings_txPowerInvalid => 'Érvénytelen TX teljesítmény (0-22 dBm)'; @override - String get settings_clientRepeat => 'Autonóm rendszer újra'; + String get settings_clientRepeat => 'Hálózaton kívüli ismétlés'; @override String get settings_clientRepeatSubtitle => - 'Engedje, hogy ez a eszköz mások számára is ismételje a hálózati csomagokat.'; + 'Engedélyezze az eszköznek, hogy megismételje a mesh-csomagokat mások számára'; @override String get settings_clientRepeatFreqWarning => - 'A hálózat nélküli kommunikációhoz 433, 869 vagy 918 MHz frekvenciát igényel.'; + 'A hálózaton kívüli ismétlés 433, 869 vagy 918 MHz frekvenciát igényel'; @override String settings_error(String message) { @@ -626,7 +621,7 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get appSettings_title => 'Alkalmazási beállítások'; + String get appSettings_title => 'Alkalmazásbeállítások'; @override String get appSettings_appearance => 'Megjelenés'; @@ -635,10 +630,10 @@ class AppLocalizationsHu extends AppLocalizations { String get appSettings_theme => 'Téma'; @override - String get appSettings_themeSystem => 'Alapértékek'; + String get appSettings_themeSystem => 'Rendszer alapértelmezett'; @override - String get appSettings_themeLight => 'Világítás'; + String get appSettings_themeLight => 'Fény'; @override String get appSettings_themeDark => 'Sötét'; @@ -647,196 +642,192 @@ class AppLocalizationsHu extends AppLocalizations { String get appSettings_language => 'Nyelv'; @override - String get appSettings_languageSystem => 'Alapértékek'; + String get appSettings_languageSystem => 'Rendszer alapértelmezett'; @override - String get appSettings_languageEn => 'Angol'; + String get appSettings_languageEn => 'angol'; @override - String get appSettings_languageFr => 'Francia'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Spanyol'; + String get appSettings_languageEs => 'Español'; @override - String get appSettings_languageDe => 'Német'; + String get appSettings_languageDe => 'Deutsch'; @override - String get appSettings_languagePl => 'Lengyel'; + String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'szlovén nyelv'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Portugál'; + String get appSettings_languagePt => 'Português'; @override - String get appSettings_languageIt => 'Olasz'; + String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => 'Kínai'; + String get appSettings_languageZh => '中文'; @override - String get appSettings_languageSv => 'Svéd'; + String get appSettings_languageSv => 'Svenska'; @override - String get appSettings_languageNl => 'Hollandi'; + String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Szlovén nyelvre fordítás'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Bulgár'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Orosz'; + String get appSettings_languageRu => 'Русский'; @override - String get appSettings_languageUk => 'Украинский'; + String get appSettings_languageUk => 'Українська'; @override - String get appSettings_enableMessageTracing => - 'Engedje meg a üzenetek nyomon követését'; + String get appSettings_enableMessageTracing => 'Üzenetkövetés engedélyezése'; @override String get appSettings_enableMessageTracingSubtitle => - 'Adja meg a üzenetek részletes útvonal- és időzítési adatokat.'; + 'Az üzenetek részletes útválasztási és időzítési metaadatainak megjelenítése'; @override String get appSettings_notifications => 'Értesítések'; @override - String get appSettings_enableNotifications => 'Engedélyezze az értesítéseket'; + String get appSettings_enableNotifications => 'Értesítések engedélyezése'; @override String get appSettings_enableNotificationsSubtitle => - 'Kapjon értesítéseket üzenetekről és hirdetésekről.'; + 'Értesítések fogadása üzenetekről és hirdetésekről'; @override String get appSettings_notificationPermissionDenied => - 'A értesítési engedély megtagadva'; + 'Értesítési engedély megtagadva'; @override - String get appSettings_notificationsEnabled => - 'A figyelmeztetések engedélyezve'; + String get appSettings_notificationsEnabled => 'Értesítések engedélyezve'; @override - String get appSettings_notificationsDisabled => - 'A figyelmeztetések kikapcsolva'; + String get appSettings_notificationsDisabled => 'Az értesítések letiltva'; @override - String get appSettings_messageNotifications => 'Üzenet értesítések'; + String get appSettings_messageNotifications => 'Üzenetértesítések'; @override String get appSettings_messageNotificationsSubtitle => - 'A figyelmeztetést megjelenítve, amikor új üzenet érkezik'; + 'Értesítés megjelenítése új üzenetek fogadásakor'; @override String get appSettings_channelMessageNotifications => - 'Csatorna-üzenetek értesítése'; + 'Csatorna üzenetek értesítései'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'A figyelmeztetést megjelenítve, amikor új üzenet érkezik a csatornáról'; + 'Értesítés megjelenítése csatornaüzenetek fogadásakor'; @override - String get appSettings_advertisementNotifications => 'Reklám értesítések'; + String get appSettings_advertisementNotifications => 'Reklám Értesítések'; @override String get appSettings_advertisementNotificationsSubtitle => - 'A figyelmeztetést megjelenítve, amikor új csomópontok kerülnek felfedezésre.'; + 'Értesítés megjelenítése új csomópontok felfedezésekor'; @override - String get appSettings_messaging => 'Üzenetek küldése'; + String get appSettings_messaging => 'Üzenetküldés'; @override String get appSettings_clearPathOnMaxRetry => - 'Egyértelmű út a Max Retry funkció használatával'; + 'Útvonal törlése a Max. újrapróbálkozásnál'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'A kapcsolat visszaállítás 5 sikertelen továbbítás után'; + 'Állítsa vissza a kapcsolati elérési utat 5 sikertelen küldési kísérlet után'; @override String get appSettings_pathsWillBeCleared => - 'Ha 5-szer sikertelenül próbálunk, a útvonalat automatikusan tisztítjuk.'; + 'Az elérési utak 5 sikertelen újrapróbálkozás után törlődnek'; @override String get appSettings_pathsWillNotBeCleared => - 'A utak automatikusan nem tisztítódnak.'; + 'Az útvonalak nem törlődnek automatikusan'; @override - String get appSettings_autoRouteRotation => 'Autóútok forgása'; + String get appSettings_autoRouteRotation => 'Auto Route Rotation'; @override String get appSettings_autoRouteRotationSubtitle => - 'Válasszon a legjobb útvonalak között, vagy válassza a vízözön-módot.'; + 'Váltson a legjobb útvonalak és az elárasztási mód között'; @override String get appSettings_autoRouteRotationEnabled => - 'Az automatikus útvonalváltás engedélyezve'; + 'Az automatikus útvonalforgatás engedélyezve'; @override String get appSettings_autoRouteRotationDisabled => - 'Az automatikus útvonal-választás funkció kikapcsolva.'; + 'Az automatikus útvonalforgatás letiltva'; @override - String get appSettings_maxRouteWeight => 'Maximális útvonal súly'; + String get appSettings_maxRouteWeight => 'Maximális útvonalsúly'; @override String get appSettings_maxRouteWeightSubtitle => - 'A lehető legnagyobb súly, amit egy útvonal sikeres szállítmányok során összegyűjthet.'; + 'Maximális súly, amelyet egy útvonal felhalmozhat a sikeres szállításokból'; @override - String get appSettings_initialRouteWeight => 'A kezdeti útvonal súlya'; + String get appSettings_initialRouteWeight => 'Az útvonal kezdeti súlya'; @override String get appSettings_initialRouteWeightSubtitle => - 'Az új, felfedezett útvonalakhoz tartozó kezdeti súly'; + 'Kezdősúly az újonnan felfedezett utakhoz'; @override - String get appSettings_routeWeightSuccessIncrement => - 'Sikerhez vezető növelés'; + String get appSettings_routeWeightSuccessIncrement => 'Siker súlynövekedés'; @override String get appSettings_routeWeightSuccessIncrementSubtitle => - 'A sikeresen teljesített útvonalhoz hozzáadott súly.'; + 'Súly hozzáadva egy útvonalhoz a sikeres szállítás után'; @override - String get appSettings_routeWeightFailureDecrement => 'Hibás súly csökkenése'; + String get appSettings_routeWeightFailureDecrement => 'Hiba súlycsökkentés'; @override String get appSettings_routeWeightFailureDecrementSubtitle => - 'A jártatásból eltávolított súly, ami a sikertelen szállítás következménye.'; + 'Sikertelen szállítás után eltávolított súly az útvonalról'; @override String get appSettings_maxMessageRetries => - 'Maximális üzenetek újraküldési próbálkozások'; + 'Üzenet újrapróbálkozások maximális száma'; @override String get appSettings_maxMessageRetriesSubtitle => - 'A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.'; + 'Az üzenet sikertelenként való megjelölése előtti újrapróbálkozások száma'; @override - String get appSettings_battery => 'Akku'; + String get appSettings_battery => 'Akkumulátor'; @override - String get appSettings_batteryChemistry => 'Aakkum töltés kémia'; + String get appSettings_batteryChemistry => 'Akkumulátor kémia'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Beállítások $deviceName-hez'; + return 'Beállítás eszközenként ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Csatlakozzon egy eszközhez, hogy kiválassza'; + 'A választáshoz csatlakozzon egy eszközhöz'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2 V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65 V)'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)'; @@ -845,36 +836,36 @@ class AppLocalizationsHu extends AppLocalizations { String get appSettings_mapDisplay => 'Térkép megjelenítése'; @override - String get appSettings_showRepeaters => 'Megismétlés'; + String get appSettings_showRepeaters => 'Ismétlők megjelenítése'; @override String get appSettings_showRepeatersSubtitle => - 'A térképen megjelenítsük a repeater-eket.'; + 'Az átjátszó csomópontok megjelenítése a térképen'; @override - String get appSettings_showChatNodes => 'Megjeleníts kommunikációs pontokat'; + String get appSettings_showChatNodes => 'Chat csomópontok megjelenítése'; @override String get appSettings_showChatNodesSubtitle => - 'A chat-szobákat megjelenítsük a térképen'; + 'A csevegési csomópontok megjelenítése a térképen'; @override - String get appSettings_showOtherNodes => 'Mutasson további csomópontokat'; + String get appSettings_showOtherNodes => 'Más csomópontok megjelenítése'; @override String get appSettings_showOtherNodesSubtitle => - 'Mutassa meg a többi hálózati elemet a térképen'; + 'Más csomóponttípusok megjelenítése a térképen'; @override - String get appSettings_timeFilter => 'Időbeli szűrés'; + String get appSettings_timeFilter => 'Időszűrő'; @override String get appSettings_timeFilterShowAll => - 'Mutassa meg az összes csomópontot'; + 'Az összes csomópont megjelenítése'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Mutasson az utolsó $hours órából származó adatokat.'; + return 'Az elmúlt $hours óra csomópontjainak megjelenítése'; } @override @@ -882,41 +873,41 @@ class AppLocalizationsHu extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Megjeleníts olyan węzveket, amelyek a következő területen lettek felfedezve:'; + 'Itt talált csomópontok megjelenítése:'; @override - String get appSettings_allTime => 'Minden időpont'; + String get appSettings_allTime => 'Minden alkalommal'; @override - String get appSettings_lastHour => 'Az utolsó óra'; + String get appSettings_lastHour => 'Utolsó óra'; @override - String get appSettings_last6Hours => 'Az utóban 6 óra'; + String get appSettings_last6Hours => 'Utolsó 6 óra'; @override - String get appSettings_last24Hours => 'Az utóbbi 24 óra'; + String get appSettings_last24Hours => 'Az elmúlt 24 óra'; @override - String get appSettings_lastWeek => 'A múlt héten'; + String get appSettings_lastWeek => 'Múlt héten'; @override - String get appSettings_offlineMapCache => 'Offline térkép tárolás'; + String get appSettings_offlineMapCache => 'Offline térképgyorsítótár'; @override String get appSettings_unitsTitle => 'Egységek'; @override - String get appSettings_unitsMetric => 'Méter (m / kilométer)'; + String get appSettings_unitsMetric => 'Metrikus (m/km)'; @override - String get appSettings_unitsImperial => 'Királyi (láb / mérföld)'; + String get appSettings_unitsImperial => 'birodalmi (ft/mi)'; @override - String get appSettings_noAreaSelected => 'Nincs kiválasztott terület.'; + String get appSettings_noAreaSelected => 'Nincs kiválasztott terület'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Kiválasztott terület (zoom: $minZoom-$maxZoom)'; + return 'Kijelölt terület (nagyítás $minZoom-$maxZoom)'; } @override @@ -924,97 +915,96 @@ class AppLocalizationsHu extends AppLocalizations { @override String get appSettings_appDebugLogging => - 'App-ban történő hibakereséshez használt naplózás'; + 'Alkalmazások hibakeresési naplózása'; @override String get appSettings_appDebugLoggingSubtitle => - 'Log alkalmazás hibaelhárítási üzenetek'; + 'A hibaelhárításhoz naplózza az alkalmazás hibakeresési üzeneteit'; @override String get appSettings_appDebugLoggingEnabled => - 'Az alkalmazás hibaelhárítási naplózás engedélyezve'; + 'Alkalmazások hibakeresési naplózása engedélyezve'; @override String get appSettings_appDebugLoggingDisabled => - 'Az alkalmazás hibaelhárítási naplózatának bekapcsolása kiküszöbölve'; + 'Az alkalmazáshibakeresési naplózás letiltva'; @override String get contacts_title => 'Kapcsolatok'; @override - String get contacts_noContacts => 'Jelenleg még nincs kapcsolat.'; + String get contacts_noContacts => 'Még nincsenek elérhetőségek'; @override String get contacts_contactsWillAppear => - 'A kapcsolatok megjelennek, amikor a eszközök hirdetnek.'; + 'A névjegyek akkor jelennek meg, amikor az eszközök hirdetnek'; @override - String get contacts_unread => 'Olvasatlan'; + String get contacts_unread => 'Nem olvasott'; @override - String get contacts_searchContactsNoNumber => 'Kapcsolatok keresése...'; + String get contacts_searchContactsNoNumber => 'Névjegyek keresése...'; @override String contacts_searchContacts(int number, String str) { - return 'Keresés $number-ban $str…'; + return 'Keresés a $number$str névjegyekben...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Keresés $number$str... Kedvencek'; + return 'Keresés a $number$str Kedvencek között...'; } @override String contacts_searchUsers(int number, String str) { - return 'Search $number$str Users...'; + return 'Keresés a $number$str felhasználók között...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Keresés $number-on, $str típusú adóállomások között...'; + return 'Keresés a $number$str átjátszók között...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Keresés $number-ban $str...'; + return 'Keresés a $number$str szobaszervereken...'; } @override - String get contacts_noUnreadContacts => 'Nincs olvasatlan üzenetek'; + String get contacts_noUnreadContacts => 'Nincsenek olvasatlan névjegyek'; @override - String get contacts_noContactsFound => - 'Nincs megtalálva semmilyen kapcsolat vagy csoport.'; + String get contacts_noContactsFound => 'Nem található névjegy vagy csoport'; @override - String get contacts_deleteContact => 'Kapcsolattól töröl'; + String get contacts_deleteContact => 'Névjegy törlése'; @override String contacts_removeConfirm(String contactName) { - return 'Hogy töröljem a $contactName nevű személyt a kontaktlistából?'; + return 'Eltávolítja a $contactName alkalmazást a névjegyek közül?'; } @override - String get contacts_manageRepeater => 'Ellenőriző eszköz kezelése'; + String get contacts_manageRepeater => 'Repeater kezelése'; @override - String get contacts_manageRoom => 'A szobai szerver kezelése'; + String get contacts_manageRoom => 'Szobaszerver kezelése'; @override - String get contacts_roomLogin => 'Szoba szerverbe való bejelentkezés'; + String get contacts_roomLogin => 'Szobaszerver bejelentkezés'; @override - String get contacts_openChat => 'Nyitott beszélgetés'; + String get contacts_openChat => 'Nyissa meg a Chat lehetőséget'; @override - String get contacts_editGroup => 'Edit csoport'; + String get contacts_editGroup => 'Csoport szerkesztése'; @override String get contacts_deleteGroup => 'Csoport törlése'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Hogy töröljem a \"$groupName\"-t?'; + return 'Eltávolítja a következőt: \"$groupName\"?'; } @override @@ -1024,46 +1014,45 @@ class AppLocalizationsHu extends AppLocalizations { String get contacts_moreOptions => 'További lehetőségek'; @override - String get contacts_searchOpen => 'Keresssz kapcsolatokat'; + String get contacts_searchOpen => 'Névjegyek keresése'; @override - String get contacts_searchClose => 'Teljesítse a keresést'; + String get contacts_searchClose => 'Keresés bezárása'; @override String get contacts_groupName => 'Csoport neve'; @override - String get contacts_groupNameRequired => - 'A csoportnak meg kell adni a nevét.'; + String get contacts_groupNameRequired => 'A csoportnév megadása kötelező'; @override - String get contacts_groupNameReserved => 'Ez a csoportnév foglalt'; + String get contacts_groupNameReserved => 'Ez a csoportnév fenntartva'; @override String contacts_groupAlreadyExists(String name) { - return 'A \"$name\" nevű csoport már létezik.'; + return 'A \"$name\" csoport már létezik'; } @override - String get contacts_filterContacts => 'Szűrj kontaktokat...'; + String get contacts_filterContacts => 'Névjegyek szűrése...'; @override String get contacts_noContactsMatchFilter => - 'Nincs találat a megadott szűrés alapján.'; + 'Egyetlen névjegy sem felel meg a szűrőnek'; @override String get contacts_noMembers => 'Nincsenek tagok'; @override - String get contacts_lastSeenNow => 'utóbbi időben'; + String get contacts_lastSeenNow => 'nemrég'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '~ $minutes perc'; + return '~ $minutes min.'; } @override - String get contacts_lastSeenHourAgo => 'Kb. 1 óra'; + String get contacts_lastSeenHourAgo => '~ 1 óra'; @override String contacts_lastSeenHoursAgo(int hours) { @@ -1071,134 +1060,135 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get contacts_lastSeenDayAgo => 'Kb. 1 nap'; + String get contacts_lastSeenDayAgo => '~ 1 nap'; @override String contacts_lastSeenDaysAgo(int days) { - return '~ $days days'; + return '~ $days nap'; } @override - String get contact_info => 'Kapcsolattartási információk'; + String get contact_info => 'Elérhetőségi adatok'; @override - String get contact_settings => 'Kapcsolat beállítások'; + String get contact_settings => 'Kapcsolati beállítások'; @override - String get contact_telemetry => 'Adatvisszaadás'; + String get contact_telemetry => 'Telemetria'; @override - String get contact_lastSeen => 'Utoljára, amikor látták'; + String get contact_lastSeen => 'Utoljára látott'; @override - String get contact_clearChat => 'Tiszta beszélgetés'; + String get contact_clearChat => 'Csevegés törlése'; @override - String get contact_teleBase => 'Adatgyűjtő központ'; + String get contact_teleBase => 'Telemetriai bázis'; @override String get contact_teleBaseSubtitle => - 'Engedje meg a akkumulátor töltöttségi szintjének és alapvető adatoknak megosztását.'; + 'Az akkumulátor töltöttségi szintjének és az alapvető telemetria megosztásának engedélyezése'; @override - String get contact_teleLoc => 'Adatkapcsolati helyszín'; + String get contact_teleLoc => 'Telemetriás hely'; @override - String get contact_teleLocSubtitle => 'Engedje meg a helyadatok megosztását'; + String get contact_teleLocSubtitle => + 'Helyadatok megosztásának engedélyezése'; @override - String get contact_teleEnv => 'Adatkapcsolati környezet'; + String get contact_teleEnv => 'Telemetriai környezet'; @override String get contact_teleEnvSubtitle => - 'Engedje meg az érzékelő adatok megosztását'; + 'A környezetérzékelő adatainak megosztásának engedélyezése'; @override String get channels_title => 'Csatornák'; @override - String get channels_noChannelsConfigured => 'Nincs konfigurált csatorna.'; + String get channels_noChannelsConfigured => + 'Nincsenek konfigurálva csatornák'; @override - String get channels_addPublicChannel => 'Hozzon létre nyilvános csatornát'; + String get channels_addPublicChannel => 'Nyilvános csatorna hozzáadása'; @override - String get channels_searchChannels => 'Keresési opciók...'; + String get channels_searchChannels => 'Csatornák keresése...'; @override - String get channels_noChannelsFound => 'Nincs megtalálható csatorna'; + String get channels_noChannelsFound => 'Nem található csatorna'; @override String channels_channelIndex(int index) { - return '$index-os csatorna'; + return 'Csatorna $index'; } @override - String get channels_public => 'A nyilvánosság számára'; + String get channels_public => 'Nyilvános'; @override String channels_via(String path) { - return 'via $path'; + return 'ezen keresztül: $path'; } @override - String get channels_private => 'Személyes'; + String get channels_private => 'Magán'; @override String get channels_editChannel => 'Csatorna szerkesztése'; @override - String get channels_muteChannel => 'Csendes csatorna'; + String get channels_muteChannel => 'Csatorna némítása'; @override - String get channels_unmuteChannel => 'Engedje be a hangot'; + String get channels_unmuteChannel => 'Csatorna némításának feloldása'; @override - String get channels_deleteChannel => 'Mozdony törlése'; + String get channels_deleteChannel => 'Csatorna törlése'; @override String channels_deleteChannelConfirm(String name) { - return 'Törlés $name? Ez nem visszafordítható.'; + return 'Törli a következőt: \"$name\"? Ezt nem lehet visszavonni.'; } @override String channels_channelDeleteFailed(String name) { - return 'Nem sikerült törölni a \"$name\" nevű csatornát.'; + return 'Nem sikerült törölni a \"$name\" csatornát'; } @override String channels_channelDeleted(String name) { - return 'A \"$name\" nevű csatorna törölve'; + return 'A \"$name\" csatorna törölve'; } @override - String get channels_addChannel => 'Csatorna hozzáadása'; + String get channels_addChannel => 'Csatorna hozzáadása lehetőségre'; @override - String get channels_channelIndexLabel => 'Csatorna index'; + String get channels_channelIndexLabel => 'Csatorna indexe'; @override String get channels_channelName => 'Csatorna neve'; @override - String get channels_usePublicChannel => 'Használja a nyilvános csatornát'; + String get channels_usePublicChannel => 'Nyilvános csatorna használata'; @override - String get channels_standardPublicPsk => - 'Általános, állami által finanszírozott PSK'; + String get channels_standardPublicPsk => 'Normál nyilvános PSK'; @override - String get channels_pskHex => 'PSK (Hexadecimális kód)'; + String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Véletlenszerűen generáljon PSK-t'; + String get channels_generateRandomPsk => 'Véletlenszerű PSK generálása'; @override - String get channels_enterChannelName => 'Kérjük, adja meg egy csatorna nevét'; + String get channels_enterChannelName => 'Kérjük, adja meg a csatorna nevét'; @override String get channels_pskMustBe32Hex => - 'A PSK 32-bázisú hexadecimális karakterből áll.'; + 'A PSK-nak 32 hexadecimális karakterből kell állnia'; @override String channels_channelAdded(String name) { @@ -1207,104 +1197,102 @@ class AppLocalizationsHu extends AppLocalizations { @override String channels_editChannelTitle(int index) { - return 'Módosítsd a csatornát $index'; + return 'Csatorna szerkesztése $index'; } @override - String get channels_smazCompression => 'SMAZ kompresszió'; + String get channels_smazCompression => 'SMAZ tömörítés'; @override - String get channels_cyr2latCompression => 'Cyr2Lat kompresszió'; + String get channels_cyr2latCompression => 'Cyr2Lat tömörítés'; @override String get channels_cyr2latCompressionDscr => - 'Néhány Cirill betűt Latin betűkkel helyettesít küldéskor.'; + 'Küldéskor egyes cirill karaktereket latin karakterekre cserél.'; @override - String get channels_cyr2latSettingsHeading => 'Cyr2Lat beállítások'; + String get channels_cyr2latSettingsHeading => 'Cyr2Lat beállítás'; @override - String get channels_cyr2latSettingsSubheading => 'Helyettesítési lista'; + String get channels_cyr2latSettingsSubheading => 'A helyettesítők listája'; @override String get channels_cyr2latSettingsDscr => - 'A karakterhelyettesítési JSON-konfiguráció szerkesztése'; + 'Szerkessze a karaktercsere JSON-konfigurációját'; @override - String get channels_cyr2latSettingsDialogHint => 'JSON-csere táblázat'; + String get channels_cyr2latSettingsDialogHint => 'JSON cseretérkép'; @override String channels_cyr2latSettingsDialogWrongJSON(Object error) { - return 'Hibás JSON: $error'; + return 'Érvénytelen JSON: $error'; } @override String channels_channelUpdated(String name) { - return 'A $name csatorna frissítve'; + return 'A \"$name\" csatorna frissítve'; } @override - String get settings_cyr2latProfileAdd => 'Cyr2Lat-profil hozzáadása'; + String get settings_cyr2latProfileAdd => 'Cyr2Lat profil hozzáadása'; @override String get settings_cyr2latProfileName => 'Profil neve'; @override - String get settings_cyr2latProfileNameEmpty => 'A profil neve nem lehet üres'; + String get settings_cyr2latProfileNameEmpty => 'A profilnév nem lehet üres'; @override - String get settings_cyr2latProfileAdded => 'A profil hozzáadása sikeres'; + String get settings_cyr2latProfileAdded => 'A profil sikeresen hozzáadva'; @override - String get settings_cyr2latProfileUpdated => 'A profil frissítése sikeres'; + String get settings_cyr2latProfileUpdated => 'A profil sikeresen frissítve'; @override - String get settings_cyr2latProfileEdit => 'Cyr2Lat profil szerkesztése'; + String get settings_cyr2latProfileEdit => 'Szerkessze a Cyr2Lat profilt'; @override String get settings_cyr2latProfileDelete => 'Cyr2Lat profil törlése'; @override - String get settings_cyr2latProfileDeleted => - 'A profil törlése sikeresen megtörtént'; + String get settings_cyr2latProfileDeleted => 'A profil sikeresen törölve'; @override String settings_cyr2latProfileDeleteDscr(String name) { - return 'Biztosan törölni szeretné a \"$name\" profilt?'; + return 'Biztos benne, hogy törölni kívánja a(z) \"$name\" profilt?'; } @override - String get channels_publicChannelAdded => 'A nyilvános csatorna hozzáadva'; + String get channels_publicChannelAdded => 'Nyilvános csatorna hozzáadva'; @override - String get channels_sortBy => 'Szűrés'; + String get channels_sortBy => 'Rendezés'; @override - String get channels_sortManual => 'Használati útmutató'; + String get channels_sortManual => 'Kézikönyv'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Legfrissebb üzenetek'; + String get channels_sortLatestMessages => 'Legújabb üzenetek'; @override - String get channels_sortUnread => 'Olvasatlan'; + String get channels_sortUnread => 'Nem olvasott'; @override - String get channels_createPrivateChannel => 'Létrehoz egy privát csatornát'; + String get channels_createPrivateChannel => + 'Hozzon létre egy privát csatornát'; @override - String get channels_createPrivateChannelDesc => - 'Titkos kulcs segítségével védelem.'; + String get channels_createPrivateChannelDesc => 'Titkos kulccsal biztosítva.'; @override - String get channels_joinPrivateChannel => - 'Csatlakozzon egy privát csatornához'; + String get channels_joinPrivateChannel => 'Csatlakozz egy privát csatornához'; @override String get channels_joinPrivateChannelDesc => - 'Kézzel adja meg a titkos kulcsot.'; + 'Adjon meg kézzel egy titkos kulcsot.'; @override String get channels_joinPublicChannel => @@ -1316,14 +1304,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channels_joinHashtagChannel => - 'Csatlakozzon egy hashtage-os csatornához'; + 'Csatlakozz egy Hashtag csatornához'; @override String get channels_joinHashtagChannelDesc => - 'Bárkinek lehet csatlakoznia a hashtagekhez tartozó csatornához.'; + 'Bárki csatlakozhat a hashtag csatornákhoz.'; @override - String get channels_scanQrCode => 'Scanned egy QR-kódot'; + String get channels_scanQrCode => 'QR-kód beolvasása'; @override String get channels_scanQrCodeComingSoon => 'Hamarosan'; @@ -1332,44 +1320,44 @@ class AppLocalizationsHu extends AppLocalizations { String get channels_enterHashtag => 'Írja be a hashtaget'; @override - String get channels_hashtagHint => 'pl. #csapat'; + String get channels_hashtagHint => 'például #csapat'; @override - String get chat_noMessages => 'Még nincs üzenet.'; + String get chat_noMessages => 'Még nincsenek üzenetek'; @override String get chat_sendMessage => 'Üzenet küldése'; @override String chat_sendMessageTo(String contactName) { - return 'Küldj üzenetet $contactName-nek'; + return 'Üzenet küldése a következő címre: $contactName'; } @override - String get chat_sendMessageToStart => 'Küldj egy üzenetet, hogy elindulj!'; + String get chat_sendMessageToStart => 'A kezdéshez küldjön üzenetet'; @override - String get chat_originalMessageNotFound => 'A eredeti üzenet nem található.'; + String get chat_originalMessageNotFound => 'Az eredeti üzenet nem található'; @override String chat_replyingTo(String name) { - return 'Replying to $name'; + return 'Válasz erre: $name'; } @override String chat_replyTo(String name) { - return 'Reply to $name'; + return 'Válasz erre: $name'; } @override - String get chat_location => 'Helyszín'; + String get chat_location => 'hely'; @override - String get chat_typeMessage => 'Írjon üzenetet...'; + String get chat_typeMessage => 'Írjon be egy üzenetet...'; @override String chat_messageTooLong(int maxBytes) { - return 'A üzenet túl hosszú (a maximális $maxBytes bájt).'; + return 'Az üzenet túl hosszú (max. $maxBytes bájt).'; } @override @@ -1379,93 +1367,91 @@ class AppLocalizationsHu extends AppLocalizations { String get chat_messageDeleted => 'Üzenet törölve'; @override - String get chat_retryingMessage => 'Újrapróbálási üzenet'; + String get chat_retryingMessage => 'Üzenet újrapróbálkozása'; @override String chat_retryCount(int current, int max) { - return 'Újrapróbál $current/$max'; + return 'Próbálja újra $current/$max'; } @override - String get chat_sendGif => 'Küldj GIF-ot'; + String get chat_sendGif => 'GIF küldése'; @override String get chat_reply => 'Válasz'; @override - String get chat_addReaction => 'Hozzon létre reakciót'; + String get chat_addReaction => 'Reakció hozzáadása'; @override - String get chat_me => 'Én'; + String get chat_me => 'Nekem'; @override - String get emojiCategorySmileys => 'Emoji'; + String get emojiCategorySmileys => 'Hangulatjelek'; @override - String get emojiCategoryGestures => 'Testmozgások'; + String get emojiCategoryGestures => 'Gesztusok'; @override - String get emojiCategoryHearts => 'Szívak'; + String get emojiCategoryHearts => 'Szívek'; @override - String get emojiCategoryObjects => 'Tárgyak'; + String get emojiCategoryObjects => 'Objektumok'; @override - String get gifPicker_title => 'Válasszon egy GIF-et'; + String get gifPicker_title => 'Válassz egy GIF-et'; @override String get gifPicker_searchHint => 'GIF-ek keresése...'; @override - String get gifPicker_poweredBy => 'Forrás: GIPHY'; + String get gifPicker_poweredBy => 'Üzemeltető: GIPHY'; @override - String get gifPicker_noGifsFound => 'Nincsenek GIF-ek megtalálva.'; + String get gifPicker_noGifsFound => 'Nem található GIF'; @override - String get gifPicker_failedLoad => 'Nem sikerült betölteni a GIF-fájlokat.'; + String get gifPicker_failedLoad => 'Nem sikerült betölteni a GIF-eket'; @override - String get gifPicker_failedSearch => 'Nem sikerült a GIF-eket megtalálni.'; + String get gifPicker_failedSearch => 'Nem sikerült a GIF-ek keresése'; @override - String get gifPicker_noInternet => 'Nincs internetkapcsolat.'; + String get gifPicker_noInternet => 'Nincs internet kapcsolat'; @override - String get debugLog_appTitle => 'App-debug log'; + String get debugLog_appTitle => 'Alkalmazás hibakeresési naplója'; @override - String get debugLog_bleTitle => 'BLE hibajelentő napló'; + String get debugLog_bleTitle => 'BLE hibakeresési napló'; @override - String get debugLog_copyLog => 'Másolat napló'; + String get debugLog_copyLog => 'Napló másolása'; @override - String get debugLog_clearLog => 'Jelzett napló'; + String get debugLog_clearLog => 'Napló törlése'; @override - String get debugLog_copied => 'Hibajelentő napló másolva'; + String get debugLog_copied => 'Hibakeresési napló másolva'; @override - String get debugLog_bleCopied => 'BLE-log másolva'; + String get debugLog_bleCopied => 'BLE napló másolva'; @override - String get debugLog_noEntries => - 'Jelenleg még nem léteznek hibaelhárítási naplókat.'; + String get debugLog_noEntries => 'Még nincsenek hibakeresési naplók'; @override String get debugLog_enableInSettings => - 'Engedje be az alkalmazás hibaelhárítási naplózását a beállítások menüben.'; + 'Engedélyezze az alkalmazás hibakeresési bejelentkezési beállításait'; @override String get debugLog_frames => 'Keretek'; @override - String get debugLog_rawLogRx => 'Az eredeti Log-RX'; + String get debugLog_rawLogRx => 'Nyers Log-RX'; @override - String get debugLog_noBleActivity => - 'Jelenleg nincs BLE-hez kapcsolódó tevékenység.'; + String get debugLog_noBleActivity => 'Még nincs BLE tevékenység'; @override String debugFrame_length(int count) { @@ -1478,11 +1464,11 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'Címzett:'; + String get debugFrame_textMessageHeader => 'Szöveges üzenet keret:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Célhely: $pubKey'; + return '- Cél PubKey: $pubKey'; } @override @@ -1492,27 +1478,27 @@ class AppLocalizationsHu extends AppLocalizations { @override String debugFrame_flags(String value) { - return '- Jelvények: 0x$value'; + return '- Zászlók: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Tartalom típusa: $type ($label)'; + return '- Szöveg típusa: $type ($label)'; } @override - String get debugFrame_textTypeCli => 'Parancssori felület (CLI)'; + String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Egyszerű, alap, hagyományos'; + String get debugFrame_textTypePlain => 'Egyszerű'; @override String debugFrame_text(String text) { - return '- Tartalom: \"$text\"'; + return '- Szöveg: \"$text\"'; } @override - String get debugFrame_hexDump => 'Hex-dump:'; + String get debugFrame_hexDump => 'Hex dump:'; @override String chat_hopsCount(int count) { @@ -1526,136 +1512,133 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get chat_removePath => 'Törölje a elérési útvonalat'; + String get chat_removePath => 'Távolítsa el az útvonalat'; @override String get chat_noPathHistoryYet => - 'Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.'; + 'Még nincs úttörténet.\nÜzenet küldése utak felfedezéséhez.'; @override String get chat_pathCleared => - 'Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.'; + 'Út megtisztítva. A következő üzenet újra felfedezi az útvonalat.'; @override - String get chat_fullPath => 'Teljes elérési út'; + String get chat_fullPath => 'Teljes útvonal'; @override - String get routing_title => 'Útvonal meghatározás'; + String get routing_title => 'Útválasztás'; @override - String get routing_modeAuto => 'Autó'; + String get routing_modeAuto => 'Auto'; @override - String get routing_modeFlood => 'Áradás'; + String get routing_modeFlood => 'Árvíz'; @override - String get routing_modeManual => 'Használati útmutató'; + String get routing_modeManual => 'Kézikönyv'; @override String get routing_modeAutoHint => - 'Automatikusan kiválasztja a legismertebb útvonalat, és ha egyik sem ismert, akkor \"vízzel\" tölti ki.'; + 'Automatikusan kiválasztja a legismertebb utat, és elárasztja, ha nem ismert.'; @override String get routing_modeFloodHint => - 'Átvisszaadások minden erősítőn keresztül. A legmegbízhatóbb megoldás, de több időt igényel.'; + 'Minden átjátszón keresztül sugároz. A legmegbízhatóbb, de több műsoridőt használ.'; @override String get routing_modeManualHint => - 'Mindig pontosan az útvonalat követi, amelyet megad.'; + 'Mindig pontosan az Ön által megadott útvonalon küld.'; @override String get routing_currentRoute => 'Jelenlegi útvonal'; @override - String get routing_directNoHops => 'Közvetlen – nincs átjáró állomás'; + String get routing_directNoHops => 'Közvetlen – nincs átjátszó ugrás'; @override String get routing_noPathYet => - 'Még nincs útvonal. A következő üzenet a keresésig vár.'; + 'Még nincs út. A következő üzenet elárasztja az útvonalat.'; @override - String get routing_floodBroadcast => - 'Azonnali továbbítás minden erősítőn keresztül.'; + String get routing_floodBroadcast => 'Minden átjátszón keresztül sugározd'; @override String get routing_editPath => 'Útvonal szerkesztése'; @override - String get routing_forgetPath => 'Felejtsd el a útvonalat'; + String get routing_forgetPath => 'Felejtsd el az utat'; @override - String get routing_knownPaths => 'Jellegzetes útvonalak'; + String get routing_knownPaths => 'Ismert utak'; @override String get routing_knownPathsHint => - 'Készíts egy útvonalat, hogy átválhass rá.'; + 'Koppintson egy elérési útra a váltáshoz.'; @override String get routing_inUse => 'Használatban'; @override - String get routing_qualityStrong => 'Erős első lépés'; + String get routing_qualityStrong => 'Erős első ugrás'; @override - String get routing_qualityGood => 'Jó első lépés'; + String get routing_qualityGood => 'Jó első ugrás'; @override - String get routing_qualityFair => 'Jó első lépés'; + String get routing_qualityFair => 'Tisztességes első ugrás'; @override - String get routing_qualityWorked => 'Előállított'; + String get routing_qualityWorked => 'Szállított'; @override - String get routing_qualityFlood => - 'Információt hallottam a katasztrófa miatt.'; + String get routing_qualityFlood => 'Árvízen keresztül hallatszott'; @override - String get routing_qualityUntested => 'Vizsgálatnak nem подвержен'; + String get routing_qualityUntested => 'Nem tesztelt'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'dolgozott $when'; } @override - String get routing_neverWorked => 'sosem megerősítve'; + String get routing_neverWorked => 'soha nem erősítették meg'; @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes kézbesítve, $failures sikertelen'; } @override - String get routing_floodDelivery => 'Vízparti szállítás'; + String get routing_floodDelivery => 'Árvízi szállítás'; @override - String get pathEditor_title => 'Út megépítése'; + String get pathEditor_title => 'Építsd meg az útvonalat'; @override String pathEditor_hopCounter(int count) { - return '$count db 64-ből'; + return '$count 64 ugrásból'; } @override String get pathEditor_noHops => - 'Még nem adtam hozzá a bazsalikomot. A lent található gombokat használhatod, hogy sorrendben adjd hozzá, vagy mentheted anélkül, hogy bazsalikomot adnál hozzá, hogy közvetlenül elküldd.'; + 'Még nincs ugrás. Koppintson az alábbi ismétlőkre, ha sorrendben szeretné felvenni őket, vagy mentse ugrás nélkül a közvetlen küldéshez.'; @override - String get pathEditor_addHops => - 'Adja hozzá a bazsaidat a megfelelő sorrendben.'; + String get pathEditor_addHops => 'Sorrendben adjuk hozzá a komlót'; @override - String get pathEditor_searchRepeaters => 'Ismétlő eszközök keresése'; + String get pathEditor_searchRepeaters => 'Ismétlő keresése'; @override - String get pathEditor_advancedHex => 'Haladó szint: alapvető hex-út'; + String get pathEditor_advancedHex => 'Haladó: nyers hexadecimális útvonal'; @override - String get pathEditor_hexLabel => 'Hex előtagok'; + String get pathEditor_hexLabel => 'Hexadecimális előtagok'; @override String get pathEditor_hexHelper => - 'Két hatjegyű szám minden lépésen, amelyek egymástól elválasztják a kommák.'; + 'Két hexadecimális karakter ugrásonként, vesszővel elválasztva'; @override String pathEditor_invalidTokens(String tokens) { @@ -1663,58 +1646,58 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get pathEditor_tooManyHops => 'A maximális szám 64.'; + String get pathEditor_tooManyHops => 'Maximum 64 ugrás'; @override - String get pathEditor_usePath => 'Használja ezt az útvonalat.'; + String get pathEditor_usePath => 'Használja ezt az utat'; @override - String get pathEditor_removeHop => 'Távolítsa el a bazsalikomot'; + String get pathEditor_removeHop => 'Távolítsa el az ugrást'; @override - String get pathEditor_unknownHop => 'Tudatlan erősítő'; + String get pathEditor_unknownHop => 'Ismeretlen átjátszó'; @override String get chat_pathSavedLocally => - 'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.'; + 'Helyben mentve. Csatlakozás a szinkronizáláshoz.'; @override - String get chat_pathDeviceConfirmed => 'A készülék megvan.'; + String get chat_pathDeviceConfirmed => 'Az eszköz megerősítve.'; @override - String get chat_pathDeviceNotConfirmed => 'A készülék még nem bizonyított.'; + String get chat_pathDeviceNotConfirmed => 'Az eszköz még nincs megerősítve.'; @override - String get chat_type => 'Típus'; + String get chat_type => 'Írja be'; @override - String get chat_path => 'Út'; + String get chat_path => 'Útvonal'; @override - String get chat_publicKey => 'Nyelvkönyv'; + String get chat_publicKey => 'Nyilvános kulcs'; @override - String get chat_compressOutgoingMessages => 'A küldött üzenetek tömörítése'; + String get chat_compressOutgoingMessages => 'A kimenő üzenetek tömörítése'; @override - String get chat_floodForced => 'Áradás (kényszerített)'; + String get chat_floodForced => 'Árvíz (kényszerített)'; @override - String get chat_directForced => 'Közvetlen (erélyes)'; + String get chat_directForced => 'Közvetlen (kényszerített)'; @override String chat_hopsForced(int count) { - return '$count ánusz (erővel)'; + return '$count ugrás (kényszerített)'; } @override - String get chat_floodAuto => 'Vízosztás (autó)'; + String get chat_floodAuto => 'Árvíz (automatikus)'; @override String get chat_direct => 'Közvetlen'; @override - String get chat_poiShared => 'Közös erőforrás'; + String get chat_poiShared => 'POI megosztva'; @override String chat_unread(int count) { @@ -1722,87 +1705,88 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get chat_markAsUnread => 'Merekje olvashatóként'; + String get chat_markAsUnread => 'Megjelölés olvasatlanként'; @override String get chat_newMessages => 'Új üzenetek'; @override - String get chat_openLink => 'Nyisd meg a linket?'; + String get chat_openLink => 'Link megnyitása?'; @override String get chat_openLinkConfirmation => - 'Szeretnéd megnyitni ezt a linket a böngésződben?'; + 'Meg akarja nyitni ezt a hivatkozást a böngészőjében?'; @override String get chat_open => 'Nyitott'; @override String chat_couldNotOpenLink(String url) { - return 'Nem sikerült megnyitni a hivat: $url'; + return 'Nem sikerült megnyitni a linket: $url'; } @override - String get chat_invalidLink => 'Érvénytelen hivatkozás formátum'; + String get chat_invalidLink => 'Érvénytelen linkformátum'; @override - String get map_title => 'Grafikus ábrázás'; + String get map_title => 'Csomópont térkép'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => + 'Keresés a csomópont nevében vagy azonosítójában'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Tevékenység'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Legutóbbi'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Állott'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Látható'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Rejtett'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Középre a csomóponton'; @override - String get map_details => 'Details'; + String get map_details => 'Részletek'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Nincs GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Nincsenek megfelelő csomópontok'; @override - String get map_lineOfSight => 'Látási vonal'; + String get map_lineOfSight => 'Látóvonal'; @override - String get map_losScreenTitle => 'Látási vonal'; + String get map_losScreenTitle => 'Látóvonal'; @override String get map_noNodesWithLocation => - 'Nincs olyan adatpont, amelyhez helyszín-információk tartoznak.'; + 'Nincsenek helyadatokkal rendelkező csomópontok'; @override String get map_nodesNeedGps => - 'A pontoknak meg kell osztaniuk GPS koordinátáikat, hogy megjelenjenek a térképen.'; + 'A csomópontoknak meg kell osztaniuk GPS-koordinátáikat\nhogy megjelenjen a térképen'; @override String map_nodesCount(int count) { - return 'Csúcsok: $count'; + return 'Csomópontok: $count'; } @override String map_pinsCount(int count) { - return 'Csapok: $count'; + return 'Pins: $count'; } @override @@ -1812,182 +1796,181 @@ class AppLocalizationsHu extends AppLocalizations { String get map_repeater => 'Ismétlő'; @override - String get map_room => 'szoba'; + String get map_room => 'Szoba'; @override String get map_sensor => 'Érzékelő'; @override - String get map_pinDm => 'Jel (DM)'; + String get map_pinDm => 'Pin (DM)'; @override - String get map_pinPrivate => 'Titkos (privát)'; + String get map_pinPrivate => 'Pin (privát)'; @override - String get map_pinPublic => 'Jelmez (nyilvános)'; + String get map_pinPublic => 'Pin (nyilvános)'; @override - String get map_lastSeen => 'Utoljára látva'; + String get map_lastSeen => 'Utoljára látott'; @override String get map_disconnectConfirm => - 'Biztosan szeretné kiírni ezt a készüléket?'; + 'Biztosan le akarja kapcsolni ezt az eszközt?'; @override - String get map_from => 'Attól'; + String get map_from => 'Tól'; @override String get map_source => 'Forrás'; @override - String get map_flags => 'Zászló'; + String get map_flags => 'Zászlók'; @override - String get map_type => 'Type'; + String get map_type => 'Írja be'; @override - String get map_path => 'Path'; + String get map_path => 'Útvonal'; @override - String get map_location => 'Location'; + String get map_location => 'hely'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => 'Becsült hely'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => 'Nyilvános kulcs'; @override - String get map_publicKeyPrefixHint => 'e.g. ab12'; + String get map_publicKeyPrefixHint => 'pl. ab12'; @override - String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt'; + String get map_shareMarkerHere => 'Oszd meg a jelölőt itt'; @override - String get map_setAsMyLocation => 'Állítsa be a jelenlegi helyzetemként'; + String get map_setAsMyLocation => 'Beállítás helyemként'; @override - String get map_pinLabel => 'Címkét ragasztani'; + String get map_pinLabel => 'Kitűzött címke'; @override String get map_label => 'Címke'; @override - String get map_pointOfInterest => 'Érdekes hely'; + String get map_pointOfInterest => 'Érdekes pont'; @override - String get map_sendToContact => 'Kapcsolatfelvételi űrlap'; + String get map_sendToContact => 'Elküldés a kapcsolattartónak'; @override - String get map_sendToChannel => 'Küldés a csatornán'; + String get map_sendToChannel => 'Küldés a csatornára'; @override - String get map_noChannelsAvailable => 'Nincs elérhető csatorna.'; + String get map_noChannelsAvailable => 'Nincs elérhető csatorna'; @override - String get map_publicLocationShare => 'Térköz, nyilvános hely'; + String get map_publicLocationShare => 'Nyilvános helymegosztás'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Most egy helyszínt megosztasz a $channelLabel csatornán. Ez a csatorna nyilvános, és bárki, aki rendelkezik a PSK-val, megtekintheti.'; + return 'Arra készül, hogy megosszon egy helyet itt: $channelLabel. Ez a csatorna nyilvános, és a PSK birtokában bárki láthatja.'; } @override String get map_connectToShareMarkers => - 'Kapcsolódjon egy eszközhöz, hogy megoszthassa a vonalzókat.'; + 'Csatlakozzon egy eszközhöz a jelölők megosztásához'; @override - String get map_filterNodes => 'Szűrési pontok'; + String get map_filterNodes => 'Csomópontok szűrése'; @override - String get map_nodeTypes => 'Vonalak típusai'; + String get map_nodeTypes => 'Csomópont típusok'; @override - String get map_chatNodes => 'Csevegési pontok'; + String get map_chatNodes => 'Chat csomópontok'; @override - String get map_repeaters => 'Újraküldők'; + String get map_repeaters => 'Ismétlők'; @override String get map_otherNodes => 'Egyéb csomópontok'; @override - String get map_showOverlaps => 'Az ismétlő kulcsok ütköznek'; + String get map_showOverlaps => 'Repeater Key átfedések'; @override - String get map_keyPrefix => 'Kulcsfontosságú előtag'; + String get map_keyPrefix => 'Kulcselőtag'; @override - String get map_filterByKeyPrefix => 'Szűrj a kulcsos előtér szerint'; + String get map_filterByKeyPrefix => 'Szűrés kulcs előtagja szerint'; @override - String get map_publicKeyPrefix => 'Névfelhasználó kulc-prefix'; + String get map_publicKeyPrefix => 'Nyilvános kulcs előtagja'; @override String get map_markers => 'Jelölők'; @override - String get map_showSharedMarkers => 'Mutassa meg a közös jeleket'; + String get map_showSharedMarkers => 'Megosztott jelölők megjelenítése'; @override String get map_showGuessedLocations => - 'Megjelenítsa a megjósolt csomópontok helyét'; + 'Talált csomópont-helyek megjelenítése'; @override - String get map_showDiscoveryContacts => - 'Megjelenítse a Discovery-nál elérhet kontaktokat'; + String get map_showDiscoveryContacts => 'Felfedezési névjegyek megjelenítése'; @override - String get map_guessedLocation => 'Tippolt hely'; + String get map_guessedLocation => 'Talált hely'; @override - String get map_lastSeenTime => 'Utoljára megjelent idő'; + String get map_lastSeenTime => 'Utoljára látott időpont'; @override - String get map_sharedPin => 'Gemeinsames PIN-kód'; + String get map_sharedPin => 'Megosztott gombostű'; @override String get map_sharedAt => 'Megosztva'; @override - String get map_joinRoom => 'Csatlakozás a szobához'; + String get map_joinRoom => 'Csatlakozz a szobához'; @override - String get map_manageRepeater => 'Ellenőriző eszköz kezelése'; + String get map_manageRepeater => 'Repeater kezelése'; @override String get map_tapToAdd => - 'Nyomj meg a csomópontokhoz, hogy hozzáadd őket az útvonalhoz.'; + 'Érintse meg a csomópontokat, hogy hozzáadja őket az útvonalhoz.'; @override - String get map_runTrace => 'Útvonal követés'; + String get map_runTrace => 'Futtatási útvonal nyomkövetése'; @override - String get map_runTraceWithReturnPath => 'Visszaforduljon az eredeti úton.'; + String get map_runTraceWithReturnPath => 'Térj vissza ugyanazon az úton.'; @override - String get map_removeLast => 'Törölj utolsó'; + String get map_removeLast => 'Utolsó eltávolítása'; @override - String get map_pathTraceCancelled => 'Az útvonal követés megszakadt.'; + String get map_pathTraceCancelled => 'Útvonal nyomkövetés törölve.'; @override - String get mapCache_title => 'Offline térkép tárolás'; + String get mapCache_title => 'Offline térképgyorsítótár'; @override String get mapCache_selectAreaFirst => - 'Válasszon egy területet, amelyet először cache-oljon.'; + 'Válassza ki a gyorsítótárba helyezendő területet'; @override String get mapCache_noTilesToDownload => - 'Nincsenek letölthető tile-ok ebben a területben.'; + 'Ehhez a területhez nem lehet letölteni csempét'; @override - String get mapCache_downloadTilesTitle => 'Letöltsd a tile-okat'; + String get mapCache_downloadTilesTitle => 'Csempe letöltése'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Töltse le $count darab tile-t offline használatra?'; + return 'Letölti a $count csempét offline használatra?'; } @override @@ -1995,39 +1978,39 @@ class AppLocalizationsHu extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'Tárolt $count darab'; + return 'Gyorsítótárazott $count csempe'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Cached $downloaded tiles ($failed failed)'; + return 'Gyorsítótárazott $downloaded csempe ($failed sikertelen)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Tiszta offline tárhely'; + String get mapCache_clearOfflineCacheTitle => 'Offline gyorsítótár törlése'; @override String get mapCache_clearOfflineCachePrompt => - 'Távolítsa el az összes tárolt térképmegjelenítőt?'; + 'Eltávolítja az összes gyorsítótárazott térképcsempét?'; @override - String get mapCache_offlineCacheCleared => 'A helyi memóriát töröltük.'; + String get mapCache_offlineCacheCleared => 'Offline gyorsítótár törölve'; @override - String get mapCache_noAreaSelected => 'Nincs kiválasztott terület.'; + String get mapCache_noAreaSelected => 'Nincs kiválasztott terület'; @override - String get mapCache_cacheArea => 'Tároló terület'; + String get mapCache_cacheArea => 'Gyorsítótár terület'; @override - String get mapCache_useCurrentView => 'Használja a jelenlegi nézetet'; + String get mapCache_useCurrentView => 'Az aktuális nézet használata'; @override String get mapCache_zoomRange => 'Zoom tartomány'; @override String mapCache_estimatedTiles(int count) { - return 'Becsült kerámiák: $count'; + return 'Becsült csempék: $count'; } @override @@ -2036,10 +2019,10 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get mapCache_downloadTilesButton => 'Letöltsd a tile-okat'; + String get mapCache_downloadTilesButton => 'Letöltés csempe'; @override - String get mapCache_clearCacheButton => 'Ósztótt adatokat'; + String get mapCache_clearCacheButton => 'Törölje a gyorsítótárat'; @override String mapCache_failedDownloads(int count) { @@ -2053,69 +2036,69 @@ class AppLocalizationsHu extends AppLocalizations { String east, String west, ) { - return 'N $north, S $south, E $east, W $west'; + return 'É $north, D $south, K $east, Ny $west'; } @override - String get time_justNow => 'Most'; + String get time_justNow => 'Éppen most'; @override String time_minutesAgo(int minutes) { - return '$minutes perckel ezelőtt'; + return '$minutes perce'; } @override String time_hoursAgo(int hours) { - return '$hours óva'; + return '$hoursórája'; } @override String time_daysAgo(int days) { - return '${days}d ago'; + return '${days}napja'; } @override String get time_hour => 'óra'; @override - String get time_hours => 'órák'; + String get time_hours => 'óra'; @override String get time_day => 'nap'; @override - String get time_days => 'napok'; + String get time_days => 'napokon'; @override - String get time_week => 'het'; + String get time_week => 'hét'; @override - String get time_weeks => 'het, hetek'; + String get time_weeks => 'hét'; @override String get time_month => 'hónap'; @override - String get time_months => 'hónapok'; + String get time_months => 'hónap'; @override - String get time_minutes => 'percek'; + String get time_minutes => 'jegyzőkönyv'; @override - String get time_allTime => 'Bármely időpont'; + String get time_allTime => 'Minden idők'; @override - String get dialog_disconnect => 'Csatlakozást megszakasztani'; + String get dialog_disconnect => 'Leválasztás'; @override String get dialog_disconnectConfirm => - 'Biztosan szeretné kiírni ezt a készüléket?'; + 'Biztosan le akarja kapcsolni ezt az eszközt?'; @override - String get login_repeaterLogin => 'Ismételt bejelentkezés'; + String get login_repeaterLogin => 'Ismétlő bejelentkezés'; @override - String get login_roomLogin => 'Szoba szerverbe való bejelentkezés'; + String get login_roomLogin => 'Szobaszerver bejelentkezés'; @override String get login_password => 'Jelszó'; @@ -2124,32 +2107,32 @@ class AppLocalizationsHu extends AppLocalizations { String get login_enterPassword => 'Adja meg a jelszót'; @override - String get login_savePassword => 'Mentse el a jelszót'; + String get login_savePassword => 'Jelszó mentése'; @override String get login_savePasswordSubtitle => - 'A jelszó biztonságosan tárolódik ezen a készüléken.'; + 'A jelszó biztonságosan lesz tárolva ezen az eszközön'; @override String get login_repeaterDescription => - 'Adja meg a repeater (ismétítő) jelszót, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.'; + 'Adja meg az ismétlő jelszavát a vendég vagy adminisztrátori hozzáféréshez.'; @override String get login_roomDescription => - 'Adja meg a belépési kódot, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.'; + 'Adja meg a szoba jelszavát a vendég vagy adminisztrátori hozzáféréshez.'; @override - String get login_routing => 'Útvonal meghatározás'; + String get login_routing => 'Útválasztás'; @override - String get login_routingMode => 'Útvonal-kezelési mód'; + String get login_routingMode => 'Útválasztási mód'; @override String get login_autoUseSavedPath => - 'Automatikus (az eddigi útvonal használata)'; + 'Automatikus (mentett útvonal használata)'; @override - String get login_forceFloodMode => 'Erőforrás-alapú áramlás mód'; + String get login_forceFloodMode => 'Force Flood mód'; @override String get login_managePaths => 'Útvonalak kezelése'; @@ -2159,81 +2142,81 @@ class AppLocalizationsHu extends AppLocalizations { @override String login_attempt(int current, int max) { - return 'Megpróbálás $current/$max-adik'; + return 'Kísérlet $current/$max'; } @override String login_failed(String error) { - return 'Belépés sikertelen: $error'; + return 'Sikertelen bejelentkezés: $error'; } @override String get login_failedMessage => - 'Belépés sikertelen. Vagy a jelszó helytelen, vagy a hálózati kapcsolat nem létesül.'; + 'Sikertelen bejelentkezés. Vagy hibás a jelszó, vagy az átjátszó nem érhető el.'; @override - String get common_reload => 'Újra töltés'; + String get common_reload => 'Újratöltés'; @override - String get common_clear => 'Egyértelmű'; + String get common_clear => 'Világos'; @override String get path_currentPathLabel => 'Jelenlegi útvonal'; @override String get path_noRepeatersFound => - 'Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.'; + 'Nem található átjátszó vagy szobaszerver.'; @override - String get repeater_management => 'Adatkapcsolás kezelése'; + String get repeater_management => 'Repeater Management'; @override - String get room_management => 'Szoba-szerver kezelés'; + String get room_management => 'Szobaszerver-kezelés'; @override - String get repeater_guest => 'Adatok a repeaterről'; + String get repeater_guest => 'Ismétlő információ'; @override - String get room_guest => 'Szoba szerver információk'; + String get room_guest => 'Szobaszerver információ'; @override - String get repeater_managementTools => 'Menedzsmentes eszközök'; + String get repeater_managementTools => 'Kezelőeszközök'; @override - String get repeater_guestTools => 'Vendégek számára elérhető eszközök'; + String get repeater_guestTools => 'Vendégeszközök'; @override String get repeater_status => 'Állapot'; @override String get repeater_statusSubtitle => - 'Megtekintheted a repeater állapotát, statisztikáit és a környező eszközök adatait.'; + 'Tekintse meg az átjátszó állapotát, a statisztikákat és a szomszédokat'; @override - String get repeater_telemetry => 'Adatvisszaadás'; + String get repeater_telemetry => 'Telemetria'; @override String get repeater_telemetrySubtitle => - 'Tekintsük a szenzorok és a rendszer állapotának adatát'; + 'Tekintse meg az érzékelők telemetriáját és a rendszerstatisztikát'; @override - String get repeater_cli => 'Parancssori felület (CLI)'; + String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Küldj parancsokat a repeaternek.'; + String get repeater_cliSubtitle => 'Parancsok küldése az átjátszónak'; @override String get repeater_neighbors => 'Szomszédok'; @override String get repeater_neighborsSubtitle => - 'Tekintsük a nullás lépésű szomszédokat.'; + 'Tekintse meg a zero hop szomszédokat.'; @override - String get repeater_settings => 'Beállítások'; + String get repeater_settings => 'Beállítások elemre'; @override - String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket'; + String get repeater_settingsSubtitle => 'Állítsa be az ismétlő paramétereit'; @override String get repeater_clockSyncAfterLogin => @@ -2241,75 +2224,76 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_clockSyncAfterLoginSubtitle => - 'Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.'; + '„Óraszinkronizálás” automatikus küldése sikeres bejelentkezés után'; @override - String get repeater_statusTitle => 'Adatkapcsolódás állapot'; + String get repeater_statusTitle => 'Repeater állapota'; @override - String get repeater_routingMode => 'Útvonal-kezelési mód'; + String get repeater_routingMode => 'Útválasztási mód'; @override - String get repeater_refresh => 'Újrafriszol'; + String get repeater_refresh => 'Frissítés'; @override - String get repeater_statusRequestTimeout => 'Az állapotkérés időtúlt.'; + String get repeater_statusRequestTimeout => + 'Az állapotkérelem időtúllépése lejárt.'; @override String repeater_errorLoadingStatus(String error) { - return 'Hiba a státusz betöltés közben: $error'; + return 'Hiba az állapot betöltésekor: $error'; } @override String get repeater_systemInformation => 'Rendszerinformációk'; @override - String get repeater_battery => 'Akku'; + String get repeater_battery => 'Akkumulátor'; @override String get repeater_clockAtLogin => 'Óra (bejelentkezéskor)'; @override - String get repeater_uptime => 'A rendszer elérhetősége'; + String get repeater_uptime => 'Üzemidő'; @override - String get repeater_queueLength => 'Várakozási sor hossza'; + String get repeater_queueLength => 'Sor hossza'; @override - String get repeater_debugFlags => 'Hibakeresési beállítások'; + String get repeater_debugFlags => 'Debug Flags'; @override - String get repeater_radioStatistics => 'Rádió statisztika'; + String get repeater_radioStatistics => 'Rádióstatisztika'; @override - String get repeater_lastRssi => 'Utolsó RSSI érték'; + String get repeater_lastRssi => 'Utolsó RSSI'; @override String get repeater_lastSnr => 'Utolsó SNR'; @override - String get repeater_noiseFloor => 'Háttérzaj szint'; + String get repeater_noiseFloor => 'Zajpadló'; @override - String get repeater_txAirtime => 'TX Airtime'; + String get repeater_txAirtime => 'TX műsoridő'; @override - String get repeater_rxAirtime => 'RX Airtime'; + String get repeater_rxAirtime => 'RX műsoridő'; @override - String get repeater_chanUtil => 'Csatorna-használat'; + String get repeater_chanUtil => 'Csatornahasználat'; @override - String get repeater_packetStatistics => 'Csomagok statisztikája'; + String get repeater_packetStatistics => 'Csomagstatisztika'; @override - String get repeater_sent => 'Elküldve'; + String get repeater_sent => 'Küldött'; @override - String get repeater_received => 'Megérkezett'; + String get repeater_received => 'Fogadva'; @override - String get repeater_duplicates => 'Duplák'; + String get repeater_duplicates => 'Ismétlődések'; @override String repeater_daysHoursMinsSecs( @@ -2318,7 +2302,7 @@ class AppLocalizationsHu extends AppLocalizations { int minutes, int seconds, ) { - return '$days days ${hours}h ${minutes}m ${seconds}s'; + return '$days nap $hoursó ${minutes}p ${seconds}s'; } @override @@ -2333,7 +2317,7 @@ class AppLocalizationsHu extends AppLocalizations { @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Áradás: $flood, Közvetlen: $direct'; + return 'Árvíz: $flood, Közvetlen: $direct'; } @override @@ -2342,29 +2326,29 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Adatátvisszaadási beállítások'; + String get repeater_settingsTitle => 'Repeater beállítások'; @override String get repeater_basicSettings => 'Alapbeállítások'; @override - String get repeater_repeaterName => 'Adóállomás neve'; + String get repeater_repeaterName => 'Ismétlő neve'; @override - String get repeater_repeaterNameHelper => 'Ez a repeater neve'; + String get repeater_repeaterNameHelper => 'Az átjátszó megjelenítési neve'; @override - String get repeater_adminPassword => 'Adminisztrátori jelszó'; + String get repeater_adminPassword => 'Admin jelszó'; @override - String get repeater_adminPasswordHelper => 'Teljes jogosultságú jelszó'; + String get repeater_adminPasswordHelper => 'Teljes hozzáférési jelszó'; @override - String get repeater_guestPassword => 'Vendég felhasználói név/jelszó'; + String get repeater_guestPassword => 'Vendégjelszó'; @override String get repeater_guestPasswordHelper => - 'Csak olvasási jogosítást biztosító jelszó'; + 'Csak olvasható hozzáférési jelszó'; @override String get repeater_radioSettings => 'Rádióbeállítások'; @@ -2373,67 +2357,67 @@ class AppLocalizationsHu extends AppLocalizations { String get repeater_frequencyMhz => 'Frekvencia (MHz)'; @override - String get repeater_frequencyHelper => '300–2500 MHz'; + String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX Power'; + String get repeater_txPower => 'TX teljesítmény'; @override String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Adatkapacitás'; + String get repeater_bandwidth => 'Sávszélesség'; @override - String get repeater_spreadingFactor => 'Terjesztési tényező'; + String get repeater_spreadingFactor => 'Spreading Factor'; @override String get repeater_codingRate => 'Kódolási sebesség'; @override - String get repeater_locationSettings => 'Helyszínbeállítások'; + String get repeater_locationSettings => 'Helybeállítások'; @override - String get repeater_latitude => 'Nyugat–keleti szélesség'; + String get repeater_latitude => 'Szélesség'; @override - String get repeater_latitudeHelper => 'Desztes fokok (pl. 37,7749)'; + String get repeater_latitudeHelper => 'Tizedes fokozatok (pl. 37,7749)'; @override - String get repeater_longitude => 'hosszúság'; + String get repeater_longitude => 'Hosszúság'; @override - String get repeater_longitudeHelper => 'Desztes fokok (pl. -122.4194)'; + String get repeater_longitudeHelper => 'Tizedes fokok (pl. -122,4194)'; @override String get repeater_features => 'Jellemzők'; @override - String get repeater_packetForwarding => 'Csomagok továbbítás'; + String get repeater_packetForwarding => 'Csomagtovábbítás'; @override String get repeater_packetForwardingSubtitle => - 'Engedje, hogy a repeater továbbítsa a csomagokat.'; + 'Ismétlő engedélyezése a csomagok továbbításához'; @override - String get repeater_guestAccess => 'Vendégek számára elérhető'; + String get repeater_guestAccess => 'Vendég hozzáférés'; @override String get repeater_guestAccessSubtitle => - 'Engedje meg a vendégek számára, hogy csak olvassák a tartalmat'; + 'Csak olvasási hozzáférés engedélyezése a vendégek számára'; @override - String get repeater_privacyMode => 'Adatvédelem mód'; + String get repeater_privacyMode => 'Adatvédelmi mód'; @override String get repeater_privacyModeSubtitle => - 'Elrejtse a nevét/a helyszínt az űrlapon'; + 'Név/hely elrejtése a hirdetésekben'; @override String get repeater_advertisementSettings => 'Reklámbeállítások'; @override - String get repeater_localAdvertInterval => 'Helyi hirdetés időtartama'; + String get repeater_localAdvertInterval => 'Helyi hirdetési intervallum'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2441,7 +2425,7 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => 'Vízosztály-hirdetés időtartama'; + String get repeater_floodAdvertInterval => 'Árvízi hirdetési intervallum'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2449,47 +2433,47 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get repeater_encryptedAdvertInterval => 'Kódolt hirdetés-szünet'; + String get repeater_encryptedAdvertInterval => 'Titkosított hirdetési időköz'; @override String get repeater_dangerZone => 'Veszélyzóna'; @override - String get repeater_rebootRepeater => 'Újraindítás'; + String get repeater_rebootRepeater => 'Reboot Repeater'; @override - String get repeater_rebootRepeaterSubtitle => 'Indítsa újra a repeater-t.'; + String get repeater_rebootRepeaterSubtitle => + 'Indítsa újra az átjátszó eszközt'; @override String get repeater_rebootRepeaterConfirm => - 'Biztosan szeretné újraindítani ezt a repeatert?'; + 'Biztosan újraindítja ezt az átjátszót?'; @override - String get repeater_regenerateIdentityKey => - 'Újra generálja az azonosító kulcsot'; + String get repeater_regenerateIdentityKey => 'Identitáskulcs újragenerálása'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Új nyilvános/személyes kulcs-párt generáljon'; + 'Új nyilvános/privát kulcspár létrehozása'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Ez új azonosítást fog létrehozni a repeater számára. Folytatni?'; + 'Ez új identitást generál az átjátszó számára. Folytatja?'; @override - String get repeater_eraseFileSystem => 'Törölje a fájlrendszert'; + String get repeater_eraseFileSystem => 'Fájlrendszer törlése'; @override String get repeater_eraseFileSystemSubtitle => - 'Formázza a duplázó fájlrendszert.'; + 'Formázza meg az átjátszó fájlrendszert'; @override String get repeater_eraseFileSystemConfirm => - 'FIGYELEM: Ez törli az összes adatot a repeater-en. Ez nem visszafordítható!'; + 'FIGYELMEZTETÉS: Ezzel törli az átjátszón lévő összes adatot. Ezt nem lehet visszavonni!'; @override String get repeater_eraseSerialOnly => - 'Az Erase funkció csak a soros konzolon érhető el.'; + 'A törlés csak soros konzolon érhető el.'; @override String repeater_commandSent(String command) { @@ -2498,64 +2482,62 @@ class AppLocalizationsHu extends AppLocalizations { @override String repeater_errorSendingCommand(String error) { - return 'Hibás parancs küldés: $error'; + return 'Hiba a parancs küldésekor: $error'; } @override - String get repeater_confirm => 'Beküldve'; + String get repeater_confirm => 'Erősítse meg'; @override - String get repeater_settingsSaved => 'Beállítások sikeresen mentve'; + String get repeater_settingsSaved => 'A beállítások sikeresen elmentve'; @override - String get repeater_rxGain => 'Nagyobb RX-jel erősítés'; + String get repeater_rxGain => 'Megnövelt RX nyereség'; @override String get repeater_rxGainHelper => - 'Magasabb érzékenység, nagyobb áramfelvétel (csak SX1262/SX1268 esetén)'; + 'Nagyobb érzékenység, nagyobb áramfelvétel (csak SX1262/SX1268)'; @override - String get repeater_refreshRxGain => 'Újraindított, fokozott RX hatás'; + String get repeater_refreshRxGain => 'Frissítse a megnövelt RX-erősítést'; @override - String get repeater_multiAcks => 'Többszörös visszaigazolások'; + String get repeater_multiAcks => 'Multi-ACK'; @override String get repeater_multiAcksSubtitle => - 'Ismerje el üzeneteket több úton is, hogy biztosítsa a jobb átadást.'; + 'Nyugtázza az üzeneteket több úton a jobb kézbesítés érdekében'; @override - String get repeater_refreshMultiAcks => - 'Frissítse a többször is kapott visszaigazolásokat.'; + String get repeater_refreshMultiAcks => 'Többszörös ACK-ek frissítése'; @override - String get repeater_networkHealth => 'Hálózati állapot'; + String get repeater_networkHealth => 'Hálózat állapota'; @override - String get repeater_loopDetect => 'Ciklusok azonosítása'; + String get repeater_loopDetect => 'Hurokérzékelés'; @override String get repeater_loopDetectHelper => - 'Készíts olyan \"vízfolyást\" megjelenítő csomagokat, amelyek úgy néznek ki, mint egy hibaút.'; + 'Dobd el az útválasztó huroknak tűnő árvízcsomagokat'; @override - String get repeater_loopDetectOff => 'Le, kikap'; + String get repeater_loopDetectOff => 'Le'; @override String get repeater_loopDetectMinimal => 'Minimális'; @override - String get repeater_loopDetectModerate => 'Közepes'; + String get repeater_loopDetectModerate => 'Mérsékelt'; @override String get repeater_loopDetectStrict => 'Szigorú'; @override - String get repeater_dutyCycle => 'Munka- és pihenőidő aránya'; + String get repeater_dutyCycle => 'Üzemi ciklus'; @override - String get repeater_dutyCycleHelper => - 'A maximális időszámítás százalékos aránya'; + String get repeater_dutyCycleHelper => 'A műsoridő maximális százaléka'; @override String repeater_dutyCyclePercent(int percent) { @@ -2567,87 +2549,87 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_ownerInfoHelper => - 'A nyilvánosan elérhető metadatak a repeaterhez'; + 'Nyilvános metaadatok ehhez az átjátszóhoz'; @override - String get repeater_refreshOwnerInfo => 'Frissítse az üzemeltető adatokat'; + String get repeater_refreshOwnerInfo => 'Frissítse az operátor adatait'; @override - String get repeater_floodMax => 'A vízmaximumos ugrások'; + String get repeater_floodMax => 'Árvíz max ugrás'; @override String get repeater_floodMaxHelper => - 'A legmagasabb szám, amely egy vízszint-csomagban szerepelhet (0-64)'; + 'Maximum ugrások, amelyeket egy árvízcsomag utazhat (0-64)'; @override - String get repeater_advancedSettings => 'Haladó'; + String get repeater_advancedSettings => 'Fejlett'; @override String get repeater_advancedSettingsSubtitle => - 'Erkélő kapcsolók tapasztalt kezelők számára'; + 'Hangológombok tapasztalt kezelőknek'; @override - String get repeater_pathHashMode => 'Út-hash mód'; + String get repeater_pathHashMode => 'Útvonal hash mód'; @override String get repeater_pathHashModeHelper => - 'A byte-ok, amelyek az alábbi repeater-ek azonosítójának kódolására szolgálnak a flood-útvonal/ciklus-észlelő címkékben. 0=1 byte (256 azonosító, akár 64 útvonal), 1=2 byte (65 000 azonosító, akár 32 útvonal), 2=3 byte (16 millió azonosító, akár 21 útvonal). A v1.13-as verziótól kezdődően és az azt követő verziókban a több byte-os útvonalak megszűntek – csak egyetlen útvonal létesül, miután a hálózat a v1.14-es verzióra vagy az azt követő verzióra frissült.'; + 'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat – csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.'; @override - String get repeater_txDelay => 'Flood TX késés'; + String get repeater_txDelay => 'Áradási TX késleltetés'; @override String get repeater_txDelayHelper => - 'Újraküldési intervallum árvíz esetén, amely a csomag átviteli idejének (0-2, alapérték 0,5) szorzata. Minél nagyobb az érték, annál kevesebb ütközés, de lassabb a továbbítás.'; + 'Újraküldési térköz az árvízi forgalomhoz, a csomag sugárzási idejének szorzójaként (0-2, alapértelmezett 0,5). Magasabb = kevesebb ütközés, de lassabb szállítás.'; @override - String get repeater_directTxDelay => 'Közvetlen TX késés'; + String get repeater_directTxDelay => 'Közvetlen TX késleltetés'; @override String get repeater_directTxDelayHelper => - 'A közvetlen (nem tömeges) forgalomhoz tartozó adatcsomagok újrádiózására szolgáló intervallum, amely a csomag átviteli idejének (0-2, alapértelmezett érték 0,3) szorzata.'; + 'A közvetlen (nem elárasztásos) forgalom újraküldési távolsága a csomag sugárzási idejének szorzójaként (0-2, alapértelmezett 0,3).'; @override - String get repeater_intThresh => 'Interferencia határ'; + String get repeater_intThresh => 'Interferencia küszöb'; @override String get repeater_intThreshHelper => - 'A határt a rádió zajszintjének kalibrálására állították, így elutasítja a fenti szint feletti interferenciákat. 0 kikapcsol – csak akkor állítsa be, ha zajos frekvencián RX hibákat észlel.'; + 'A küszöbérték átkerült a rádió zajszint-kalibrációjához, így az e szint feletti interferenciát elutasítja. 0 letilt – csak akkor emel, ha zajos sávban RX hibákat lát.'; @override - String get repeater_agcResetInterval => 'AGC visszazárási intervallum'; + String get repeater_agcResetInterval => 'AGC reset intervallum'; @override String get repeater_agcResetIntervalHelper => - 'Mennyi időnként kell a rádió automatikus hangerőszabályozását visszaállítani, hogy kijavítsa a problémát? A visszaállítás időtartama: másoderek, amely 4-szeresével osztható. A 0 érték a periodikus visszaállítás kikapcsolását jelzi.'; + 'Milyen gyakran kell visszaállítani a rádió automatikus erősítésszabályozását, hogy helyreálljon a beragadt erősítési állapotból. A másodpercek, 4 többszörösére csökkentve. 0 letiltja az időszakos visszaállításokat.'; @override - String get repeater_actionsTitle => 'Tevékenységek'; + String get repeater_actionsTitle => 'Akciók'; @override - String get repeater_sendAdvert => 'Eljuttass flood hirdetést'; + String get repeater_sendAdvert => 'Árvízhirdetés küldése'; @override String get repeater_sendAdvertSubtitle => - 'Terjesztse egy árvíz elleni reklámot a hálózaton keresztül.'; + 'Adjon árvízreklámot a hálózaton keresztül'; @override - String get repeater_sendAdvertZeroHop => 'Küldj egy közvetlen hirdetést'; + String get repeater_sendAdvertZeroHop => 'Zéró ugrású hirdetés küldése'; @override String get repeater_sendAdvertZeroHopSubtitle => - 'Adja közzé egyetlen átjáró hirdetést (nincs átjátszás).'; + 'Egyugrásos hirdetés sugárzása (közvetítés nélkül)'; @override - String get repeater_clockSync => 'Synchronizálja az órát'; + String get repeater_clockSync => 'Óra szinkronizálása most'; @override String get repeater_clockSyncSubtitle => - 'Állítsa be a telefon időzítését a repeaterhez.'; + 'Tolja a telefon idejét az átjátszóhoz'; @override String repeater_actionSucceeded(String action) { - return '$action sikert aratott'; + return '$action sikerült'; } @override @@ -2657,37 +2639,36 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_settingsSavedRebootNeeded => - 'Beállítások mentve – újraindítsa a repeatert, hogy alkalmazza'; + 'A beállítások mentve – az átjátszó újraindítása az alkalmazáshoz'; @override String repeater_settingsPartialFailure(String failures) { - return 'Bizonyos beállítások nem sikerültek: $failures'; + return 'Néhány beállítás nem sikerült: $failures'; } @override String repeater_errorSavingSettings(String error) { - return 'Hibás beállítások mentése: $error'; + return 'Hiba a beállítások mentésekor: $error'; } @override - String get repeater_refreshBasicSettings => 'Visszaállítás az alapértékekre'; + String get repeater_refreshBasicSettings => + 'Frissítse az alapvető beállításokat'; @override - String get repeater_refreshRadioSettings => 'Frissítse a rádió beállításait'; + String get repeater_refreshRadioSettings => 'Frissítse a rádióbeállításokat'; @override - String get repeater_refreshTxPower => 'Újraindítás TX-támogatással'; + String get repeater_refreshTxPower => 'TX teljesítmény frissítése'; @override - String get repeater_refreshPacketForwarding => - 'Csomagok továbbításának frissítése'; + String get repeater_refreshPacketForwarding => 'Csomagtovábbítás frissítése'; @override - String get repeater_refreshGuestAccess => 'Újraindítás vendégHozzáférés'; + String get repeater_refreshGuestAccess => 'Vendég hozzáférés frissítése'; @override - String get repeater_refreshPrivacyMode => - 'Visszaállítás a magánéletvédő módra'; + String get repeater_refreshPrivacyMode => 'Frissítse az adatvédelmi módot'; @override String repeater_refreshed(String label) { @@ -2696,27 +2677,27 @@ class AppLocalizationsHu extends AppLocalizations { @override String repeater_errorRefreshing(String label) { - return 'Hiba a $label frissítés közben'; + return 'Hiba a $label frissítésekor'; } @override - String get repeater_cliTitle => 'CLI (parancssori felület)'; + String get repeater_cliTitle => 'Repeater CLI'; @override - String get repeater_debugNextCommand => 'Hibakeresés, következő parancs'; + String get repeater_debugNextCommand => 'Debug Next Command'; @override - String get repeater_commandHelp => 'Segítség'; + String get repeater_commandHelp => 'Command Help'; @override - String get repeater_clearHistory => 'Egyértelmű történet'; + String get repeater_clearHistory => 'Törölje az előzményeket'; @override - String get repeater_noCommandsSent => 'Még egyik parancsot sem küldtünk.'; + String get repeater_noCommandsSent => 'Még nem küldtek parancsot'; @override String get repeater_typeCommandOrUseQuick => - 'Írja be a parancsot alább, vagy használja a gyors parancsokat.'; + 'Írjon be egy parancsot alább, vagy használjon gyorsparancsokat'; @override String get repeater_enterCommandHint => 'Írja be a parancsot...'; @@ -2728,10 +2709,10 @@ class AppLocalizationsHu extends AppLocalizations { String get repeater_nextCommand => 'Következő parancs'; @override - String get repeater_enterCommandFirst => 'Add meg először egy parancsot'; + String get repeater_enterCommandFirst => 'Először írjon be egy parancsot'; @override - String get repeater_cliCommandFrameTitle => 'CLI parancssor felépítése'; + String get repeater_cliCommandFrameTitle => 'CLI parancskeret'; @override String repeater_cliCommandError(String error) { @@ -2739,362 +2720,361 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Kapcsold össze a nevet'; + String get repeater_cliQuickGetName => 'Get Name'; @override - String get repeater_cliQuickGetRadio => 'Szerezd a rádiót'; + String get repeater_cliQuickGetRadio => 'Szerezz rádiót'; @override - String get repeater_cliQuickGetTx => 'Szerezd a TX-t'; + String get repeater_cliQuickGetTx => 'Szerezd meg a TX-et'; @override String get repeater_cliQuickNeighbors => 'Szomszédok'; @override - String get repeater_cliQuickVersion => 'Verzió'; + String get repeater_cliQuickVersion => 'Változat'; @override - String get repeater_cliQuickAdvertise => 'Hirdetés'; + String get repeater_cliQuickAdvertise => 'Hirdet'; @override - String get repeater_cliQuickClock => 'óra'; + String get repeater_cliQuickClock => 'Óra'; @override String get repeater_cliQuickClockSync => 'Óra szinkronizálás'; @override - String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat'; + String get repeater_cliQuickDiscovery => 'Fedezze fel a szomszédokat'; @override - String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot'; + String get repeater_cliHelpAdvert => 'Reklámcsomagot küld'; @override String get repeater_cliHelpReboot => - 'Újraindítja a készüléket. (Kérjük, vegye figyelembe, hogy valószínűleg \"Időhiba\" üzenetet fog kapni, ami normális)'; + 'Újraindítja az eszközt. (megjegyzendő, hogy \"Időtúllépés\" jelenik meg, ami normális)'; @override String get repeater_cliHelpClock => - 'A jelenlegi időt mutatja az egyes eszközök karórája alapján.'; + 'Megjeleníti az aktuális időt az eszköz órájánként.'; @override String get repeater_cliHelpPassword => - 'Új adminisztrációs jelszót állít be a eszköz számára.'; + 'Új rendszergazdai jelszót állít be az eszközhöz.'; @override String get repeater_cliHelpVersion => - 'Megjeleníti a készülék verzióját és a szoftver verziószámát.'; + 'Megmutatja az eszköz verzióját és a firmware felépítési dátumát.'; @override String get repeater_cliHelpClearStats => - 'Visszaállítja a különböző statisztikai mérőszámokat a nullára.'; + 'Nullára állítja a különböző statisztikai számlálókat.'; @override - String get repeater_cliHelpSetAf => 'Beállítja az idő-szabályozási tényezőt.'; + String get repeater_cliHelpSetAf => 'Beállítja a műsoridő-tényezőt.'; @override String get repeater_cliHelpSetTx => - 'Beállítja a LoRa átviteli teljesítményt dBm-ben (a rendszer újraindításával alkalmazható).'; + 'A LoRa adási teljesítményét dBm-ben állítja be. (újraindítás az alkalmazáshoz)'; @override String get repeater_cliHelpSetRepeat => - 'Engedélyezi vagy tiltja meg a repeater szerepet ezen a csomón.'; + 'Engedélyezi vagy letiltja a csomópont ismétlő szerepét.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Szoba szerver) Ha \"igen\", akkor üres jelszóval történő bejelentkezés engedélyezett lesz, de nem lehet üzeneteket küldeni a szobában. (Csak olvasási funkció)'; + '(Szobaszerver) Ha \'be\', akkor az üres jelszó bejelentkezés engedélyezett, de nem lehet postázni a szobába. (csak olvasható)'; @override String get repeater_cliHelpSetFloodMax => - 'Beállítja a bejövő adatcsomagok maximális számát (ha ez a érték nagyobb vagy egyenlő a maximális értékkel, a csomag nem továbbítódik).'; + 'Beállítja a bejövő árvízcsomag ugrásainak maximális számát (ha >= max, a csomag nem kerül továbbításra)'; @override String get repeater_cliHelpSetIntThresh => - 'Beállítja az interferencia határértéket (dB-ben). Az alapérték 14. Ha 0-ra állítja, kiküntheti a csatornák közötti interferencia detektálást.'; + 'Beállítja az interferencia küszöböt (DB-ben). Az alapértelmezett érték 14. Állítsa 0-ra a csatornainterferencia-érzékelés letiltásához.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Beállítja az intervallumot, amely a \"Automatikus gain\" szabályozó újraindításához szükséges. Beállítás értéke 0, ha a funkciót le kell tiltani.'; + 'Beállítja az Auto Gain Controller alaphelyzetbe állításának intervallumát. A letiltáshoz állítsa 0-ra.'; @override String get repeater_cliHelpSetMultiAcks => - 'Engedélyezi vagy kikapcsolja a „dupla visszaigazolás” funkciót.'; + 'Engedélyezi vagy letiltja a „dupla ACK” funkciót.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Beállítja az időzítő intervallumot percenként, hogy egy helyi (nincs átjáró) hirdetési csomagot küldjen. Beállítás értéke 0, ha a funkciót le szeretné tiltani.'; + 'Beállítja a helyi (nulla ugrású) hirdetési csomag küldésének időzítési időközét percekben. A letiltáshoz állítsa 0-ra.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Beállítja az időzítő intervallumot órában, hogy egy \"áramló\" hirdetési üzenetet küldjön. Beállítás értéke 0, ha a funkciót kikapcsolni kell.'; + 'Beállítja az időzítő intervallumát órákban az árvízhirdetési csomag küldéséhez. A letiltáshoz állítsa 0-ra.'; @override String get repeater_cliHelpSetGuestPassword => - 'Beállítja/frissíti a vendég felhasználói fiókot. (Ez lehetővé teszi a visszatérő felhasználók számára, hogy a \"Statistika lekérdezése\" kérést elküldjék)'; + 'Beállítja/frissíti a vendég jelszavát. (Repeaterek esetén a vendégbejelentkezés elküldheti a \"Statisztikák lekérése\" kérést)'; @override - String get repeater_cliHelpSetName => 'Megadja az űrlap neve.'; + String get repeater_cliHelpSetName => 'Beállítja a hirdetés nevét.'; @override String get repeater_cliHelpSetLat => - 'Beállítja az hirdetés térképen megjelenő pont koordinátájának (tizedes fokokban) a latitude-ját.'; + 'Beállítja a hirdetéstérkép szélességi fokát. (tizedes fok)'; @override String get repeater_cliHelpSetLon => - 'Beállítja az hirdetés térképen megjelenő hosszúság koordinátát (tizedes fokokban).'; + 'Beállítja a hirdetési térkép hosszúsági fokát. (tizedes fok)'; @override String get repeater_cliHelpSetRadio => - 'Teljesen új rádióparamétereket állít be, és azokat a beállításokba menti. Az alkalmazásához \"újraindítás\" parancs szükséges.'; + 'Teljesen új rádióparamétereket állít be, és elmenti a beállításokhoz. Az alkalmazáshoz \"reboot\" parancs szükséges.'; @override String get repeater_cliHelpSetRxDelay => - 'Beállítások (kísérleti): Alapérték (legalább 1 értékre kell állítani, hogy hatás legyen), amely alapján a fogadott csomagokhoz enyhe késést alkalmazunk, a jelet ereje/pontszám alapján. 0-ra állítva a funkciót lekapcsoljuk.'; + 'Beállítja a (kísérleti) bázist (az effektushoz > 1-nek kell lennie) a fogadott csomagok enyhe késleltetéséhez, a jelerősség/pontszám alapján. A letiltáshoz állítsa 0-ra.'; @override String get repeater_cliHelpSetTxDelay => - 'Beállítja egy tényezőt, amely a légköri idővel szorozva, egy áramlás-üzem módú csomaghoz, valamint egy véletlenszerű slot-rendszerhez, hogy késleltesse a továbbítását. (az ütközések valószínűségének csökkentése érdekében)'; + 'Beállít egy tényezőt, megszorozva az adásidővel egy elárasztásos módú csomaghoz és egy véletlenszerű résrendszerhez, hogy késleltesse a továbbítását. (az ütközések valószínűségének csökkentése érdekében)'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Hasonló a txdelay-hez, de ebben az esetben egy véletlenszerű késést alkalmazunk a közvetlen módú csomagok továbbításakor.'; + 'Ugyanaz, mint a txdelay, de véletlenszerű késleltetés alkalmazására a közvetlen módú csomagok továbbítására.'; @override String get repeater_cliHelpSetBridgeEnabled => - 'Engedélyez/Tiltás a híd funkciójának.'; + 'Bridge engedélyezése/letiltása.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Állíts be egy késleztatást a csomagok újbóli továbbításakor.'; + 'Állítsa be a késleltetést a csomagok újraküldése előtt.'; @override String get repeater_cliHelpSetBridgeSource => - 'Döntse el, hogy a híd fogadott vagy elküldött csomagokat fogja-e továbbítani.'; + 'Válassza ki, hogy a híd újraküldje-e a fogadott vagy továbbított csomagokat.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Állítsa be a soros kommunikáció sebességét az RS232 hídok számára.'; + 'Soros kapcsolat átviteli sebességének beállítása rs232 hidakhoz.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Állítsa be a titkos kapcsolatot az ESPNOW hídokhoz.'; + 'Állítsa be a hídtitkot espnow hidakhoz.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Lehetővé teszi a felhasználónak, hogy egyedi tényezőt állíts be a riportolt akkumulátor feszültségének módosításához (ez csak bizonyos alkatrészeken támogatott).'; + 'Egyéni tényezőt állít be a jelentett akkumulátorfeszültség beállításához (csak bizonyos kártyákon támogatott).'; @override String get repeater_cliHelpTempRadio => - 'Időjárás szerinti rádióparamétereket állít be a megadott időtartamra, majd visszaállítja az eredeti beállításokat. (Nem menti a beállításokat a beállítások részben).'; + 'Ideiglenes rádióparamétereket állít be a megadott számú percre, majd visszaáll az eredeti rádióparaméterekre. (NEM menti a beállításokba).'; @override String get repeater_cliHelpSetPerm => - 'A ACL-t módosítja. Ha a \"permissions\" érték 0, akkor eltávolítja a megfelelő bejegyzést (a pubkey előtag alapján). Új bejegyzést hoz létre, ha a pubkey-hex teljes hossza, és jelenleg nem szerepel az ACL-ben. A bejegyzést frissíti a megfelelő pubkey előtag alapján. A engedélyek különbözőek a különböző firmware szerepek között, de az alsó 2 bit a következő értékeket képviseli: 0 (Vendég), 1 (Csak olvasás), 2 (Olvasás és írás), 3 (Adminisztrátor)'; + 'Módosítja az ACL-t. Eltávolítja a megfelelő bejegyzést (pubkey előtag alapján), ha az „engedélyek” értéke nulla. Új bejegyzést ad hozzá, ha a pubkey-hex teljes hosszúságú, és jelenleg nincs az ACL-ben. Frissíti a bejegyzést a pubkey előtag egyeztetésével. Az engedélybitek firmware-szerepkörönként változnak, de az alacsony 2 bit a következő: 0 (Vendég), 1 (Csak olvasható), 2 (Olvasás, írás), 3 (Adminisztrátor)'; @override String get repeater_cliHelpGetBridgeType => - 'Kapcsolatok: hid típusú, RS232, ESPNOW'; + 'Hidatípust nem kap, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Elindítja a csomagok naplózását a fájlrendszerbe.'; + 'Elindítja a csomagnaplózást a fájlrendszerbe.'; @override String get repeater_cliHelpLogStop => - 'Megállítja a csomagok naplózását a fájlrendszerbe.'; + 'Leállítja a csomagok naplózását a fájlrendszerbe.'; @override String get repeater_cliHelpLogErase => - 'Törli a fájlrendszerből a csomagok log-fájljait.'; + 'Törli a csomagnaplókat a fájlrendszerből.'; @override String get repeater_cliHelpNeighbors => - 'Mutat egy listát, amely tartalmazza a más repeater-ek által hallott adatok listáját, amelyek 0-hop hirdetések révén érhetők el. Minden sor az alábbi formát követi: id-prefix-hex:timestamp:snr-times-4'; + 'Megjeleníti a nulla ugrású hirdetéseken keresztül hallható egyéb átjátszó csomópontok listáját. Minden sor id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Törli az első, a megadott kulcs-prefix (hexadecimális formában) alapján megegyező bejegyzést a szomszédok listájából.'; + 'Eltávolítja az első egyező bejegyzést (a pubkey előtag (hex) alapján) a szomszédok listájából.'; @override String get repeater_cliHelpRegion => - '(sorozat) Lista az összes meghatározott területet és a jelenlegi árvízvédelmi engedélyeket.'; + '(csak soros) Felsorolja az összes meghatározott régiót és az aktuális árvízi engedélyeket.'; @override String get repeater_cliHelpRegionLoad => - 'FIGYELEM: ez egy speciális, több parancsot tartalmazó futtatás. Minden következő parancs egy területtel kapcsolatos, amely egyenletes szóközökkel (a szülő-gyermek kapcsolatot jelző) megkülönböztethető. A parancs végrehajtása egy üres sor/parancs küldésével történik.'; + 'MEGJEGYZÉS: ez egy speciális többparancsos hívás. Minden következő parancs egy régiónév (szóközökkel behúzva a szülőhierarchiát jelölve, legalább egy szóközzel). Üres sor/parancs küldésével megszűnik.'; @override String get repeater_cliHelpRegionGet => - 'Keresések egy adott név előtérrel (vagy \"*\" globális hatókörre). Válasz: \"-> region-név (szülő-név) \'F\'\"'; + 'Régiót keres adott név előtaggal (vagy \"*\" a globális hatókörhöz). A válasz a következővel: \"-> régiónév (szülőnév) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Hozzáad vagy frissíti egy régió definíciót megadott néven.'; + 'Adott névvel rendelkező régiódefiníció hozzáadása vagy frissítése.'; @override String get repeater_cliHelpRegionRemove => - 'Eltávolítja a megadott nevet használó régió-definíciót. (pontosan meg kell egyeznie, és nem lehet gyermekrégiója)'; + 'Eltávolítja a megadott nevű régiódefiníciót. (pontosan meg kell egyeznie, és nem lehetnek alárendelt régiók)'; @override String get repeater_cliHelpRegionAllowf => - 'Beállítja a megadott területre vonatkozó \"víz\" jogosultságot. (A globális/régi beállítások esetén a \"*\" jelölő)'; + 'Beállítja az \'F\'lood engedélyt az adott régióhoz. (\"*\" a globális/örökölt hatókörre)'; @override String get repeater_cliHelpRegionDenyf => - 'Eltávolítja a megadott területre vonatkozó \"F\"lood (víz) engedélyt. (FIGYELEM: jelenleg nem javasolt ezt a globális/régi verzióban használni!!)'; + 'Eltávolítja az \'F\'lood engedélyt az adott régióhoz. (MEGJEGYZÉS: ebben a szakaszban NEM tanácsos ezt használni a globális/örökölt hatókörön!!)'; @override String get repeater_cliHelpRegionHome => - 'Visszaállítja a jelenlegi „otthoni” régiót. (Ez a beállítás még nem került alkalmazásra, csak jövőbeli használatra fenyelve)'; + 'Az aktuális „otthoni” régióval válaszol. (A megjegyzés még bárhol érvényes, jövőre fenntartva)'; @override - String get repeater_cliHelpRegionHomeSet => 'Beállítja a \"házi\" régiót.'; + String get repeater_cliHelpRegionHomeSet => 'Beállítja az „otthoni” régiót.'; @override String get repeater_cliHelpRegionSave => - 'Megőrzi a régió listát/térképet a tárolóban.'; + 'Megőrzi a régiólistát/leképezést a tárhelyen.'; @override String get repeater_cliHelpGps => - 'Megadja a GPS állapotát. Ha a GPS kikapcsolva van, akkor csak \"ki\" választot ad, ha be van, akkor \"be\", \"állapot\", \"pozíció\", \"satellitok száma\" értékeket ad.'; + 'Megadja a gps állapotát. Ha a gps ki van kapcsolva, akkor csak kikapcsolva válaszol, ha be van kapcsolva, akkor válaszol: be, állapot, javítás, sat count'; @override - String get repeater_cliHelpGpsOnOff => 'Engedi a GPS működés állapotát.'; + String get repeater_cliHelpGpsOnOff => + 'Bekapcsolja a gps tápellátási állapotát.'; @override String get repeater_cliHelpGpsSync => - 'A hálózati időt az GPS óra időjével szinkronizálja.'; + 'Szinkronizálja a csomópont idejét a gps órával.'; @override String get repeater_cliHelpGpsSetLoc => - 'Beállítja a węsz pozícióját GPS koordináták alapján, és menti a beállításokat.'; + 'Beállítja a csomópont pozícióját a gps koordinátákra és menti a beállításokat.'; @override String get repeater_cliHelpGpsAdvert => - 'Adja meg a hirdetés konfigurációjának helyszín-információját:\n- none: ne tartalmazza a helyszínt a hirdetésekben\n- share: megosztja a GPS-helyszínt (SensorManager-ből)\n- prefs: hirdeti a beállításokban tárolt helyszínt'; + 'Megadja a csomópont helyhirdetési konfigurációját:\n- nincs: ne adja meg a helyet a hirdetésekben\n- megosztás: GPS hely megosztása (a SensorManagerből)\n- preferenciák: a beállításokban tárolt hely hirdetése'; @override String get repeater_cliHelpGpsAdvertSet => - 'Beállítja a hirdetés helyszín-specifikus beállításait.'; + 'Beállítja a helyhirdetés konfigurációját.'; @override - String get repeater_commandsListTitle => 'Parancsok listája'; + String get repeater_commandsListTitle => 'Parancslista'; @override String get repeater_commandsListNote => - 'FIGYELEM: a különböző \"set ...\" parancsok mellett létezik egy \"get ...\" parancs is.'; + 'MEGJEGYZÉS: a különféle \"set ...\" parancsokhoz van egy \"get ...\" parancs is.'; @override String get repeater_general => 'Általános'; @override - String get repeater_settingsCategory => 'Beállítások'; + String get repeater_settingsCategory => 'Beállítások elemre'; @override String get repeater_bridge => 'Híd'; @override - String get repeater_logging => 'Naplózás'; + String get repeater_logging => 'Fakitermelés'; @override - String get repeater_neighborsRepeaterOnly => - 'Szomszédok (Csak ismétlő funkció)'; + String get repeater_neighborsRepeaterOnly => 'Szomszédok (csak átjátszó)'; @override String get repeater_regionManagementRepeaterOnly => - 'Regionális menedzsment (Csak egyirányú kommunikáció)'; + 'Régiókezelés (csak ismétlő)'; @override String get repeater_regionNote => - 'Region-specifikus parancsokat vezettek be a régiók definiálására és a hozzájuk tartozó engedélyek kezelésére.'; + 'Régióparancsok kerültek bevezetésre a régiódefiníciók és engedélyek kezelésére.'; @override - String get repeater_gpsManagement => 'GPS-vezérlés'; + String get repeater_gpsManagement => 'GPS kezelés'; @override String get repeater_gpsNote => - 'Az GPS-al kapcsolatos funkciók lehetővé teszik a helyszín-személyesítéssel kapcsolatos feladatok kezelését.'; + 'A gps parancs bevezetésre került a helyhez kapcsolódó témák kezeléséhez.'; @override - String get repeater_getCategory => 'Szereplő értékek'; + String get repeater_getCategory => 'Szerezzen értékeket'; @override - String get repeater_powerMgmt => - 'Energiahatékonyság, energiafelhasználás optimalizálása'; + String get repeater_powerMgmt => 'Energiagazdálkodás'; @override String get repeater_sensors => 'Érzékelők'; @override String get repeater_cliHelpPowerOff => - 'Át kapcsolja a készüléket. (nincs válasz elvárás)'; + 'Kikapcsolja a készüléket. (nem várható válasz)'; @override String get repeater_cliHelpClkReboot => - 'Visszaállítja az órát egy ismert időpontra, majd újraindítja a készüléket.'; + 'Visszaállítja az órát egy ismert korszakra, és újraindítja az eszközt.'; @override String get repeater_cliHelpAdvertZeroHop => - 'Küld egy közvetlen szomszédoknak szóló hirdetést, amely közvetlen kapcsolatot igényel.'; + 'Zéró ugrású hirdetést küld (csak a közvetlen szomszédok számára).'; @override String get repeater_cliHelpStartOta => - 'Elindítja a vezeték nélküli útvonalon történő firmware frissítést a támogatott kártyákon.'; + 'Elindít egy vezeték nélküli firmware-frissítést a támogatott kártyákon.'; @override String get repeater_cliHelpTime => - 'Beállítja a eszköz óráját a megadott Unix-időpont (Unix epoch) időpontra. Az óra nem tud visszanyúlni.'; + 'Beállítja az eszköz óráját a megadott Unix korszak másodpercekre. Az óra nem tud visszafelé mozogni.'; @override String get repeater_cliHelpBoard => - 'Megjeleníti a gyártó nevét/a hardver azonosítóját.'; + 'Megmutatja a kártya gyártóját/hardver azonosítóját.'; @override String get repeater_cliHelpDiscoverNeighbors => - 'Kérést küld a közeli eszközöknek, hogy azok is megtalálják egymást. (Csak egy repeater eszköz számára)'; + 'Csomópont-felderítési kérelmet küld a közeli szomszédoknak. (Csak ismétlő)'; @override String get repeater_cliHelpPowersaving => - 'Megmutatja, hogy a takarékos üzemmód engedélyezve van-e vagy nem.'; + 'Megmutatja, hogy az energiatakarékos mód be vagy ki van-e kapcsolva.'; @override String get repeater_cliHelpPowersavingOnOff => - 'Engedélyezi vagy kikapcsolja a takarékos üzemmódot (ha támogatott).'; + 'Engedélyezi vagy letiltja az energiatakarékos módot (ahol támogatott).'; @override String get repeater_cliHelpErase => - '(Csak sorozatban) Formázza a eszköz fájlrendszerét. Eltávolítja az összes beállítást és a kapcsolatokat.'; + '(Csak soros) Formázza az eszköz fájlrendszerét. Törli az összes beállítást és névjegyet.'; @override String get repeater_cliHelpSetDutyCycle => - 'Beállítja a maximális engedélyezett átviteli időtartamot százalékban (1-100). Belsőleg módosítja az időtartam-szabályozást.'; + 'Beállítja a maximális megengedett átviteli munkaciklust százalékban (1-100). Belsőleg állítja be a műsoridőtényezőt.'; @override String get repeater_cliHelpSetPrvKey => - '(Csak sorozatban) Cseréli a eszköz egyedi kulcsát. Az alkalmazáshoz újraindítás szükséges. Új nyilvános kulcsot generál.'; + '(Csak soros) Lecseréli az eszközazonosító privát kulcsot. Újraindítás szükséges az alkalmazáshoz. Új nyilvános kulcsot generál.'; @override String get repeater_cliHelpSetRadioRxGain => - '(Csak SX126x család) A növelt RX erősítést be- és kikapcsolható, így a nagyobb áramfelvétel esetén is javítható a érzékenység.'; + '(csak SX126x) Bekapcsolja a megnövelt RX-erősítést a jobb érzékenység érdekében nagyobb áramfelvétel mellett.'; @override String get repeater_cliHelpSetOwnerInfo => - 'Megadja az üvegezésben megjelenő tulajdonos elérhetőségeinek szövegét. Használja a \'|\' karaktert új sorok elválasztására.'; + 'Beállítja a hirdetésekben szereplő tulajdonos elérhetőségi adatait. A \'|\' használata újsorokhoz.'; @override String get repeater_cliHelpSetPathHashMode => - 'Beállítja a hálózati útvonal-hash módot. 0 = régebbi, 1 = szabványos, 2 = szigorú. Hatással van a hálózati útvonalak megadatalására.'; + 'Beállítja az útvonal-kivonat módot. 0 = örökölt, 1 = szabványos, 2 = szigorú. Befolyásolja az útválasztási útvonalak egyeztetését.'; @override String get repeater_cliHelpSetLoopDetect => - 'Beállítja a hibaforrás-keresés érzékenységét: kikapcsolva, minimális, közepes vagy szigorú.'; + 'Beállítja az útválasztási hurok észlelésének érzékenységét: ki, minimális, közepes vagy szigorú.'; @override String get repeater_cliHelpSetFreq => - '(Csak soros mód) Gyorsan beállítja a frekvenciát. A rendszer újraindítás szükséges. A teljes rádióparaméterek beállításához a \"rádió beállítás\" funkciót javaslom.'; + '(Csak soros) Gyorsan beállítja csak a frekvenciát. Újraindítás szükséges. A „rádió beállítása” előnyben részesítse a teljes rádióparamétereket.'; @override String get repeater_cliHelpSetBridgeChannel => - '(Csak ESPNow híd esetén) Beállítja a híd által használt WiFi-csatornát (1-14).'; + '(Csak ESPNow bridge) Beállítja a híd által használt WiFi csatornát (1-14).'; @override String get repeater_cliHelpGetName => @@ -3102,108 +3082,108 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_cliHelpGetRole => - 'Megmutatja a firmware funkcióját (repeater, szobai szerver stb.).'; + 'Megmutatja a firmware szerepkörét (Repeater, Room Server stb.).'; @override String get repeater_cliHelpGetPublicKey => - 'Megjeleníti a eszköz nyilvános kulcsát.'; + 'Megjeleníti az eszköz nyilvános kulcsát.'; @override String get repeater_cliHelpGetPrvKey => - '(Csak soros mód) Megjeleníti a eszköz privát kulcsát. Kezelje titkos információként.'; + '(Csak soros) Az eszköz privát kulcsát mutatja. Kezelje titokként.'; @override String get repeater_cliHelpGetRepeat => - 'Megmutatja, hogy a csomagok továbbításának (repeater funkció) engedélyezve van-e vagy nem.'; + 'Megmutatja, hogy a csomagtovábbítás (ismétlő szerepkör) be vagy ki van-e kapcsolva.'; @override String get repeater_cliHelpGetTx => - 'Megmutatja a jelenlegi TX (átvitel) teljesítményt dBm-ben.'; + 'Az aktuális TX teljesítményt mutatja dBm-ben.'; @override String get repeater_cliHelpGetFreq => - 'Megjeleníti a konfigurált rádiófrekvenciát MHz-ben.'; + 'Megjeleníti a beállított rádiófrekvenciát MHz-ben.'; @override String get repeater_cliHelpGetRadio => - 'Megjeleníti az összes rádióparamétert: frekvencia, sávszélesség, széttétező tényező, kódolási ráta.'; + 'A teljes rádióparamétereket mutatja: frekvencia, sávszélesség, szórási tényező, kódolási sebesség.'; @override String get repeater_cliHelpGetRadioRxGain => - '(Csak SX126x családra) Megjeleníti az RX erősítés állapotát.'; + '(csak SX126x) Megjeleníti az RX megnövelt erősítési állapotát.'; @override - String get repeater_cliHelpGetAf => 'Megmutatja az aktuális időadó tényezőt.'; + String get repeater_cliHelpGetAf => 'Megmutatja az aktuális műsoridőt.'; @override String get repeater_cliHelpGetDutyCycle => - 'Megmutatja az aktuális engedélyezett működési ciklust százalékban.'; + 'Megjeleníti az aktuális megengedett munkaciklust százalékban.'; @override String get repeater_cliHelpGetIntThresh => - 'Megmutatja a csatornák közötti interferencia szintjét dB-ben.'; + 'A csatorna interferencia küszöbértékét mutatja dB-ben.'; @override String get repeater_cliHelpGetAgcResetInterval => - 'Megmutatja az AGC (automatikus gain-kontroll) visszaállítási időt másodpercekben.'; + 'Az AGC reset intervallumát mutatja másodpercben.'; @override String get repeater_cliHelpGetMultiAcks => - 'Megmutatja, hogy a kettős visszaigazolás (double-ACK) mód engedélyezve van-e (1), vagy kikapcsolva (0).'; + 'Megmutatja, hogy a dupla ACK mód be van-e kapcsolva (1) vagy ki (0).'; @override String get repeater_cliHelpGetAllowReadOnly => - 'Megmutatja, hogy a vendég csak olvasási jogosítást engedélyez-e.'; + 'Megmutatja, hogy a vendég csak olvasási hozzáférés engedélyezett-e.'; @override String get repeater_cliHelpGetAdvertInterval => - 'Megmutatja a helyi hirdetés időtartamát percenként.'; + 'A helyi hirdetési intervallumot mutatja percben.'; @override String get repeater_cliHelpGetFloodAdvertInterval => - 'Megmutatja az aktuális időzítést, amikor megjelenik a vízparti reklám, órákonként.'; + 'Megjeleníti az árvízhirdetés intervallumát órákban.'; @override String get repeater_cliHelpGetGuestPassword => - 'Megjeleníti a konfigurált vendégjelszót.'; + 'Megjeleníti a beállított vendégjelszót.'; @override - String get repeater_cliHelpGetLat => 'Megjeleníti a beállított szélességet.'; + String get repeater_cliHelpGetLat => + 'Megjeleníti a beállított szélességi fokot.'; @override - String get repeater_cliHelpGetLon => 'Megjeleníti a beállított hosszúságot.'; + String get repeater_cliHelpGetLon => 'A beállított hosszúságot mutatja.'; @override - String get repeater_cliHelpGetRxDelay => - 'Megmutatja az alapértéket a késéshez.'; + String get repeater_cliHelpGetRxDelay => 'Az rxdelay alapértékét mutatja.'; @override String get repeater_cliHelpGetTxDelay => - 'Megmutatja a vízszint-érzékelő jelátviteli késésének tényezőjét.'; + 'Megjeleníti az elárasztási mód txdelay tényezőjét.'; @override String get repeater_cliHelpGetDirectTxDelay => - 'Megmutatja a közvetlen módban használt késés tényezőt.'; + 'A közvetlen módú txdelay tényezőt mutatja.'; @override String get repeater_cliHelpGetFloodMax => - 'Megmutatja a maximális vízszint-emelkedés mértékét.'; + 'Megmutatja a maximális elárasztási ugrás számát.'; @override String get repeater_cliHelpGetOwnerInfo => - 'Megjeleníti az tulajdonos elérhetőségének szövegét.'; + 'Megjeleníti a tulajdonos kapcsolatfelvételi adatait.'; @override String get repeater_cliHelpGetPathHashMode => - 'Megjeleníti a hash-alapú mód (0/1/2) beállításokat.'; + 'Az elérési út-kivonat módot mutatja (0/1/2).'; @override String get repeater_cliHelpGetLoopDetect => - 'Mutatja a cikkszám-azonosító érzékenységet.'; + 'A hurokérzékelés érzékenységét mutatja.'; @override String get repeater_cliHelpGetAcl => - '(Csak sorozat) A repeateren található hozzáférési szabályok listája.'; + '(Csak soros) Felsorolja a hozzáférés-vezérlési bejegyzéseket az átjátszón.'; @override String get repeater_cliHelpGetBridgeEnabled => @@ -3211,109 +3191,109 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_cliHelpGetBridgeDelay => - 'Megmutatja a hídon bekövetkező késést másodpercben.'; + 'A híd késleltetését mutatja ms-ban.'; @override String get repeater_cliHelpGetBridgeSource => - 'Megmutatja, hogy a híd RX vagy TX csomagokat fogad-e.'; + 'Megmutatja, hogy a híd RX vagy TX csomagokat naplóz-e.'; @override String get repeater_cliHelpGetBridgeBaud => - '(Csak RS232 híd) Megjeleníti a híd sebességét.'; + '(csak RS232 híd) A híd adatátviteli sebességét mutatja.'; @override String get repeater_cliHelpGetBridgeChannel => - '(Csak ESPNow híd) Megjeleníti a híd által használt WiFi csatornát.'; + '(csak ESPNow bridge) A híd WiFi csatornáját mutatja.'; @override String get repeater_cliHelpGetBridgeSecret => - '(Csak ESPNow híd esetén) Megjeleníti a híd által megosztott titkos kulcsot.'; + '(Csak ESPNow bridge) Megjeleníti a híd megosztott titkát.'; @override String get repeater_cliHelpGetBootloaderVer => - '(Csak NRF52 esetén) Megjeleníti a bootloader verzióját.'; + '(Csak NRF52) A rendszerbetöltő verzióját mutatja.'; @override String get repeater_cliHelpGetAdcMultiplier => - 'A bemeneti feszültség-átalakító (akkumulátor-feszültség-szabályozó) működését mutatja.'; + 'Az ADC szorzót mutatja (akkumulátor-feszültség skálázás).'; @override String get repeater_cliHelpGetPwrMgtSupport => - 'Megállapítja, hogy a felügyelet rendelkezik-e energiahatékonysági támogatással.'; + 'Beszámol arról, hogy a testület rendelkezik-e hatalomkezelési támogatással.'; @override String get repeater_cliHelpGetPwrMgtSource => - 'Megmutatja az aktuális energiaforrást: külső vagy akkumulátor.'; + 'Az aktuális áramforrást mutatja: külső vagy akkumulátor.'; @override String get repeater_cliHelpGetPwrMgtBootReason => - 'Megjeleníti az utolsó újraindítás és leállítás okait.'; + 'Megmutatja a legutóbbi alaphelyzetbe állítás és leállítás okait.'; @override String get repeater_cliHelpGetPwrMgtBootMv => - 'Megjeleníti a rendszerindításkor mért akkumulátor feszültséget millivoltban (mV).'; + 'Megmutatja a rendszerindítási akkumulátorfeszültséget mV-ban.'; @override String get repeater_cliHelpSensorGet => - 'Beolvas egy felhasználó által definiált szenzor beállítást kulcs segítségével.'; + 'Kulcs segítségével beolvassa az egyéni szenzorbeállítást.'; @override - String get repeater_cliHelpSensorSet => - 'Egyedi szenzorbeállítások létrehozása.'; + String get repeater_cliHelpSensorSet => 'Egyéni szenzorbeállítást ír.'; @override String get repeater_cliHelpSensorList => - 'Összesíti az összes egyedi szenzor beállításot, oldalanként, opcionális kezdő index alapján.'; + 'Felsorolja az összes egyéni érzékelőbeállítást, oldalszámozással az opcionális kezdőindexből.'; @override String get repeater_cliHelpRegionDefault => - 'Megmutatja a jelenlegi alapértelmezett régió határait.'; + 'Megjeleníti az aktuális alapértelmezett régióhatókört.'; @override String get repeater_cliHelpRegionDefaultSet => - 'Beállítja az alapértelmezett régió hatókörét. Használja a \"\" értéket a törléshez.'; + 'Beállítja az alapértelmezett régió hatókört. A törléshez használja a \"\" parancsot.'; @override String get repeater_cliHelpRegionListAllowed => - 'Felhasználható területek, ahol árvíz esetén forgalmat engedélyeznek.'; + 'Felsorolja azokat a régiókat, amelyek lehetővé teszik az árvízi forgalmat.'; @override String get repeater_cliHelpRegionListDenied => - 'Felhasznál, amelyek elutasítják a árvíz okozta forgalmat.'; + 'Felsorolja azokat a régiókat, amelyek megtagadják az árvízi forgalmat.'; @override String get repeater_cliHelpStatsPackets => - '(Csak sorozat) A csomagok szintjén történő statisztikát mutat.'; + '(Csak soros) Csomag szintű statisztikákat jelenít meg.'; @override String get repeater_cliHelpStatsRadio => - '(Csak sorozat) Mutat rádióstatisztikákat.'; + '(Csak soros) Rádióstatisztikák megjelenítése.'; @override String get repeater_cliHelpStatsCore => - '(Csak soros mód) A főfirmware-adatokat mutatja.'; + '(Csak soros) Az alapvető firmware-statisztikák megjelenítése.'; @override - String get telemetry_receivedData => 'Kapott adatokat a szenzorokról'; + String get telemetry_receivedData => 'Fogadott telemetriai adatok'; @override - String get telemetry_requestTimeout => 'Az adatkapcsolati kérés sikertelen.'; + String get telemetry_requestTimeout => + 'A telemetriai kérés időtúllépése lejárt.'; @override String telemetry_errorLoading(String error) { - return 'Hiba az adatok begyűjtésében: $error'; + return 'Hiba a telemetria betöltésekor: $error'; } @override - String get telemetry_noData => 'Nincsenek elérhető telemetriadatok.'; + String get telemetry_noData => 'Nincsenek telemetriai adatok.'; @override String telemetry_channelTitle(int channel) { - return '$channel csatorna'; + return 'Csatorna $channel'; } @override - String get telemetry_batteryLabel => 'Akku'; + String get telemetry_batteryLabel => 'Akkumulátor'; @override String get telemetry_voltageLabel => 'Feszültség'; @@ -3344,7 +3324,7 @@ class AppLocalizationsHu extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius °C / $fahrenheit °F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -3363,13 +3343,13 @@ class AppLocalizationsHu extends AppLocalizations { String get telemetry_genericLabel => 'Általános érzékelő'; @override - String get telemetry_luminosityLabel => 'Fényerő'; + String get telemetry_luminosityLabel => 'Fényesség'; @override String get telemetry_presenceLabel => 'Jelenlét'; @override - String get telemetry_humidityLabel => 'Páratartalom'; + String get telemetry_humidityLabel => 'Nedvesség'; @override String get telemetry_accelerometerLabel => 'Gyorsulásmérő'; @@ -3390,7 +3370,7 @@ class AppLocalizationsHu extends AppLocalizations { String get telemetry_concentrationLabel => 'Koncentráció'; @override - String get telemetry_powerLabel => 'Teljesítmény'; + String get telemetry_powerLabel => 'Hatalom'; @override String get telemetry_distanceLabel => 'Távolság'; @@ -3405,7 +3385,7 @@ class AppLocalizationsHu extends AppLocalizations { String get telemetry_timeLabel => 'Idő'; @override - String get telemetry_gyrometerLabel => 'Giroszkóp'; + String get telemetry_gyrometerLabel => 'Girométer'; @override String get telemetry_colourLabel => 'Szín'; @@ -3417,7 +3397,7 @@ class AppLocalizationsHu extends AppLocalizations { String get telemetry_switchLabel => 'Kapcsoló'; @override - String get telemetry_polylineLabel => 'Töröttvonal'; + String get telemetry_polylineLabel => 'Vonallánc'; @override String telemetry_altitudeValue(String meters) { @@ -3475,70 +3455,70 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get telemetry_autoFetchQuantity => 'Kérések száma'; + String get telemetry_autoFetchQuantity => 'Mennyiséget kér'; @override String get telemetry_error => 'Nem sikerült lekérni az adatokat'; @override - String get neighbors_receivedData => 'Kapott szomszédok adatait'; + String get neighbors_receivedData => 'Szomszédok adatok fogadása'; @override - String get neighbors_requestTimedOut => - 'A szomszédok kérik, hogy tiltsák le a kamerát.'; + String get neighbors_requestTimedOut => 'A szomszédok kérése lejárt.'; @override String neighbors_errorLoading(String error) { - return 'Hiba a szomszédok betöltésében: $error'; + return 'Hiba a szomszédok betöltésekor: $error'; } @override - String get neighbors_repeatersNeighbors => 'Ismétlő eszközök, szomszédok'; + String get neighbors_repeatersNeighbors => 'Ismétlők Szomszédok'; @override - String get neighbors_noData => 'Nincsenek elérhető szomszédokról adatok.'; + String get neighbors_noData => + 'A szomszédokról nem állnak rendelkezésre adatok.'; @override String neighbors_unknownContact(String pubkey) { - return 'Tudatlan $pubkey'; + return 'Ismeretlen $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Értsd: $time sitten'; + return 'Hallva: $time ezelőtt'; } @override - String get channelPath_title => 'Csomagok útvonala'; + String get channelPath_title => 'Csomag elérési útja'; @override - String get channelPath_viewMap => 'Megtekinthető térkép'; + String get channelPath_viewMap => 'Térkép megtekintése'; @override String get channelPath_otherObservedPaths => 'Egyéb megfigyelt utak'; @override - String get channelPath_repeaterHops => 'Adat továbbító lépések'; + String get channelPath_repeaterHops => 'Repeater Hops'; @override String get channelPath_noHopDetails => - 'Ez a csomag nem tartalmaz részletes információkat a \"hop\" (vagy más hasonló) szót használó kifejezésekről.'; + 'Ennél a csomagnál a komlórészletek nincsenek megadva.'; @override String get channelPath_messageDetails => 'Üzenet részletei'; @override - String get channelPath_senderLabel => 'Megküldő'; + String get channelPath_senderLabel => 'Feladó'; @override String get channelPath_timeLabel => 'Idő'; @override - String get channelPath_repeatsLabel => 'Ismétli'; + String get channelPath_repeatsLabel => 'Ismétlődik'; @override String channelPath_pathLabel(int index) { - return 'Útvonal $index'; + return '$index elérési út'; } @override @@ -3546,11 +3526,11 @@ class AppLocalizationsHu extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Megfigyelt útvonal: $index • $hops'; + return 'Megfigyelt útvonal $index • $hops'; } @override - String get channelPath_noLocationData => 'Nincs helyszínadat.'; + String get channelPath_noLocationData => 'Nincsenek helyadatok'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -3563,7 +3543,7 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Megfejt'; + String get channelPath_unknownPath => 'Ismeretlen'; @override String get channelPath_floodPath => 'Árvíz'; @@ -3573,12 +3553,12 @@ class AppLocalizationsHu extends AppLocalizations { @override String channelPath_observedZeroOf(int total) { - return '0-ból $total'; + return '0 / $total ugrás'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed of $total hops'; + return '$observed / $total ugrás'; } @override @@ -3586,18 +3566,18 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Ez a útvonal nem támogat repeater-t.'; + 'Nincs elérhető átjátszó hely ehhez az útvonalhoz.'; @override String channelPath_primaryPath(int index) { - return 'Útvonal $index (Elsődleges)'; + return '$index elérési út (elsődleges)'; } @override - String get channelPath_pathLabelTitle => 'Út'; + String get channelPath_pathLabelTitle => 'Útvonal'; @override - String get channelPath_observedPathHeader => 'Megfigyelt útvonal'; + String get channelPath_observedPathHeader => 'Megfigyelt ösvény'; @override String channelPath_selectedPathLabel(String label, String prefixes) { @@ -3606,191 +3586,192 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channelPath_noHopDetailsAvailable => - 'Ez a csomag nem tartalmaz részletes információkat a szállításhoz.'; + 'Ehhez a csomaghoz nem állnak rendelkezésre ugrási részletek.'; @override - String get channelPath_unknownRepeater => 'Tudatlan erősítő'; + String get channelPath_unknownRepeater => 'Ismeretlen Repeater'; @override - String get community_title => 'Helyi közösség'; + String get community_title => 'Közösség'; @override - String get community_create => 'Teremtsd meg a közösséget'; + String get community_create => 'Közösség létrehozása'; @override String get community_createDesc => - 'Légyon létre egy új közösséget, és osszák meg QR-kód segítségével.'; + 'Hozzon létre egy új közösséget, és ossza meg QR-kóddal.'; @override - String get community_join => 'Csatlakozjon'; + String get community_join => 'Csatlakozik'; @override - String get community_joinTitle => 'Csatlakozzon a közösséghez'; + String get community_joinTitle => 'Csatlakozz a közösséghez'; @override String community_joinConfirmation(String name) { - return 'Szeretne csatlakozni a közösséghez, $name?'; + return 'Szeretnél csatlakozni a \"$name\" közösséghez?'; } @override - String get community_scanQr => 'QR-kód olvasó a közösség számára'; + String get community_scanQr => 'Scan Community QR'; @override String get community_scanInstructions => - 'Fordítsa a kamerát egy közösségi QR-kód irányába.'; + 'Irányítsa a kamerát egy közösségi QR-kódra'; @override - String get community_showQr => 'Megjelenítse a QR-kódot'; + String get community_showQr => 'QR-kód megjelenítése'; @override - String get community_publicChannel => 'Összetartó, közösségi'; + String get community_publicChannel => 'Közösségi Nyilvános'; @override - String get community_hashtagChannel => 'Helyi hashtaget'; + String get community_hashtagChannel => 'Közösségi hashtag'; @override - String get community_name => 'Helyi közösség neve'; + String get community_name => 'Közösség neve'; @override - String get community_enterName => 'Kérjük, a közösség nevét írja be.'; + String get community_enterName => 'Adja meg a közösség nevét'; @override String community_created(String name) { - return 'A \"$name\" nevű közösség létrehozva'; + return 'A \"$name\" közösség létrehozva'; } @override String community_joined(String name) { - return 'Csatlakozott a $name közösséghez'; + return 'Csatlakozott a \"$name\" közösséghez'; } @override - String get community_qrTitle => 'Osszpontosítás a közösségben'; + String get community_qrTitle => 'Közösség megosztása'; @override String community_qrInstructions(String name) { - return 'Scanned this QR-kódot, hogy csatlakozhat a $name csoporthoz.'; + return 'Olvassa be ezt a QR-kódot a \"$name\" csoporthoz való csatlakozáshoz'; } @override String get community_hashtagPrivacyHint => - 'A közösségi hashtagekhez tartozó csatornák csak a közösség tagjai számára érhetők el.'; + 'A közösségi hashtag csatornákhoz csak a közösség tagjai csatlakozhatnak'; @override String get community_invalidQrCode => 'Érvénytelen közösségi QR-kód'; @override - String get community_alreadyMember => 'Már tag vagy'; + String get community_alreadyMember => 'Már tag'; @override String community_alreadyMemberMessage(String name) { - return 'Már tagja $name-nek.'; + return 'Ön már tagja a(z) \"$name\".'; } @override String get community_addPublicChannel => - 'Hozzon létre egy közösségi nyilvános csatornát'; + 'Közösségi nyilvános csatorna hozzáadása'; @override String get community_addPublicChannelHint => - 'Automatikusan hozzon létre ezt a csatornát a közösség számára.'; + 'Nyilvános csatorna automatikus hozzáadása ehhez a közösséghez'; @override - String get community_noCommunities => 'Még egyik közösség sem csatlakozott.'; + String get community_noCommunities => 'Még nem csatlakozott közösség'; @override String get community_scanOrCreate => - 'Scelle egy QR-kódot, vagy hozzon létre egy közösséget, hogy elinduljon.'; + 'Olvassa be a QR-kódot, vagy hozzon létre egy közösséget a kezdéshez'; @override String get community_manageCommunities => 'Közösségek kezelése'; @override - String get community_delete => 'Hagyományos közösségi élet'; + String get community_delete => 'Kilépés a közösségből'; @override String community_deleteConfirm(String name) { - return 'Hagyom $name-et?'; + return 'Kilép a(z) „$name” programból?'; } @override String community_deleteChannelsWarning(int count) { - return 'Ezem törli is $count csatornát és a hozzá tartozó üzeneteket.'; + return 'Ezzel $count csatornát és azok üzeneteit is törli.'; } @override String community_deleted(String name) { - return 'A közösség, amely $name'; + return 'Kilépett a \"$name\" közösségből'; } @override - String get community_regenerateSecret => 'Titkos visszaállítás'; + String get community_regenerateSecret => 'Regeneráld a Titkot'; @override String community_regenerateSecretConfirm(String name) { - return 'Újra kell generálni a titkos kulcsot $name számára? Minden tagnak be kell szkennelnie az új QR-kódot, hogy továbbra is kommunikálhasson.'; + return 'Újragenerálja a titkos kulcsot a következőhöz: \"$name\"? A kommunikáció folytatásához minden tagnak be kell olvasnia az új QR-kódot.'; } @override - String get community_regenerate => 'Újraalakítás'; + String get community_regenerate => 'Regenerátum'; @override String community_secretRegenerated(String name) { - return 'Titkos kulcs megújult $name számára.'; + return 'Titok újra létrehozva a következőhöz: \"$name\"'; } @override - String get community_updateSecret => 'Frissítési titok'; + String get community_updateSecret => 'Frissítse a Titkot'; @override String community_secretUpdated(String name) { - return 'Titkos információ frissítve $name számára'; + return 'Titok frissítve a következőhöz: \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scanned a új QR-kódot, hogy frissítsük a $name számára megőrzött titkos információt.'; + return 'Olvassa be az új QR-kódot a „$name” titkának frissítéséhez'; } @override - String get community_addHashtagChannel => 'Adjon egy közösségi hashtaget'; + String get community_addHashtagChannel => 'Közösségi hashtag hozzáadása'; @override String get community_addHashtagChannelDesc => - 'Hozz létre egy hashtage-os csatornát ennek a közösségnek'; + 'Adj hozzá egy hashtag-csatornát ehhez a közösséghez'; @override - String get community_selectCommunity => 'Válasszon közösséget'; + String get community_selectCommunity => 'Válassza a Közösség lehetőséget'; @override String get community_regularHashtag => 'Rendszeres hashtag'; @override String get community_regularHashtagDesc => - 'Önmagas szintű hashtaget (bárki csatlakozhat)'; + 'Nyilvános hashtag (bárki csatlakozhat)'; @override - String get community_communityHashtag => 'Helyi hashtaget'; + String get community_communityHashtag => 'Közösségi hashtag'; @override - String get community_communityHashtagDesc => 'Csak a közösség tagjai számára'; + String get community_communityHashtagDesc => + 'Privát a közösség tagjai számára'; @override String community_forCommunity(String name) { - return '$name számára'; + return '$name'; } @override String get listFilter_tooltip => 'Szűrés és rendezés'; @override - String get listFilter_sortBy => 'Szűrés'; + String get listFilter_sortBy => 'Rendezés'; @override - String get listFilter_latestMessages => 'Legfrissebb üzenetek'; + String get listFilter_latestMessages => 'Legújabb üzenetek'; @override - String get listFilter_heardRecently => 'Úgy hallottam, hogy...'; + String get listFilter_heardRecently => 'Nemrég hallottam'; @override String get listFilter_az => 'A-Z'; @@ -3799,25 +3780,25 @@ class AppLocalizationsHu extends AppLocalizations { String get listFilter_filters => 'Szűrők'; @override - String get listFilter_all => 'Mind'; + String get listFilter_all => 'Minden'; @override String get listFilter_favorites => 'Kedvencek'; @override - String get listFilter_addToFavorites => 'Megerősítés kívánságlistára'; + String get listFilter_addToFavorites => 'Hozzáadás a kedvencekhez'; @override - String get listFilter_removeFromFavorites => 'Törölj a kedvencekből'; + String get listFilter_removeFromFavorites => 'Eltávolítás a kedvencek közül'; @override String get listFilter_users => 'Felhasználók'; @override - String get listFilter_repeaters => 'Újraküldők'; + String get listFilter_repeaters => 'Ismétlők'; @override - String get listFilter_roomServers => 'Szoba-szolgálatok'; + String get listFilter_roomServers => 'Szobaszerverek'; @override String get listFilter_unreadOnly => 'Csak olvasatlan'; @@ -3829,61 +3810,60 @@ class AppLocalizationsHu extends AppLocalizations { String get pathTrace_you => 'Te'; @override - String get pathTrace_failed => 'A útvonal követése sikertelen.'; + String get pathTrace_failed => 'Az útvonal nyomon követése nem sikerült.'; @override - String get pathTrace_notAvailable => - 'Az útvonal követési funkció nem elérhető.'; + String get pathTrace_notAvailable => 'Útvonal nyomkövetés nem érhető el.'; @override - String get pathTrace_refreshTooltip => 'Út mentesség frissítése.'; + String get pathTrace_refreshTooltip => 'Path Trace frissítése.'; @override String get pathTrace_someHopsNoLocation => - 'Egy vagy több búzavirág hiányozik a helyszínéről!'; + 'Egy vagy több komló helye hiányzik!'; @override - String get pathTrace_clearTooltip => 'Egyértelmű út.'; + String get pathTrace_clearTooltip => 'Tiszta útvonal.'; @override String get losSelectStartEnd => - 'Válassza ki a kezdő és a végpontokat a LOS-hoz.'; + 'Válassza ki a LOS kezdő és záró csomópontját.'; @override String losRunFailed(String error) { - return 'A látószög ellenőrzése sikertelen: $error'; + return 'A rálátás ellenőrzése sikertelen: $error'; } @override - String get losClearAllPoints => 'Teljesen tisztázzuk az összes pontot'; + String get losClearAllPoints => 'Törölje az összes pontot'; @override String get losRunToViewElevationProfile => - 'Használja a LOS-t, hogy megtekinthesse a magasságkülönbségek diagramját.'; + 'Futtassa a LOS-t a magassági profil megtekintéséhez'; @override String get losMenuTitle => 'LOS menü'; @override String get losMenuSubtitle => - 'A térképen található pontok kiválasztására vagy a térképen hosszúra nyomva, hogy egyedi pontokat definiálhassunk.'; + 'Érintse meg a csomópontokat, vagy tartsa lenyomva a térképet az egyéni pontokhoz'; @override - String get losShowDisplayNodes => 'Megjelenítsen a megjelenítési egységeket'; + String get losShowDisplayNodes => 'Megjelenítési csomópontok megjelenítése'; @override String get losCustomPoints => 'Egyedi pontok'; @override String losCustomPointLabel(int index) { - return 'Egyedi $index'; + return 'Egyéni $index'; } @override - String get losPointA => 'A pont A'; + String get losPointA => 'A pont'; @override - String get losPointB => 'Pont B'; + String get losPointB => 'B pont'; @override String losAntennaA(String value, String unit) { @@ -3892,14 +3872,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String losAntennaB(String value, String unit) { - return 'Antenna B: $value $unit'; + return 'B antenna: $value $unit'; } @override - String get losRun => 'Futtass a LOS-on'; + String get losRun => 'Futtassa a LOS-t'; @override - String get losNoElevationData => 'Nincsenek emelkedési adatok.'; + String get losNoElevationData => 'Nincsenek magassági adatok'; @override String losProfileClear( @@ -3908,7 +3888,7 @@ class AppLocalizationsHu extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit'; + return '$distance $distanceUnit, tiszta LOS, minimális távolság $clearance $heightUnit'; } @override @@ -3918,7 +3898,7 @@ class AppLocalizationsHu extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, amelyet $obstruction akadályoz meg $heightUnit-ban'; + return '$distance $distanceUnit, blokkolta: $obstruction $heightUnit'; } @override @@ -3929,48 +3909,48 @@ class AppLocalizationsHu extends AppLocalizations { @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total tisztított, $blocked blokkolt, $unknown ismeretlen'; + return 'LOS: $clear/$total tiszta, $blocked blokkolva, $unknown ismeretlen'; } @override String get losErrorElevationUnavailable => - 'Az alábbi minták esetében nem áll rendelkezésre magasságadat.'; + 'Egy vagy több mintához nem állnak rendelkezésre magassági adatok.'; @override String get losErrorInvalidInput => - 'Hibás vagy hiányos táblázatok a LOS (Loss of Signal) számításához.'; + 'Érvénytelen pontok/emelkedési adatok a LOS számításhoz.'; @override - String get losRenameCustomPoint => 'Állítsa meg a saját pont nevét'; + String get losRenameCustomPoint => 'Egyéni pont átnevezése'; @override String get losPointName => 'Pont neve'; @override - String get losShowPanelTooltip => 'Megjelenítse a LOS paneelt'; + String get losShowPanelTooltip => 'LOS panel megjelenítése'; @override - String get losHidePanelTooltip => 'Rejtse el a LOS paneelt'; + String get losHidePanelTooltip => 'LOS panel elrejtése'; @override String get losElevationAttribution => - 'Magasságadatok: Open-Meteo (CC BY 4.0)'; + 'Magassági adatok: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio Horizont'; + String get losLegendRadioHorizon => 'Rádióhorizont'; @override - String get losLegendLosBeam => 'LOS jelzés'; + String get losLegendLosBeam => 'LOS gerenda'; @override - String get losLegendTerrain => 'Terület'; + String get losLegendTerrain => 'Terep'; @override - String get losBlockedSpotsTitle => 'Foglalhatatlan területek'; + String get losBlockedSpotsTitle => 'Blokkolt helyek'; @override String get losBlockedSpotsHint => - 'A blokkolt területet megjelölve, hogy a térképen kiemeljük.'; + 'Koppintson egy blokkolt helyre, hogy kijelölje azt a térképen.'; @override String losBlockedSpotChip( @@ -3993,18 +3973,17 @@ class AppLocalizationsHu extends AppLocalizations { String distanceUnit, String distanceFromB, ) { - return 'Elakadt a $obstruction miatt, $heightUnit magasságban, $distanceFromA méterrel A-tól és $distanceFromB méterrel B-től ($distanceUnit).'; + return 'Letiltja: $obstruction $heightUnit, $distanceFromA A-tól és $distanceFromB B-től ($distanceUnit).'; } @override - String get losFrequencyLabel => 'Hatósság'; + String get losFrequencyLabel => 'Frekvencia'; @override - String get losFrequencyInfoTooltip => 'Lásd a számítás részleteit'; + String get losFrequencyInfoTooltip => 'Tekintse meg a számítás részleteit'; @override - String get losFrequencyDialogTitle => - 'A rádióhullámok hatótávolságának kiszámítása'; + String get losFrequencyDialogTitle => 'Rádióhorizont számítás'; @override String losFrequencyDialogDescription( @@ -4013,86 +3992,86 @@ class AppLocalizationsHu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'A $baselineK értékből kezdve, $baselineFreq MHz-os frekvencián, a számítás az aktuális $frequencyMHz MHz-os sávhoz igazítja a k-tényezőt, amely meghatározza a görbös rádióhatótávolság határát.'; + return 'A k=$baselineK értéktől kezdve $baselineFreq MHz-en a számítás az aktuális $frequencyMHz MHz-es sávhoz igazítja a k-tényezőt, amely meghatározza az ívelt rádióhorizont-sapkát.'; } @override - String get contacts_pathTrace => 'Útvonal követése'; + String get contacts_pathTrace => 'Út nyom'; @override String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'Az útvonal követése a repeaterig'; + String get contacts_repeaterPathTrace => + 'Útvonal nyomkövetése az átjátszóhoz'; @override - String get contacts_repeaterPing => 'Ping-szinkronizáló'; + String get contacts_repeaterPing => 'Ping átjátszó'; @override - String get contacts_roomPathTrace => 'Kapcsolat a szobai szerverrel'; + String get contacts_roomPathTrace => 'Útvonal nyomkövetése a szobaszerverhez'; @override - String get contacts_roomPing => 'Ping-szolgáló szerver'; + String get contacts_roomPing => 'Ping szoba szerver'; @override - String get contacts_chatTraceRoute => 'Útvonal meghatározása'; + String get contacts_chatTraceRoute => 'Út nyomvonala'; @override String contacts_pathTraceTo(String name) { - return 'Keresse meg a $name címét.'; + return 'Útvonal nyomon követése ide: $name'; } @override - String get contacts_clipboardEmpty => 'A kiválasztott szöveg üres.'; + String get contacts_clipboardEmpty => 'A vágólap üres.'; @override - String get contacts_invalidAdvertFormat => 'Érvénytelen kontaktinformáció'; + String get contacts_invalidAdvertFormat => + 'Érvénytelen kapcsolattartási adatok'; @override - String get contacts_contactImported => 'Kapcsolat létrejött.'; + String get contacts_contactImported => 'A névjegy importálása megtörtént.'; @override String get contacts_contactImportFailed => - 'Nem sikerült a kapcsolatot importálni.'; + 'Nem sikerült importálni a névjegyet.'; @override - String get contacts_zeroHopAdvert => 'Zero Hop reklám'; + String get contacts_zeroHopAdvert => 'Zero Hop hirdetés'; @override - String get contacts_floodAdvert => 'Árvízre vonatkozó hirdetés'; + String get contacts_floodAdvert => 'Árvíz hirdetés'; @override - String get contacts_copyAdvertToClipboard => - 'Másolja a hirdetést a kiválasztási ablakba'; + String get contacts_copyAdvertToClipboard => 'Hirdetés másolása a vágólapra'; @override String get contacts_addContactFromClipboard => - 'Adjon hozzá egy kapcsolatot a kiválasztott listából'; + 'Névjegy hozzáadása a vágólapról'; @override - String get contacts_ShareContact => 'Másolja a kapcsolatot a kiválasztóba'; + String get contacts_ShareContact => 'Névjegy másolása a vágólapra'; @override - String get contacts_ShareContactZeroHop => - 'Ossza meg a kapcsolatot hirdetés segítségével'; + String get contacts_ShareContactZeroHop => 'Kapcsolat megosztása hirdetéssel'; @override String get contacts_zeroHopContactAdvertSent => - 'Kapcsolatot a hirdetésen keresztül.'; + 'Az elérhetőséget hirdetéssel küldték el.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Nem sikerült a kapcsolatot elküldeni.'; + 'Nem sikerült elküldeni a névjegyet.'; @override - String get contacts_contactAdvertCopied => 'A hirdetés másolva a vágólapra.'; + String get contacts_contactAdvertCopied => 'A hirdetés a vágólapra másolva.'; @override String get contacts_contactAdvertCopyFailed => - 'Az hirdetés másolása a vágólapra sikertelen.'; + 'A hirdetés vágólapra másolása nem sikerült.'; @override - String get notification_activityTitle => 'MeshCore tevékenységek'; + String get notification_activityTitle => 'MeshCore tevékenység'; @override String notification_messagesCount(int count) { @@ -4129,199 +4108,197 @@ class AppLocalizationsHu extends AppLocalizations { @override String notification_newTypeDiscovered(String contactType) { - return 'Új $contactType megtalálva'; + return 'Új $contactType felfedezve'; } @override - String get notification_receivedNewMessage => 'Új üzenetet kaptam'; + String get notification_receivedNewMessage => 'Új üzenet érkezett'; @override String get settings_gpxExportRepeaters => - 'Külső eszközök / helyi szerver a GPX formátumba'; + 'Ismétlők / szobaszerver exportálása GPX-be'; @override String get settings_gpxExportRepeatersSubtitle => - 'Exportálható repeater/szobaterm-szerver, amely egy GPX fájlban tárolja a helyzetet.'; + 'Exportálja az átjátszókat / szobaszervereket a hellyel GPX-fájlba.'; @override - String get settings_gpxExportContacts => 'GPX export funkciók'; + String get settings_gpxExportContacts => 'Exportáljon társokat a GPX-be'; @override String get settings_gpxExportContactsSubtitle => - 'Az export funkció lehetővé teszi, hogy a GPS fájlban megadott helyszínen is megőrizzük az útvonalat.'; + 'A hellyel rendelkező kísérőket GPX-fájlba exportálja.'; @override - String get settings_gpxExportAll => - 'Exportálja az összes kapcsolatot GPX formátumban.'; + String get settings_gpxExportAll => 'Az összes névjegy exportálása a GPX-be'; @override String get settings_gpxExportAllSubtitle => - 'Az összes elérhetőséget, amelyekhez egy helyszín tartozik, egy GPX fájlba exportálja.'; + 'A hellyel rendelkező összes névjegyet GPX-fájlba exportálja.'; @override - String get settings_gpxExportSuccess => - 'A GPX fájl sikeresen exportálva lett.'; + String get settings_gpxExportSuccess => 'A GPX fájl sikeresen exportálva.'; @override - String get settings_gpxExportNoContacts => 'Nincs exportálható kapcsolatok.'; + String get settings_gpxExportNoContacts => + 'Nincsenek exportálandó névjegyek.'; @override - String get settings_gpxExportNotAvailable => - 'Nem támogatott a jelenlegi eszközön/rendszeren.'; + String get settings_gpxExportNotAvailable => 'Az Ön eszköze/OS nem támogatja'; @override - String get settings_gpxExportError => 'Hiba történt az export során.'; + String get settings_gpxExportError => 'Hiba történt az exportálás során.'; @override String get settings_gpxExportRepeatersRoom => - 'Adatátvisszaadó eszközök és helyiségi szerverek helyei'; + 'Repeater és szobaszerver helyei'; @override - String get settings_gpxExportChat => 'Kapcsolódó helyszínek'; + String get settings_gpxExportChat => 'Társ helyek'; @override - String get settings_gpxExportAllContacts => 'Az összes kapcsolat helyszíne'; + String get settings_gpxExportAllContacts => 'Minden kapcsolattartó hely'; @override String get settings_gpxExportShareText => - 'A meshcore-open-ból exportált térkéadatumok'; + 'A térképadatok a meshcore-openből exportálva'; @override String get settings_gpxExportShareSubject => - 'meshcore-open GPX formátumú térképi adatok export'; + 'meshcore-open GPX térképadatok exportálása'; @override - String get snrIndicator_nearByRepeaters => 'Helyszíni erősítők'; + String get snrIndicator_nearByRepeaters => 'Közeli átjátszók'; @override - String get snrIndicator_lastSeen => 'Utoljára, amikor látták'; + String get snrIndicator_lastSeen => 'Utoljára látott'; @override - String get contactsSettings_title => 'Kapcsolatok beállításai'; + String get contactsSettings_title => 'Névjegyek beállításai'; @override String get contactsSettings_autoAddTitle => 'Automatikus felfedezés'; @override String get contactsSettings_otherTitle => - 'Egyéb kapcsolattal kapcsolatos beállítások'; + 'Egyéb kapcsolattartási beállítások'; @override String get contactsSettings_autoAddUsersTitle => - 'Automatikus felhasználói hozzáadás'; + 'Felhasználók automatikus hozzáadása'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Engedje, hogy a segítő automatikusan hozzáadja az új felhasználókat.'; + 'Engedélyezze a társ számára, hogy automatikusan hozzáadja a felfedezett felhasználókat.'; @override String get contactsSettings_autoAddRepeatersTitle => - 'Automatikus visszatöltés'; + 'Ismétlők automatikus hozzáadása'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Engedje, hogy a segítő eszköz automatikusan hozzáadja az új, megtalált jelzőállomásokat.'; + 'Engedélyezze a társnak, hogy automatikusan hozzáadja a felfedezett ismétlőket.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Automatikus szobák szerverek hozzáadása'; + 'Szobaszerverek automatikus hozzáadása'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Engedje, hogy a segítő automatikusan hozzáadja az új, megtalált hálózati szervereket.'; + 'Engedélyezze a társ számára, hogy automatikusan hozzáadja a felfedezett szobaszervereket.'; @override String get contactsSettings_autoAddSensorsTitle => - 'Automatikus érzékelők hozzáadása'; + 'Érzékelők automatikus hozzáadása'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Engedje, hogy a kísérő automatikusan hozzáadja az új, megtalált szenzorokat.'; + 'Engedélyezze a társ számára a felfedezett érzékelők automatikus hozzáadását.'; @override - String get contactsSettings_overwriteOldestTitle => 'Felülírja a legrégebbet'; + String get contactsSettings_overwriteOldestTitle => 'A legrégebbi felülírása'; @override String get contactsSettings_overwriteOldestSubtitle => - 'Amikor a névsor telítődik, a legidősebb, de még nem kedvencként jelölt személyt helyettesíti egy újabb.'; + 'Amikor a névjegylista megtelik, a legrégebbi, nem kedvenc névjegy lecserélődik.'; @override - String get discoveredContacts_Title => 'Megtalált kapcsolatok'; + String get discoveredContacts_Title => 'Felfedezett kapcsolatok'; @override - String get discoveredContacts_noMatching => 'Nincs megegyező kapcsolat.'; + String get discoveredContacts_noMatching => 'Nincsenek megfelelő névjegyek'; @override - String get discoveredContacts_searchHint => 'Keress új kapcsolatokat'; + String get discoveredContacts_searchHint => + 'Keresés a talált névjegyek között'; @override String get discoveredContacts_contactAdded => 'Kapcsolat hozzáadva'; @override - String get discoveredContacts_addContact => 'Adjon személyhez'; + String get discoveredContacts_addContact => 'Névjegy hozzáadása elemre'; @override - String get discoveredContacts_copyContact => - 'Másolja a kapcsolatot a vágólapra'; + String get discoveredContacts_copyContact => 'Névjegy másolása a vágólapra'; @override String get discoveredContacts_deleteContact => - 'Törölj a feltalált kapcsolatot'; + 'A felfedezett névjegy törlése'; @override String get discoveredContacts_deleteContactAll => - 'Törölj minden megtalált kapcsolatot'; + 'Törölje az összes felfedezett névjegyet'; @override String get discoveredContacts_deleteContactAllContent => - 'Biztos, hogy szeretné törölni az összes eddig megtalált kapcsolatot?'; + 'Biztosan törli az összes felfedezett névjegyet?'; @override String get chat_sendCooldown => - 'Kérjük, várjon egy pillanatot, mielőtt újra elküldené.'; + 'Kérjük, várjon egy pillanatot, mielőtt újra elküldi.'; @override String get appSettings_jumpToOldestUnread => - 'Jelentkezzen az legörebb, olvasatlan üzenetre'; + 'Ugrás a legrégebbi olvasatlanra'; @override String get appSettings_jumpToOldestUnreadSubtitle => - 'Amikor egy új csevet indítunk, amelyben vannak olvashatatlan üzenetek, görgessük a listát, hogy a legelső, olvashatatlan üzenet megjelenjen, nem pedig az utolsó.'; + 'Ha olvasatlan üzeneteket tartalmazó csevegést nyit meg, görgessen az első olvasatlanra a legutóbbi helyett.'; @override - String get appSettings_languageHu => 'Magyar'; + String get appSettings_languageHu => 'magyar'; @override - String get appSettings_languageJa => 'Japán'; + String get appSettings_languageJa => 'japán'; @override - String get appSettings_languageKo => 'Koreai'; + String get appSettings_languageKo => 'koreai'; @override - String get radioStats_tooltip => 'Rádió és hálózati statisztikák'; + String get radioStats_tooltip => 'Rádió és mesh statisztika'; @override - String get radioStats_screenTitle => 'Rádió statisztikák'; + String get radioStats_screenTitle => 'Rádió statisztika'; @override String get radioStats_notConnected => - 'Csatlakozzon egy eszközhöz, hogy megtekinthesse a rádió adatok statisztikáit.'; + 'Csatlakozzon egy eszközhöz a rádióstatisztikák megtekintéséhez.'; @override String get radioStats_firmwareTooOld => - 'A rádió statisztikákhoz v8 vagy újabb verziójú szoftver szükséges.'; + 'A rádióstatisztikákhoz v8-as vagy újabb firmware szükséges.'; @override - String get radioStats_waiting => 'Adatokra vár…'; + String get radioStats_waiting => 'Várakozás az adatokra…'; @override String radioStats_noiseFloor(int noiseDbm) { - return 'Háttérzaj szint: $noiseDbm dBm'; + return 'Zajszint: $noiseDbm dBm'; } @override String radioStats_lastRssi(int rssiDbm) { - return 'Utolsó RSSI érték: $rssiDbm dBm'; + return 'Utolsó RSSI: $rssiDbm dBm'; } @override @@ -4331,56 +4308,56 @@ class AppLocalizationsHu extends AppLocalizations { @override String radioStats_txAir(int seconds) { - return 'TX-es idő (összesen): $seconds másodperc'; + return 'TX műsoridő (összesen): $seconds s'; } @override String radioStats_rxAir(int seconds) { - return 'RX használat időtartama (összesen): $seconds s'; + return 'RX műsoridő (összesen): $seconds s'; } @override String get radioStats_chartCaption => - 'Háttérzaj szint (dBm) a legutóbbi minták alapján.'; + 'Zajszint (dBm) a legutóbbi mintákhoz képest.'; @override String radioStats_stripNoise(int noiseDbm) { - return 'Háttérzaj szint: $noiseDbm dBm'; + return 'Zajszint: $noiseDbm dBm'; } @override - String get radioStats_stripWaiting => 'Rádió adatok begyűjtése…'; + String get radioStats_stripWaiting => 'Rádióstatisztikák lekérése…'; @override - String get radioStats_settingsTile => 'Rádió statisztikák'; + String get radioStats_settingsTile => 'Rádió statisztika'; @override - String get radioStats_settingsSubtitle => - 'Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama'; + String get radioStats_settingsSubtitle => 'Zajszint, RSSI, SNR és műsoridő'; @override String get translation_title => 'Fordítás'; @override - String get translation_enableTitle => 'Engedje meg a fordítást'; + String get translation_enableTitle => 'Fordítás engedélyezése'; @override String get translation_enableSubtitle => - 'Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.'; + 'Fordítsa le a bejövő üzeneteket, és engedélyezze a küldés előtti fordítást.'; @override - String get translation_composerTitle => 'Fordítsa el, mielőtt elküldi'; + String get translation_composerTitle => 'Küldés előtt fordítsa le'; @override String get translation_composerSubtitle => - 'Ellenőrzi a zeneszerző fordítási ikon alapértékét.'; + 'Szabályozza a zeneszerző fordítási ikonjának alapértelmezett állapotát.'; @override - String get translation_autoIncomingTitle => 'Üzenetek automatikus fordítása'; + String get translation_autoIncomingTitle => + 'A bejövő üzenetek automatikus fordítása'; @override String get translation_autoIncomingSubtitle => - 'Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.'; + 'Automatikusan lefordítja az üzeneteket értesítésekhez, csevegéshez vagy csatornához.'; @override String get translation_translateMessage => 'Üzenet fordítása'; @@ -4389,40 +4366,39 @@ class AppLocalizationsHu extends AppLocalizations { String get translation_targetLanguage => 'Célnyelv'; @override - String get translation_useAppLanguage => - 'Használja az alkalmazás nyelvének beállítását.'; + String get translation_useAppLanguage => 'Használja az alkalmazás nyelvét'; @override String get translation_downloadedModelLabel => 'Letöltött modell'; @override String get translation_presetModelLabel => - 'Előre definiált Hugging Face-modell'; + 'Előre beállított átölelő arc modell'; @override - String get translation_manualUrlLabel => 'Manuális modell URL'; + String get translation_manualUrlLabel => 'Kézi modell URL'; @override - String get translation_downloadModel => 'Letöltés'; + String get translation_downloadModel => 'Modell letöltése'; @override String get translation_downloading => 'Letöltés...'; @override - String get translation_working => 'Munkában vagyok...'; + String get translation_working => 'Dolgozó...'; @override - String get translation_stop => 'Halt'; + String get translation_stop => 'Stop'; @override String get translation_mergingChunks => - 'A letöltött részek összeállítása a végleges fájlba...'; + 'Letöltött darabok egyesítése a végső fájlba...'; @override - String get translation_downloadedModels => 'Letöltött modelok'; + String get translation_downloadedModels => 'Letöltött modellek'; @override - String get translation_deleteModel => 'Törölje a modellt'; + String get translation_deleteModel => 'Modell törlése'; @override String get translation_modelDownloaded => 'Fordítási modell letöltve.'; @@ -4436,147 +4412,146 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get translation_enterUrlFirst => 'Addon először egy modell URL-t.'; + String get translation_enterUrlFirst => 'Először adja meg a modell URL-jét.'; @override - String get scanner_linuxPairingShowPin => 'Megjelenítse a PIN-kódot'; + String get scanner_linuxPairingShowPin => 'PIN-kód megjelenítése'; @override - String get scanner_linuxPairingHidePin => 'Rejtse el a PIN-kódot'; + String get scanner_linuxPairingHidePin => 'PIN elrejtése'; @override String get scanner_linuxPairingPinTitle => 'Bluetooth párosítási PIN'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).'; + return 'Írja be a $deviceName PIN-kódját (ha nincs, hagyja üresen).'; } @override - String get translation_messageTranslation => 'Üzenet fordítása'; + String get translation_messageTranslation => 'Üzenet fordítás'; @override - String get translation_translateBeforeSending => - 'Fordítsa el, mielőtt elküldi'; + String get translation_translateBeforeSending => 'Küldés előtt fordítsa le'; @override String get translation_composerEnabledHint => - 'A üzenetek fordítását a küldés előtt elvégezzük.'; + 'Az üzeneteket elküldés előtt lefordítják.'; @override String get translation_composerDisabledHint => - 'Küldj üzeneteket az eredeti, nyomtatott nyelven.'; + 'Üzeneteket küldhet az eredeti gépelt nyelven.'; @override String translation_translateTo(String language) { - return 'Fordítás $language-ra'; + return 'Fordítás $language'; } @override String get translation_translationOptions => 'Fordítási lehetőségek'; @override - String get translation_systemLanguage => 'Rendszer nyelvé'; + String get translation_systemLanguage => 'Rendszer nyelve'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore fut'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'A BLE-kapcsolat fenntartása'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return 'Törölve $name'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return 'Nem sikerült törölni: $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'Nem sikerült frissíteni a csatornát: $error'; } @override - String get contact_typeChat => 'Chat'; + String get contact_typeChat => 'Csevegés'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => 'Ismétlő'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => 'Szoba'; @override - String get contact_typeSensor => 'Sensor'; + String get contact_typeSensor => 'Érzékelő'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => 'Ismeretlen'; @override - String get map_zoomIn => 'Nagyítva'; + String get map_zoomIn => 'Nagyítás'; @override - String get map_zoomOut => 'Kicsökkentett nézet'; + String get map_zoomOut => 'Kicsinyítés'; @override - String get map_centerMap => 'Központi tér térkép'; + String get map_centerMap => 'Középső térkép'; @override String get chrome_bluetoothRequiresChromium => - 'A Web Bluetooth-hoz egy Chromium-alapú böngésző szükséges.'; + 'A Web Bluetooth használatához Chromium böngésző szükséges'; @override String channels_communityShortId(String id) { - return 'Az azonosító: $id...'; + return 'ID: $id...'; } @override - String get pathTrace_legendGpsConfirmed => 'GPS-en megerősítve'; + String get pathTrace_legendGpsConfirmed => 'GPS megerősítve'; @override - String get pathTrace_legendInferred => 'Feltehető helyzet'; + String get pathTrace_legendInferred => 'Kikövetkeztetett pozíció'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Egyetlen'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Kombinált'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Játék'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Szünet'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Visszajátszás'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Előző ugrás'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Következő ugrás'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Csomaganimáció megjelenítése'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Csomaganimáció elrejtése'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return '$current ugrás / $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Megfigyelt útvonalak: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Elsődleges'; @override String pathMap_alternate(int index) { @@ -4588,8 +4563,8 @@ class AppLocalizationsHu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count ugrás', + one: '1 ugrás', ); return '$_temp0'; } @@ -4600,14 +4575,14 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Megosztott szegmens'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Becsült szegmens'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return '$count elérési út használja'; } @override @@ -4615,33 +4590,34 @@ class AppLocalizationsHu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count ugrásnak nincs helyadata — a megjelenített útvonal részleges', + one: '1 ugrásnak nincs helyadata — a megjelenített útvonal részleges', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Összes megjelenítése'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Útvonal elrejtése'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Útvonal megjelenítése'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Panel összecsukása'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Panel kibontása'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Nincs hely'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Nézet zárolása a csomaghoz'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'A nézet feloldása a csomagból'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index de44f72f..fc18841a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -24,7 +24,7 @@ class AppLocalizationsIt extends AppLocalizations { String get common_cancel => 'Annulla'; @override - String get common_ok => 'Va bene'; + String get common_ok => 'OK'; @override String get common_connect => 'Connetti'; @@ -45,7 +45,7 @@ class AppLocalizationsIt extends AppLocalizations { String get common_close => 'Chiudi'; @override - String get common_done => 'Done'; + String get common_done => 'Fatto'; @override String get common_edit => 'Modifica'; @@ -90,13 +90,13 @@ class AppLocalizationsIt extends AppLocalizations { String get common_enable => 'Abilita'; @override - String get common_disable => 'Disattivare'; + String get common_disable => 'Disattiva'; @override String get common_undo => 'Annulla'; @override - String get messageStatus_sent => 'Invia'; + String get messageStatus_sent => 'Inviato'; @override String get messageStatus_delivered => 'Consegnato'; @@ -108,7 +108,7 @@ class AppLocalizationsIt extends AppLocalizations { String get messageStatus_failed => 'Impossibile inviare'; @override - String get messageStatus_repeated => 'Sentito ripetutamente'; + String get messageStatus_repeated => 'Ricevuto ripetutamente'; @override String get common_reboot => 'Riavvia'; @@ -299,7 +299,7 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get scanner_stop => 'Interrompere'; + String get scanner_stop => 'Ferma'; @override String get scanner_scan => 'Scansiona'; @@ -323,10 +323,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'La funzionalità Bluetooth non è disponibile nel browser. Connettetevi tramite USB invece.'; @override - String get device_quickSwitch => 'Passa velocemente'; + String get device_quickSwitch => 'Passa rapidamente'; @override String get device_meshcore => 'MeshCore'; @@ -335,7 +335,7 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_title => 'Impostazioni'; @override - String get settings_deviceInfo => 'Informazioni Dispositivo'; + String get settings_deviceInfo => 'Informazioni dispositivo'; @override String get settings_appSettings => 'Impostazioni App'; @@ -364,7 +364,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_radioSettingsSubtitle => - 'Frequenza, potenza, fattore di dispersione'; + 'Frequenza, potenza, fattore di diffusione'; @override String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate'; @@ -373,17 +373,18 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_location => 'Posizione'; @override - String get settings_locationSubtitle => 'coordinate GPS'; + String get settings_locationSubtitle => 'Coordinate GPS'; @override - String get settings_locationUpdated => 'Posizione aggiornata'; + String get settings_locationUpdated => + 'Posizione e impostazioni GPS aggiornate'; @override String get settings_locationBothRequired => 'Inserire sia la latitudine che la longitudine.'; @override - String get settings_locationInvalid => 'Latitudine o longitudine non valida.'; + String get settings_locationInvalid => 'Latitudine o longitudine non valide.'; @override String get settings_locationGPSEnable => 'Abilita GPS'; @@ -477,44 +478,43 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_actions => 'Azioni'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'Elimina tutti i percorsi'; @override String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + 'Cancella tutti i dati di percorso dai contatti.'; @override - String get settings_sendAdvertisement => 'Invia Annuncio'; + String get settings_sendAdvertisement => 'Invia annuncio'; @override - String get settings_sendAdvertisementSubtitle => 'Presenza trasmessa ora'; + String get settings_sendAdvertisementSubtitle => 'Trasmetti ora la presenza'; @override String get settings_advertisementSent => 'Annuncio inviato'; @override - String get settings_syncTime => 'Tempo di sincronizzazione'; + String get settings_syncTime => 'Sincronizza ora'; @override String get settings_syncTimeSubtitle => 'Imposta l\'orologio del dispositivo sull\'ora del telefono'; @override - String get settings_timeSynchronized => 'Sincronizzato nel tempo'; + String get settings_timeSynchronized => 'Ora sincronizzata'; @override - String get settings_refreshContacts => 'Aggiorna Contatti'; + String get settings_refreshContacts => 'Aggiorna contatti'; @override String get settings_refreshContactsSubtitle => - 'Ricaricare l\'elenco dei contatti dal dispositivo'; + 'Ricarica l\'elenco dei contatti dal dispositivo'; @override - String get settings_rebootDevice => 'Riavvia Dispositivo'; + String get settings_rebootDevice => 'Riavvia dispositivo'; @override - String get settings_rebootDeviceSubtitle => - 'Riavviare il dispositivo MeshCore'; + String get settings_rebootDeviceSubtitle => 'Riavvia il dispositivo MeshCore'; @override String get settings_rebootDeviceConfirm => @@ -542,15 +542,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String settings_aboutVersion(String version) { - return 'MeshCore Open versione $version'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => 'Progetto Open Source MeshCore 2024'; + String get settings_aboutLegalese => '2026 Progetto open source MeshCore'; @override String get settings_aboutDescription => - 'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.'; + 'Un client Flutter open source per i dispositivi MeshCore di rete mesh LoRa.'; @override String get settings_aboutOpenMeteoAttribution => @@ -572,10 +572,10 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_infoPublicKey => 'Chiave Pubblica'; @override - String get settings_infoContactsCount => 'Numero contatti'; + String get settings_infoContactsCount => 'Numero di contatti'; @override - String get settings_infoChannelCount => 'Numero Canale'; + String get settings_infoChannelCount => 'Numero di canali'; @override String get settings_presets => 'Preset'; @@ -593,19 +593,19 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_bandwidth => 'Larghezza di banda'; @override - String get settings_spreadingFactor => 'Fattore di Spettro'; + String get settings_spreadingFactor => 'Fattore di diffusione'; @override String get settings_codingRate => 'Tasso di Codifica'; @override - String get settings_txPower => 'TX Potenza (dBm)'; + String get settings_txPower => 'Potenza TX (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)'; + String get settings_txPowerInvalid => 'Potenza TX non valida (0-22 dBm)'; @override String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"'; @@ -633,10 +633,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_theme => 'Tema'; @override - String get appSettings_themeSystem => 'Impostazione predefinita del sistema'; + String get appSettings_themeSystem => 'Predefinito del sistema'; @override - String get appSettings_themeLight => 'Luce'; + String get appSettings_themeLight => 'Chiaro'; @override String get appSettings_themeDark => 'Scuro'; @@ -645,7 +645,7 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_language => 'Lingua'; @override - String get appSettings_languageSystem => 'Predefinito di sistema'; + String get appSettings_languageSystem => 'Predefinito del sistema'; @override String get appSettings_languageEn => 'Inglese'; @@ -748,7 +748,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetry => - 'Cancella Percorso su Massimo Riprovo'; + 'Cancella percorso al massimo dei tentativi'; @override String get appSettings_clearPathOnMaxRetrySubtitle => @@ -756,18 +756,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_pathsWillBeCleared => - 'I percorsi verranno puliti dopo 5 tentativi falliti.'; + 'I percorsi verranno cancellati dopo 5 tentativi falliti.'; @override String get appSettings_pathsWillNotBeCleared => - 'I percorsi non verranno eliminati automaticamente.'; + 'I percorsi non verranno cancellati automaticamente.'; @override String get appSettings_autoRouteRotation => 'Rotazione Percorso Automatico'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alterna tra i percorsi migliori e la modalità alluvione'; + 'Alterna tra i percorsi migliori e la modalità flood'; @override String get appSettings_autoRouteRotationEnabled => @@ -904,7 +904,7 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_unitsTitle => 'Unità'; @override - String get appSettings_unitsMetric => 'Metrico (m/km)'; + String get appSettings_unitsMetric => 'Metrico (m / km)'; @override String get appSettings_unitsImperial => 'Imperiale (ft / mi)'; @@ -925,15 +925,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_appDebugLoggingSubtitle => - 'Messaggi di debug dell\'app Log per la risoluzione dei problemi'; + 'Registra i messaggi di debug dell\'app per la risoluzione dei problemi'; @override String get appSettings_appDebugLoggingEnabled => - 'Logging di debug dell\'app abilitato'; + 'Registrazione di debug dell\'app abilitata'; @override String get appSettings_appDebugLoggingDisabled => - 'Logging del debug dell\'app disabilitato'; + 'Registrazione di debug dell\'app disabilitata'; @override String get contacts_title => 'Contatti'; @@ -943,13 +943,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_contactsWillAppear => - 'I contatti appariranno quando i dispositivi pubblicizzano.'; + 'I contatti appariranno quando i dispositivi si annunciano.'; @override String get contacts_unread => 'Non letti'; @override - String get contacts_searchContactsNoNumber => 'Cerca Contatti...'; + String get contacts_searchContactsNoNumber => 'Cerca contatti...'; @override String contacts_searchContacts(int number, String str) { @@ -958,22 +958,22 @@ class AppLocalizationsIt extends AppLocalizations { @override String contacts_searchFavorites(int number, String str) { - return 'Cerca $number$str Preferiti...'; + return 'Cerca $number$str preferiti...'; } @override String contacts_searchUsers(int number, String str) { - return 'Cerca $number$str Utenti...'; + return 'Cerca $number$str utenti...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Cerca $number$str Ripetitori...'; + return 'Cerca $number$str ripetitori...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Cerca $number$str server Room...'; + return 'Cerca $number$str server stanza...'; } @override @@ -991,22 +991,22 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Gestisci Ripetitore'; + String get contacts_manageRepeater => 'Gestisci ripetitore'; @override - String get contacts_manageRoom => 'Gestisci Server Camera'; + String get contacts_manageRoom => 'Gestisci server stanza'; @override - String get contacts_roomLogin => 'Login Camera'; + String get contacts_roomLogin => 'Accesso server stanza'; @override - String get contacts_openChat => 'Apri Chat'; + String get contacts_openChat => 'Apri chat'; @override - String get contacts_editGroup => 'Modifica Gruppo'; + String get contacts_editGroup => 'Modifica gruppo'; @override - String get contacts_deleteGroup => 'Elimina Gruppo'; + String get contacts_deleteGroup => 'Elimina gruppo'; @override String contacts_deleteGroupConfirm(String groupName) { @@ -1014,16 +1014,16 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get contacts_newGroup => 'Nuovo Gruppo'; + String get contacts_newGroup => 'Nuovo gruppo'; @override String get contacts_moreOptions => 'Ulteriori opzioni'; @override - String get contacts_searchOpen => 'Cerca contatti'; + String get contacts_searchOpen => 'Apri ricerca'; @override - String get contacts_searchClose => 'Ricerca avanzata'; + String get contacts_searchClose => 'Chiudi ricerca'; @override String get contacts_groupName => 'Nome gruppo'; @@ -1050,7 +1050,7 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_noMembers => 'Nessun membro'; @override - String get contacts_lastSeenNow => 'Ultimo avvistamento ora'; + String get contacts_lastSeenNow => 'Poco fa'; @override String contacts_lastSeenMinsAgo(int minutes) { @@ -1134,7 +1134,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channels_via(String path) { - return 'via $path'; + return 'tramite $path'; } @override @@ -1168,16 +1168,16 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get channels_addChannel => 'Aggiungi Canale'; + String get channels_addChannel => 'Aggiungi canale'; @override - String get channels_channelIndexLabel => 'Indice Canale'; + String get channels_channelIndexLabel => 'Indice canale'; @override String get channels_channelName => 'Nome canale'; @override - String get channels_usePublicChannel => 'Utilizza il canale pubblico'; + String get channels_usePublicChannel => 'Usa il canale pubblico'; @override String get channels_standardPublicPsk => 'PSK pubblico standard'; @@ -1186,8 +1186,7 @@ class AppLocalizationsIt extends AppLocalizations { String get channels_pskHex => 'PSK (esadecimale)'; @override - String get channels_generateRandomPsk => - 'Genera una chiave di permutazione casuale'; + String get channels_generateRandomPsk => 'Genera una PSK casuale'; @override String get channels_enterChannelName => 'Inserisci un nome per il canale'; @@ -1281,7 +1280,7 @@ class AppLocalizationsIt extends AppLocalizations { String get channels_sortManual => 'Manuale'; @override - String get channels_sortAZ => 'D-Z'; + String get channels_sortAZ => 'A-Z'; @override String get channels_sortLatestMessages => 'Ultimi messaggi'; @@ -1475,7 +1474,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String debugFrame_destinationPubKey(String pubKey) { - return '- Destinazione PubChiave: $pubKey'; + return '- Chiave pubblica di destinazione: $pubKey'; } @override @@ -1527,7 +1526,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_pathCleared => - 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; + 'Percorso cancellato. Il prossimo messaggio riscoprirà il percorso.'; @override String get chat_fullPath => 'Percorso Completo'; @@ -1536,78 +1535,77 @@ class AppLocalizationsIt extends AppLocalizations { String get routing_title => 'Instradamento'; @override - String get routing_modeAuto => 'Auto'; + String get routing_modeAuto => 'Automatico'; @override - String get routing_modeFlood => 'Inondazione'; + String get routing_modeFlood => 'Flood'; @override String get routing_modeManual => 'Manuale'; @override String get routing_modeAutoHint => - 'Seleziona automaticamente il percorso più noto, e in caso di assenza di informazioni, utilizza un percorso casuale.'; + 'Seleziona automaticamente il percorso migliore noto, ricorrendo al flood quando non ne è noto nessuno.'; @override String get routing_modeFloodHint => - 'Trasmissioni tramite ogni ripetitore. Il metodo più affidabile, ma richiede più tempo di trasmissione.'; + 'Trasmette tramite ogni ripetitore. È il più affidabile, ma usa più airtime.'; @override String get routing_modeManualHint => - 'Invia sempre esattamente il percorso che hai definito.'; + 'Invia sempre lungo il percorso esatto che hai definito.'; @override String get routing_currentRoute => 'Percorso attuale'; @override String get routing_directNoHops => - 'Diretto — senza passaggi tramite ripetitori'; + 'Diretto — nessun salto tramite ripetitore'; @override String get routing_noPathYet => - 'Al momento non è stata individuata alcuna via. Il messaggio viene inviato ripetutamente finché non viene trovata una rotta.'; + 'Nessun percorso ancora. Il prossimo messaggio userà il flood finché non viene scoperto un percorso.'; @override - String get routing_floodBroadcast => - 'Trasmissione attraverso ogni ripetitore'; + String get routing_floodBroadcast => 'Trasmissione tramite ogni ripetitore'; @override - String get routing_editPath => 'Percorso di modifica'; + String get routing_editPath => 'Modifica percorso'; @override - String get routing_forgetPath => 'Dimentica il percorso'; + String get routing_forgetPath => 'Dimentica percorso'; @override String get routing_knownPaths => 'Percorsi noti'; @override String get routing_knownPathsHint => - 'Seleziona un percorso per accedere a questa opzione.'; + 'Tocca un percorso per passare a quello.'; @override String get routing_inUse => 'In uso'; @override - String get routing_qualityStrong => 'Primo salto molto deciso'; + String get routing_qualityStrong => 'Primo hop forte'; @override - String get routing_qualityGood => 'Primo tentativo di successo'; + String get routing_qualityGood => 'Primo hop buono'; @override - String get routing_qualityFair => 'Primo salto di qualità'; + String get routing_qualityFair => 'Primo hop discreto'; @override - String get routing_qualityWorked => 'È stato consegnato'; + String get routing_qualityWorked => 'Ha consegnato'; @override - String get routing_qualityFlood => 'Ho sentito tramite un messaggio urgente'; + String get routing_qualityFlood => 'Ricevuto via flood'; @override String get routing_qualityUntested => 'Non testato'; @override String routing_lastWorked(String when) { - return 'worked $when'; + return 'ha funzionato $when'; } @override @@ -1615,30 +1613,29 @@ class AppLocalizationsIt extends AppLocalizations { @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes consegnati, $failures falliti'; } @override - String get routing_floodDelivery => 'Consegna in caso di alluvione'; + String get routing_floodDelivery => 'Consegna flood'; @override - String get pathEditor_title => 'Creare percorso'; + String get pathEditor_title => 'Crea percorso'; @override String pathEditor_hopCounter(int count) { - return '$count tra 64 varietà di luppolo'; + return '$count su 64 salti'; } @override String get pathEditor_noHops => - 'Al momento non ci sono ingredienti aggiuntivi. Per aggiungerli nell\'ordine desiderato, cliccate sui ripetitori sottostanti. In alternativa, potete salvare la ricetta senza ingredienti aggiuntivi per inviarla direttamente.'; + 'Nessun salto ancora. Tocca i ripetitori qui sotto per aggiungerli in ordine, oppure salva senza salti per inviare direttamente.'; @override - String get pathEditor_addHops => - 'Aggiungere i luppoli nell\'ordine desiderato.'; + String get pathEditor_addHops => 'Aggiungi salti in ordine'; @override - String get pathEditor_searchRepeaters => 'Ricerca ripetitori'; + String get pathEditor_searchRepeaters => 'Cerca ripetitori'; @override String get pathEditor_advancedHex => 'Avanzato: percorso esadecimale grezzo'; @@ -1656,20 +1653,20 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get pathEditor_tooManyHops => 'Massimo 64 orari'; + String get pathEditor_tooManyHops => 'Massimo 64 salti'; @override String get pathEditor_usePath => 'Utilizza questo percorso'; @override - String get pathEditor_removeHop => 'Rimuovere il luppolo'; + String get pathEditor_removeHop => 'Rimuovi salto'; @override String get pathEditor_unknownHop => 'Ripetitore sconosciuto'; @override String get chat_pathSavedLocally => - 'Salvatato localmente. Connetti per sincronizzare.'; + 'Salvato localmente. Connettiti per sincronizzare.'; @override String get chat_pathDeviceConfirmed => 'Dispositivo confermato.'; @@ -1679,22 +1676,22 @@ class AppLocalizationsIt extends AppLocalizations { 'Dispositivo non confermato ancora.'; @override - String get chat_type => 'Digita'; + String get chat_type => 'Tipo'; @override String get chat_path => 'Percorso'; @override - String get chat_publicKey => 'Chiave Pubblica'; + String get chat_publicKey => 'Chiave pubblica'; @override String get chat_compressOutgoingMessages => 'Comprimi messaggi in uscita'; @override - String get chat_floodForced => 'Inondazione (forzata)'; + String get chat_floodForced => 'Flood (forzato)'; @override - String get chat_directForced => 'Riavvia (forzato)'; + String get chat_directForced => 'Diretto (forzato)'; @override String chat_hopsForced(int count) { @@ -1702,13 +1699,13 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get chat_floodAuto => 'Inondazione (auto)'; + String get chat_floodAuto => 'Flood (automatico)'; @override - String get chat_direct => 'Salva'; + String get chat_direct => 'Diretto'; @override - String get chat_poiShared => 'Punti di Interesse Condivisi'; + String get chat_poiShared => 'POI condiviso'; @override String chat_unread(int count) { @@ -1722,7 +1719,7 @@ class AppLocalizationsIt extends AppLocalizations { String get chat_newMessages => 'Nuovi messaggi'; @override - String get chat_openLink => 'Aprire il link?'; + String get chat_openLink => 'Apri il link?'; @override String get chat_openLinkConfirmation => @@ -1740,40 +1737,40 @@ class AppLocalizationsIt extends AppLocalizations { String get chat_invalidLink => 'Formato di link non valido'; @override - String get map_title => 'Mappa Nodi'; + String get map_title => 'Mappa nodi'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Cerca il nome o l\'ID del nodo'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Attività'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Recente'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Obsoleto'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Visibile'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Nascosto'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Centra sul nodo'; @override - String get map_details => 'Details'; + String get map_details => 'Dettagli'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Senza GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Nessun nodo corrispondente trovato.'; @override String get map_lineOfSight => 'Linea di vista'; @@ -1795,7 +1792,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Puntatori: $count'; + return 'Pin: $count'; } @override @@ -1811,10 +1808,10 @@ class AppLocalizationsIt extends AppLocalizations { String get map_sensor => 'Sensore'; @override - String get map_pinDm => 'Codice PIN (DM)'; + String get map_pinDm => 'Pin (DM)'; @override - String get map_pinPrivate => 'Blocco (Privato)'; + String get map_pinPrivate => 'Pin (Privato)'; @override String get map_pinPublic => 'Pin (Pubblico)'; @@ -1836,31 +1833,31 @@ class AppLocalizationsIt extends AppLocalizations { String get map_flags => 'Bandiere'; @override - String get map_type => 'Type'; + String get map_type => 'Tipo'; @override - String get map_path => 'Path'; + String get map_path => 'Percorso'; @override - String get map_location => 'Location'; + String get map_location => 'Posizione'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => 'Posizione stimata'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => 'Chiave pubblica'; @override - String get map_publicKeyPrefixHint => 'e.g. ab12'; + String get map_publicKeyPrefixHint => 'es. ab12'; @override - String get map_shareMarkerHere => 'Condividi marcatore qui'; + String get map_shareMarkerHere => 'Condividi segnaposto qui'; @override String get map_setAsMyLocation => 'Imposta come la mia posizione'; @override - String get map_pinLabel => 'Etichetta PIN'; + String get map_pinLabel => 'Etichetta pin'; @override String get map_label => 'Etichetta'; @@ -1878,7 +1875,7 @@ class AppLocalizationsIt extends AppLocalizations { String get map_noChannelsAvailable => 'Nessun canale disponibile'; @override - String get map_publicLocationShare => 'Condividi in una posizione pubblica'; + String get map_publicLocationShare => 'Condivisione pubblica della posizione'; @override String map_publicLocationShareConfirm(String channelLabel) { @@ -1887,28 +1884,28 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_connectToShareMarkers => - 'Connetti a un dispositivo per condividere i segnaposti'; + 'Connettiti a un dispositivo per condividere i segnaposti'; @override - String get map_filterNodes => 'Filtra Nodi'; + String get map_filterNodes => 'Filtra nodi'; @override - String get map_nodeTypes => 'Tipi di Nodo'; + String get map_nodeTypes => 'Tipi di nodo'; @override - String get map_chatNodes => 'Nodi di Chat'; + String get map_chatNodes => 'Nodi chat'; @override String get map_repeaters => 'Ripetitori'; @override - String get map_otherNodes => 'Altri Nodi'; + String get map_otherNodes => 'Altri nodi'; @override - String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore'; + String get map_showOverlaps => 'Sovrapposizioni chiave ripetitore'; @override - String get map_keyPrefix => 'Prefisso Chiave'; + String get map_keyPrefix => 'Prefisso chiave'; @override String get map_filterByKeyPrefix => 'Filtra per prefisso chiave'; @@ -1917,25 +1914,25 @@ class AppLocalizationsIt extends AppLocalizations { String get map_publicKeyPrefix => 'Prefisso chiave pubblica'; @override - String get map_markers => 'Segnaposto'; + String get map_markers => 'Segnaposti'; @override - String get map_showSharedMarkers => 'Mostra i segnaposto condivisi'; + String get map_showSharedMarkers => 'Mostra segnaposti condivisi'; @override String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; @override - String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery'; + String get map_showDiscoveryContacts => 'Mostra contatti di scoperta'; @override - String get map_guessedLocation => 'Località indovinata'; + String get map_guessedLocation => 'Posizione stimata'; @override - String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione'; + String get map_lastSeenTime => 'Ora dell\'ultimo visto'; @override - String get map_sharedPin => 'Condividi PIN'; + String get map_sharedPin => 'Pin condiviso'; @override String get map_sharedAt => 'Condiviso'; @@ -1944,17 +1941,16 @@ class AppLocalizationsIt extends AppLocalizations { String get map_joinRoom => 'Unisciti alla stanza'; @override - String get map_manageRepeater => 'Gestisci Ripetitore'; + String get map_manageRepeater => 'Gestisci ripetitore'; @override String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.'; @override - String get map_runTrace => 'Esegui Path Trace'; + String get map_runTrace => 'Esegui tracciamento percorso'; @override - String get map_runTraceWithReturnPath => - 'Tornare indietro sullo stesso percorso'; + String get map_runTraceWithReturnPath => 'Ritorna lungo lo stesso percorso.'; @override String get map_removeLast => 'Rimuovi ultimo'; @@ -1963,7 +1959,7 @@ class AppLocalizationsIt extends AppLocalizations { String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.'; @override - String get mapCache_title => 'Cache Mappa Offline'; + String get mapCache_title => 'Cache mappa offline'; @override String get mapCache_selectAreaFirst => @@ -1974,7 +1970,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Nessun tile da scaricare per questa area'; @override - String get mapCache_downloadTilesTitle => 'Scarica mattoncini'; + String get mapCache_downloadTilesTitle => 'Scarica tessere'; @override String mapCache_downloadTilesPrompt(int count) { @@ -1986,12 +1982,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'Cacheggiate $count tile'; + return 'Tessere memorizzate nella cache: $count'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Tile memorizzati $downloaded ($failed falliti)'; + return 'Tessere memorizzate nella cache $downloaded ($failed fallite)'; } @override @@ -2008,17 +2004,17 @@ class AppLocalizationsIt extends AppLocalizations { String get mapCache_noAreaSelected => 'Nessun\'area selezionata'; @override - String get mapCache_cacheArea => 'Area Cache'; + String get mapCache_cacheArea => 'Area cache'; @override String get mapCache_useCurrentView => 'Utilizza la visualizzazione corrente'; @override - String get mapCache_zoomRange => 'Intervallo Zoom'; + String get mapCache_zoomRange => 'Intervallo zoom'; @override String mapCache_estimatedTiles(int count) { - return 'Stima dei mattoni: $count'; + return 'Tessere stimate: $count'; } @override @@ -2027,10 +2023,10 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get mapCache_downloadTilesButton => 'Scarica Tessere'; + String get mapCache_downloadTilesButton => 'Scarica tessere'; @override - String get mapCache_clearCacheButton => 'Svuota Cache'; + String get mapCache_clearCacheButton => 'Svuota cache'; @override String mapCache_failedDownloads(int count) { @@ -2093,7 +2089,7 @@ class AppLocalizationsIt extends AppLocalizations { String get time_minutes => 'minuti'; @override - String get time_allTime => 'Tutto il Tempo'; + String get time_allTime => 'Sempre'; @override String get dialog_disconnect => 'Disconnetti'; @@ -2103,10 +2099,10 @@ class AppLocalizationsIt extends AppLocalizations { 'Sei sicuro di voler disconnetterti da questo dispositivo?'; @override - String get login_repeaterLogin => 'Login Ripetitore'; + String get login_repeaterLogin => 'Accesso ripetitore'; @override - String get login_roomLogin => 'Login Camera'; + String get login_roomLogin => 'Accesso server stanza'; @override String get login_password => 'Password'; @@ -2139,10 +2135,10 @@ class AppLocalizationsIt extends AppLocalizations { String get login_autoUseSavedPath => 'Utilizza il percorso salvato'; @override - String get login_forceFloodMode => 'Modalità Inondamento Forzato'; + String get login_forceFloodMode => 'Modalità flood forzata'; @override - String get login_managePaths => 'Gestisci Percorsi'; + String get login_managePaths => 'Gestisci percorsi'; @override String get login_login => 'Accedi'; @@ -2162,7 +2158,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.'; @override - String get common_reload => 'Ricaricare'; + String get common_reload => 'Ricarica'; @override String get common_clear => 'Cancella'; @@ -2172,10 +2168,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get path_noRepeatersFound => - 'Non sono stati trovati ripetitori o server di stanza.'; + 'Non sono stati trovati ripetitori o server stanza.'; @override - String get repeater_management => 'Gestione Ripetitori'; + String get repeater_management => 'Gestione ripetitori'; @override String get room_management => 'Gestione del Server di Camera'; @@ -2187,7 +2183,7 @@ class AppLocalizationsIt extends AppLocalizations { String get room_guest => 'Informazioni sul server'; @override - String get repeater_managementTools => 'Strumenti di Gestione'; + String get repeater_managementTools => 'Strumenti di gestione'; @override String get repeater_guestTools => 'Strumenti per gli ospiti'; @@ -2216,8 +2212,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_neighbors => 'Vicini'; @override - String get repeater_neighborsSubtitle => - 'Visualizza vicini di salto pari a zero.'; + String get repeater_neighborsSubtitle => 'Visualizza vicini a salto zero.'; @override String get repeater_settings => 'Impostazioni'; @@ -2235,7 +2230,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.'; @override - String get repeater_statusTitle => 'Stato del Ripetitore'; + String get repeater_statusTitle => 'Stato del ripetitore'; @override String get repeater_routingMode => 'Modalità di routing'; @@ -2261,16 +2256,16 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_clockAtLogin => 'Orologio (all\'accesso)'; @override - String get repeater_uptime => 'Disponibilità'; + String get repeater_uptime => 'Tempo di attività'; @override - String get repeater_queueLength => 'Lunghezza della coda'; + String get repeater_queueLength => 'Lunghezza coda'; @override - String get repeater_debugFlags => 'Impostazioni Debug'; + String get repeater_debugFlags => 'Flag di debug'; @override - String get repeater_radioStatistics => 'Statistiche Radio'; + String get repeater_radioStatistics => 'Statistiche radio'; @override String get repeater_lastRssi => 'Ultimo RSSI'; @@ -2279,19 +2274,19 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_lastSnr => 'Ultimo SNR'; @override - String get repeater_noiseFloor => 'Livello del Rumore'; + String get repeater_noiseFloor => 'Rumore di fondo'; @override - String get repeater_txAirtime => 'TX Airtime'; + String get repeater_txAirtime => 'Tempo d\'aria TX'; @override - String get repeater_rxAirtime => 'RX Airtime'; + String get repeater_rxAirtime => 'Tempo d\'aria RX'; @override String get repeater_chanUtil => 'Utilizzo del canale'; @override - String get repeater_packetStatistics => 'Statistiche del Pacchetto'; + String get repeater_packetStatistics => 'Statistiche pacchetti'; @override String get repeater_sent => 'Inviato'; @@ -2314,17 +2309,17 @@ class AppLocalizationsIt extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Totale: $total, Inondazione: $flood, Diretto: $direct'; + return 'Totale: $total, Flood: $flood, Diretto: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Totale: $total, Inondazione: $flood, Diretto: $direct'; + return 'Totale: $total, Flood: $flood, Diretto: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Inondazione: $flood, Diretto: $direct'; + return 'Flood: $flood, Diretto: $direct'; } @override @@ -2343,20 +2338,20 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_repeaterNameHelper => - 'Visualizza il nome di questo ripetitore'; + 'Nome visualizzato per questo ripetitore'; @override - String get repeater_adminPassword => 'Password Amministratore'; + String get repeater_adminPassword => 'Password amministratore'; @override - String get repeater_adminPasswordHelper => 'Accesso completo password'; + String get repeater_adminPasswordHelper => 'Password per accesso completo'; @override - String get repeater_guestPassword => 'Password Ospite'; + String get repeater_guestPassword => 'Password ospite'; @override String get repeater_guestPasswordHelper => - 'Accesso in sola lettura con password'; + 'Password per accesso in sola lettura'; @override String get repeater_radioSettings => 'Impostazioni Radio'; @@ -2368,7 +2363,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX Potenza'; + String get repeater_txPower => 'Potenza TX'; @override String get repeater_txPowerHelper => '1-30 dBm'; @@ -2377,13 +2372,13 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_bandwidth => 'Larghezza di banda'; @override - String get repeater_spreadingFactor => 'Fattore di propagazione'; + String get repeater_spreadingFactor => 'Fattore di diffusione'; @override String get repeater_codingRate => 'Tasso di Codifica'; @override - String get repeater_locationSettings => 'Impostazioni Luogo'; + String get repeater_locationSettings => 'Impostazioni posizione'; @override String get repeater_latitude => 'Latitudine'; @@ -2402,14 +2397,14 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_features => 'Caratteristiche'; @override - String get repeater_packetForwarding => 'Instradamento Pacchetti'; + String get repeater_packetForwarding => 'Inoltro pacchetti'; @override String get repeater_packetForwardingSubtitle => 'Abilita il ripetitore per inoltrare i pacchetti'; @override - String get repeater_guestAccess => 'Accesso Ospite'; + String get repeater_guestAccess => 'Accesso ospite'; @override String get repeater_guestAccessSubtitle => @@ -2420,13 +2415,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Nascondere nome/luogo negli annunci'; + 'Nascondi nome/posizione negli annunci'; @override - String get repeater_advertisementSettings => 'Impostazioni Annuncio'; + String get repeater_advertisementSettings => 'Impostazioni annuncio'; @override - String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale'; + String get repeater_localAdvertInterval => 'Intervallo annuncio locale'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2434,8 +2429,7 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => - 'Intervallo Pubblicità Inondazione'; + String get repeater_floodAdvertInterval => 'Intervallo annuncio flood'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2444,7 +2438,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervallo Pubblicitario Crittografato'; + 'Intervallo annuncio crittografato'; @override String get repeater_dangerZone => 'Zona Pericolosa'; @@ -2697,7 +2691,7 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get repeater_cliTitle => 'Riprova CLI'; + String get repeater_cliTitle => 'CLI del ripetitore'; @override String get repeater_debugNextCommand => 'Riavvia Comando Prossimo'; @@ -2751,16 +2745,16 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_cliQuickVersion => 'Versione'; @override - String get repeater_cliQuickAdvertise => 'Pubblicare'; + String get repeater_cliQuickAdvertise => 'Annuncia'; @override String get repeater_cliQuickClock => 'Orologio'; @override - String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio'; + String get repeater_cliQuickClockSync => 'Sincronizza orologio'; @override - String get repeater_cliQuickDiscovery => 'Scopri i Vicini'; + String get repeater_cliQuickDiscovery => 'Scopri vicini'; @override String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario'; @@ -2799,7 +2793,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetAllowReadOnly => - '(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; + '(Server stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; @override String get repeater_cliHelpSetFloodMax => @@ -2850,14 +2844,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetTxDelay => - 'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).'; + 'Imposta un fattore moltiplicativo sul tempo di trasmissione dei pacchetti in modalità flood e su un sistema a slot casuali, per ritardarne l\'inoltro (riducendo la probabilità di collisioni).'; @override String get repeater_cliHelpSetDirectTxDelay => 'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.'; + String get repeater_cliHelpSetBridgeEnabled => + 'Abilita/disabilita il bridge.'; @override String get repeater_cliHelpSetBridgeDelay => @@ -2865,7 +2860,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.'; + 'Scegli se il bridge ritrasmette i pacchetti ricevuti o trasmessi.'; @override String get repeater_cliHelpSetBridgeBaud => @@ -2889,7 +2884,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpGetBridgeType => - 'Ottiene tipo ponte nessuno, rs232, espnow'; + 'Mostra il tipo di bridge: nessuno, rs232, espnow'; @override String get repeater_cliHelpLogStart => @@ -2933,15 +2928,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpRegionAllowf => - 'Imposta il permesso di \'F\'lood per la regione specificata. (\'\' per lo scope globale/legacy)'; + 'Imposta il permesso \'F\'lood per la regione specificata. (\'*\' per lo scope globale/legacy)'; @override String get repeater_cliHelpRegionDenyf => - 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).'; + 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: al momento non è consigliato usarlo sullo scope globale/legacy!!).'; @override String get repeater_cliHelpRegionHome => - 'Risposte con la regione \'home\' corrente. (Nota applicata finora, riservata per il futuro)'; + 'Risponde con la regione \'home\' corrente. (Nota non ancora applicata, riservata per il futuro)'; @override String get repeater_cliHelpRegionHomeSet => 'Imposta la regione \'home\'.'; @@ -3156,7 +3151,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpGetFloodAdvertInterval => - 'Indica l\'intervallo pubblicitario per la trasmissione del servizio di allerta alluvioni, espresso in ore.'; + 'Indica l\'intervallo dell\'annuncio flood, espresso in ore.'; @override String get repeater_cliHelpGetGuestPassword => @@ -3173,7 +3168,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpGetTxDelay => - 'Mostra il fattore di ritardo in modalità di allarme.'; + 'Mostra il fattore txdelay in modalità flood.'; @override String get repeater_cliHelpGetDirectTxDelay => @@ -3181,7 +3176,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpGetFloodMax => - 'Mostra il numero massimo di salti dovuto all\'inondazione.'; + 'Mostra il numero massimo di salti del flood.'; @override String get repeater_cliHelpGetOwnerInfo => @@ -3268,11 +3263,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpRegionListAllowed => - 'Elenca le regioni che consentono il transito di veicoli in caso di allagamenti.'; + 'Elenca le regioni che consentono il traffico flood.'; @override String get repeater_cliHelpRegionListDenied => - 'Elenca le regioni che vietano il transito in caso di alluvioni.'; + 'Elenca le regioni che negano il traffico flood.'; @override String get repeater_cliHelpStatsPackets => @@ -3474,10 +3469,10 @@ class AppLocalizationsIt extends AppLocalizations { String get telemetry_error => 'Impossibile recuperare i dati'; @override - String get neighbors_receivedData => 'Ricevute dati vicini'; + String get neighbors_receivedData => 'Dati dei vicini ricevuti'; @override - String get neighbors_requestTimedOut => 'I vicini richiedono un timeout.'; + String get neighbors_requestTimedOut => 'Richiesta dei vicini scaduta.'; @override String neighbors_errorLoading(String error) { @@ -3485,10 +3480,10 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get neighbors_repeatersNeighbors => 'Ripetitori Vicini'; + String get neighbors_repeatersNeighbors => 'Vicini dei ripetitori'; @override - String get neighbors_noData => 'Nessun dato sugli vicini disponibile.'; + String get neighbors_noData => 'Nessun dato sui vicini disponibile.'; @override String neighbors_unknownContact(String pubkey) { @@ -3504,29 +3499,29 @@ class AppLocalizationsIt extends AppLocalizations { String get channelPath_title => 'Percorso Pacchetto'; @override - String get channelPath_viewMap => 'Visualizza la mappa'; + String get channelPath_viewMap => 'Visualizza mappa'; @override - String get channelPath_otherObservedPaths => 'Altri Percorsi Osservati'; + String get channelPath_otherObservedPaths => 'Altri percorsi osservati'; @override - String get channelPath_repeaterHops => 'Passaggi Ripetitore'; + String get channelPath_repeaterHops => 'Salti del ripetitore'; @override String get channelPath_noHopDetails => 'I dettagli relativi a questo pacchetto non sono forniti.'; @override - String get channelPath_messageDetails => 'Dettagli Messaggio'; + String get channelPath_messageDetails => 'Dettagli messaggio'; @override String get channelPath_senderLabel => 'Mittente'; @override - String get channelPath_timeLabel => 'Tempo'; + String get channelPath_timeLabel => 'Ora'; @override - String get channelPath_repeatsLabel => 'Ripeti'; + String get channelPath_repeatsLabel => 'Ripetizioni'; @override String channelPath_pathLabel(int index) { @@ -3558,10 +3553,10 @@ class AppLocalizationsIt extends AppLocalizations { String get channelPath_unknownPath => 'Sconosciuto'; @override - String get channelPath_floodPath => 'Inondazione'; + String get channelPath_floodPath => 'Flood'; @override - String get channelPath_directPath => 'Salva'; + String get channelPath_directPath => 'Diretto'; @override String channelPath_observedZeroOf(int total) { @@ -3574,11 +3569,11 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get channelPath_mapTitle => 'Mappa del Percorso'; + String get channelPath_mapTitle => 'Mappa del percorso'; @override String get channelPath_noRepeaterLocations => - 'Non sono disponibili posizioni per i ripetitori per questo percorso.'; + 'Non sono disponibili posizioni dei ripetitori per questo percorso.'; @override String channelPath_primaryPath(int index) { @@ -3783,10 +3778,10 @@ class AppLocalizationsIt extends AppLocalizations { String get listFilter_latestMessages => 'Ultimi messaggi'; @override - String get listFilter_heardRecently => 'Sentito di recente'; + String get listFilter_heardRecently => 'Rilevato di recente'; @override - String get listFilter_az => 'D-Z'; + String get listFilter_az => 'A-Z'; @override String get listFilter_filters => 'Filtri'; @@ -3810,10 +3805,10 @@ class AppLocalizationsIt extends AppLocalizations { String get listFilter_repeaters => 'Ripetitori'; @override - String get listFilter_roomServers => 'Server della stanza'; + String get listFilter_roomServers => 'Server stanza'; @override - String get listFilter_unreadOnly => 'Solo non letto'; + String get listFilter_unreadOnly => 'Solo non letti'; @override String get listFilter_newGroup => 'Nuovo gruppo'; @@ -3829,14 +3824,14 @@ class AppLocalizationsIt extends AppLocalizations { 'Tracciamento del percorso non disponibile.'; @override - String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.'; + String get pathTrace_refreshTooltip => 'Aggiorna tracciamento percorso.'; @override String get pathTrace_someHopsNoLocation => - 'Uno o più dei luppoli mancano di una posizione!'; + 'Uno o più hop non hanno una posizione!'; @override - String get pathTrace_clearTooltip => 'Pulisci percorso'; + String get pathTrace_clearTooltip => 'Cancella percorso'; @override String get losSelectStartEnd => @@ -4488,23 +4483,23 @@ class AppLocalizationsIt extends AppLocalizations { @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'Impossibile aggiornare il canale: $error'; } @override String get contact_typeChat => 'Chat'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => 'Ripetitore'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => 'Stanza'; @override - String get contact_typeSensor => 'Sensor'; + String get contact_typeSensor => 'Sensore'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => 'Sconosciuto'; @override String get map_zoomIn => 'Ingrandisci'; @@ -4513,7 +4508,7 @@ class AppLocalizationsIt extends AppLocalizations { String get map_zoomOut => 'Riduci la visualizzazione'; @override - String get map_centerMap => 'Mappa del centro'; + String get map_centerMap => 'Centra mappa'; @override String get chrome_bluetoothRequiresChromium => @@ -4531,48 +4526,48 @@ class AppLocalizationsIt extends AppLocalizations { String get pathTrace_legendInferred => 'Posizione dedotta'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Singola'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Combinata'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Riproduci'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Pausa'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Ripeti'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Salto precedente'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Salto successivo'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Mostra l\'animazione del pacchetto'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Nascondi l\'animazione del pacchetto'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Salto $current di $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Percorsi osservati: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primario'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternativo $index'; } @override @@ -4580,8 +4575,8 @@ class AppLocalizationsIt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count salti', + one: '1 salto', ); return '$_temp0'; } @@ -4592,14 +4587,14 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Segmento condiviso'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Segmento stimato'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Utilizzato da $count percorsi'; } @override @@ -4607,33 +4602,35 @@ class AppLocalizationsIt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count salti non hanno una posizione specifica — il percorso mostrato è parziale', + one: + '1 salto non ha una posizione specifica — il percorso mostrato è parziale', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Mostra tutti'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Nascondi percorso'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Mostra percorso'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Comprimi pannello'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Espandi pannello'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Nessuna posizione'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Blocca la vista sul pacchetto'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Sblocca la vista dal pacchetto'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 0e6f5922..dddb89e9 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -24,10 +24,10 @@ class AppLocalizationsJa extends AppLocalizations { String get common_cancel => 'キャンセル'; @override - String get common_ok => '了解'; + String get common_ok => 'OK'; @override - String get common_connect => '接続する'; + String get common_connect => '接続'; @override String get common_unknownDevice => '不明なデバイス'; @@ -45,7 +45,7 @@ class AppLocalizationsJa extends AppLocalizations { String get common_close => '閉じる'; @override - String get common_done => 'Done'; + String get common_done => '完了'; @override String get common_edit => '編集'; @@ -57,28 +57,28 @@ class AppLocalizationsJa extends AppLocalizations { String get common_settings => '設定'; @override - String get common_disconnect => '切断する'; + String get common_disconnect => '切断'; @override - String get common_connected => '接続されている'; + String get common_connected => '接続済み'; @override - String get common_disconnected => '切断'; + String get common_disconnected => '切断済み'; @override - String get common_create => '作成する'; + String get common_create => '作成'; @override - String get common_continue => '続き'; + String get common_continue => '続行'; @override - String get common_share => '共有する'; + String get common_share => '共有'; @override String get common_copy => 'コピー'; @override - String get common_retry => '再試'; + String get common_retry => '再試行'; @override String get common_hide => '隠す'; @@ -87,10 +87,10 @@ class AppLocalizationsJa extends AppLocalizations { String get common_remove => '削除'; @override - String get common_enable => '有効化する'; + String get common_enable => '有効にする'; @override - String get common_disable => '無効化する'; + String get common_disable => '無効にする'; @override String get common_undo => '元に戻す'; @@ -136,34 +136,34 @@ class AppLocalizationsJa extends AppLocalizations { String get common_interval => '間隔'; @override - String get scanner_title => 'MeshCore オープン'; + String get scanner_title => 'MeshCore Open'; @override String get connectionChoiceUsbLabel => 'USB'; @override - String get connectionChoiceBluetoothLabel => 'ブルートゥース'; + String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get connectionChoiceTcpLabel => 'TCP (Transmission Control Protocol)'; + String get connectionChoiceTcpLabel => 'TCP'; @override - String get tcpScreenTitle => 'TCP を使用して接続'; + String get tcpScreenTitle => 'TCP で接続'; @override - String get tcpHostLabel => 'IPアドレス'; + String get tcpHostLabel => '接続先'; @override - String get tcpHostHint => '192.168.40.10'; + String get tcpHostHint => '192.168.40.10 / example.com'; @override - String get tcpPortLabel => '港'; + String get tcpPortLabel => 'ポート'; @override String get tcpPortHint => '5000'; @override - String get tcpStatus_notConnected => 'エンドポイントを入力し、接続する'; + String get tcpStatus_notConnected => '接続先を入力して接続してください'; @override String tcpStatus_connectingTo(String endpoint) { @@ -171,7 +171,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get tcpErrorHostRequired => 'IPアドレスが必要です。'; + String get tcpErrorHostRequired => '接続先は必須です。'; @override String get tcpErrorPortInvalid => 'ポート番号は1から65535の範囲で指定してください。'; @@ -191,18 +191,17 @@ class AppLocalizationsJa extends AppLocalizations { String get usbScreenTitle => 'USB経由で接続'; @override - String get usbScreenSubtitle => '検出されたシリアルデバイスを選択し、MeshCoreノードに直接接続してください。'; + String get usbScreenSubtitle => '検出されたシリアルデバイスを選択して、MeshCore ノードに直接接続します。'; @override - String get usbScreenStatus => 'USBデバイスを選択する'; + String get usbScreenStatus => 'USB デバイスを選択してください'; @override String get usbScreenNote => - 'USBシリアルポートは、サポートされているAndroidデバイスおよびデスクトッププラットフォームで利用可能です。'; + 'USB シリアルは、対応する Android デバイスとデスクトップ プラットフォームで利用できます。'; @override - String get usbScreenEmptyState => - 'USBデバイスが見つかりませんでした。「別のUSBデバイスを接続して、再度確認してください。」'; + String get usbScreenEmptyState => 'USB デバイスが見つかりません。接続してから再度更新してください。'; @override String get usbErrorPermissionDenied => 'USBへのアクセス許可が拒否されました。'; @@ -214,7 +213,7 @@ class AppLocalizationsJa extends AppLocalizations { String get usbErrorInvalidPort => '有効なUSBデバイスを選択してください。'; @override - String get usbErrorBusy => '別のUSB接続の要求がすでに処理中です。'; + String get usbErrorBusy => '別の USB 接続要求がすでに進行中です。'; @override String get usbErrorNotConnected => 'USBデバイスは接続されていません。'; @@ -229,13 +228,13 @@ class AppLocalizationsJa extends AppLocalizations { String get usbErrorUnsupported => 'このプラットフォームでは、USBシリアル通信はサポートされていません。'; @override - String get usbErrorAlreadyActive => 'USB接続はすでに確立されています。'; + String get usbErrorAlreadyActive => 'USB 接続はすでにアクティブです。'; @override String get usbErrorNoDeviceSelected => 'USBデバイスは選択されていません。'; @override - String get usbErrorPortClosed => 'USB接続は確立されていません。'; + String get usbErrorPortClosed => 'USB 接続は開かれていません。'; @override String get usbErrorConnectTimedOut => @@ -245,7 +244,7 @@ class AppLocalizationsJa extends AppLocalizations { String get usbFallbackDeviceName => 'ウェブシリアルデバイス'; @override - String get usbStatus_notConnected => 'USBデバイスを選択する'; + String get usbStatus_notConnected => 'USB デバイスを選択してください'; @override String get usbStatus_connecting => 'USBデバイスへの接続中...'; @@ -272,14 +271,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String scanner_connectedTo(String deviceName) { - return '$deviceName に接続'; + return '$deviceName に接続済み'; } @override - String get scanner_searchingDevices => 'MeshCoreデバイスの検索'; + String get scanner_searchingDevices => 'MeshCore デバイスを検索中...'; @override - String get scanner_tapToScan => 'MeshCore デバイスを検索するには、「スキャン」ボタンをタップしてください。'; + String get scanner_tapToScan => 'MeshCore デバイスを見つけるには、「スキャン」をタップしてください。'; @override String scanner_connectionFailed(String error) { @@ -293,10 +292,10 @@ class AppLocalizationsJa extends AppLocalizations { String get scanner_scan => 'スキャン'; @override - String get scanner_bluetoothOff => 'Bluetooth はオフになっています'; + String get scanner_bluetoothOff => 'Bluetooth はオフです'; @override - String get scanner_bluetoothOffMessage => 'Bluetoothを有効にして、デバイスを検索してください。'; + String get scanner_bluetoothOffMessage => 'Bluetooth を有効にしてデバイスを検索してください。'; @override String get scanner_chromeRequired => 'Chrome ブラウザが必須です'; @@ -306,17 +305,17 @@ class AppLocalizationsJa extends AppLocalizations { 'このWebアプリケーションは、Bluetooth機能を利用するために、Google ChromeまたはChromiumベースのブラウザが必要です。'; @override - String get scanner_enableBluetooth => 'Bluetoothを有効にする'; + String get scanner_enableBluetooth => 'Bluetooth を有効にする'; @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'ブラウザでは Bluetooth は利用できません。代わりに USB で接続してください。'; @override - String get device_quickSwitch => '素早い切り替え'; + String get device_quickSwitch => 'クイックスイッチ'; @override - String get device_meshcore => 'メッシュコア'; + String get device_meshcore => 'MeshCore'; @override String get settings_title => '設定'; @@ -343,7 +342,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_nodeNameHint => 'ノード名を入力してください'; @override - String get settings_nodeNameUpdated => '氏名変更'; + String get settings_nodeNameUpdated => '名前を更新しました'; @override String get settings_radioSettings => 'ラジオ設定'; @@ -370,13 +369,13 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_locationInvalid => '無効な緯度または経度。'; @override - String get settings_locationGPSEnable => 'GPS機能有効'; + String get settings_locationGPSEnable => 'GPS を有効にする'; @override - String get settings_locationGPSEnableSubtitle => 'GPSが自動的に位置情報を更新できるようにする。'; + String get settings_locationGPSEnableSubtitle => 'GPS が自動的に位置情報を更新できるようにします。'; @override - String get settings_locationIntervalSec => 'GPS データの取得間隔(秒)'; + String get settings_locationIntervalSec => 'GPS 更新間隔(秒)'; @override String get settings_locationIntervalInvalid => @@ -389,26 +388,26 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_longitude => '経度'; @override - String get settings_contactSettings => '連絡設定'; + String get settings_contactSettings => '連絡先設定'; @override - String get settings_contactSettingsSubtitle => '連絡先を追加する設定'; + String get settings_contactSettingsSubtitle => '連絡先の追加方法に関する設定'; @override String get settings_privacyMode => 'プライバシーモード'; @override - String get settings_privacyModeSubtitle => '広告に名前/場所を記載しない'; + String get settings_privacyModeSubtitle => '広告に名前や位置を表示しない'; @override String get settings_privacyModeToggle => - 'プライバシーモードをオンにして、広告に表示される名前や場所を非表示にします。'; + 'プライバシーモードを有効にすると、広告に表示される名前と位置を非表示にします。'; @override String get settings_privacyModeEnabled => 'プライバシーモードが有効になっています'; @override - String get settings_privacyModeDisabled => 'プライバシーモードは無効化されています'; + String get settings_privacyModeDisabled => 'プライバシーモードは無効です'; @override String get settings_privacy => 'プライバシー設定'; @@ -445,50 +444,49 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_advertLocationSubtitle => '広告に場所を記載してください。'; @override - String get settings_multiAck => '複数のACK(応答)'; + String get settings_multiAck => 'マルチ ACK'; @override String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました'; @override - String get settings_actions => '行動'; + String get settings_actions => '操作'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => 'すべての経路を削除'; @override - String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + String get settings_deleteAllPathsSubtitle => '連絡先からすべての経路データを消去します。'; @override - String get settings_sendAdvertisement => '広告を送信する'; + String get settings_sendAdvertisement => '広告を送信'; @override - String get settings_sendAdvertisementSubtitle => '現在、放送での活動'; + String get settings_sendAdvertisementSubtitle => '現在の存在を送信します'; @override - String get settings_advertisementSent => '広告が送信されました'; + String get settings_advertisementSent => '広告を送信しました'; @override - String get settings_syncTime => '同期時間'; + String get settings_syncTime => '時刻を同期'; @override - String get settings_syncTimeSubtitle => 'デバイスの時刻を、携帯電話の時刻に合わせる'; + String get settings_syncTimeSubtitle => 'デバイスの時刻をスマートフォンに合わせます'; @override - String get settings_timeSynchronized => '時間同期'; + String get settings_timeSynchronized => '時刻を同期しました'; @override - String get settings_refreshContacts => '連絡先を更新する'; + String get settings_refreshContacts => '連絡先を更新'; @override - String get settings_refreshContactsSubtitle => 'デバイスから連絡先リストを再読み込みする'; + String get settings_refreshContactsSubtitle => 'デバイスから連絡先リストを再読み込みします'; @override - String get settings_rebootDevice => 'デバイスを再起動する'; + String get settings_rebootDevice => 'デバイスを再起動'; @override - String get settings_rebootDeviceSubtitle => 'MeshCore デバイスを再起動する'; + String get settings_rebootDeviceSubtitle => 'MeshCore デバイスを再起動します'; @override String get settings_rebootDeviceConfirm => @@ -505,7 +503,7 @@ class AppLocalizationsJa extends AppLocalizations { 'BLE/TCP/USB 関連のコマンド、応答、および生のデータ'; @override - String get settings_appDebugLog => 'アプリケーションのデバッグログ'; + String get settings_appDebugLog => 'アプリのデバッグログ'; @override String get settings_appDebugLogSubtitle => 'アプリケーションのデバッグメッセージ'; @@ -515,15 +513,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String settings_aboutVersion(String version) { - return 'MeshCore Open $version版'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => '2026年のMeshCoreオープンソースプロジェクト'; + String get settings_aboutLegalese => '2026 MeshCore オープンソースプロジェクト'; @override String get settings_aboutDescription => - 'MeshCore LoRaメッシュネットワークデバイス用の、オープンソースのFlutterクライアント。'; + 'MeshCore LoRa メッシュネットワークデバイス向けのオープンソース Flutter クライアント。'; @override String get settings_aboutOpenMeteoAttribution => @@ -581,19 +579,19 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)'; @override - String get settings_clientRepeat => 'オフグリッドリピータ'; + String get settings_clientRepeat => 'オフグリッド中継'; @override String get settings_clientRepeatSubtitle => - 'このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。'; + 'このデバイスが他のデバイス向けにメッシュパケットを中継できるようにします。'; @override String get settings_clientRepeatFreqWarning => - 'オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。'; + 'オフグリッド中継には 433、869、または 918 MHz の周波数が必要です。'; @override String settings_error(String message) { - return 'エラー:$message'; + return 'エラー: $message'; } @override @@ -606,19 +604,19 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_theme => 'テーマ'; @override - String get appSettings_themeSystem => 'システムデフォルト'; + String get appSettings_themeSystem => 'システム設定'; @override - String get appSettings_themeLight => '光'; + String get appSettings_themeLight => 'ライト'; @override - String get appSettings_themeDark => '暗い'; + String get appSettings_themeDark => 'ダーク'; @override String get appSettings_language => '言語'; @override - String get appSettings_languageSystem => 'システムデフォルト'; + String get appSettings_languageSystem => 'システム設定'; @override String get appSettings_languageEn => '英語'; @@ -666,11 +664,11 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_languageUk => 'ウクライナ語'; @override - String get appSettings_enableMessageTracing => 'メッセージ追跡機能を有効にする'; + String get appSettings_enableMessageTracing => 'メッセージ追跡を有効にする'; @override String get appSettings_enableMessageTracingSubtitle => - 'メッセージに関する詳細な経路およびタイミングに関するメタデータを表示する'; + 'メッセージの詳細な経路とタイミングのメタデータを表示します'; @override String get appSettings_notifications => '通知'; @@ -679,7 +677,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_enableNotifications => '通知を有効にする'; @override - String get appSettings_enableNotificationsSubtitle => 'メッセージや広告に関する通知を受け取る'; + String get appSettings_enableNotificationsSubtitle => 'メッセージや広告の通知を受け取ります'; @override String get appSettings_notificationPermissionDenied => '通知の許可が拒否されました'; @@ -715,77 +713,75 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_messaging => 'メッセージング'; @override - String get appSettings_clearPathOnMaxRetry => 'マックスリトライでの明確な手順'; + String get appSettings_clearPathOnMaxRetry => '最大再試行時に経路を消去'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - '5回送信に失敗した場合、連絡経路をリセットする'; + '5 回送信に失敗したら連絡先の経路をリセットします'; @override - String get appSettings_pathsWillBeCleared => '5回失敗した後、経路が再開されます。'; + String get appSettings_pathsWillBeCleared => '5 回失敗すると経路を消去します'; @override - String get appSettings_pathsWillNotBeCleared => 'パスは自動で削除されません。'; + String get appSettings_pathsWillNotBeCleared => '経路は自動では消去されません'; @override - String get appSettings_autoRouteRotation => '自動ルートの切り替え'; + String get appSettings_autoRouteRotation => '自動経路切り替え'; @override - String get appSettings_autoRouteRotationSubtitle => '最適なルートと、フラッドモードを切り替える'; + String get appSettings_autoRouteRotationSubtitle => '最適な経路とフラッドモードを切り替えます'; @override - String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています'; + String get appSettings_autoRouteRotationEnabled => '自動経路切り替えが有効です'; @override - String get appSettings_autoRouteRotationDisabled => '自動ルートの変更機能が無効になっています。'; + String get appSettings_autoRouteRotationDisabled => '自動経路切り替えは無効です'; @override - String get appSettings_maxRouteWeight => '最大ルート重量'; + String get appSettings_maxRouteWeight => '最大経路重み'; @override - String get appSettings_maxRouteWeightSubtitle => - 'ある経路が、成功裏に配送された場合に、積み上げられる最大重量'; + String get appSettings_maxRouteWeightSubtitle => '成功配送によって経路に蓄積できる最大重み'; @override - String get appSettings_initialRouteWeight => '初期ルートの重み'; + String get appSettings_initialRouteWeight => '初期経路重み'; @override - String get appSettings_initialRouteWeightSubtitle => '新たに発見された経路の初期重量'; + String get appSettings_initialRouteWeightSubtitle => '新しく発見された経路の初期重み'; @override String get appSettings_routeWeightSuccessIncrement => '成功時の重み増加'; @override String get appSettings_routeWeightSuccessIncrementSubtitle => - '配送が成功した場合に、経路に追加される重量'; + '成功配送後に経路へ加算する重み'; @override String get appSettings_routeWeightFailureDecrement => '失敗時の重み減少'; @override String get appSettings_routeWeightFailureDecrementSubtitle => - '配送に失敗した際に、経路から取り除かれた重量'; + '失敗配送後に経路から差し引く重み'; @override String get appSettings_maxMessageRetries => '最大メッセージ再試行回数'; @override - String get appSettings_maxMessageRetriesSubtitle => - 'メッセージを「失敗」とマークするまでの、再試行回数'; + String get appSettings_maxMessageRetriesSubtitle => 'メッセージを失敗として扱うまでの再試行回数'; @override String get appSettings_battery => 'バッテリー'; @override - String get appSettings_batteryChemistry => '電池の化学'; + String get appSettings_batteryChemistry => 'バッテリー種別'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return '$deviceName 単位'; + return '$deviceName ごとに設定'; } @override - String get appSettings_batteryChemistryConnectFirst => 'デバイスを選択するために接続する'; + String get appSettings_batteryChemistryConnectFirst => '選択するにはデバイスに接続してください'; @override String get appSettings_batteryNmc => '18650型 NMC (3.0-4.2V)'; @@ -797,22 +793,22 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @override - String get appSettings_mapDisplay => '地図の表示'; + String get appSettings_mapDisplay => '地図表示'; @override - String get appSettings_showRepeaters => '繰り返し再生機能'; + String get appSettings_showRepeaters => 'リピータを表示'; @override String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する'; @override - String get appSettings_showChatNodes => 'チャットノードの表示'; + String get appSettings_showChatNodes => 'チャットノードを表示'; @override String get appSettings_showChatNodesSubtitle => '地図上にチャットノードを表示する'; @override - String get appSettings_showOtherNodes => '他のノードを表示する'; + String get appSettings_showOtherNodes => 'その他のノードを表示'; @override String get appSettings_showOtherNodesSubtitle => '地図上に、他のノードの種類を表示する'; @@ -821,24 +817,24 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_timeFilter => '時間フィルター'; @override - String get appSettings_timeFilterShowAll => 'すべてのノードを表示する'; + String get appSettings_timeFilterShowAll => 'すべてのノードを表示'; @override String appSettings_timeFilterShowLast(int hours) { - return '過去 $hours 時間のノードを表示する'; + return '過去 $hours 時間以内に発見されたノードを表示'; } @override - String get appSettings_mapTimeFilter => '地図の表示期間を絞り込む'; + String get appSettings_mapTimeFilter => '地図の時間フィルター'; @override - String get appSettings_showNodesDiscoveredWithin => '以下の範囲内で発見されたノードを表示する:'; + String get appSettings_showNodesDiscoveredWithin => '次の期間内に発見されたノードを表示:'; @override - String get appSettings_allTime => 'すべての期間'; + String get appSettings_allTime => '全期間'; @override - String get appSettings_lastHour => '直前の'; + String get appSettings_lastHour => '過去1時間'; @override String get appSettings_last6Hours => '過去6時間'; @@ -847,54 +843,52 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_last24Hours => '過去24時間'; @override - String get appSettings_lastWeek => '先週'; + String get appSettings_lastWeek => '過去1週間'; @override - String get appSettings_offlineMapCache => 'オフライン用地図キャッシュ'; + String get appSettings_offlineMapCache => 'オフライン地図キャッシュ'; @override String get appSettings_unitsTitle => '単位'; @override - String get appSettings_unitsMetric => 'メートル (m) / キロメートル (km)'; + String get appSettings_unitsMetric => 'メートル法 (m / km)'; @override - String get appSettings_unitsImperial => '帝国 (フィート / マイル)'; + String get appSettings_unitsImperial => 'ヤード・ポンド法 (ft / mi)'; @override - String get appSettings_noAreaSelected => '選択されたエリアはありません'; + String get appSettings_noAreaSelected => 'エリアが選択されていません'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return '選択された範囲(ズームレベル:$minZoom~$maxZoom)'; + return 'エリア選択中(ズーム $minZoom-$maxZoom)'; } @override String get appSettings_debugCard => 'デバッグ'; @override - String get appSettings_appDebugLogging => 'アプリケーションのデバッグ用ログ'; + String get appSettings_appDebugLogging => 'アプリのデバッグログ'; @override String get appSettings_appDebugLoggingSubtitle => - 'ログアプリのデバッグメッセージ(トラブルシューティング用)'; + 'トラブルシューティング用にアプリのデバッグメッセージを記録します'; @override - String get appSettings_appDebugLoggingEnabled => - 'アプリケーションのデバッグ用ログ機能が有効になっています。'; + String get appSettings_appDebugLoggingEnabled => 'アプリのデバッグログは有効です'; @override - String get appSettings_appDebugLoggingDisabled => - 'アプリケーションのデバッグログが無効化されています。'; + String get appSettings_appDebugLoggingDisabled => 'アプリのデバッグログは無効です'; @override String get contacts_title => '連絡先'; @override - String get contacts_noContacts => '現時点では、連絡先はまだありません。'; + String get contacts_noContacts => 'まだ連絡先はありません'; @override - String get contacts_contactsWillAppear => 'デバイスが広告を行う際に、連絡先が表示されます。'; + String get contacts_contactsWillAppear => 'デバイスが広告を送信すると連絡先が表示されます'; @override String get contacts_unread => '未読'; @@ -904,27 +898,27 @@ class AppLocalizationsJa extends AppLocalizations { @override String contacts_searchContacts(int number, String str) { - return '$number件の$strに関する連絡先を検索...'; + return '連絡先を検索... $number 件の $str'; } @override String contacts_searchFavorites(int number, String str) { - return '$number件の$strを検索...'; + return 'お気に入りを検索... $number 件の $str'; } @override String contacts_searchUsers(int number, String str) { - return '$number件の$strに関するユーザーを検索する...'; + return 'ユーザーを検索... $number 件の $str'; } @override String contacts_searchRepeaters(int number, String str) { - return '$number $str までの検索...'; + return 'リピータを検索... $number 件の $str'; } @override String contacts_searchRoomServers(int number, String str) { - return '$number $str 部屋のサーバーを検索する...'; + return 'ルームサーバーを検索... $number 件の $str'; } @override @@ -945,16 +939,16 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_manageRepeater => 'リピータの管理'; @override - String get contacts_manageRoom => 'ルームサーバーの管理'; + String get contacts_manageRoom => 'ルームサーバーを管理'; @override - String get contacts_roomLogin => 'ルームサーバーへのログイン'; + String get contacts_roomLogin => 'ルームサーバーログイン'; @override - String get contacts_openChat => '自由な会話'; + String get contacts_openChat => 'チャットを開く'; @override - String get contacts_editGroup => '編集グループ'; + String get contacts_editGroup => 'グループを編集'; @override String get contacts_deleteGroup => 'グループを削除'; @@ -983,44 +977,44 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_groupNameRequired => 'グループ名が必須です'; @override - String get contacts_groupNameReserved => 'このグループ名はすでに使用されています。'; + String get contacts_groupNameReserved => 'このグループ名は予約済みです'; @override String contacts_groupAlreadyExists(String name) { - return 'グループ「$name」はすでに存在しています'; + return 'グループ「$name」はすでに存在します'; } @override - String get contacts_filterContacts => '連絡先をフィルタリングする…'; + String get contacts_filterContacts => '連絡先をフィルタ...'; @override - String get contacts_noContactsMatchFilter => '指定された条件に合致する連絡先は見つかりませんでした。'; + String get contacts_noContactsMatchFilter => '条件に一致する連絡先はありません'; @override - String get contacts_noMembers => 'メンバーはいない'; + String get contacts_noMembers => 'メンバーなし'; @override String get contacts_lastSeenNow => '最近'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '~$minutes 分'; + return '約 $minutes 分前'; } @override - String get contacts_lastSeenHourAgo => '約1時間'; + String get contacts_lastSeenHourAgo => '約 1 時間前'; @override String contacts_lastSeenHoursAgo(int hours) { - return '~ $hours 時間'; + return '約 $hours 時間前'; } @override - String get contacts_lastSeenDayAgo => '~1日'; + String get contacts_lastSeenDayAgo => '約 1 日前'; @override String contacts_lastSeenDaysAgo(int days) { - return '~$days日間'; + return '約 $days 日前'; } @override @@ -1063,10 +1057,10 @@ class AppLocalizationsJa extends AppLocalizations { String get channels_noChannelsConfigured => '設定されたチャンネルがありません'; @override - String get channels_addPublicChannel => 'パブリックチャンネルを追加する'; + String get channels_addPublicChannel => '公開チャンネルを追加'; @override - String get channels_searchChannels => '検索オプション...'; + String get channels_searchChannels => 'チャンネルを検索...'; @override String get channels_noChannelsFound => 'チャンネルが見つかりませんでした'; @@ -1077,27 +1071,27 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get channels_public => '一般の人々'; + String get channels_public => '公開'; @override String channels_via(String path) { - return 'via $path'; + return '経由: $path'; } @override - String get channels_private => '個人の'; + String get channels_private => '非公開'; @override - String get channels_editChannel => 'チャンネルを編集する'; + String get channels_editChannel => 'チャンネルを編集'; @override - String get channels_muteChannel => 'ミュート機能'; + String get channels_muteChannel => 'チャンネルをミュート'; @override - String get channels_unmuteChannel => 'ミュートを解除する'; + String get channels_unmuteChannel => 'チャンネルのミュートを解除'; @override - String get channels_deleteChannel => 'チャンネルを削除する'; + String get channels_deleteChannel => 'チャンネルを削除'; @override String channels_deleteChannelConfirm(String name) { @@ -1127,13 +1121,13 @@ class AppLocalizationsJa extends AppLocalizations { String get channels_usePublicChannel => 'パブリックチャンネルを使用する'; @override - String get channels_standardPublicPsk => '標準的な公用 PSK'; + String get channels_standardPublicPsk => '公開用の標準 PSK'; @override - String get channels_pskHex => 'PSK (ヘックス)'; + String get channels_pskHex => 'PSK(16 進数)'; @override - String get channels_generateRandomPsk => 'ランダムなPSK(正交符号分割変調)を生成する'; + String get channels_generateRandomPsk => 'ランダムな PSK を生成'; @override String get channels_enterChannelName => 'チャンネル名を入力してください'; @@ -1143,7 +1137,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String channels_channelAdded(String name) { - return 'チャンネル「$name」を追加'; + return 'チャンネル「$name」を追加しました'; } @override @@ -1161,20 +1155,20 @@ class AppLocalizationsJa extends AppLocalizations { String get channels_cyr2latCompressionDscr => '送信時に一部のキリル文字をラテン文字に置き換えます。'; @override - String get channels_cyr2latSettingsHeading => 'cyr2latの設定'; + String get channels_cyr2latSettingsHeading => 'Cyr2Lat 設定'; @override String get channels_cyr2latSettingsSubheading => '置換リスト'; @override - String get channels_cyr2latSettingsDscr => '文字置換のJSON設定を編集する'; + String get channels_cyr2latSettingsDscr => '文字置換の JSON 設定を編集します'; @override - String get channels_cyr2latSettingsDialogHint => 'JSON置換マップ'; + String get channels_cyr2latSettingsDialogHint => 'JSON 置換マップ'; @override String channels_cyr2latSettingsDialogWrongJSON(Object error) { - return '不正なJSON: $error'; + return '無効な JSON: $error'; } @override @@ -1183,7 +1177,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get settings_cyr2latProfileAdd => 'Cyr2Latプロファイルの追加'; + String get settings_cyr2latProfileAdd => 'Cyr2Lat プロファイルを追加'; @override String get settings_cyr2latProfileName => 'プロファイル名'; @@ -1192,33 +1186,33 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_cyr2latProfileNameEmpty => 'プロファイル名は空にできません'; @override - String get settings_cyr2latProfileAdded => 'プロファイルが正常に追加されました'; + String get settings_cyr2latProfileAdded => 'プロファイルを追加しました'; @override - String get settings_cyr2latProfileUpdated => 'プロファイルの更新に成功しました'; + String get settings_cyr2latProfileUpdated => 'プロファイルを更新しました'; @override - String get settings_cyr2latProfileEdit => 'Cyr2Latプロファイルを編集'; + String get settings_cyr2latProfileEdit => 'Cyr2Lat プロファイルを編集'; @override - String get settings_cyr2latProfileDelete => 'Cyr2Latプロファイルを削除'; + String get settings_cyr2latProfileDelete => 'Cyr2Lat プロファイルを削除'; @override - String get settings_cyr2latProfileDeleted => 'プロファイルの削除に成功しました'; + String get settings_cyr2latProfileDeleted => 'プロファイルを削除しました'; @override String settings_cyr2latProfileDeleteDscr(String name) { - return 'プロファイル \"$name\" を削除してもよろしいですか?'; + return 'プロファイル「$name」を削除しますか?'; } @override - String get channels_publicChannelAdded => 'パブリックチャンネルが追加されました'; + String get channels_publicChannelAdded => '公開チャンネルを追加しました'; @override String get channels_sortBy => '並び替え'; @override - String get channels_sortManual => 'マニュアル'; + String get channels_sortManual => '手動'; @override String get channels_sortAZ => 'AからZ'; @@ -1233,19 +1227,19 @@ class AppLocalizationsJa extends AppLocalizations { String get channels_createPrivateChannel => 'プライベートチャンネルを作成する'; @override - String get channels_createPrivateChannelDesc => '秘密鍵を使用して保護されています。'; + String get channels_createPrivateChannelDesc => '秘密鍵で保護されます。'; @override String get channels_joinPrivateChannel => 'プライベートチャンネルに参加する'; @override - String get channels_joinPrivateChannelDesc => '手動で秘密のキーを入力する。'; + String get channels_joinPrivateChannelDesc => '秘密鍵を手動で入力します。'; @override String get channels_joinPublicChannel => '公開チャンネルに参加する'; @override - String get channels_joinPublicChannelDesc => 'このチャンネルには、誰でも参加できます。'; + String get channels_joinPublicChannelDesc => '誰でもこのチャンネルに参加できます。'; @override String get channels_joinHashtagChannel => 'ハッシュタグチャンネルに参加する'; @@ -1300,7 +1294,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'メッセージが長すぎる($maxBytes バイトを超える)。'; + return 'メッセージが長すぎます($maxBytes バイトを超えています)'; } @override @@ -1310,90 +1304,90 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_messageDeleted => 'メッセージは削除されました'; @override - String get chat_retryingMessage => '再試行メッセージ'; + String get chat_retryingMessage => 'メッセージを再試行中'; @override String chat_retryCount(int current, int max) { - return '$current / $max 回目'; + return '$current/$max 回目'; } @override - String get chat_sendGif => 'GIFを送信する'; + String get chat_sendGif => 'GIF を送信'; @override String get chat_reply => '返信'; @override - String get chat_addReaction => '反応を追加'; + String get chat_addReaction => 'リアクションを追加'; @override String get chat_me => '私'; @override - String get emojiCategorySmileys => '笑顔の絵文字'; + String get emojiCategorySmileys => '顔文字'; @override - String get emojiCategoryGestures => '身振り、動作'; + String get emojiCategoryGestures => 'ジェスチャー'; @override - String get emojiCategoryHearts => '心'; + String get emojiCategoryHearts => 'ハート'; @override - String get emojiCategoryObjects => '対象物'; + String get emojiCategoryObjects => '物'; @override - String get gifPicker_title => 'GIF を選択してください'; + String get gifPicker_title => 'GIF を選択'; @override - String get gifPicker_searchHint => 'GIFの検索...'; + String get gifPicker_searchHint => 'GIF を検索...'; @override - String get gifPicker_poweredBy => 'GIPHYによる提供'; + String get gifPicker_poweredBy => 'GIPHY 提供'; @override - String get gifPicker_noGifsFound => 'GIF形式のファイルは見つかりませんでした'; + String get gifPicker_noGifsFound => 'GIF が見つかりませんでした'; @override - String get gifPicker_failedLoad => 'GIFファイルの読み込みに失敗しました'; + String get gifPicker_failedLoad => 'GIF の読み込みに失敗しました'; @override - String get gifPicker_failedSearch => 'GIFファイルの検索に失敗しました'; + String get gifPicker_failedSearch => 'GIF の検索に失敗しました'; @override - String get gifPicker_noInternet => 'インターネット接続なし'; + String get gifPicker_noInternet => 'インターネット接続がありません'; @override - String get debugLog_appTitle => 'アプリケーションのデバッグログ'; + String get debugLog_appTitle => 'アプリのデバッグログ'; @override String get debugLog_bleTitle => 'BLE デバッグログ'; @override - String get debugLog_copyLog => '記録'; + String get debugLog_copyLog => 'ログをコピー'; @override - String get debugLog_clearLog => '詳細なログ'; + String get debugLog_clearLog => 'ログをクリア'; @override - String get debugLog_copied => 'デバッグログをコピー'; + String get debugLog_copied => 'デバッグログをコピーしました'; @override - String get debugLog_bleCopied => 'BLEログのコピー'; + String get debugLog_bleCopied => 'BLE ログをコピーしました'; @override - String get debugLog_noEntries => 'デバッグログはまだ生成されていません'; + String get debugLog_noEntries => 'まだデバッグログはありません'; @override - String get debugLog_enableInSettings => 'アプリのデバッグログを有効にするには、設定から操作してください。'; + String get debugLog_enableInSettings => '設定でアプリのデバッグログを有効にしてください'; @override String get debugLog_frames => 'フレーム'; @override - String get debugLog_rawLogRx => '生のログ-RX'; + String get debugLog_rawLogRx => '生の Log-RX'; @override - String get debugLog_noBleActivity => '現時点では、BLE関連の活動は行われていません。'; + String get debugLog_noBleActivity => 'まだ BLE アクティビティはありません'; @override String debugFrame_length(int count) { @@ -1406,7 +1400,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'テキストメッセージ用フレーム:'; + String get debugFrame_textMessageHeader => 'テキストメッセージフレーム:'; @override String debugFrame_destinationPubKey(String pubKey) { @@ -1429,18 +1423,18 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get debugFrame_textTypeCli => 'CLI(コマンドラインインターフェース)'; + String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'シンプルな'; + String get debugFrame_textTypePlain => 'プレーン'; @override String debugFrame_text(String text) { - return '- テキスト:「$text」'; + return '- テキスト: \"$text\"'; } @override - String get debugFrame_hexDump => 'ヘックスダンプ:'; + String get debugFrame_hexDump => '16 進ダンプ:'; @override String chat_hopsCount(int count) { @@ -1454,127 +1448,126 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get chat_removePath => 'パスを削除する'; + String get chat_removePath => '経路を削除'; @override - String get chat_noPathHistoryYet => 'まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。'; + String get chat_noPathHistoryYet => 'まだ経路履歴はありません。\n経路を見つけるにはメッセージを送信してください。'; @override - String get chat_pathCleared => '経路が確保されました。次のメッセージでルートを再確認します。'; + String get chat_pathCleared => '経路をクリアしました。次のメッセージで再探索します。'; @override - String get chat_fullPath => 'フルパス'; + String get chat_fullPath => '完全な経路'; @override String get routing_title => '経路設定'; @override - String get routing_modeAuto => '自動車'; + String get routing_modeAuto => '自動'; @override - String get routing_modeFlood => '洪水'; + String get routing_modeFlood => 'フラッド'; @override - String get routing_modeManual => 'マニュアル'; + String get routing_modeManual => '手動'; @override - String get routing_modeAutoHint => '最も一般的な経路を自動的に選択し、経路が不明な場合は、水没状態にします。'; + String get routing_modeAutoHint => '既知の最良経路を自動で選び、経路がないときはフラッドします。'; @override String get routing_modeFloodHint => - 'すべてのリピーターを通じて放送。最も信頼性が高いですが、より多くの時間を使用します。'; + 'すべてのリピータを通じてブロードキャストします。最も信頼性は高いですが、空中時間を多く使います。'; @override - String get routing_modeManualHint => '常に、あなたが設定した正確な経路を辿って移動します。'; + String get routing_modeManualHint => '常に、設定した正確な経路を使います。'; @override - String get routing_currentRoute => '現在までのルート'; + String get routing_currentRoute => '現在の経路'; @override - String get routing_directNoHops => '直接接続—中継装置を経由しない'; + String get routing_directNoHops => '直接 - 中継ホップなし'; @override - String get routing_noPathYet => - 'まだ経路は確立されていません。「次のメッセージを送信し、経路が特定されるまで待ちます」。'; + String get routing_noPathYet => 'まだ経路はありません。次のメッセージは経路が見つかるまでフラッドされます。'; @override - String get routing_floodBroadcast => 'すべてのリピーターを通じて放送'; + String get routing_floodBroadcast => 'すべてのリピータにブロードキャスト'; @override - String get routing_editPath => 'パスの編集'; + String get routing_editPath => '経路を編集'; @override - String get routing_forgetPath => '道にこだわらない'; + String get routing_forgetPath => '経路を忘れる'; @override String get routing_knownPaths => '既知の経路'; @override - String get routing_knownPathsHint => 'そのアプリケーションに切り替えるためのショートカットを作成します。'; + String get routing_knownPathsHint => '経路をタップして切り替えます。'; @override String get routing_inUse => '使用中'; @override - String get routing_qualityStrong => '最初の段階で大きな成果を上げる'; + String get routing_qualityStrong => '強い最初のホップ'; @override - String get routing_qualityGood => '最初の成功'; + String get routing_qualityGood => '良好な最初のホップ'; @override - String get routing_qualityFair => '最初の試みは成功を収めた'; + String get routing_qualityFair => 'まずまずの最初のホップ'; @override - String get routing_qualityWorked => '完了しました'; + String get routing_qualityWorked => '配信済み'; @override - String get routing_qualityFlood => '氾濫によって伝聞'; + String get routing_qualityFlood => 'フラッドで受信'; @override String get routing_qualityUntested => '未検証'; @override String routing_lastWorked(String when) { - return '$whenに勤務'; + return '$when に確認'; } @override - String get routing_neverWorked => '確認されていない'; + String get routing_neverWorked => '未確認'; @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes 件配信済み、$failures 件失敗'; } @override - String get routing_floodDelivery => '洪水による配送'; + String get routing_floodDelivery => 'フラッド配送'; @override - String get pathEditor_title => '経路の作成'; + String get pathEditor_title => '経路を作成'; @override String pathEditor_hopCounter(int count) { - return '64個のホップのうち、$count個'; + return '64 ホップ中 $count'; } @override String get pathEditor_noHops => - 'まだホップは追加されていません。ホップを順番に追加するには、以下の「タップ」ボタンをクリックしてください。または、ホップを一切追加せずに直接送信するには、「保存」ボタンをクリックしてください。'; + 'まだホップはありません。下のリピータをタップして順番に追加するか、ホップなしで保存して直接送信します。'; @override - String get pathEditor_addHops => 'ホップを、指定された順番に加える'; + String get pathEditor_addHops => '順番にホップを追加'; @override - String get pathEditor_searchRepeaters => '繰り返し検索'; + String get pathEditor_searchRepeaters => 'リピータを検索'; @override - String get pathEditor_advancedHex => '高度なレベル:生のヘックスパス'; + String get pathEditor_advancedHex => '詳細: 生の 16 進経路'; @override - String get pathEditor_hexLabel => 'ヘックスプレフィックス'; + String get pathEditor_hexLabel => '16 進接頭辞'; @override - String get pathEditor_hexHelper => '各ホップごとに2つのハッシュ文字を、カンマで区切って記述'; + String get pathEditor_hexHelper => 'ホップごとに 16 進数 2 文字をカンマ区切りで入力します'; @override String pathEditor_invalidTokens(String tokens) { @@ -1582,25 +1575,25 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get pathEditor_tooManyHops => '最大64段階'; + String get pathEditor_tooManyHops => '最大 64 ホップ'; @override - String get pathEditor_usePath => 'この経路を使用してください'; + String get pathEditor_usePath => 'この経路を使用'; @override - String get pathEditor_removeHop => 'ホップを取り除く'; + String get pathEditor_removeHop => 'ホップを削除'; @override - String get pathEditor_unknownHop => '不明な増幅器'; + String get pathEditor_unknownHop => '不明なリピータ'; @override - String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。'; + String get chat_pathSavedLocally => 'ローカルに保存しました。同期するには接続してください。'; @override - String get chat_pathDeviceConfirmed => 'デバイスの確認済み。'; + String get chat_pathDeviceConfirmed => 'デバイスで確認済み'; @override - String get chat_pathDeviceNotConfirmed => 'デバイスの確認はまだできていません。'; + String get chat_pathDeviceNotConfirmed => 'デバイスではまだ確認されていません'; @override String get chat_type => '種類'; @@ -1612,17 +1605,17 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_publicKey => '公開鍵'; @override - String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する'; + String get chat_compressOutgoingMessages => '送信メッセージを圧縮する'; @override - String get chat_floodForced => 'フラッド(強制的な)'; + String get chat_floodForced => 'フラッド(強制)'; @override - String get chat_directForced => '直接的な(強制的な)'; + String get chat_directForced => 'ダイレクト(強制)'; @override String chat_hopsForced(int count) { - return '$count 本のホップ(強制的に採取)'; + return '$count ホップ(強制)'; } @override @@ -1632,7 +1625,7 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_direct => '直接'; @override - String get chat_poiShared => '共有されたPOI'; + String get chat_poiShared => '共有された POI'; @override String chat_unread(int count) { @@ -1646,17 +1639,17 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_newMessages => '新しいメッセージ'; @override - String get chat_openLink => 'リンクを開く?'; + String get chat_openLink => 'リンクを開きますか?'; @override - String get chat_openLinkConfirmation => 'このリンクをブラウザで開くことはご希望ですか?'; + String get chat_openLinkConfirmation => 'このリンクをブラウザで開きますか?'; @override String get chat_open => '開く'; @override String chat_couldNotOpenLink(String url) { - return 'リンクを開けられませんでした: $url'; + return 'リンクを開けませんでした: $url'; } @override @@ -1666,58 +1659,58 @@ class AppLocalizationsJa extends AppLocalizations { String get map_title => 'ノードマップ'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'ノード名またはIDで検索'; @override - String get map_activity => 'Activity'; + String get map_activity => 'アクティビティ'; @override - String get map_online => 'Online'; + String get map_online => 'オンライン'; @override - String get map_recent => 'Recent'; + String get map_recent => '最近'; @override - String get map_stale => 'Stale'; + String get map_stale => '古い'; @override - String get map_visible => 'Visible'; + String get map_visible => '表示中'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => '非表示'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'ノードを中央に表示'; @override - String get map_details => 'Details'; + String get map_details => '詳細'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'GPS なし'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => '一致するノードなし'; @override - String get map_lineOfSight => '視界'; + String get map_lineOfSight => '見通し'; @override - String get map_losScreenTitle => '視界'; + String get map_losScreenTitle => '見通し'; @override - String get map_noNodesWithLocation => '位置情報データを持つノードは存在しません'; + String get map_noNodesWithLocation => '位置情報を持つノードはありません'; @override - String get map_nodesNeedGps => 'ノードは、地図上に表示されるために、GPS座標を共有する必要があります。'; + String get map_nodesNeedGps => 'ノードを地図に表示するには GPS 座標の共有が必要です'; @override String map_nodesCount(int count) { - return 'ノード:$count'; + return 'ノード: $count'; } @override String map_pinsCount(int count) { - return 'ピン:$count個'; + return 'ピン: $count'; } @override @@ -1736,46 +1729,46 @@ class AppLocalizationsJa extends AppLocalizations { String get map_pinDm => 'ピン(DM)'; @override - String get map_pinPrivate => 'プライベート(非公開)'; + String get map_pinPrivate => 'ピン(非公開)'; @override - String get map_pinPublic => '公開 (一般公開)'; + String get map_pinPublic => 'ピン(公開)'; @override - String get map_lastSeen => '最後に確認された場所'; + String get map_lastSeen => '最後に確認'; @override String get map_disconnectConfirm => '本当にこのデバイスとの接続を解除したいですか?'; @override - String get map_from => '~から'; + String get map_from => '送信元'; @override String get map_source => '出典'; @override - String get map_flags => '旗'; + String get map_flags => 'フラグ'; @override - String get map_type => 'Type'; + String get map_type => '種類'; @override - String get map_path => 'Path'; + String get map_path => '経路'; @override - String get map_location => 'Location'; + String get map_location => '位置'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => '推定位置'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => '公開鍵'; @override String get map_publicKeyPrefixHint => 'e.g. ab12'; @override - String get map_shareMarkerHere => 'この場所でシェア'; + String get map_shareMarkerHere => 'ここにマーカーを共有'; @override String get map_setAsMyLocation => '現在地として設定'; @@ -1787,30 +1780,30 @@ class AppLocalizationsJa extends AppLocalizations { String get map_label => 'ラベル'; @override - String get map_pointOfInterest => '注目すべき点'; + String get map_pointOfInterest => '興味地点'; @override String get map_sendToContact => '連絡先へ送信'; @override - String get map_sendToChannel => '特定のチャンネルに送信する'; + String get map_sendToChannel => 'チャンネルへ送信'; @override String get map_noChannelsAvailable => '利用可能なチャンネルはありません'; @override - String get map_publicLocationShare => '公共スペースの共有'; + String get map_publicLocationShare => '公開位置情報の共有'; @override String map_publicLocationShareConfirm(String channelLabel) { - return '現在、$channelLabel で位置情報を共有する準備をしています。このチャンネルは公開されており、PSK を持つ誰でも閲覧できます。'; + return '$channelLabel で位置情報を共有しようとしています。このチャンネルは公開されており、PSK を持つ誰でも閲覧できます。'; } @override - String get map_connectToShareMarkers => '他のデバイスと接続して、マーカーを共有する'; + String get map_connectToShareMarkers => 'マーカーを共有するにはデバイスに接続してください'; @override - String get map_filterNodes => 'フィルタノード'; + String get map_filterNodes => 'ノードを絞り込む'; @override String get map_nodeTypes => 'ノードの種類'; @@ -1828,10 +1821,10 @@ class AppLocalizationsJa extends AppLocalizations { String get map_showOverlaps => 'リピータキーの重複'; @override - String get map_keyPrefix => '主要なプレフィックス'; + String get map_keyPrefix => 'キー接頭辞'; @override - String get map_filterByKeyPrefix => '主要なプレフィックスでフィルタリングする'; + String get map_filterByKeyPrefix => 'キー接頭辞で絞り込む'; @override String get map_publicKeyPrefix => '公開鍵のプレフィックス'; @@ -1846,7 +1839,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_showGuessedLocations => '推測されたノードの位置を表示する'; @override - String get map_showDiscoveryContacts => 'Discovery社の連絡先を表示する'; + String get map_showDiscoveryContacts => 'Discovery 連絡先を表示'; @override String get map_guessedLocation => '推測された場所'; @@ -1855,7 +1848,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_lastSeenTime => '最後に確認された時間'; @override - String get map_sharedPin => '共有パスワード'; + String get map_sharedPin => '共有ピン'; @override String get map_sharedAt => '共有済み'; @@ -1867,19 +1860,19 @@ class AppLocalizationsJa extends AppLocalizations { String get map_manageRepeater => 'リピータの管理'; @override - String get map_tapToAdd => 'ノードをクリックして、パスに追加します。'; + String get map_tapToAdd => 'ノードをタップして経路に追加します。'; @override - String get map_runTrace => 'パスの追跡を実行'; + String get map_runTrace => '経路トレースを実行'; @override String get map_runTraceWithReturnPath => '元の経路に戻る。'; @override - String get map_removeLast => '最後のものを削除'; + String get map_removeLast => '最後を削除'; @override - String get map_pathTraceCancelled => 'パスの追跡は中止。'; + String get map_pathTraceCancelled => '経路トレースはキャンセルされました'; @override String get mapCache_title => 'オフライン用地図キャッシュ'; @@ -1903,12 +1896,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return '$count 個のタイルをキャッシュ'; + return '$count 枚のタイルをキャッシュしました'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Cached $downloaded tiles ($failed failed)'; + return '$downloaded 枚をキャッシュ済み($failed 件失敗)'; } @override @@ -1939,7 +1932,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String mapCache_downloadedTiles(int completed, int total) { - return 'Downloaded $completed / $total'; + return 'ダウンロード済み $completed / $total'; } @override @@ -2078,7 +2071,7 @@ class AppLocalizationsJa extends AppLocalizations { String get common_reload => '再読み込み'; @override - String get common_clear => '明確'; + String get common_clear => 'クリア'; @override String get path_currentPathLabel => '現在の経路'; @@ -2135,11 +2128,10 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_settingsSubtitle => 'リピータのパラメータを設定する'; @override - String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; + String get repeater_clockSyncAfterLogin => 'ログイン後に時刻を同期'; @override - String get repeater_clockSyncAfterLoginSubtitle => - 'ログインが成功した場合、自動的に「時刻同期」を送信する。'; + String get repeater_clockSyncAfterLoginSubtitle => 'ログイン成功時に自動で時刻同期を送信します'; @override String get repeater_statusTitle => '再送ステータス'; @@ -2168,7 +2160,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_clockAtLogin => 'ログイン時の時刻表示'; @override - String get repeater_uptime => '稼働率'; + String get repeater_uptime => '稼働時間'; @override String get repeater_queueLength => '待ち行列の長さ'; @@ -2189,10 +2181,10 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_noiseFloor => 'ノイズレベル'; @override - String get repeater_txAirtime => 'TXの放送時間'; + String get repeater_txAirtime => 'TX 送信時間'; @override - String get repeater_rxAirtime => 'RX 空き時間'; + String get repeater_rxAirtime => 'RX 受信時間'; @override String get repeater_chanUtil => 'チャンネルの利用状況'; @@ -2201,10 +2193,10 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_packetStatistics => 'パケット統計'; @override - String get repeater_sent => '送信'; + String get repeater_sent => '送信済み'; @override - String get repeater_received => '受領'; + String get repeater_received => '受信済み'; @override String get repeater_duplicates => '重複'; @@ -2503,25 +2495,25 @@ class AppLocalizationsJa extends AppLocalizations { 'ラジオの自動ゲイン制御をリセットする頻度について:ゲインが固定状態になった場合に、回復するために、何度リセットするかを設定します。4の倍数でリセットする場合、0を設定すると、定期的なリセットは停止します。'; @override - String get repeater_actionsTitle => '行動'; + String get repeater_actionsTitle => '操作'; @override - String get repeater_sendAdvert => 'フラッドに関する広告を送信'; + String get repeater_sendAdvert => 'フラッド広告を送信'; @override - String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、フラッドに関する広告を放送する'; + String get repeater_sendAdvertSubtitle => 'ネットワーク全体にフラッド広告をブロードキャストします'; @override - String get repeater_sendAdvertZeroHop => 'ゼロホップ形式の広告を送信する'; + String get repeater_sendAdvertZeroHop => 'ゼロホップ広告を送信'; @override - String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(リピータなし)'; + String get repeater_sendAdvertZeroHopSubtitle => '1 ホップ広告を送信します(リピータなし)'; @override - String get repeater_clockSync => '現在、時刻を同期する'; + String get repeater_clockSync => '時刻を同期'; @override - String get repeater_clockSyncSubtitle => 'スマートフォンの時刻をルーターに設定する'; + String get repeater_clockSyncSubtitle => 'スマートフォンの時刻をデバイスに設定します'; @override String repeater_actionSucceeded(String action) { @@ -2576,7 +2568,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_cliTitle => 'リピータのコマンドラインインターフェース'; + String get repeater_cliTitle => 'リピータ CLI'; @override String get repeater_debugNextCommand => '次のコマンドのデバッグ'; @@ -2585,7 +2577,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_commandHelp => 'コマンドヘルプ'; @override - String get repeater_clearHistory => '明確な歴史'; + String get repeater_clearHistory => '履歴をクリア'; @override String get repeater_noCommandsSent => 'まだコマンドは送信されていません'; @@ -2621,22 +2613,22 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliQuickGetRadio => 'ラジオを聴く'; @override - String get repeater_cliQuickGetTx => 'TXを入手する'; + String get repeater_cliQuickGetTx => 'TX を取得'; @override - String get repeater_cliQuickNeighbors => '近隣住民'; + String get repeater_cliQuickNeighbors => '近隣'; @override String get repeater_cliQuickVersion => 'バージョン'; @override - String get repeater_cliQuickAdvertise => '広告'; + String get repeater_cliQuickAdvertise => '広告送信'; @override - String get repeater_cliQuickClock => '時計'; + String get repeater_cliQuickClock => '時刻'; @override - String get repeater_cliQuickClockSync => 'クロック同期'; + String get repeater_cliQuickClockSync => '時刻同期'; @override String get repeater_cliQuickDiscovery => '近隣を発見する'; @@ -3387,7 +3379,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed of $total hops'; + return '$observed/$total ホップ'; } @override @@ -3419,36 +3411,36 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_unknownRepeater => '不明な増幅機'; @override - String get community_title => '地域'; + String get community_title => 'コミュニティ'; @override - String get community_create => 'コミュニティを構築する'; + String get community_create => 'コミュニティを作成'; @override - String get community_createDesc => '新しいコミュニティを作成し、QRコードで共有する。'; + String get community_createDesc => '新しいコミュニティを作成して QR コードで共有します。'; @override - String get community_join => '参加する'; + String get community_join => '参加'; @override - String get community_joinTitle => 'コミュニティに参加する'; + String get community_joinTitle => 'コミュニティに参加'; @override String community_joinConfirmation(String name) { - return '$nameさんのようなコミュニティに参加したいですか?'; + return '$name のコミュニティに参加しますか?'; } @override - String get community_scanQr => 'コミュニティのQRコードをスキャン'; + String get community_scanQr => 'コミュニティの QR コードをスキャン'; @override - String get community_scanInstructions => 'カメラを、地域のQRコードを向けて'; + String get community_scanInstructions => 'カメラをコミュニティの QR コードに向けてください'; @override - String get community_showQr => 'QRコードを表示する'; + String get community_showQr => 'QR コードを表示'; @override - String get community_publicChannel => '地域住民向け'; + String get community_publicChannel => '公開チャンネル'; @override String get community_hashtagChannel => 'コミュニティ用ハッシュタグ'; @@ -3461,12 +3453,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String community_created(String name) { - return 'コミュニティ「$name」が作成されました'; + return 'コミュニティ「$name」を作成しました'; } @override String community_joined(String name) { - return '$name のコミュニティに参加'; + return '$name のコミュニティに参加しました'; } @override @@ -3474,45 +3466,45 @@ class AppLocalizationsJa extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'このQRコードをスキャンして、$nameに参加してください。'; + return 'この QR コードをスキャンして $name に参加してください。'; } @override String get community_hashtagPrivacyHint => - 'コミュニティハッシュタグのチャンネルは、コミュニティのメンバーのみが参加できます。'; + 'コミュニティのハッシュタグチャンネルには、コミュニティのメンバーだけが参加できます。'; @override String get community_invalidQrCode => '無効なコミュニティQRコード'; @override - String get community_alreadyMember => 'すでに会員である'; + String get community_alreadyMember => 'すでにメンバーです'; @override String community_alreadyMemberMessage(String name) { - return 'あなたはすでに $name の会員です。'; + return 'あなたはすでに $name のメンバーです。'; } @override - String get community_addPublicChannel => 'コミュニティ用の公開チャンネルを追加'; + String get community_addPublicChannel => '公開チャンネルを追加'; @override - String get community_addPublicChannelHint => 'このコミュニティの公開チャンネルを自動的に追加する'; + String get community_addPublicChannelHint => 'このコミュニティの公開チャンネルを自動で追加します'; @override - String get community_noCommunities => 'まだコミュニティは形成されていません。'; + String get community_noCommunities => 'まだコミュニティはありません'; @override - String get community_scanOrCreate => 'QRコードをスキャンするか、コミュニティを作成して開始してください。'; + String get community_scanOrCreate => 'QR コードをスキャンするか、コミュニティを作成して始めてください。'; @override - String get community_manageCommunities => 'コミュニティの管理'; + String get community_manageCommunities => 'コミュニティを管理'; @override - String get community_delete => 'コミュニティからの離脱'; + String get community_delete => 'コミュニティを離脱'; @override String community_deleteConfirm(String name) { - return '$nameを辞める?'; + return '$name から離脱しますか?'; } @override @@ -3522,62 +3514,62 @@ class AppLocalizationsJa extends AppLocalizations { @override String community_deleted(String name) { - return 'コミュニティ「$name」を離れる'; + return 'コミュニティ「$name」から離脱しました'; } @override - String get community_regenerateSecret => '秘密の復元'; + String get community_regenerateSecret => '秘密鍵を再生成'; @override String community_regenerateSecretConfirm(String name) { - return '$name の秘密鍵を再生成しますか? 継続的に通信するため、すべてのメンバーは新しいQRコードをスキャンする必要があります。'; + return '$name の秘密鍵を再生成しますか? 継続して通信するには、すべてのメンバーが新しい QR コードをスキャンする必要があります。'; } @override - String get community_regenerate => '再生'; + String get community_regenerate => '再生成'; @override String community_secretRegenerated(String name) { - return '$name への秘密が再設定されました'; + return '$name の秘密鍵を再生成しました'; } @override - String get community_updateSecret => '秘密情報の更新'; + String get community_updateSecret => '秘密鍵を更新'; @override String community_secretUpdated(String name) { - return '$name 向けの秘密設定を更新'; + return '$name の秘密設定を更新しました'; } @override String community_scanToUpdateSecret(String name) { - return '新しいQRコードをスキャンして、$nameの秘密情報を更新してください。'; + return '新しい QR コードをスキャンして $name の秘密設定を更新してください。'; } @override - String get community_addHashtagChannel => 'コミュニティのハッシュタグを追加'; + String get community_addHashtagChannel => 'ハッシュタグチャンネルを追加'; @override - String get community_addHashtagChannelDesc => 'このコミュニティ用のハッシュタグチャンネルを追加する'; + String get community_addHashtagChannelDesc => 'このコミュニティ用のハッシュタグチャンネルを追加します'; @override String get community_selectCommunity => 'コミュニティを選択'; @override - String get community_regularHashtag => '定期的なハッシュタグ'; + String get community_regularHashtag => '通常のハッシュタグ'; @override - String get community_regularHashtagDesc => '一般のハッシュタグ(誰でも参加可能)'; + String get community_regularHashtagDesc => '公開ハッシュタグ(誰でも参加可能)'; @override String get community_communityHashtag => 'コミュニティ用ハッシュタグ'; @override - String get community_communityHashtagDesc => 'コミュニティメンバーのみへの限定'; + String get community_communityHashtagDesc => 'コミュニティメンバーのみ利用できます'; @override String community_forCommunity(String name) { - return '$name 様'; + return '$name 向け'; } @override @@ -4236,40 +4228,40 @@ class AppLocalizationsJa extends AppLocalizations { String get translation_systemLanguage => 'システム言語'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore 実行中'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'BLE 接続を維持しています'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return '$name を削除しました'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return '削除に失敗しました: $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return 'チャンネルの更新に失敗しました: $error'; } @override - String get contact_typeChat => 'Chat'; + String get contact_typeChat => 'チャット'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => 'リピータ'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => 'ルーム'; @override - String get contact_typeSensor => 'Sensor'; + String get contact_typeSensor => 'センサー'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => '不明'; @override String get map_zoomIn => 'ズームイン'; @@ -4278,11 +4270,11 @@ class AppLocalizationsJa extends AppLocalizations { String get map_zoomOut => 'ズームアウト'; @override - String get map_centerMap => '中心地図'; + String get map_centerMap => '地図を中央に移動'; @override String get chrome_bluetoothRequiresChromium => - 'Web Bluetooth は、Chromium ブラウザが必要です。'; + 'Web Bluetooth には Chromium ベースのブラウザが必要です。'; @override String channels_communityShortId(String id) { @@ -4296,48 +4288,48 @@ class AppLocalizationsJa extends AppLocalizations { String get pathTrace_legendInferred => '推測される位置'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => '単独表示'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => '統合表示'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => '再生'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => '一時停止'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'リプレイ'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => '前のホップ'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => '次のホップ'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'パケットアニメーションを表示'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'パケットアニメーションを非表示'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return '$current/$total ホップ目'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return '観測された経路: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => '主要'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return '代替 $index'; } @override @@ -4345,8 +4337,8 @@ class AppLocalizationsJa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count ホップ', + one: '1 ホップ', ); return '$_temp0'; } @@ -4357,14 +4349,14 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => '共有セグメント'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => '概算のセグメント'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return '$count 経路で使用されています'; } @override @@ -4372,33 +4364,33 @@ class AppLocalizationsJa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count つのホップに位置情報がありません - 表示中の経路は一部です', + one: '1 つのホップに位置情報がありません - 表示中の経路は一部です', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'すべて表示'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => '経路を非表示'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => '経路を表示'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'パネルを折りたたむ'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'パネルを展開'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => '位置情報なし'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'パケットを追跡'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'パケットの追跡を解除'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 302ff925..f17ed6ee 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -24,10 +24,10 @@ class AppLocalizationsKo extends AppLocalizations { String get common_cancel => '취소'; @override - String get common_ok => '알겠습니다'; + String get common_ok => '확인'; @override - String get common_connect => '연결'; + String get common_connect => '연결하기'; @override String get common_unknownDevice => '알 수 없는 장치'; @@ -45,7 +45,7 @@ class AppLocalizationsKo extends AppLocalizations { String get common_close => '닫기'; @override - String get common_done => 'Done'; + String get common_done => '완료'; @override String get common_edit => '수정'; @@ -60,13 +60,13 @@ class AppLocalizationsKo extends AppLocalizations { String get common_disconnect => '연결 해제'; @override - String get common_connected => '연결된'; + String get common_connected => '연결됨'; @override - String get common_disconnected => '단절'; + String get common_disconnected => '연결 해제됨'; @override - String get common_create => '만들다'; + String get common_create => '만들기'; @override String get common_continue => '계속'; @@ -81,40 +81,40 @@ class AppLocalizationsKo extends AppLocalizations { String get common_retry => '다시 시도'; @override - String get common_hide => '숨기다'; + String get common_hide => '숨기기'; @override String get common_remove => '제거'; @override - String get common_enable => '활성화'; + String get common_enable => '사용'; @override - String get common_disable => '비활성화'; + String get common_disable => '사용 안 함'; @override - String get common_undo => '취소'; + String get common_undo => '되돌리기'; @override - String get messageStatus_sent => '발송'; + String get messageStatus_sent => '전송됨'; @override - String get messageStatus_delivered => '배송 완료'; + String get messageStatus_delivered => '전달됨'; @override - String get messageStatus_pending => '발송'; + String get messageStatus_pending => '전송 중'; @override - String get messageStatus_failed => '발송 실패'; + String get messageStatus_failed => '전송 실패'; @override - String get messageStatus_repeated => '반복적으로 들었습니다'; + String get messageStatus_repeated => '반복 수신됨'; @override String get common_reboot => '재부팅'; @override - String get common_loading => '로딩 중...'; + String get common_loading => '불러오는 중...'; @override String get common_notAvailable => '—'; @@ -136,7 +136,7 @@ class AppLocalizationsKo extends AppLocalizations { String get common_interval => '간격'; @override - String get scanner_title => 'MeshCore 공개'; + String get scanner_title => 'MeshCore Open'; @override String get connectionChoiceUsbLabel => 'USB'; @@ -154,16 +154,16 @@ class AppLocalizationsKo extends AppLocalizations { String get tcpHostLabel => 'IP 주소'; @override - String get tcpHostHint => '192.168.40.10'; + String get tcpHostHint => '192.168.40.10 / example.com'; @override - String get tcpPortLabel => '항'; + String get tcpPortLabel => '포트'; @override String get tcpPortHint => '5000'; @override - String get tcpStatus_notConnected => '목적지 주소 입력 후 연결'; + String get tcpStatus_notConnected => '엔드포인트를 입력한 뒤 연결하세요.'; @override String tcpStatus_connectingTo(String endpoint) { @@ -194,26 +194,26 @@ class AppLocalizationsKo extends AppLocalizations { String get usbScreenSubtitle => '감지된 시리얼 장치를 선택하고 MeshCore 노드에 직접 연결하십시오.'; @override - String get usbScreenStatus => 'USB 장치를 선택합니다.'; + String get usbScreenStatus => 'USB 장치를 선택하세요.'; @override - String get usbScreenNote => 'USB 직렬 통신은 지원되는 안드로이드 장치 및 데스크톱 플랫폼에서 활성화됩니다.'; + String get usbScreenNote => + 'USB 직렬 통신은 지원되는 Android 기기 및 데스크톱 플랫폼에서 사용할 수 있습니다.'; @override - String get usbScreenEmptyState => - 'USB 장치가 탐지되지 않았습니다. USB 장치를 연결하고 다시 시도해 보세요.'; + String get usbScreenEmptyState => 'USB 장치가 없습니다. 하나 연결한 뒤 새로고침하세요.'; @override String get usbErrorPermissionDenied => 'USB 접근 권한이 거부되었습니다.'; @override - String get usbErrorDeviceMissing => '선택한 USB 장치는 더 이상 사용 불가능합니다.'; + String get usbErrorDeviceMissing => '선택한 USB 장치를 더 이상 사용할 수 없습니다.'; @override String get usbErrorInvalidPort => '유효한 USB 장치를 선택하세요.'; @override - String get usbErrorBusy => '또 다른 USB 연결 요청이 이미 진행 중입니다.'; + String get usbErrorBusy => '다른 USB 연결 요청이 이미 진행 중입니다.'; @override String get usbErrorNotConnected => 'USB 장치가 연결되지 않았습니다.'; @@ -222,23 +222,23 @@ class AppLocalizationsKo extends AppLocalizations { String get usbErrorOpenFailed => '선택한 USB 장치를 열 수 없습니다.'; @override - String get usbErrorConnectFailed => '선택한 USB 장치에 연결에 실패했습니다.'; + String get usbErrorConnectFailed => '선택한 USB 장치에 연결하지 못했습니다.'; @override String get usbErrorUnsupported => '이 플랫폼에서는 USB 직렬 통신을 지원하지 않습니다.'; @override - String get usbErrorAlreadyActive => 'USB 연결이 이미 활성화되어 있습니다.'; + String get usbErrorAlreadyActive => 'USB 연결이 이미 활성 상태입니다.'; @override String get usbErrorNoDeviceSelected => 'USB 장치가 선택되지 않았습니다.'; @override - String get usbErrorPortClosed => 'USB 연결이 활성화되지 않았습니다.'; + String get usbErrorPortClosed => 'USB 연결이 열려 있지 않습니다.'; @override String get usbErrorConnectTimedOut => - '연결이 시간 초과되었습니다. 장치가 USB Companion 펌웨어를 가지고 있는지 확인해 주세요.'; + '연결 시간이 초과되었습니다. 장치에 USB Companion 펌웨어가 있는지 확인하세요.'; @override String get usbFallbackDeviceName => '웹 시리얼 장치'; @@ -292,30 +292,30 @@ class AppLocalizationsKo extends AppLocalizations { String get scanner_scan => '스캔'; @override - String get scanner_bluetoothOff => '블루투스는 꺼져 있습니다.'; + String get scanner_bluetoothOff => '블루투스가 꺼져 있습니다.'; @override - String get scanner_bluetoothOffMessage => '블루투스를 켜서 장치를 검색해주세요.'; + String get scanner_bluetoothOffMessage => '기기를 검색하려면 블루투스를 켜세요.'; @override - String get scanner_chromeRequired => '크롬 브라우저 필요'; + String get scanner_chromeRequired => 'Chrome 브라우저 필요'; @override String get scanner_chromeRequiredMessage => - '이 웹 애플리케이션은 블루투드 지원을 위해 Google Chrome 또는 Chromium 기반 브라우저가 필요합니다.'; + '이 웹 앱은 블루투스 지원을 위해 Google Chrome 또는 Chromium 기반 브라우저가 필요합니다.'; @override - String get scanner_enableBluetooth => '블루투스 활성화'; + String get scanner_enableBluetooth => '블루투스 켜기'; @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + '브라우저에서는 블루투스를 사용할 수 없습니다. 대신 USB로 연결하세요.'; @override String get device_quickSwitch => '빠른 전환'; @override - String get device_meshcore => '메쉬코어'; + String get device_meshcore => 'MeshCore'; @override String get settings_title => '설정'; @@ -348,7 +348,7 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_radioSettings => '라디오 설정'; @override - String get settings_radioSettingsSubtitle => '주파수, 전력, 스펙트럼'; + String get settings_radioSettingsSubtitle => '주파수, 전력, 확산 계수'; @override String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.'; @@ -454,32 +454,31 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_actions => '행동'; @override - String get settings_deleteAllPaths => 'Delete All Paths'; + String get settings_deleteAllPaths => '모든 경로 삭제'; @override - String get settings_deleteAllPathsSubtitle => - 'Clear all path data from contacts.'; + String get settings_deleteAllPathsSubtitle => '연락처의 모든 경로 데이터를 지웁니다.'; @override String get settings_sendAdvertisement => '광고 전송'; @override - String get settings_sendAdvertisementSubtitle => '방송 활동'; + String get settings_sendAdvertisementSubtitle => '현재 존재를 방송합니다.'; @override - String get settings_advertisementSent => '광고 전송'; + String get settings_advertisementSent => '광고가 전송되었습니다.'; @override - String get settings_syncTime => '동기화 시간'; + String get settings_syncTime => '시간 동기화'; @override String get settings_syncTimeSubtitle => '장치 시계를 휴대폰 시간으로 설정'; @override - String get settings_timeSynchronized => '시간 동기화'; + String get settings_timeSynchronized => '시간이 동기화되었습니다.'; @override - String get settings_refreshContacts => '연락처 갱신'; + String get settings_refreshContacts => '연락처 새로고침'; @override String get settings_refreshContactsSubtitle => '장치에서 연락처 목록을 다시 불러오기'; @@ -488,14 +487,13 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_rebootDevice => '장치 재부팅'; @override - String get settings_rebootDeviceSubtitle => 'MeshCore 장치를 재부팅하세요.'; + String get settings_rebootDeviceSubtitle => 'MeshCore 장치를 재부팅합니다.'; @override - String get settings_rebootDeviceConfirm => - '정말로 장치를 재부팅하시겠습니까? 이 경우 연결이 끊어집니다.'; + String get settings_rebootDeviceConfirm => '정말 장치를 재부팅하시겠습니까? 연결이 끊어집니다.'; @override - String get settings_debug => '디버깅'; + String get settings_debug => '디버그'; @override String get settings_companionDebugLog => '동반 디버깅 로그'; @@ -505,25 +503,25 @@ class AppLocalizationsKo extends AppLocalizations { 'BLE/TCP/USB 명령어, 응답 및 원시 데이터'; @override - String get settings_appDebugLog => '앱 디버깅 로그'; + String get settings_appDebugLog => '앱 디버그 로그'; @override - String get settings_appDebugLogSubtitle => '애플리케이션 디버깅 메시지'; + String get settings_appDebugLogSubtitle => '애플리케이션 디버그 메시지'; @override - String get settings_about => '소개'; + String get settings_about => '정보'; @override String settings_aboutVersion(String version) { - return 'MeshCore Open $version 버전'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => '2026년 MeshCore 오픈 소스 프로젝트'; + String get settings_aboutLegalese => '2026 MeshCore 오픈 소스 프로젝트'; @override String get settings_aboutDescription => - 'MeshCore LoRa 메시 네트워크 장치를 위한 오픈 소스 Flutter 클라이언트.'; + 'MeshCore LoRa 메시 네트워크 장치를 위한 오픈소스 Flutter 클라이언트.'; @override String get settings_aboutOpenMeteoAttribution => @@ -551,7 +549,7 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_infoChannelCount => '채널 수'; @override - String get settings_presets => '기본 설정'; + String get settings_presets => '프리셋'; @override String get settings_frequency => '주파수 (MHz)'; @@ -572,24 +570,24 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_codingRate => '코딩 속도'; @override - String get settings_txPower => 'TX 전력 (dBm)'; + String get settings_txPower => '송신 전력 (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => '유효하지 않은 TX 전력 (0-22 dBm)'; + String get settings_txPowerInvalid => '유효하지 않은 송신 전력 (0-22 dBm)'; @override String get settings_clientRepeat => '오프그리드 반복'; @override String get settings_clientRepeatSubtitle => - '이 장치가 다른 사람들을 위해 메시 패킷을 반복하도록 허용합니다.'; + '이 장치가 다른 장치의 메시 패킷을 반복하도록 허용합니다.'; @override String get settings_clientRepeatFreqWarning => - '오프그리드(무전력) 시스템 재연결에는 433MHz, 869MHz, 또는 918MHz 주파수가 필요합니다.'; + '오프그리드 반복에는 433MHz, 869MHz 또는 918MHz 주파수가 필요합니다.'; @override String settings_error(String message) { @@ -600,19 +598,19 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_title => '앱 설정'; @override - String get appSettings_appearance => '외관'; + String get appSettings_appearance => '모양'; @override - String get appSettings_theme => '주제'; + String get appSettings_theme => '테마'; @override - String get appSettings_themeSystem => '기본 설정'; + String get appSettings_themeSystem => '시스템 기본값'; @override - String get appSettings_themeLight => '빛'; + String get appSettings_themeLight => '밝음'; @override - String get appSettings_themeDark => '어둡다'; + String get appSettings_themeDark => '어두움'; @override String get appSettings_language => '언어'; @@ -633,7 +631,7 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_languageDe => '독일어'; @override - String get appSettings_languagePl => '폴란드'; + String get appSettings_languagePl => '폴란드어'; @override String get appSettings_languageSl => '슬로베니아어'; @@ -654,16 +652,16 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_languageNl => '네덜란드어'; @override - String get appSettings_languageSk => '슬로베니아어'; + String get appSettings_languageSk => '슬로바키아어'; @override - String get appSettings_languageBg => '불가리'; + String get appSettings_languageBg => '불가리아어'; @override String get appSettings_languageRu => '러시아어'; @override - String get appSettings_languageUk => '우크라이나'; + String get appSettings_languageUk => '우크라이나어'; @override String get appSettings_enableMessageTracing => '메시지 추적 기능 활성화'; @@ -685,10 +683,10 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_notificationPermissionDenied => '알림 권한 거부'; @override - String get appSettings_notificationsEnabled => '알림 기능 활성화'; + String get appSettings_notificationsEnabled => '알림 사용'; @override - String get appSettings_notificationsDisabled => '알림 기능 끄기'; + String get appSettings_notificationsDisabled => '알림 사용 안 함'; @override String get appSettings_messageNotifications => '메시지 알림'; @@ -714,23 +712,24 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_messaging => '메시징'; @override - String get appSettings_clearPathOnMaxRetry => 'Max 재시도 시 경로 명확하게 설정'; + String get appSettings_clearPathOnMaxRetry => '최대 재시도 시 경로 지우기'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - '5번의 전송 시도가 실패하면 연락 경로를 재설정'; + '전송 시도가 5번 실패하면 연락 경로를 재설정합니다.'; @override - String get appSettings_pathsWillBeCleared => '5번의 시도 실패 후, 해당 경로가 확보될 것입니다.'; + String get appSettings_pathsWillBeCleared => '5번 실패하면 해당 경로를 지웁니다.'; @override - String get appSettings_pathsWillNotBeCleared => '경로는 자동으로 정리되지 않습니다.'; + String get appSettings_pathsWillNotBeCleared => '경로를 자동으로 지우지 않습니다.'; @override String get appSettings_autoRouteRotation => '자동 경로 순환'; @override - String get appSettings_autoRouteRotationSubtitle => '최적 경로와 방수 모드 사이를 전환'; + String get appSettings_autoRouteRotationSubtitle => + '최적 경로와 플러드 모드 사이를 전환합니다.'; @override String get appSettings_autoRouteRotationEnabled => '자동 경로 순환 기능 활성화'; @@ -739,31 +738,31 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => '자동 경로 순환 기능 비활성화'; @override - String get appSettings_maxRouteWeight => '최대 경로 무게'; + String get appSettings_maxRouteWeight => '최대 경로 가중치'; @override String get appSettings_maxRouteWeightSubtitle => - '한 경로가 성공적인 배송을 통해 누적할 수 있는 최대 무게'; + '한 경로가 성공적인 전송을 통해 누적할 수 있는 최대 가중치'; @override String get appSettings_initialRouteWeight => '초기 경로 가중치'; @override - String get appSettings_initialRouteWeightSubtitle => '새롭게 발견된 경로의 초기 무게'; + String get appSettings_initialRouteWeightSubtitle => '새로 발견된 경로의 초기 가중치'; @override - String get appSettings_routeWeightSuccessIncrement => '성공 횟수 증가'; + String get appSettings_routeWeightSuccessIncrement => '성공 시 증가'; @override String get appSettings_routeWeightSuccessIncrementSubtitle => - '성공적으로 배송된 경로에 추가된 무게'; + '성공적으로 전송된 경로에 추가되는 가중치'; @override - String get appSettings_routeWeightFailureDecrement => '오류 가중치 감소'; + String get appSettings_routeWeightFailureDecrement => '실패 시 감소'; @override String get appSettings_routeWeightFailureDecrementSubtitle => - '배송 실패 후 경로에서 제거된 무게'; + '전송 실패 후 경로에서 제거되는 가중치'; @override String get appSettings_maxMessageRetries => '최대 메시지 재시도 횟수'; @@ -775,15 +774,16 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_battery => '배터리'; @override - String get appSettings_batteryChemistry => '배터리 화학'; + String get appSettings_batteryChemistry => '배터리 종류'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return '$deviceName 당분간'; + return '$deviceName별'; } @override - String get appSettings_batteryChemistryConnectFirst => '장치를 선택하기 위해 연결'; + String get appSettings_batteryChemistryConnectFirst => + '배터리 종류를 선택하려면 먼저 장치를 연결하세요.'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @@ -798,10 +798,10 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_mapDisplay => '지도 표시'; @override - String get appSettings_showRepeaters => '반복 기능 표시'; + String get appSettings_showRepeaters => '리피터 표시'; @override - String get appSettings_showRepeatersSubtitle => '지도에 반복자 노드를 표시'; + String get appSettings_showRepeatersSubtitle => '지도에 리피터 노드를 표시'; @override String get appSettings_showChatNodes => '채팅 노드 표시'; @@ -813,7 +813,7 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_showOtherNodes => '다른 노드 표시'; @override - String get appSettings_showOtherNodesSubtitle => '지도에서 다른 노드 유형을 표시'; + String get appSettings_showOtherNodesSubtitle => '지도에 다른 노드 유형을 표시'; @override String get appSettings_timeFilter => '시간 필터'; @@ -823,20 +823,20 @@ class AppLocalizationsKo extends AppLocalizations { @override String appSettings_timeFilterShowLast(int hours) { - return '지난 $hours 시간 동안의 노드 표시'; + return '최근 $hours시간 동안의 노드 표시'; } @override - String get appSettings_mapTimeFilter => '지도 필터'; + String get appSettings_mapTimeFilter => '지도 시간 필터'; @override - String get appSettings_showNodesDiscoveredWithin => '다음 내역에서 발견된 노드 표시:'; + String get appSettings_showNodesDiscoveredWithin => '다음 기간 내에 발견된 노드 표시:'; @override - String get appSettings_allTime => '모든 시간'; + String get appSettings_allTime => '전체 기간'; @override - String get appSettings_lastHour => '지난 시간'; + String get appSettings_lastHour => '지난 1시간'; @override String get appSettings_last6Hours => '지난 6시간'; @@ -854,10 +854,10 @@ class AppLocalizationsKo extends AppLocalizations { String get appSettings_unitsTitle => '단위'; @override - String get appSettings_unitsMetric => '단위 (m / km)'; + String get appSettings_unitsMetric => '미터법 (m / km)'; @override - String get appSettings_unitsImperial => '제국 (피트/마일)'; + String get appSettings_unitsImperial => '영국식 (ft / mi)'; @override String get appSettings_noAreaSelected => '선택된 영역 없음'; @@ -868,13 +868,14 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get appSettings_debugCard => '디버깅'; + String get appSettings_debugCard => '디버그'; @override - String get appSettings_appDebugLogging => '앱 디버깅 로깅'; + String get appSettings_appDebugLogging => '앱 디버그 로깅'; @override - String get appSettings_appDebugLoggingSubtitle => '로그 앱 디버깅 메시지 (문제 해결을 위한)'; + String get appSettings_appDebugLoggingSubtitle => + '문제 해결을 위한 앱 디버그 메시지를 기록합니다.'; @override String get appSettings_appDebugLoggingEnabled => '앱 디버깅 로깅 활성화'; @@ -999,7 +1000,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String contacts_lastSeenMinsAgo(int minutes) { - return '~ $minutes min.'; + return '~ $minutes분'; } @override @@ -1007,7 +1008,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String contacts_lastSeenHoursAgo(int hours) { - return '~ $hours hours'; + return '~ $hours시간'; } @override @@ -1076,7 +1077,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String channels_via(String path) { - return 'via $path'; + return '$path 경유'; } @override @@ -1427,7 +1428,7 @@ class AppLocalizationsKo extends AppLocalizations { String get debugFrame_textTypeCli => '명령줄 인터페이스 (CLI)'; @override - String get debugFrame_textTypePlain => '단순한'; + String get debugFrame_textTypePlain => '일반 텍스트'; @override String debugFrame_text(String text) { @@ -1467,109 +1468,109 @@ class AppLocalizationsKo extends AppLocalizations { String get routing_modeAuto => '자동'; @override - String get routing_modeFlood => '홍수'; + String get routing_modeFlood => '플러드'; @override - String get routing_modeManual => '사용 설명서'; + String get routing_modeManual => '수동'; @override String get routing_modeAutoHint => - '가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없을 경우에는 무작위로 경로를 선택합니다.'; + '가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없으면 플러드로 전환합니다.'; @override String get routing_modeFloodHint => - '모든 증폭기를 통해 방송됩니다. 가장 안정적이지만, 더 많은 송출 시간을 사용합니다.'; + '모든 중계기를 통해 방송합니다. 가장 안정적이지만 송신 시간을 더 많이 사용합니다.'; @override - String get routing_modeManualHint => '항상 설정하신 정확한 경로를 따라 이동합니다.'; + String get routing_modeManualHint => '항상 지정한 정확한 경로를 따릅니다.'; @override String get routing_currentRoute => '현재 경로'; @override - String get routing_directNoHops => '직접 연결 – 중계 장치 사용 없이'; + String get routing_directNoHops => '직접 연결 - 중계 없음'; @override String get routing_noPathYet => '아직 경로가 없습니다. 다음 메시지가 도착할 때까지 계속 탐색합니다.'; @override - String get routing_floodBroadcast => '모든 증폭기를 통해 방송'; + String get routing_floodBroadcast => '모든 중계기를 통해 방송'; @override String get routing_editPath => '경로 편집'; @override - String get routing_forgetPath => '길을 잊어라'; + String get routing_forgetPath => '경로 지우기'; @override String get routing_knownPaths => '알려진 경로'; @override - String get routing_knownPathsHint => '해당 항목으로 전환하기 위한 경로를 선택합니다.'; + String get routing_knownPathsHint => '전환할 경로를 선택하세요.'; @override String get routing_inUse => '사용 중'; @override - String get routing_qualityStrong => '강력한 첫 번째 단계'; + String get routing_qualityStrong => '매우 좋음'; @override - String get routing_qualityGood => '좋은 첫 시작'; + String get routing_qualityGood => '좋음'; @override - String get routing_qualityFair => '처음 시도'; + String get routing_qualityFair => '보통'; @override - String get routing_qualityWorked => '완료됨'; + String get routing_qualityWorked => '작동함'; @override - String get routing_qualityFlood => '홍수 피해 상황을 통해 들었습니다.'; + String get routing_qualityFlood => '플러드로 수신됨'; @override - String get routing_qualityUntested => '검증되지 않음'; + String get routing_qualityUntested => '미검증'; @override String routing_lastWorked(String when) { - return '$when에 일했습니다'; + return '$when에 작동'; } @override - String get routing_neverWorked => '확인되지 않음'; + String get routing_neverWorked => '아직 작동한 적 없음'; @override String routing_deliveryCounts(int successes, int failures) { - return '$successes delivered, $failures failed'; + return '$successes건 성공, $failures건 실패'; } @override - String get routing_floodDelivery => '홍수 피해 지역 배송'; + String get routing_floodDelivery => '플러드 전송'; @override String get pathEditor_title => '경로 만들기'; @override String pathEditor_hopCounter(int count) { - return '64개의 홉 중 $count'; + return '64개 중 $count 홉'; } @override String get pathEditor_noHops => - '현재 홉은 추가되지 않았습니다. 아래의 탭을 사용하여 순서대로 추가하거나, 홉 없이 바로 전송하려면 \"홉 없음\"으로 저장하십시오.'; + '아직 홉이 추가되지 않았습니다. 아래 탭을 사용해 순서대로 추가하거나, 홉 없이 바로 보내려면 \"홉 없음\"으로 저장하세요.'; @override - String get pathEditor_addHops => '홉을 순서대로 첨가해주세요.'; + String get pathEditor_addHops => '홉을 순서대로 추가하세요.'; @override - String get pathEditor_searchRepeaters => '반복 검색'; + String get pathEditor_searchRepeaters => '리피터 검색'; @override - String get pathEditor_advancedHex => '고급: 원시 헥스 경로'; + String get pathEditor_advancedHex => '고급: 원시 HEX 경로'; @override - String get pathEditor_hexLabel => '헥스 접두사'; + String get pathEditor_hexLabel => 'HEX 접두사'; @override - String get pathEditor_hexHelper => '각 홉마다 2개의 6자리 숫자, 쉼표로 구분'; + String get pathEditor_hexHelper => '각 홉마다 2개의 16진수 바이트, 쉼표로 구분'; @override String pathEditor_invalidTokens(String tokens) { @@ -1580,7 +1581,7 @@ class AppLocalizationsKo extends AppLocalizations { String get pathEditor_tooManyHops => '최대 64개의 홉'; @override - String get pathEditor_usePath => '이 경로를 사용하세요'; + String get pathEditor_usePath => '이 경로 사용'; @override String get pathEditor_removeHop => '홉 제거'; @@ -1589,16 +1590,16 @@ class AppLocalizationsKo extends AppLocalizations { String get pathEditor_unknownHop => '알 수 없는 중계기'; @override - String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결'; + String get chat_pathSavedLocally => '로컬에 저장되었습니다. 동기화할 장치에 연결하세요.'; @override - String get chat_pathDeviceConfirmed => '장치 확인 완료.'; + String get chat_pathDeviceConfirmed => '장치가 확인되었습니다.'; @override String get chat_pathDeviceNotConfirmed => '기기가 아직 확인되지 않았습니다.'; @override - String get chat_type => '종류'; + String get chat_type => '유형'; @override String get chat_path => '경로'; @@ -1610,18 +1611,18 @@ class AppLocalizationsKo extends AppLocalizations { String get chat_compressOutgoingMessages => '전송되는 메시지 압축'; @override - String get chat_floodForced => '홍수 (강제)'; + String get chat_floodForced => '플러드 (강제)'; @override - String get chat_directForced => '직접적인 (강제적인)'; + String get chat_directForced => '직접 (강제)'; @override String chat_hopsForced(int count) { - return '$count번 띄우기 (강제)'; + return '$count홉 (강제)'; } @override - String get chat_floodAuto => '홍수 (자동)'; + String get chat_floodAuto => '플러드 (자동)'; @override String get chat_direct => '직접'; @@ -1661,37 +1662,37 @@ class AppLocalizationsKo extends AppLocalizations { String get map_title => '노드 매핑'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => '노드 이름 또는 ID 검색'; @override - String get map_activity => 'Activity'; + String get map_activity => '활동'; @override - String get map_online => 'Online'; + String get map_online => '온라인'; @override - String get map_recent => 'Recent'; + String get map_recent => '최근'; @override - String get map_stale => 'Stale'; + String get map_stale => '오래됨'; @override - String get map_visible => 'Visible'; + String get map_visible => '보임'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => '숨김'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => '노드 중심으로 보기'; @override - String get map_details => 'Details'; + String get map_details => '세부 정보'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'GPS 없음'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => '일치하는 노드가 없습니다.'; @override String get map_lineOfSight => '시야'; @@ -1752,22 +1753,22 @@ class AppLocalizationsKo extends AppLocalizations { String get map_flags => '깃발'; @override - String get map_type => 'Type'; + String get map_type => '유형'; @override - String get map_path => 'Path'; + String get map_path => '경로'; @override - String get map_location => 'Location'; + String get map_location => '위치'; @override - String get map_estLocation => 'Est. Location'; + String get map_estLocation => '추정 위치'; @override - String get map_publicKey => 'Public Key'; + String get map_publicKey => '공개 키'; @override - String get map_publicKeyPrefixHint => 'e.g. ab12'; + String get map_publicKeyPrefixHint => '예: ab12'; @override String get map_shareMarkerHere => '여기에서 마커 공유'; @@ -1903,7 +1904,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Cached $downloaded tiles ($failed failed)'; + return '캐시된 타일 $downloaded개 ($failed개 실패)'; } @override @@ -1934,7 +1935,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String mapCache_downloadedTiles(int completed, int total) { - return 'Downloaded $completed / $total'; + return '다운로드됨 $completed / $total'; } @override @@ -1968,7 +1969,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String time_hoursAgo(int hours) { - return '${hours}h ago'; + return '$hours시간 전'; } @override @@ -2069,10 +2070,10 @@ class AppLocalizationsKo extends AppLocalizations { '로그인에 실패했습니다. 비밀번호가 잘못되었거나, 연결이 되지 않는 것 같습니다.'; @override - String get common_reload => '다시 로드'; + String get common_reload => '다시 불러오기'; @override - String get common_clear => '명확하게'; + String get common_clear => '지우기'; @override String get path_currentPathLabel => '현재 경로'; @@ -3314,7 +3315,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Heard: $time ago'; + return '수신: $time 전'; } @override @@ -3386,7 +3387,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed of $total hops'; + return '$observed/$total 홉 관찰됨'; } @override @@ -3687,7 +3688,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String losAntennaB(String value, String unit) { - return 'Antenna B: $value $unit'; + return '안테나 B: $value $unit'; } @override @@ -3703,7 +3704,7 @@ class AppLocalizationsKo extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit'; + return '$distance $distanceUnit, LOS 확보, 최소 여유 $clearance $heightUnit'; } @override @@ -3713,7 +3714,7 @@ class AppLocalizationsKo extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, blocked by $obstruction $heightUnit'; + return '$distance $distanceUnit, $obstruction $heightUnit에 의해 차단됨'; } @override @@ -3784,7 +3785,7 @@ class AppLocalizationsKo extends AppLocalizations { String distanceUnit, String distanceFromB, ) { - return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + return '$obstruction $heightUnit에 의해 차단됨, A에서 $distanceFromA, B에서 $distanceFromB ($distanceUnit)'; } @override @@ -4234,52 +4235,53 @@ class AppLocalizationsKo extends AppLocalizations { String get translation_systemLanguage => '시스템 언어'; @override - String get background_serviceTitle => 'MeshCore running'; + String get background_serviceTitle => 'MeshCore 실행 중'; @override - String get background_serviceText => 'Keeping BLE connected'; + String get background_serviceText => 'BLE 연결 유지 중'; @override String appSettings_translationModelDeleted(String name) { - return 'Deleted $name'; + return '$name 삭제됨'; } @override String appSettings_translationModelDeleteFailed(String error) { - return 'Failed to delete: $error'; + return '삭제 실패: $error'; } @override String channels_channelUpdateFailed(String error) { - return 'Failed to update channel: $error'; + return '채널 업데이트 실패: $error'; } @override - String get contact_typeChat => 'Chat'; + String get contact_typeChat => '채팅'; @override - String get contact_typeRepeater => 'Repeater'; + String get contact_typeRepeater => '리피터'; @override - String get contact_typeRoom => 'Room'; + String get contact_typeRoom => '룸'; @override - String get contact_typeSensor => 'Sensor'; + String get contact_typeSensor => '센서'; @override - String get contact_typeUnknown => 'Unknown'; + String get contact_typeUnknown => '알 수 없음'; @override - String get map_zoomIn => '줌 인'; + String get map_zoomIn => '확대'; @override - String get map_zoomOut => '줌 아웃'; + String get map_zoomOut => '축소'; @override - String get map_centerMap => '중심 지도'; + String get map_centerMap => '지도 중앙 맞추기'; @override - String get chrome_bluetoothRequiresChromium => '웹 블루투스는 크롬 브라우저가 필요합니다.'; + String get chrome_bluetoothRequiresChromium => + '웹 블루투스는 Chromium 기반 브라우저가 필요합니다.'; @override String channels_communityShortId(String id) { @@ -4287,54 +4289,54 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get pathTrace_legendGpsConfirmed => 'GPS 확인 완료'; + String get pathTrace_legendGpsConfirmed => 'GPS로 확인됨'; @override - String get pathTrace_legendInferred => '추론된 위치'; + String get pathTrace_legendInferred => '추정된 위치'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => '단일'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => '결합'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => '재생'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => '일시 정지'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => '다시 재생'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => '이전 홉'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => '다음 홉'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => '패킷 애니메이션 표시'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => '패킷 애니메이션 숨기기'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return '$current/$total 홉'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return '관찰된 경로: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => '주 경로'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return '대체 $index'; } @override @@ -4342,8 +4344,8 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count 홉', + one: '1 홉', ); return '$_temp0'; } @@ -4354,14 +4356,14 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => '공유 구간'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => '추정 구간'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return '$count개의 경로에서 사용됨'; } @override @@ -4369,33 +4371,33 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count 홉은 위치가 없어 표시된 경로가 일부입니다', + one: '1 홉은 위치가 없어 표시된 경로가 일부입니다', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => '모두 보기'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => '경로 숨기기'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => '경로 표시'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => '패널 접기'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => '패널 펼치기'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => '위치 없음'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => '패킷 고정'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => '패킷 고정 해제'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 9a8f1615..bf0a47eb 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -319,7 +319,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Bluetooth is niet beschikbaar in de browser. Verbind dan via USB.'; @override String get device_quickSwitch => 'Snelle overschakeling'; @@ -1726,10 +1726,10 @@ class AppLocalizationsNl extends AppLocalizations { String get map_title => 'Kaart van de knopen'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Zoek op naam of ID van de knoop'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Activiteit'; @override String get map_online => 'Online'; @@ -1738,25 +1738,25 @@ class AppLocalizationsNl extends AppLocalizations { String get map_recent => 'Recent'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Verouderd'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Zichtbaar'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Verborgen'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Centreer op node'; @override String get map_details => 'Details'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Geen GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Geen overeenkomende nodes'; @override String get map_lineOfSight => 'Zichtlijn'; @@ -4502,48 +4502,48 @@ class AppLocalizationsNl extends AppLocalizations { String get pathTrace_legendInferred => 'Afgeleide positie'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Enkel'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Gezamenlijk'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Afspelen'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Pauze'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Herhalen'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Vorige hop'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Volgende hop'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Pakketanimatie tonen'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Pakketanimatie verbergen'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Hop $current van $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Waargenomen paden: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primair'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternatief $index'; } @override @@ -4563,14 +4563,14 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Gedeeld segment'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Geschat segment'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Gebruikt door $count paden'; } @override @@ -4578,33 +4578,34 @@ class AppLocalizationsNl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count hops hebben geen locatie — het weergegeven pad is onvolledig', + one: '1 hop heeft geen locatie — het weergegeven pad is onvolledig', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Toon alles'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Verberg pad'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Toon pad'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Paneel inklappen'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Paneel uitklappen'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Geen locatie'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Weergave vergrendelen op pakket'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Weergave ontgrendelen van pakket'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2ec5378b..50b40989 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -324,7 +324,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Bluetooth nie jest dostępny w przeglądarce. Połącz się przez USB.'; @override String get device_quickSwitch => 'Szybka zmiana'; @@ -1755,37 +1755,37 @@ class AppLocalizationsPl extends AppLocalizations { String get map_title => 'Mapa węzłów'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Wyszukaj nazwę lub identyfikator węzła'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Aktywność'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Ostatnie'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Nieaktualne'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Widoczny'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Ukryty'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Wyśrodkuj na węźle'; @override - String get map_details => 'Details'; + String get map_details => 'Szczegóły'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Brak GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Brak pasujących węzłów'; @override String get map_lineOfSight => 'Linia wzroku'; @@ -4540,48 +4540,48 @@ class AppLocalizationsPl extends AppLocalizations { String get pathTrace_legendInferred => 'Wywnioskowana pozycja'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Pojedyncza'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Połączone'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Odtwórz'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Wstrzymaj'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Odtwórz ponownie'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Poprzedni skok'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Następny skok'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Pokaż animację pakietu'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Ukryj animację pakietu'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Skok $current z $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Obserwowane trasy: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Główna'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alt. $index'; } @override @@ -4589,8 +4589,10 @@ class AppLocalizationsPl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count skoku', + many: '$count skoków', + few: '$count skoki', + one: '1 skok', ); return '$_temp0'; } @@ -4601,14 +4603,14 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Wspólny segment'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Szacunkowy segment'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Wykorzystywane przez $count ścieżek'; } @override @@ -4616,33 +4618,38 @@ class AppLocalizationsPl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count skoku nie ma lokalizacji — pokazana ścieżka jest niekompletna', + many: + '$count skoków nie ma lokalizacji — pokazana ścieżka jest niekompletna', + few: + '$count skoki nie mają lokalizacji — pokazana ścieżka jest niekompletna', + one: '1 skok nie ma lokalizacji — pokazana ścieżka jest niekompletna', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Pokaż wszystkie'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Ukryj ścieżkę'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Wyświetl trasę'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Zwiń panel'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Rozwiń panel'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Brak lokalizacji'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Śledź pakiet'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Przestań śledzić pakiet'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 3eae0e3c..4e5aa75f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -322,7 +322,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'A funcionalidade Bluetooth não está disponível no navegador. Conecte-se via USB em vez disso.'; @override String get device_quickSwitch => 'Mudar rapidamente'; @@ -1739,37 +1739,37 @@ class AppLocalizationsPt extends AppLocalizations { String get map_title => 'Mapa de Nós'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Pesquisar por nome ou ID do nó'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Atividade'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Recente'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Vencido'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Visível'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Escondido'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Centralizar no nó'; @override - String get map_details => 'Details'; + String get map_details => 'Detalhes'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Sem GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Nenhum nó encontrado'; @override String get map_lineOfSight => 'Linha de visão'; @@ -4519,44 +4519,44 @@ class AppLocalizationsPt extends AppLocalizations { String get pathTrace_legendInferred => 'Posição inferida'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Único'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Combinado'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Reproduzir'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Pausa'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Repetir'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Salto anterior'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Próximo salto'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Exibir animação do pacote'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Ocultar a animação do pacote'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Salto $current de $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Caminhos observados: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primário'; @override String pathMap_alternate(int index) { @@ -4568,8 +4568,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count saltos', + one: '1 salto', ); return '$_temp0'; } @@ -4580,14 +4580,14 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Segmento compartilhado'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Segmento estimado'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Utilizado em $count caminhos'; } @override @@ -4595,33 +4595,33 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count saltos não têm localização — o caminho mostrado é parcial', + one: '1 salto não tem localização — o caminho mostrado é parcial', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Mostrar tudo'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Esconder caminho'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Mostrar o caminho'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Recolher painel'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Expandir painel'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Sem localização'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Fixar vista no pacote'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Liberar vista do pacote'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b61320c9..f5e6fd8e 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -322,7 +322,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Bluetooth недоступен в браузере. Подключитесь через USB.'; @override String get device_quickSwitch => 'Быстрое переключение'; @@ -1744,37 +1744,37 @@ class AppLocalizationsRu extends AppLocalizations { String get map_title => 'Карта нод'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Поиск по имени или ID узла'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Активность'; @override - String get map_online => 'Online'; + String get map_online => 'Онлайн'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Недавно'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Устаревший'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Видимый'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Скрытый'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Центрировать на узле'; @override - String get map_details => 'Details'; + String get map_details => 'Детали'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Без GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Не найдено соответствующих узлов'; @override String get map_lineOfSight => 'Линия видимости'; @@ -4535,48 +4535,48 @@ class AppLocalizationsRu extends AppLocalizations { String get pathTrace_legendInferred => 'Выведенная позиция'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Одиночный'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Объединённые'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Воспроизвести'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Пауза'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Повтор'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Предыдущий хоп'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Следующий хоп'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Показать анимацию пакета'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Скрыть анимацию пакета'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Хоп $current из $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Наблюдаемые маршруты: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Основной'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Альт $index'; } @override @@ -4584,8 +4584,10 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count хопов', + many: '$count хопов', + few: '$count хопа', + one: '$count хоп', ); return '$_temp0'; } @@ -4596,14 +4598,14 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Общий сегмент'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Расчётный сегмент'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Используется в $count маршрутах'; } @override @@ -4611,33 +4613,35 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count хопов не имеют координат — показанный путь неполный', + many: '$count хопов не имеют координат — показанный путь неполный', + few: '$count хопа не имеют координат — показанный путь неполный', + one: '$count хоп не имеет координат — показанный путь неполный', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Показать всё'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Скрыть путь'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Показать маршрут'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Скрыть панель'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Расширить панель'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Нет координат'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Следить за пакетом'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Не следить за пакетом'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 71286d55..789d3586 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -321,7 +321,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Funkcia Bluetooth nie je dostupná v prehliadači. Prepojte sa pomocou USB.'; @override String get device_quickSwitch => 'Rýchle prepínač'; @@ -1731,37 +1731,37 @@ class AppLocalizationsSk extends AppLocalizations { String get map_title => 'Mapa uzlov'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Vyhľadajte podľa názvu alebo ID uzla'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Aktivita'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Nedávne'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Neaktuálne'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Viditeľný'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Skrytý'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Nacentrovať na uzol'; @override - String get map_details => 'Details'; + String get map_details => 'Podrobnosti'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Bez GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Nenašli sa žiadne zodpovedajúce uzly.'; @override String get map_lineOfSight => 'Úroveň výhľadu'; @@ -4501,48 +4501,48 @@ class AppLocalizationsSk extends AppLocalizations { String get pathTrace_legendInferred => 'Odvodená poloha'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Jednotlivý'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Spojené'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Prehrať'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Pozastaviť'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Prehrať znova'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Predchádzajúci skok'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Nasledujúci skok'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Zobraziť animáciu paketu'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Skryť animáciu paketu'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Skok $current z $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Pozorované cesty: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primárna'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternatívny $index'; } @override @@ -4550,8 +4550,9 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count skokov', + few: '$count skoky', + one: '1 skok', ); return '$_temp0'; } @@ -4562,14 +4563,14 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Spoločný segment'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Odhadovaný segment'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Používané $count cestami'; } @override @@ -4577,33 +4578,34 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count skokov nemá polohu — zobrazená trasa je neúplná', + few: '$count skoky nemajú polohu — zobrazená trasa je neúplná', + one: '1 skok nemá polohu — zobrazená trasa je neúplná', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Zobraziť všetky'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Skryť cestu'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Zobraziť trasu'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Zatvoriť panel'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Rozbaliť panel'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Bez polohy'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Uzamknúť pohľad na paket'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Odomknúť pohľad od paketu'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 8f09c5ce..46718ece 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -320,7 +320,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Funkcija Bluetooth v brskalniku ni na voljo. Povežite se preko USB-ja namesto tega.'; @override String get device_quickSwitch => 'Hitro preklop'; @@ -1724,37 +1724,37 @@ class AppLocalizationsSl extends AppLocalizations { String get map_title => 'Mapa omrežja'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Iščite ime ali ID vozlišča'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Dejavnost'; @override - String get map_online => 'Online'; + String get map_online => 'V omrežju'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Nedavni'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Zastarelo'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Vidno'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Skrit'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Centriraj na vozlišče'; @override - String get map_details => 'Details'; + String get map_details => 'Podrobnosti'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Brez GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Ni ujemajočih se vozlišč'; @override String get map_lineOfSight => 'Linija vida'; @@ -4500,48 +4500,48 @@ class AppLocalizationsSl extends AppLocalizations { String get pathTrace_legendInferred => 'Izpeljana lokacija'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Posamično'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Skupno'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Predvajaj'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Premor'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Ponovitev'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Prejšnji skok'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Naslednji skok'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Prikaži animacijo paketa'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Skrij animacijo paketa'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Skok $current od $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Opazovane poti: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primarna'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternativa $index'; } @override @@ -4549,8 +4549,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count skokov', + few: '$count skoki', + two: '2 skoka', + one: '1 skok', ); return '$_temp0'; } @@ -4561,14 +4563,14 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Deljen segment'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Ocenjen segment'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Uporablja $count poti'; } @override @@ -4576,33 +4578,35 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count skokov nima lokacije — prikazana pot je delna', + few: '$count skoki nimajo lokacije — prikazana pot je delna', + two: '2 skoka nimata lokacije — prikazana pot je delna', + one: '1 skok nima lokacije — prikazana pot je delna', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Pokaži vse'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Skrij pot'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Pokaži pot'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Strni ploščo'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Razširi ploščo'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Brez lokacije'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Zakleni pogled na paket'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Odkleni pogled od paketa'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 4a281eef..56ef80d5 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -318,7 +318,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Bluetooth är inte tillgängligt i webbläsaren. Anslut istället via USB.'; @override String get device_quickSwitch => 'Snabb växling'; @@ -1717,37 +1717,37 @@ class AppLocalizationsSv extends AppLocalizations { String get map_title => 'Nodkarta'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Sök efter nodens namn eller ID'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Aktivitet'; @override String get map_online => 'Online'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Nyligen'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Inaktuell'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Synlig'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Dold'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Centrera på nod'; @override - String get map_details => 'Details'; + String get map_details => 'Detaljer'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Ingen GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Inga matchande noder'; @override String get map_lineOfSight => 'Synlinje'; @@ -4474,48 +4474,48 @@ class AppLocalizationsSv extends AppLocalizations { String get pathTrace_legendInferred => 'Antagen position'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Enkel'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Kombinerat'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Spela'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Pausa'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Återspela'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Föregående hopp'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Nästa hopp'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Visa paketanimering'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Dölj paketanimering'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Hopp $current av $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Observerade vägar: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Primär'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Alternativ $index'; } @override @@ -4523,8 +4523,8 @@ class AppLocalizationsSv extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count hopp', + one: '1 hopp', ); return '$_temp0'; } @@ -4535,14 +4535,14 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Delat segment'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Uppskattat segment'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Används av $count vägar'; } @override @@ -4550,33 +4550,33 @@ class AppLocalizationsSv extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count hopp saknar position — den visade vägen är ofullständig', + one: '1 hopp saknar position — den visade vägen är ofullständig', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Visa allt'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Dölj väg'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Visa väg'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Fäll ihop panel'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Expandera panel'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Ingen position'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Lås vy till paket'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Lås upp vy från paket'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 6471e563..961ca982 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -321,7 +321,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + 'Bluetooth недоступний у браузері. Підключіться через USB.'; @override String get device_quickSwitch => 'Швидке перемикання'; @@ -1737,37 +1737,37 @@ class AppLocalizationsUk extends AppLocalizations { String get map_title => 'Карта вузлів'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => 'Назва або ID вузла'; @override - String get map_activity => 'Activity'; + String get map_activity => 'Активність'; @override - String get map_online => 'Online'; + String get map_online => 'Онлайн'; @override - String get map_recent => 'Recent'; + String get map_recent => 'Нещодавні'; @override - String get map_stale => 'Stale'; + String get map_stale => 'Застаріло'; @override - String get map_visible => 'Visible'; + String get map_visible => 'Видимий'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => 'Прихований'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => 'Центрувати на вузлі'; @override - String get map_details => 'Details'; + String get map_details => 'Деталі'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => 'Без GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => 'Не знайдено відповідних вузлів'; @override String get map_lineOfSight => 'Пряма видимість'; @@ -4534,48 +4534,48 @@ class AppLocalizationsUk extends AppLocalizations { String get pathTrace_legendInferred => 'Висновок щодо положення'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => 'Один'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => 'Об\'єднаний'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => 'Відтворити'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => 'Призупинити'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => 'Повтор'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => 'Попередній перехід'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => 'Наступний перехід'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => 'Відобразити анімацію пакета'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => 'Приховати анімацію пакета'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return 'Перехід $current з $total'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return 'Зафіксовані маршрути: $count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => 'Основний'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return 'Альт. $index'; } @override @@ -4583,8 +4583,10 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count переходів', + many: '$count переходів', + few: '$count переходи', + one: '1 перехід', ); return '$_temp0'; } @@ -4595,14 +4597,14 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => 'Об\'єднаний сегмент'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => 'Орієнтовний сегмент'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return 'Використовується $count шляхами'; } @override @@ -4610,33 +4612,36 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: + '$count переходів не мають геопозиції — показаний шлях є частковим', + many: '$count переходів не мають геопозиції — показаний шлях є частковим', + few: '$count переходи не мають геопозиції — показаний шлях є частковим', + one: '1 перехід не має геопозиції — показаний шлях є частковим', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => 'Показати все'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => 'Приховати шлях'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => 'Показати шлях'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => 'Згорнути панель'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => 'Розгорнути панель'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => 'Без геопозиції'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => 'Прив\'язати вигляд до пакету'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => 'Відв\'язати вигляд від пакету'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9add4a3e..9d496d9a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -306,8 +306,7 @@ class AppLocalizationsZh extends AppLocalizations { String get scanner_enableBluetooth => '启用蓝牙'; @override - String get scanner_bluetoothWebUnsupported => - 'Bluetooth isn\'t available in the browser. Connect over USB instead.'; + String get scanner_bluetoothWebUnsupported => '浏览器不支持蓝牙,请改用 USB 连接。'; @override String get device_quickSwitch => '快速切换'; @@ -1639,37 +1638,37 @@ class AppLocalizationsZh extends AppLocalizations { String get map_title => '节点地图'; @override - String get map_searchHint => 'Search node name or ID'; + String get map_searchHint => '搜索节点名称或ID'; @override - String get map_activity => 'Activity'; + String get map_activity => '活动'; @override - String get map_online => 'Online'; + String get map_online => '在线'; @override - String get map_recent => 'Recent'; + String get map_recent => '最近'; @override - String get map_stale => 'Stale'; + String get map_stale => '过时'; @override - String get map_visible => 'Visible'; + String get map_visible => '可见'; @override - String get map_hidden => 'Hidden'; + String get map_hidden => '已隐藏'; @override - String get map_centerOnNode => 'Center on node'; + String get map_centerOnNode => '以节点为中心'; @override - String get map_details => 'Details'; + String get map_details => '详细信息'; @override - String get map_noGps => 'No GPS'; + String get map_noGps => '无 GPS'; @override - String get map_noResults => 'No matching nodes'; + String get map_noResults => '未找到匹配的节点'; @override String get map_lineOfSight => '视线'; @@ -4184,48 +4183,48 @@ class AppLocalizationsZh extends AppLocalizations { String get pathTrace_legendInferred => '推测的位置'; @override - String get pathMap_viewSingle => 'Single'; + String get pathMap_viewSingle => '单条'; @override - String get pathMap_viewCombined => 'Combined'; + String get pathMap_viewCombined => '综合'; @override - String get pathMap_play => 'Play'; + String get pathMap_play => '播放'; @override - String get pathMap_pause => 'Pause'; + String get pathMap_pause => '暂停'; @override - String get pathMap_replay => 'Replay'; + String get pathMap_replay => '重播'; @override - String get pathMap_stepBack => 'Previous hop'; + String get pathMap_stepBack => '上一跳'; @override - String get pathMap_stepForward => 'Next hop'; + String get pathMap_stepForward => '下一跳'; @override - String get pathMap_animationOn => 'Show packet animation'; + String get pathMap_animationOn => '显示数据包动画'; @override - String get pathMap_animationOff => 'Hide packet animation'; + String get pathMap_animationOff => '隐藏数据包动画'; @override String pathMap_hopOf(int current, int total) { - return 'Hop $current of $total'; + return '第 $current 跳,共 $total 跳'; } @override String pathMap_observedPaths(int count) { - return 'Observed paths: $count'; + return '观测到的路径:$count'; } @override - String get pathMap_primary => 'Primary'; + String get pathMap_primary => '主路径'; @override String pathMap_alternate(int index) { - return 'Alt $index'; + return '备用 $index'; } @override @@ -4233,8 +4232,8 @@ class AppLocalizationsZh extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops', - one: '1 hop', + other: '$count 跳', + one: '1 跳', ); return '$_temp0'; } @@ -4245,14 +4244,14 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get pathMap_legendShared => 'Shared segment'; + String get pathMap_legendShared => '共享路段'; @override - String get pathMap_legendEstimated => 'Estimated segment'; + String get pathMap_legendEstimated => '估算路段'; @override String pathMap_sharedNodeCount(int count) { - return 'Used by $count paths'; + return '已被 $count 条路径使用'; } @override @@ -4260,33 +4259,33 @@ class AppLocalizationsZh extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hops have no location — the shown path is partial', - one: '1 hop has no location — the shown path is partial', + other: '$count 跳无位置信息 — 显示的路径不完整', + one: '1 跳无位置信息 — 显示的路径不完整', ); return '$_temp0'; } @override - String get pathMap_showAllPaths => 'Show all'; + String get pathMap_showAllPaths => '显示全部'; @override - String get pathMap_hidePath => 'Hide path'; + String get pathMap_hidePath => '隐藏路径'; @override - String get pathMap_showPath => 'Show path'; + String get pathMap_showPath => '显示路径'; @override - String get pathMap_collapsePanel => 'Collapse panel'; + String get pathMap_collapsePanel => '收起面板'; @override - String get pathMap_expandPanel => 'Expand panel'; + String get pathMap_expandPanel => '展开面板'; @override - String get pathMap_noLocation => 'No location'; + String get pathMap_noLocation => '无位置'; @override - String get pathMap_followPacket => 'Lock view to packet'; + String get pathMap_followPacket => '锁定视图跟随数据包'; @override - String get pathMap_unfollowPacket => 'Unlock view from packet'; + String get pathMap_unfollowPacket => '解锁视图跟随'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7c72900b..8eb7f331 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2446,5 +2446,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth vereist een Chromium-browser.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS-locatie bevestigd", - "pathTrace_legendInferred": "Afgeleide positie" + "pathTrace_legendInferred": "Afgeleide positie", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_activity": "Activiteit", + "map_searchHint": "Zoek op naam of ID van de knoop", + "scanner_bluetoothWebUnsupported": "Bluetooth is niet beschikbaar in de browser. Verbind dan via USB.", + "map_online": "Online", + "map_recent": "Recent", + "map_stale": "Verouderd", + "map_visible": "Zichtbaar", + "map_hidden": "Verborgen", + "map_centerOnNode": "Centreer op node", + "map_details": "Details", + "map_noGps": "Geen GPS", + "map_noResults": "Geen overeenkomende nodes", + "pathMap_viewSingle": "Enkel", + "pathMap_viewCombined": "Gezamenlijk", + "pathMap_play": "Afspelen", + "pathMap_pause": "Pauze", + "pathMap_replay": "Herhalen", + "pathMap_stepBack": "Vorige hop", + "pathMap_stepForward": "Volgende hop", + "pathMap_animationOn": "Pakketanimatie tonen", + "pathMap_animationOff": "Pakketanimatie verbergen", + "pathMap_hopOf": "Hop {current} van {total}", + "pathMap_observedPaths": "Waargenomen paden: {count}", + "pathMap_primary": "Primair", + "pathMap_alternate": "Alternatief {index}", + "pathMap_hopCount": "{count, plural, =1{1 hop} other{{count} hops}}", + "pathMap_legendShared": "Gedeeld segment", + "pathMap_legendEstimated": "Geschat segment", + "pathMap_sharedNodeCount": "Gebruikt door {count} paden", + "pathMap_partialAnimation": "{count, plural, =1{1 hop heeft geen locatie — het weergegeven pad is onvolledig} other{{count} hops hebben geen locatie — het weergegeven pad is onvolledig}}", + "pathMap_showAllPaths": "Toon alles", + "pathMap_hidePath": "Verberg pad", + "pathMap_showPath": "Toon pad", + "pathMap_collapsePanel": "Paneel inklappen", + "pathMap_expandPanel": "Paneel uitklappen", + "pathMap_noLocation": "Geen locatie", + "pathMap_followPacket": "Weergave vergrendelen op pakket", + "pathMap_unfollowPacket": "Weergave ontgrendelen van pakket", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 92fc6797..d68df31d 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2484,5 +2484,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth wymaga przeglądarki Chromium.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS potwierdzone", - "pathTrace_legendInferred": "Wywnioskowana pozycja" + "pathTrace_legendInferred": "Wywnioskowana pozycja", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_online": "Online", + "map_searchHint": "Wyszukaj nazwę lub identyfikator węzła", + "scanner_bluetoothWebUnsupported": "Bluetooth nie jest dostępny w przeglądarce. Połącz się przez USB.", + "map_activity": "Aktywność", + "map_recent": "Ostatnie", + "map_stale": "Nieaktualne", + "map_visible": "Widoczny", + "map_hidden": "Ukryty", + "map_centerOnNode": "Wyśrodkuj na węźle", + "map_details": "Szczegóły", + "map_noGps": "Brak GPS", + "map_noResults": "Brak pasujących węzłów", + "pathMap_viewSingle": "Pojedyncza", + "pathMap_viewCombined": "Połączone", + "pathMap_play": "Odtwórz", + "pathMap_pause": "Wstrzymaj", + "pathMap_replay": "Odtwórz ponownie", + "pathMap_stepBack": "Poprzedni skok", + "pathMap_stepForward": "Następny skok", + "pathMap_animationOn": "Pokaż animację pakietu", + "pathMap_animationOff": "Ukryj animację pakietu", + "pathMap_hopOf": "Skok {current} z {total}", + "pathMap_observedPaths": "Obserwowane trasy: {count}", + "pathMap_primary": "Główna", + "pathMap_alternate": "Alt. {index}", + "pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoki} many{{count} skoków} other{{count} skoku}}", + "pathMap_legendShared": "Wspólny segment", + "pathMap_legendEstimated": "Szacunkowy segment", + "pathMap_sharedNodeCount": "Wykorzystywane przez {count} ścieżek", + "pathMap_partialAnimation": "{count, plural, =1{1 skok nie ma lokalizacji — pokazana ścieżka jest niekompletna} few{{count} skoki nie mają lokalizacji — pokazana ścieżka jest niekompletna} many{{count} skoków nie ma lokalizacji — pokazana ścieżka jest niekompletna} other{{count} skoku nie ma lokalizacji — pokazana ścieżka jest niekompletna}}", + "pathMap_showAllPaths": "Pokaż wszystkie", + "pathMap_hidePath": "Ukryj ścieżkę", + "pathMap_showPath": "Wyświetl trasę", + "pathMap_collapsePanel": "Zwiń panel", + "pathMap_expandPanel": "Rozwiń panel", + "pathMap_noLocation": "Brak lokalizacji", + "pathMap_followPacket": "Śledź pakiet", + "pathMap_unfollowPacket": "Przestań śledzić pakiet", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index e9c72cf4..43187cc9 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2446,5 +2446,99 @@ "chrome_bluetoothRequiresChromium": "O Web Bluetooth requer um navegador Chromium.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS confirmado", - "pathTrace_legendInferred": "Posição inferida" + "pathTrace_legendInferred": "Posição inferida", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_online": "Online", + "map_activity": "Atividade", + "scanner_bluetoothWebUnsupported": "A funcionalidade Bluetooth não está disponível no navegador. Conecte-se via USB em vez disso.", + "map_searchHint": "Pesquisar por nome ou ID do nó", + "map_recent": "Recente", + "map_stale": "Vencido", + "map_visible": "Visível", + "map_hidden": "Escondido", + "map_centerOnNode": "Centralizar no nó", + "map_details": "Detalhes", + "map_noGps": "Sem GPS", + "map_noResults": "Nenhum nó encontrado", + "pathMap_viewSingle": "Único", + "pathMap_viewCombined": "Combinado", + "pathMap_play": "Reproduzir", + "pathMap_pause": "Pausa", + "pathMap_stepBack": "Salto anterior", + "pathMap_replay": "Repetir", + "pathMap_stepForward": "Próximo salto", + "pathMap_animationOn": "Exibir animação do pacote", + "pathMap_animationOff": "Ocultar a animação do pacote", + "pathMap_hopOf": "Salto {current} de {total}", + "pathMap_observedPaths": "Caminhos observados: {count}", + "pathMap_primary": "Primário", + "pathMap_alternate": "Alt {index}", + "pathMap_hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}", + "pathMap_legendShared": "Segmento compartilhado", + "pathMap_legendEstimated": "Segmento estimado", + "pathMap_sharedNodeCount": "Utilizado em {count} caminhos", + "pathMap_partialAnimation": "{count, plural, =1{1 salto não tem localização — o caminho mostrado é parcial} other{{count} saltos não têm localização — o caminho mostrado é parcial}}", + "pathMap_showAllPaths": "Mostrar tudo", + "pathMap_hidePath": "Esconder caminho", + "pathMap_showPath": "Mostrar o caminho", + "pathMap_collapsePanel": "Recolher painel", + "pathMap_expandPanel": "Expandir painel", + "pathMap_noLocation": "Sem localização", + "pathMap_followPacket": "Fixar vista no pacote", + "pathMap_unfollowPacket": "Liberar vista do pacote", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 58ddacc8..8a8f85aa 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1749,5 +1749,99 @@ "chrome_bluetoothRequiresChromium": "Для работы Web Bluetooth требуется браузер на основе Chromium.", "channels_communityShortId": "Идентификатор: {id}...", "pathTrace_legendGpsConfirmed": "GPS подтверждено", - "pathTrace_legendInferred": "Выведенная позиция" + "pathTrace_legendInferred": "Выведенная позиция", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_searchHint": "Поиск по имени или ID узла", + "map_online": "Онлайн", + "scanner_bluetoothWebUnsupported": "Bluetooth недоступен в браузере. Подключитесь через USB.", + "map_activity": "Активность", + "map_recent": "Недавно", + "map_stale": "Устаревший", + "map_visible": "Видимый", + "map_hidden": "Скрытый", + "map_centerOnNode": "Центрировать на узле", + "map_details": "Детали", + "map_noGps": "Без GPS", + "map_noResults": "Не найдено соответствующих узлов", + "pathMap_viewSingle": "Одиночный", + "pathMap_viewCombined": "Объединённые", + "pathMap_play": "Воспроизвести", + "pathMap_pause": "Пауза", + "pathMap_replay": "Повтор", + "pathMap_stepBack": "Предыдущий хоп", + "pathMap_stepForward": "Следующий хоп", + "pathMap_animationOn": "Показать анимацию пакета", + "pathMap_animationOff": "Скрыть анимацию пакета", + "pathMap_hopOf": "Хоп {current} из {total}", + "pathMap_observedPaths": "Наблюдаемые маршруты: {count}", + "pathMap_primary": "Основной", + "pathMap_alternate": "Альт {index}", + "pathMap_hopCount": "{count, plural, one{{count} хоп} few{{count} хопа} many{{count} хопов} other{{count} хопов}}", + "pathMap_legendShared": "Общий сегмент", + "pathMap_legendEstimated": "Расчётный сегмент", + "pathMap_sharedNodeCount": "Используется в {count} маршрутах", + "pathMap_partialAnimation": "{count, plural, one{{count} хоп не имеет координат — показанный путь неполный} few{{count} хопа не имеют координат — показанный путь неполный} many{{count} хопов не имеют координат — показанный путь неполный} other{{count} хопов не имеют координат — показанный путь неполный}}", + "pathMap_showAllPaths": "Показать всё", + "pathMap_hidePath": "Скрыть путь", + "pathMap_collapsePanel": "Скрыть панель", + "pathMap_showPath": "Показать маршрут", + "pathMap_expandPanel": "Расширить панель", + "pathMap_noLocation": "Нет координат", + "pathMap_followPacket": "Следить за пакетом", + "pathMap_unfollowPacket": "Не следить за пакетом", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 2a8ac872..f6852078 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2446,5 +2446,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth vyžaduje prehliadač Chromium.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS potvrdilo", - "pathTrace_legendInferred": "Odvodená poloha" + "pathTrace_legendInferred": "Odvodená poloha", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_online": "Online", + "scanner_bluetoothWebUnsupported": "Funkcia Bluetooth nie je dostupná v prehliadači. Prepojte sa pomocou USB.", + "map_searchHint": "Vyhľadajte podľa názvu alebo ID uzla", + "map_activity": "Aktivita", + "map_recent": "Nedávne", + "map_stale": "Neaktuálne", + "map_hidden": "Skrytý", + "map_visible": "Viditeľný", + "map_centerOnNode": "Nacentrovať na uzol", + "map_details": "Podrobnosti", + "map_noGps": "Bez GPS", + "map_noResults": "Nenašli sa žiadne zodpovedajúce uzly.", + "pathMap_viewSingle": "Jednotlivý", + "pathMap_viewCombined": "Spojené", + "pathMap_play": "Prehrať", + "pathMap_pause": "Pozastaviť", + "pathMap_replay": "Prehrať znova", + "pathMap_stepBack": "Predchádzajúci skok", + "pathMap_stepForward": "Nasledujúci skok", + "pathMap_animationOn": "Zobraziť animáciu paketu", + "pathMap_animationOff": "Skryť animáciu paketu", + "pathMap_hopOf": "Skok {current} z {total}", + "pathMap_observedPaths": "Pozorované cesty: {count}", + "pathMap_primary": "Primárna", + "pathMap_alternate": "Alternatívny {index}", + "pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoky} other{{count} skokov}}", + "pathMap_legendShared": "Spoločný segment", + "pathMap_legendEstimated": "Odhadovaný segment", + "pathMap_sharedNodeCount": "Používané {count} cestami", + "pathMap_partialAnimation": "{count, plural, =1{1 skok nemá polohu — zobrazená trasa je neúplná} few{{count} skoky nemajú polohu — zobrazená trasa je neúplná} other{{count} skokov nemá polohu — zobrazená trasa je neúplná}}", + "pathMap_showAllPaths": "Zobraziť všetky", + "pathMap_hidePath": "Skryť cestu", + "pathMap_showPath": "Zobraziť trasu", + "pathMap_collapsePanel": "Zatvoriť panel", + "pathMap_expandPanel": "Rozbaliť panel", + "pathMap_noLocation": "Bez polohy", + "pathMap_followPacket": "Uzamknúť pohľad na paket", + "pathMap_unfollowPacket": "Odomknúť pohľad od paketu", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 0c7ce198..e423ab67 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2446,5 +2446,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth zahteva brskalnik Chromium.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS potrdilo", - "pathTrace_legendInferred": "Izpeljana lokacija" + "pathTrace_legendInferred": "Izpeljana lokacija", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "scanner_bluetoothWebUnsupported": "Funkcija Bluetooth v brskalniku ni na voljo. Povežite se preko USB-ja namesto tega.", + "map_searchHint": "Iščite ime ali ID vozlišča", + "map_online": "V omrežju", + "map_activity": "Dejavnost", + "map_recent": "Nedavni", + "map_stale": "Zastarelo", + "map_visible": "Vidno", + "map_hidden": "Skrit", + "map_centerOnNode": "Centriraj na vozlišče", + "map_details": "Podrobnosti", + "map_noGps": "Brez GPS", + "map_noResults": "Ni ujemajočih se vozlišč", + "pathMap_viewSingle": "Posamično", + "pathMap_viewCombined": "Skupno", + "pathMap_play": "Predvajaj", + "pathMap_pause": "Premor", + "pathMap_replay": "Ponovitev", + "pathMap_stepBack": "Prejšnji skok", + "pathMap_stepForward": "Naslednji skok", + "pathMap_animationOn": "Prikaži animacijo paketa", + "pathMap_animationOff": "Skrij animacijo paketa", + "pathMap_hopOf": "Skok {current} od {total}", + "pathMap_observedPaths": "Opazovane poti: {count}", + "pathMap_primary": "Primarna", + "pathMap_alternate": "Alternativa {index}", + "pathMap_legendShared": "Deljen segment", + "pathMap_legendEstimated": "Ocenjen segment", + "pathMap_sharedNodeCount": "Uporablja {count} poti", + "pathMap_partialAnimation": "{count, plural, =1{1 skok nima lokacije — prikazana pot je delna} =2{2 skoka nimata lokacije — prikazana pot je delna} few{{count} skoki nimajo lokacije — prikazana pot je delna} other{{count} skokov nima lokacije — prikazana pot je delna}}", + "pathMap_hopCount": "{count, plural, =1{1 skok} =2{2 skoka} few{{count} skoki} other{{count} skokov}}", + "pathMap_showAllPaths": "Pokaži vse", + "pathMap_hidePath": "Skrij pot", + "pathMap_showPath": "Pokaži pot", + "pathMap_collapsePanel": "Strni ploščo", + "pathMap_expandPanel": "Razširi ploščo", + "pathMap_noLocation": "Brez lokacije", + "pathMap_followPacket": "Zakleni pogled na paket", + "pathMap_unfollowPacket": "Odkleni pogled od paketa", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index e5ae55d8..5366d0e0 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2446,5 +2446,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth kräver en Chromium-baserad webbläsare.", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS-verifierat", - "pathTrace_legendInferred": "Antagen position" + "pathTrace_legendInferred": "Antagen position", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_online": "Online", + "scanner_bluetoothWebUnsupported": "Bluetooth är inte tillgängligt i webbläsaren. Anslut istället via USB.", + "map_activity": "Aktivitet", + "map_searchHint": "Sök efter nodens namn eller ID", + "map_recent": "Nyligen", + "map_stale": "Inaktuell", + "map_visible": "Synlig", + "map_hidden": "Dold", + "map_centerOnNode": "Centrera på nod", + "map_details": "Detaljer", + "map_noGps": "Ingen GPS", + "map_noResults": "Inga matchande noder", + "pathMap_viewSingle": "Enkel", + "pathMap_viewCombined": "Kombinerat", + "pathMap_play": "Spela", + "pathMap_pause": "Pausa", + "pathMap_replay": "Återspela", + "pathMap_stepBack": "Föregående hopp", + "pathMap_stepForward": "Nästa hopp", + "pathMap_animationOn": "Visa paketanimering", + "pathMap_animationOff": "Dölj paketanimering", + "pathMap_hopOf": "Hopp {current} av {total}", + "pathMap_observedPaths": "Observerade vägar: {count}", + "pathMap_primary": "Primär", + "pathMap_alternate": "Alternativ {index}", + "pathMap_hopCount": "{count, plural, =1{1 hopp} other{{count} hopp}}", + "pathMap_legendShared": "Delat segment", + "pathMap_legendEstimated": "Uppskattat segment", + "pathMap_sharedNodeCount": "Används av {count} vägar", + "pathMap_partialAnimation": "{count, plural, =1{1 hopp saknar position — den visade vägen är ofullständig} other{{count} hopp saknar position — den visade vägen är ofullständig}}", + "pathMap_showAllPaths": "Visa allt", + "pathMap_hidePath": "Dölj väg", + "pathMap_showPath": "Visa väg", + "pathMap_collapsePanel": "Fäll ihop panel", + "pathMap_expandPanel": "Expandera panel", + "pathMap_noLocation": "Ingen position", + "pathMap_unfollowPacket": "Lås upp vy från paket", + "pathMap_followPacket": "Lås vy till paket", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 4ceecbb2..8f82d3d6 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2426,5 +2426,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth вимагає браузера на основі Chromium", "channels_communityShortId": "ID: {id}...", "pathTrace_legendGpsConfirmed": "GPS підтверджено", - "pathTrace_legendInferred": "Висновок щодо положення" + "pathTrace_legendInferred": "Висновок щодо положення", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "scanner_bluetoothWebUnsupported": "Bluetooth недоступний у браузері. Підключіться через USB.", + "map_searchHint": "Назва або ID вузла", + "map_activity": "Активність", + "map_online": "Онлайн", + "map_recent": "Нещодавні", + "map_stale": "Застаріло", + "map_visible": "Видимий", + "map_hidden": "Прихований", + "map_centerOnNode": "Центрувати на вузлі", + "map_details": "Деталі", + "map_noGps": "Без GPS", + "map_noResults": "Не знайдено відповідних вузлів", + "pathMap_viewSingle": "Один", + "pathMap_viewCombined": "Об'єднаний", + "pathMap_play": "Відтворити", + "pathMap_pause": "Призупинити", + "pathMap_replay": "Повтор", + "pathMap_stepForward": "Наступний перехід", + "pathMap_stepBack": "Попередній перехід", + "pathMap_animationOn": "Відобразити анімацію пакета", + "pathMap_animationOff": "Приховати анімацію пакета", + "pathMap_hopOf": "Перехід {current} з {total}", + "pathMap_observedPaths": "Зафіксовані маршрути: {count}", + "pathMap_primary": "Основний", + "pathMap_alternate": "Альт. {index}", + "pathMap_hopCount": "{count, plural, =1{1 перехід} few{{count} переходи} many{{count} переходів} other{{count} переходів}}", + "pathMap_legendShared": "Об'єднаний сегмент", + "pathMap_legendEstimated": "Орієнтовний сегмент", + "pathMap_sharedNodeCount": "Використовується {count} шляхами", + "pathMap_partialAnimation": "{count, plural, =1{1 перехід не має геопозиції — показаний шлях є частковим} few{{count} переходи не мають геопозиції — показаний шлях є частковим} many{{count} переходів не мають геопозиції — показаний шлях є частковим} other{{count} переходів не мають геопозиції — показаний шлях є частковим}}", + "pathMap_showAllPaths": "Показати все", + "pathMap_hidePath": "Приховати шлях", + "pathMap_showPath": "Показати шлях", + "pathMap_collapsePanel": "Згорнути панель", + "pathMap_expandPanel": "Розгорнути панель", + "pathMap_noLocation": "Без геопозиції", + "pathMap_followPacket": "Прив'язати вигляд до пакету", + "pathMap_unfollowPacket": "Відв'язати вигляд від пакету", + "pathMap_gpsCount": "{confirmed}/{total} GPS" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d1ee964c..e7129d19 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2451,5 +2451,99 @@ "chrome_bluetoothRequiresChromium": "Web Bluetooth 需要 Chromium 浏览器", "channels_communityShortId": "ID:{id}...", "pathTrace_legendGpsConfirmed": "通过GPS确认", - "pathTrace_legendInferred": "推测的位置" + "pathTrace_legendInferred": "推测的位置", + "@pathMap_hopOf": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_observedPaths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_alternate": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@pathMap_hopCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_gpsCount": { + "placeholders": { + "confirmed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "@pathMap_sharedNodeCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@pathMap_partialAnimation": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_online": "在线", + "scanner_bluetoothWebUnsupported": "浏览器不支持蓝牙,请改用 USB 连接。", + "map_activity": "活动", + "map_searchHint": "搜索节点名称或ID", + "map_recent": "最近", + "map_visible": "可见", + "map_stale": "过时", + "map_hidden": "已隐藏", + "map_centerOnNode": "以节点为中心", + "map_details": "详细信息", + "map_noGps": "无 GPS", + "map_noResults": "未找到匹配的节点", + "pathMap_viewSingle": "单条", + "pathMap_viewCombined": "综合", + "pathMap_play": "播放", + "pathMap_pause": "暂停", + "pathMap_replay": "重播", + "pathMap_stepBack": "上一跳", + "pathMap_stepForward": "下一跳", + "pathMap_animationOn": "显示数据包动画", + "pathMap_animationOff": "隐藏数据包动画", + "pathMap_hopOf": "第 {current} 跳,共 {total} 跳", + "pathMap_observedPaths": "观测到的路径:{count}", + "pathMap_primary": "主路径", + "pathMap_alternate": "备用 {index}", + "pathMap_hopCount": "{count, plural, =1{1 跳} other{{count} 跳}}", + "pathMap_legendShared": "共享路段", + "pathMap_legendEstimated": "估算路段", + "pathMap_sharedNodeCount": "已被 {count} 条路径使用", + "pathMap_showAllPaths": "显示全部", + "pathMap_hidePath": "隐藏路径", + "pathMap_showPath": "显示路径", + "pathMap_collapsePanel": "收起面板", + "pathMap_expandPanel": "展开面板", + "pathMap_noLocation": "无位置", + "pathMap_followPacket": "锁定视图跟随数据包", + "pathMap_unfollowPacket": "解锁视图跟随", + "pathMap_gpsCount": "{confirmed}/{total} GPS", + "pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}" } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index c46c413a..bc2ca1bd 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -1466,114 +1466,136 @@ class _ChannelMessagePathMapScreenState border: Border.all(color: MeshPalette.line2), ), clipBehavior: Clip.antiAlias, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 4, 0), - child: Row( + child: DefaultTextStyle( + style: const TextStyle(color: MeshPalette.ink), + child: IconTheme( + data: const IconThemeData(color: MeshPalette.ink), + child: TextButtonTheme( + data: TextButtonThemeData( + style: TextButton.styleFrom(foregroundColor: MeshPalette.ink), + ), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 4, 0), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - l10n.channelPath_repeaterHops, - style: const TextStyle( - fontWeight: FontWeight.w600, - ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + l10n.channelPath_repeaterHops, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + formatDistance( + selectedDisplay?.distanceMeters ?? + _pathDistance, + isImperial: isImperial, + ), + style: MeshTheme.mono( + fontSize: 12, + color: MeshPalette.ink2, + ), + ), + ], ), - ), - Text( - formatDistance( - selectedDisplay?.distanceMeters ?? - _pathDistance, - isImperial: isImperial, + const SizedBox(height: 4), + PathMiniLegend( + combined: combined, + showInferred: false, ), - style: MeshTheme.mono( - fontSize: 12, - color: MeshPalette.ink2, - ), - ), - ], + ], + ), ), - const SizedBox(height: 4), - PathMiniLegend( - combined: combined, - showInferred: false, + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + _panelCollapsed + ? Icons.expand_less + : Icons.expand_more, + size: 20, + ), + tooltip: _panelCollapsed + ? l10n.pathMap_expandPanel + : l10n.pathMap_collapsePanel, + onPressed: () => setState( + () => _panelCollapsed = !_panelCollapsed, + ), ), ], ), ), - IconButton( - visualDensity: VisualDensity.compact, - icon: Icon( - _panelCollapsed ? Icons.expand_less : Icons.expand_more, - size: 20, - ), - tooltip: _panelCollapsed - ? l10n.pathMap_expandPanel - : l10n.pathMap_collapsePanel, - onPressed: () => - setState(() => _panelCollapsed = !_panelCollapsed), + PathAnimationControls( + playback: _playback, + selected: selectedDisplay, + animationEnabled: _animationEnabled, + onToggleAnimation: () => setState(() { + _animationEnabled = !_animationEnabled; + if (!_animationEnabled) _playback.stop(); + }), + followEnabled: _followPacket, + onToggleFollow: _toggleFollowPacket, ), + if (!_panelCollapsed) ...[ + if (selectedDisplay != null && + selectedDisplay.unresolvedHops > 0) + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), + child: Text( + l10n.pathMap_partialAnimation( + selectedDisplay.unresolvedHops, + ), + style: TextStyle( + fontSize: 10.5, + color: MeshPalette.warn, + ), + ), + ), + if (combined) + PathSummaryList( + paths: entries.map((e) => e.display).toList(), + selectedId: selectedDisplay?.id ?? '', + hiddenIds: _hiddenPathIds, + isImperial: isImperial, + onSelect: (display) { + for (final entry in entries) { + if (entry.display.id == display.id) { + _selectEntry(entry); + break; + } + } + }, + onToggleVisibility: (display) => + _togglePathVisibility( + display, + entries, + selectedDisplay, + ), + onShowAll: () => setState(_hiddenPathIds.clear), + ), + const Divider(height: 1), + Expanded( + child: _buildHopListView( + hops, + selectedDisplay, + hopUseCount, + ), + ), + ], ], ), ), - PathAnimationControls( - playback: _playback, - selected: selectedDisplay, - animationEnabled: _animationEnabled, - onToggleAnimation: () => setState(() { - _animationEnabled = !_animationEnabled; - if (!_animationEnabled) _playback.stop(); - }), - followEnabled: _followPacket, - onToggleFollow: _toggleFollowPacket, - ), - if (!_panelCollapsed) ...[ - if (selectedDisplay != null && - selectedDisplay.unresolvedHops > 0) - Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), - child: Text( - l10n.pathMap_partialAnimation( - selectedDisplay.unresolvedHops, - ), - style: TextStyle(fontSize: 10.5, color: MeshPalette.warn), - ), - ), - if (combined) - PathSummaryList( - paths: entries.map((e) => e.display).toList(), - selectedId: selectedDisplay?.id ?? '', - hiddenIds: _hiddenPathIds, - isImperial: isImperial, - onSelect: (display) { - for (final entry in entries) { - if (entry.display.id == display.id) { - _selectEntry(entry); - break; - } - } - }, - onToggleVisibility: (display) => _togglePathVisibility( - display, - entries, - selectedDisplay, - ), - onShowAll: () => setState(_hiddenPathIds.clear), - ), - const Divider(height: 1), - Expanded( - child: _buildHopListView(hops, selectedDisplay, hopUseCount), - ), - ], - ], + ), ), ), ), diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 50ec64d9..783b28ba 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -227,7 +227,13 @@ class MessageRetryService extends ChangeNotifier { void _onMessageResolved(String messageId, String contactKey) { if (_resolvedMessages.contains(messageId)) return; _resolvedMessages.add(messageId); - _activeMessages.remove(messageId); + // If cleanup already removed this message from the active set, it has + // already pumped the queues; avoid double-pumping. + if (!_activeMessages.remove(messageId)) return; + _pumpQueues(contactKey); + } + + void _pumpQueues(String contactKey) { // Pump this contact's queue first, then any other contacts that are waiting. _sendNextForContact(contactKey); for (final key in _sendQueue.keys) { @@ -495,11 +501,17 @@ class MessageRetryService extends ChangeNotifier { (_, mapping) => mapping.messageId == messageId, ); _expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId); + final contactKey = _pendingContacts[messageId]?.publicKeyHex; _pendingMessages.remove(messageId); _pendingContacts.remove(messageId); _attemptPathHistory.remove(messageId); _timeoutTimers.remove(messageId); _resolvedMessages.remove(messageId); + // Cancellation (and other cleanup paths) must release the active in-flight + // slot and pump waiting queues so the global cap does not stall forever. + if (_activeMessages.remove(messageId) && contactKey != null) { + _pumpQueues(contactKey); + } } void _handleTimeout(String messageId) { diff --git a/pubspec.yaml b/pubspec.yaml index 62dc3bb3..dcefc016 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 9.0.0+12 +version: 9.5.0+13 environment: sdk: ^3.9.2 diff --git a/untranslated.json b/untranslated.json index 9e0501e8..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,715 +1 @@ -{ - "bg": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "de": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "es": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "fr": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "hu": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "it": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "ja": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "ko": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "nl": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "pl": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "pt": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "ru": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "sk": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "sl": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "sv": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "uk": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ], - - "zh": [ - "scanner_bluetoothWebUnsupported", - "map_searchHint", - "map_activity", - "map_online", - "map_recent", - "map_stale", - "map_visible", - "map_hidden", - "map_centerOnNode", - "map_details", - "map_noGps", - "map_noResults", - "pathMap_viewSingle", - "pathMap_viewCombined", - "pathMap_play", - "pathMap_pause", - "pathMap_replay", - "pathMap_stepBack", - "pathMap_stepForward", - "pathMap_animationOn", - "pathMap_animationOff", - "pathMap_hopOf", - "pathMap_observedPaths", - "pathMap_primary", - "pathMap_alternate", - "pathMap_hopCount", - "pathMap_gpsCount", - "pathMap_legendShared", - "pathMap_legendEstimated", - "pathMap_sharedNodeCount", - "pathMap_partialAnimation", - "pathMap_showAllPaths", - "pathMap_hidePath", - "pathMap_showPath", - "pathMap_collapsePanel", - "pathMap_expandPanel", - "pathMap_noLocation", - "pathMap_followPacket", - "pathMap_unfollowPacket" - ] -} +{} \ No newline at end of file From ea657a964a8e7b58bee3d336575c914d44c3c5aa Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 13 Jun 2026 02:12:00 -0700 Subject: [PATCH 14/16] Update documentation and dependencies for MeshCore Open - Replaced sqflite with shared_preferences for local key-value storage in README.md - Updated gradle.properties to include builtInKotlin and newDsl flags - Enhanced translation feature documentation in additional-features.md - Modified BLE protocol documentation to include new command and response codes in ble-protocol.md - Clarified channel management details in channels.md - Improved chat and messaging documentation, including message path viewing and translation options in chat-and-messaging.md - Updated contacts management details in contacts.md - Revised map and location documentation for inferred locations and user interface changes in map-and-location.md - Adjusted navigation flow in navigation.md to reflect changes in screen transitions - Updated notification system details in notifications.md - Enhanced repeater management documentation in repeater-management.md - Clarified scanner and connection process in scanner-and-connection.md - Reorganized settings documentation for better clarity and added new node and location settings in settings.md --- CLAUDE.md | 19 +-- README.md | 4 +- android/gradle.properties | 4 + documentation/additional-features.md | 1 + documentation/ble-protocol.md | 6 +- documentation/channels.md | 17 ++- documentation/chat-and-messaging.md | 13 +- documentation/contacts.md | 35 +++-- documentation/map-and-location.md | 43 +++--- documentation/navigation.md | 20 +-- documentation/notifications.md | 4 +- documentation/repeater-management.md | 10 +- documentation/scanner-and-connection.md | 28 ++-- documentation/settings.md | 166 +++++++++++++----------- 14 files changed, 203 insertions(+), 167 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d3b0cad0..6b706653 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ lib/ ├── models/ # Plain data classes (Contact, Channel, Message, Community, …) ├── services/ # ChangeNotifier services + IO services (retry, translation, ML, …) ├── storage/ # SharedPreferences-backed stores, scoped per device key -├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers) +├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers, path hop resolution) ├── utils/ # Platform / IO / UX utilities (logger, GPX export, dialogs) ├── theme/ # MeshPalette (defined, not yet wired in main.dart) ├── l10n/ # ARB localization for 18 locales @@ -194,14 +194,14 @@ enum MeshCoreConnectionState { ## Dependencies -App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2` +App version: `9.5.0+13` — Dart SDK constraint: `^3.9.2` **Connectivity** | Package | Version | Purpose | |---------|---------|---------| | flutter_blue_plus | ^2.1.0 | BLE scanning, connecting, and UART data transfer | -| flutter_blue_plus_platform_interface | ^8.2.1 | Platform-interface layer required by flutter_blue_plus | +| flutter_blue_plus_platform_interface | ^9.0.2 | Platform-interface layer required by flutter_blue_plus | | flserial | git (MeshEnvy fork) | USB serial transport for wired device connections (TODO: upstream pending) | **State / Storage** @@ -232,7 +232,7 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2` | Package | Version | Purpose | |---------|---------|---------| -| material_symbols_icons | ^4.2906.0 | Extended Material Symbols icon set (line-of-sight, etc.) | +| material_symbols_icons | ^4.2928.1 | Extended Material Symbols icon set (line-of-sight, etc.) | | flutter_svg | ^2.0.10+1 | Renders SVG assets (custom icons such as LoS indicator) | | cached_network_image | ^3.4.1 | Caches map tile images downloaded over the network | | flutter_cache_manager | ^3.4.1 | Underlying cache manager used by cached_network_image | @@ -246,7 +246,7 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2` | Package | Version | Purpose | |---------|---------|---------| -| flutter_local_notifications | ^20.1.0 | Shows local push notifications for incoming messages | +| flutter_local_notifications | ^22.0.0 | Shows local push notifications for incoming messages | | flutter_foreground_task | ^9.2.0 | Keeps the app alive in background to maintain BLE/USB connection | **ML / AI** @@ -255,7 +255,8 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2` |---------|---------|---------| | ml_algo | ^16.0.0 | OLS regression used in `timeout_prediction_service.dart` to predict message ACK timeouts | | ml_dataframe | ^1.0.0 | DataFrame input format required by ml_algo | -| llamadart | >=0.6.8 <0.7.0 | On-device LLM inference used in `translation_service.dart` for message translation | +| llamadart | ^0.8.0 | On-device LLM inference used in `translation_service.dart` for message translation | +| flutter_langdetect | ^0.0.1 | Detects a message's source language in `translation_service.dart` before translating | **Misc** @@ -263,8 +264,8 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2` |---------|---------|---------| | http | ^1.2.0 | Fetches tile URLs and any remote API calls | | url_launcher | ^6.3.0 | Opens URLs in the system browser from linkified chat text | -| share_plus | ^12.0.1 | Shares files (e.g. exported GPX tracks) via the system share sheet | -| package_info_plus | ^9.0.0 | Reads app version/build number displayed in settings | +| share_plus | ^13.1.0 | Shares files (e.g. exported GPX tracks) via the system share sheet | +| package_info_plus | ^10.1.0 | Reads app version/build number displayed in settings | | web | ^1.1.1 | Web-platform APIs for USB serial and browser detection on Flutter Web | | intl | any | Internationalization and locale formatting (required by flutter_localizations) | | build_pipe | ^0.3.1 | CI/CD build pipeline configuration (web release builds with versioned assets) | @@ -333,4 +334,4 @@ PWA scaffold present but boilerplate (`manifest.json` and `index.html` are unmod | `lib/services/translation_service.dart` | On-device LLM translation (llamadart) | | `lib/storage/prefs_manager.dart` | SharedPreferences singleton initialized in `main()` | | `lib/screens/scanner_screen.dart` | Home screen — BLE scan and connect | -| `pubspec.yaml` | Dependencies and project metadata (current version `8.0.0+11`) | +| `pubspec.yaml` | Dependencies and project metadata (current version `9.5.0+13`) | diff --git a/README.md b/README.md index 041ea6e9..acf1215a 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,12 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh |---------|---------| | flutter_blue_plus | Bluetooth Low Energy communication | | provider | State management | -| sqflite | Local database storage | +| shared_preferences | Local key-value storage (scoped per device) | | flutter_map | Interactive map display | | latlong2 | Geographic coordinate handling | | flutter_local_notifications | Background notification support | -| smaz | Message compression | | pointycastle | Cryptographic operations | +| llamadart | On-device LLM message translation | | intl | Internationalization and date formatting | ## Getting Started diff --git a/android/gradle.properties b/android/gradle.properties index f018a618..475a6280 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/documentation/additional-features.md b/documentation/additional-features.md index 5dfd2686..997c2c11 100644 --- a/documentation/additional-features.md +++ b/documentation/additional-features.md @@ -198,6 +198,7 @@ Tap the translate button on any received message. On first use, the GGUF model f ### How It Works - Model files are managed by `TranslationFileStore`; download progress is shown in-place +- Before translating, the source language is automatically detected using the `flutter_langdetect` package. If the detected language already matches the target language, translation is skipped - Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android) - Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble - Each translation is cached; re-tapping shows the cached result without re-running inference diff --git a/documentation/ble-protocol.md b/documentation/ble-protocol.md index f9d2acb3..9e72b459 100644 --- a/documentation/ble-protocol.md +++ b/documentation/ble-protocol.md @@ -56,6 +56,7 @@ enum MeshCoreConnectionState { - `Lilygo` - `HT-` - `LowMesh_MC_` + - `NRF52` 2. **Connect** with 15-second timeout (6 seconds on Linux) 3. **Request MTU** 185 bytes (non-web only) 4. **Discover services** and locate NUS @@ -115,13 +116,15 @@ On unexpected disconnection, auto-reconnect with exponential backoff: | 32 | CMD_SET_CHANNEL | Set channel name and PSK | | 36 | CMD_SEND_TRACE_PATH | Request path trace | | 38 | CMD_SET_OTHER_PARAMS | Set misc parameters | -| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry | +| 39 | CMD_SEND_TELEMETRY_REQ | Request sensor telemetry | | 40 | CMD_GET_CUSTOM_VAR | Get custom variables | | 41 | CMD_SET_CUSTOM_VAR | Set a custom variable | | 50 | CMD_SEND_BINARY_REQ | Send binary request | +| 56 | CMD_GET_STATS | Request companion radio stats | | 57 | CMD_SEND_ANON_REQ | Send anonymous request | | 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration | | 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration | +| 61 | CMD_SET_PATH_HASH_MODE | Set path hash width (bytes per hop) | ## Response / Push Codes (Device → App) @@ -145,6 +148,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff: | 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) | | 18 | RESP_CODE_CHANNEL_INFO | Channel definition | | 21 | RESP_CODE_CUSTOM_VARS | Custom variables | +| 24 | RESP_CODE_STATS | Companion radio stats | | 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags | | 0x80 | PUSH_CODE_ADVERT | Known contact re-seen | | 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact | diff --git a/documentation/channels.md b/documentation/channels.md index 49db176a..ff915882 100644 --- a/documentation/channels.md +++ b/documentation/channels.md @@ -4,7 +4,7 @@ Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh. -Up to 8 channels (indices 0–7) can be active simultaneously on one device. +The number of active channels is determined by the firmware (default 40); the device reports its actual limit at login. ## How to Access @@ -17,7 +17,7 @@ QuickSwitchBar tab 1 (middle) from any main screen. | Public | Globe | Green | Fixed well-known PSK; any device can join | | Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention | | Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key | -| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret | +| Community | Groups/Tag | Magenta | PSK derived via HMAC-SHA256 from a community's shared secret | ## Channels List Screen @@ -26,12 +26,12 @@ QuickSwitchBar tab 1 (middle) from any main screen. - **Search bar** with live text filtering (300ms debounce) - **Sort/filter button** - **Scrollable list of channel cards**, each showing: - - Type icon with color coding (purple badge overlay for community channels) + - Type icon with color coding (magenta badge overlay for community channels) - Channel name (or "Channel N" if unnamed) - Unread badge (if messages are unread) - Drag handle (when manual sort is active) - **"+" FAB** to add a new channel -- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings +- **Overflow menu**: Disconnect, Manage Communities, Settings If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown. @@ -59,7 +59,7 @@ Tap the "+" FAB to open a dialog with six options: | Action | Description | |---|---| -| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) | +| Edit | Change name, PSK (with a dice icon to generate a random PSK), SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit), or Cyr2Lat encoding toggle (transliterates Cyrillic to Latin for compatibility) | | Mute / Unmute | Toggle push notification suppression for this channel | | Delete | Remove the channel from the device (confirmation required) | @@ -100,8 +100,7 @@ Tap a channel card to open the channel chat screen. ### Message Path Viewing -- **Mobile**: Tap a message bubble to view its routing path -- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop) +- **All platforms**: Long-press (or right-click on desktop) a message bubble → "Path" - Opens the Channel Message Path Screen (see [Additional Features](additional-features.md)) ### Context Actions (Long-Press / Right-Click) @@ -109,7 +108,7 @@ Tap a channel card to open the channel chat screen. | Action | Availability | Description | |---|---|---| | Reply | All messages | Triggers reply mode | -| Path | Desktop only | Opens message path view | +| Path | All messages | Opens message path view | | Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) | | Copy | All messages | Copies text to clipboard | | Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread | @@ -142,7 +141,7 @@ From the channels screen overflow menu → "Manage Communities". Opens a draggab - **Tap a community** to directly show its QR code for sharing - **Popup menu** per community: - **Show QR** — displays the QR code for sharing with new members - - **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed) + - **Leave Community** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed) ## How Channels Differ from Direct Messages diff --git a/documentation/chat-and-messaging.md b/documentation/chat-and-messaging.md index baf2ab8d..0905326d 100644 --- a/documentation/chat-and-messaging.md +++ b/documentation/chat-and-messaging.md @@ -18,17 +18,15 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen. - **Title**: Contact name - **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details. -- **Action buttons**: - - **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing - - **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries. - - **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle +- **Action button**: + - **Overflow menu** (⋮ icon): Contains Routing, Info, Telemetry, Settings, and Clear Chat. Routing opens the routing sheet where you can switch between Auto, Direct, and Flood routing and manage recent paths (hop count, round-trip time, age, success count, color-coded by repeater). Info shows a dialog with contact type, path, GPS coordinates, and public key. ### Message List - Scrollable list with newest messages at the bottom - **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background - **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name) -- Bubble width capped at 65% of screen width +- Bubble width capped at 72% of screen width - Hyperlinks rendered as tappable green underlined text - **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset - **Jump to bottom**: Floating button appears when scrolled away from the bottom @@ -87,7 +85,7 @@ When a direct message is sent: 1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed 2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts -3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate) +3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise from the device's own `est_timeout` in `RESP_CODE_SENT` (clamped to the physics range); otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate). The result is capped at 45 seconds. 4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...) 5. **Max retries**: Configurable (default 5, range 2–10) 6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered" @@ -114,8 +112,9 @@ Add emoji reactions to incoming messages (not your own): | Action | Availability | Description | |---|---|---| | Add reaction | Incoming messages only | Opens emoji picker | -| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path | +| View path | All platforms: long-press/right-click menu | Shows message routing path | | Copy | All messages | Copies text to clipboard | +| Translate | Incoming messages only (when translation is enabled and not yet translated) | Translates the message on-demand using the on-device model | | Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread | | Delete | All messages | Removes locally (not from mesh) | | Retry | Failed outgoing messages | Re-sends the message | diff --git a/documentation/contacts.md b/documentation/contacts.md index 7ff5ad79..2a875d27 100644 --- a/documentation/contacts.md +++ b/documentation/contacts.md @@ -6,18 +6,17 @@ The Contacts screen is the primary hub for managing mesh nodes your radio has a ## How to Access -- Automatically shown after connecting to a device -- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens +- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens (Channels is shown first after connecting) - Back navigation from Chat or Settings screens ## Contact Types | Type | Avatar Color | Icon | Description | |---|---|---|---| -| Chat | Blue | Chat bubble | Another user's mesh radio | -| Repeater | Orange | Cell tower | A mesh repeater/relay node | -| Room | Purple | Group | A room server for group chat | -| Sensor | Green | Sensors | A sensor device | +| Chat | Blue | Initials / emoji | Another user's mesh radio | +| Repeater | Amber | Cell tower | A mesh repeater/relay node | +| Room | Magenta | Meeting room | A room server for group chat | +| Sensor | Teal | Sensors | A sensor device | ## Contact List @@ -73,42 +72,42 @@ Groups are stored per radio identity (scoped by public key). | Action | Availability | Description | |---|---|---| -| Ping | Repeaters (always) | Opens PathTraceMapScreen targeting the repeater | -| Path Trace | Rooms (always); Chat/Sensor if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available | +| Ping | Repeaters only | Opens PathTraceMapScreen targeting the repeater | +| Path Trace | Rooms (always); Chat/Sensor only if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available | | Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen | | Room Login | Rooms only | Login dialog → ChatScreen | | Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) | -| Open Chat | Chat/Sensor | Same as single tap | | Add/Remove Favorite | All types | Toggles the favorite flag | -| Share Contact | All types | Copies `meshcore://` URI to clipboard | +| Share Contact | All types | Requests advert from device → copies `meshcore://` URI to clipboard | | Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop | | Delete Contact | All types | Confirmation dialog → removes from device and clears messages | ## App Bar Menus -The Contacts screen has **two separate popup menus** in the app bar: +The Contacts screen has a single **three-dot overflow menu** (`⋮`) in the app bar: -**Antenna icon menu** (contact sharing): +- Discovered Contacts — opens the DiscoveryScreen +- Add Contact from Clipboard — reads a `meshcore://` URI from clipboard and imports it +- *(divider)* - Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes - Flood Advert — broadcasts across the full mesh network - Copy Advert to Clipboard — copies your `meshcore://` URI for sharing externally -- Add Contact from Clipboard — reads a `meshcore://` URI from clipboard and imports it - -**Three-dot overflow menu**: +- *(divider)* - Disconnect — disconnects from the device -- Discovered Contacts — opens the DiscoveryScreen - Settings — opens the Settings screen +A **floating action button** (person-add icon) provides a shortcut sheet to "Add Contact from Clipboard" or "Discovered Contacts". + ## Adding Contacts ### Automatic (Passive) When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings). ### Import from Clipboard -Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://` URI from clipboard and imports it to the device. +Overflow menu (or the FAB shortcut) → "Add Contact from Clipboard". Reads a `meshcore://` URI from clipboard and imports it to the device. ### Import from Discovered Contacts -Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts. +Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts. ## Contact Sharing Format diff --git a/documentation/map-and-location.md b/documentation/map-and-location.md index 93ba6345..08bd1cf8 100644 --- a/documentation/map-and-location.md +++ b/documentation/map-and-location.md @@ -35,9 +35,9 @@ Location pins shared in chat messages are displayed as flags: Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent). -### Predicted / Guessed Locations (Semi-Transparent) +### Predicted / Guessed Locations -Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers. +Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as markers with a `not_listed_location` icon and a muted grey or colored border, visually distinct from confirmed-location markers. #### Why guessed locations exist @@ -55,19 +55,19 @@ In a mesh network, every message hops through one or more repeaters on its way t 5. **Compute the estimated position**: - **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other. - - **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80–120m) applied for visual separation. + - **Two or more anchors**: The position is a weighted average of all anchor coordinates (each subsequent anchor weighted at half the previous one, biasing toward the first), with a smaller offset radius (120m for 2 anchors, 80m for 3+) applied for visual separation. 6. **Assign confidence level**: - - **High confidence** (2+ anchors): Displayed at 55% opacity. - - **Low confidence** (1 anchor): Displayed at 30% opacity. + - **High confidence** (2+ anchors): The marker border uses the node's type color (brighter border). + - **Low confidence** (1 anchor): The marker border is rendered in a muted grey. 7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild. #### How to read guessed locations on the map -- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix. -- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions. -- **More transparent** (30%): Lower confidence — based on a single repeater anchor only. +- **Marker with `not_listed_location` icon**: This is a guessed position, not a confirmed GPS fix. +- **Colored border** (type color): Higher confidence — the contact was seen through 2 or more repeaters with known positions. +- **Grey border**: Lower confidence — based on a single repeater anchor only. - Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated. - Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle). @@ -110,9 +110,16 @@ A map with a polyline showing the route from your node through repeater hops to - **Green circles**: Hops with known GPS coordinates - **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts) - **Red endpoint**: Target contact with known GPS -- **Purple semi-transparent endpoint**: Target with guessed position +- **Magenta endpoint**: Target with guessed position -A legend card at the bottom lists each hop pair with SNR quality icons and total path distance. +A bottom panel shows each hop pair with SNR quality icons and total path distance. When multiple observed paths are available, a **Single / Combined** toggle appears at the top of the map. In Combined view, all paths are overlaid; shared segments are highlighted with a white halo and a path count badge appears on shared nodes. + +The bottom panel also provides **packet animation controls**: +- **Animation toggle** (on/off) +- **Step back / Play / Step forward / Replay** buttons +- **Follow packet lock** — keeps the map camera centered on the moving packet dot +- **Speed selector** (0.5×, 1×, 2×, 4×) +- A live **"Hop x of y · from → to"** label that tracks the active segment ### How It Works Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte. @@ -125,17 +132,17 @@ Sends a trace request frame over the mesh. The repeater network traces the path From the main map, tap the terrain/antenna icon. ### What the User Sees -A full-screen map with a collapsible control panel containing: -- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow) -- **Status**: Clear (green) or blocked (red) with distance and minimum clearance -- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button +A full-screen map with a draggable bottom sheet containing: +- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow); obstruction points are marked as clickable dots on the chart +- **Status summary**: Clear (green), Marginal (amber, within 5 m of obstruction), or Blocked (red) with distance and clearance/obstruction amount +- **Options section** (collapsible): Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button ### Key Interactions -- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable) +- **Long-press the map** to add custom endpoints (pushpin markers, renameable/deleteable) - **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set - **Antenna heights** are adjustable for both endpoints -- **Map line** between endpoints is colored green (clear) or red (blocked) -- Terrain elevation is fetched from the Open-Meteo API (21–81 sample points, cached 24 hours) +- **Map line** between endpoints is colored green (clear), amber (marginal), or red (blocked) +- Terrain elevation is fetched from the Open-Meteo API (21, 41, or 81 sample points depending on link distance, cached 24 hours) - K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz --- @@ -149,7 +156,7 @@ Settings → App Settings → Map Display → Offline Map Cache - Map with a blue polygon overlay showing previously selected cache bounds - Bounding box coordinates card - **Cache Area** controls: "Use Current View" and Clear buttons -- **Zoom Range** slider (3–18) with estimated tile count +- **Zoom Range** range slider (3–18, dual-handle for min and max) with estimated tile count - **Download progress** bar (when downloading) - **Download Tiles** and **Clear Cache** buttons diff --git a/documentation/navigation.md b/documentation/navigation.md index b0994fd4..1d8f05a2 100644 --- a/documentation/navigation.md +++ b/documentation/navigation.md @@ -5,7 +5,7 @@ The app follows this general flow: ``` -Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen +Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Channels Screen ``` After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**. @@ -24,7 +24,7 @@ Tapping a tab replaces the current screen with a subtle fade + slight horizontal ## Disconnection -- The disconnect button (available in the Settings screen and other main screens) shows a confirmation dialog before disconnecting +- The disconnect button (available in the overflow menu of each main screen) shows a confirmation dialog before disconnecting - If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback) - This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens @@ -38,11 +38,11 @@ Tapping a tab replaces the current screen with a subtle fade + slight horizontal ``` ScannerScreen (root, always on stack) - ├─ [BLE connect] → push → ContactsScreen - ├─ [TCP FAB] → push → TcpScreen - │ └─ [TCP connected] → pushReplacement → ContactsScreen - └─ [USB FAB] → push → UsbScreen - └─ [USB connected] → pushReplacement → ContactsScreen + ├─ [BLE connect] → push → ChannelsScreen + ├─ [TCP icon button] → push → TcpScreen + │ └─ [TCP connected] → pushReplacement → ChannelsScreen + └─ [USB icon button] → push → UsbScreen + └─ [USB connected] → pushReplacement → ChannelsScreen ContactsScreen (selected=0) ├─ [quick-switch 1] → pushReplacement → ChannelsScreen @@ -60,9 +60,9 @@ ChannelsScreen (selected=1) MapScreen (selected=2) ├─ [quick-switch 0] → pushReplacement → ContactsScreen ├─ [quick-switch 1] → pushReplacement → ChannelsScreen - ├─ [radar button] → push → PathTraceMapScreen - ├─ [terrain button] → push → LineOfSightMapScreen - └─ [long-press] → share marker / set location + ├─ [radar menu item] → enters in-map path trace mode (push → PathTraceMapScreen after path is built) + ├─ [terrain menu item] → push → LineOfSightMapScreen + └─ [long-press] → share marker sheet Settings (push from any main screen) └─ [App Settings] → push → AppSettingsScreen diff --git a/documentation/notifications.md b/documentation/notifications.md index 7db9f0fa..00de3cde 100644 --- a/documentation/notifications.md +++ b/documentation/notifications.md @@ -22,7 +22,7 @@ MeshCore Open provides both **system notifications** (push-style OS alerts) and ### 3. Advertisement Notifications - **Triggered when**: A new node is discovered on the mesh for the first time -- **Title**: "New [type] discovered" (e.g., "New chat node discovered") +- **Title**: "New [type] discovered" (e.g., "New Chat discovered") - **Body**: Contact's name - **Priority**: Default - **Android channel**: `adverts` @@ -43,7 +43,7 @@ Red numeric badges appear throughout the UI: - **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages - **Channels list**: Each channel row shows an unread badge - **Chat screen subtitle**: Shows unread count inline -- Badges cap at "99+" for display +- Badges cap at "9999+" for display ### How Unread Counts Work diff --git a/documentation/repeater-management.md b/documentation/repeater-management.md index 754d4e28..a05bcd82 100644 --- a/documentation/repeater-management.md +++ b/documentation/repeater-management.md @@ -34,8 +34,8 @@ The central management screen showing: |---|---|---| | Status | Repeater Status Screen | All users | | Telemetry | Telemetry Screen | All users | -| CLI | Repeater CLI Screen | Admin only | | Neighbors | Neighbors Screen | All users | +| CLI | Repeater CLI Screen | Admin only | | Settings | Repeater Settings Screen | Admin only | The battery chemistry selector and CLI/Settings cards are hidden from guest users. @@ -89,7 +89,7 @@ A terminal-style interface for sending commands directly to the repeater. - Type a command and press send (or Enter on desktop) - Up/down arrows navigate through command history - Quick-command buttons populate and send common commands -- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty) +- Overflow menu (three-dot icon): "Debug next command" option shows raw frame debug info for the next typed command (shows error snackbar if input field is empty) - Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately - Clear icon: Wipes the command/response history - Failed/timed-out commands are automatically retried once @@ -112,7 +112,9 @@ The in-app help reference (help icon) documents all known commands. Categories: **Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv` -**Sensors**: `sensor get {key}` +**Sensors**: `sensor get {key}`, `sensor set {key} {value}`, `sensor list [start]` + +**GPS Management**: `gps`, `gps {on|off}`, `gps sync`, `gps setloc`, `gps advert`, `gps advert {none|share|prefs}` **Region Management**: `region`, `region load`, `region get`, `region put`, `region remove`, `region allowf`, `region denyf`, `region home`, `region save`, `region default`, `region list allowed`, `region list denied` @@ -207,7 +209,7 @@ Nine configuration cards, each with its own per-field refresh button(s): **Danger Zone** (red-styled card) - Reboot repeater (sends `reboot` with confirmation dialog) -- Erase filesystem (serial-only; shows informational snackbar only — no command is sent over the air) +- Erase filesystem (serial-only; shows a confirmation dialog, then an informational snackbar — no command is sent over the air) ### Key Interactions - **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater diff --git a/documentation/scanner-and-connection.md b/documentation/scanner-and-connection.md index 080e4483..c73457b8 100644 --- a/documentation/scanner-and-connection.md +++ b/documentation/scanner-and-connection.md @@ -28,9 +28,11 @@ The BLE Scanner is the app's home screen, displayed immediately on launch. **Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets. -**Bottom FAB Row**: Up to three floating action buttons: -- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only) -- **TCP/IP** button - Opens TCP connection screen (all non-web platforms) +**App Bar Actions**: Icon buttons in the top-right corner of the app bar: +- **USB** icon button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only) +- **TCP/IP** icon button - Opens TCP connection screen (all non-web platforms) + +**Bottom FAB**: A single floating action button: - **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off ### Device Tile @@ -51,7 +53,7 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s ### How Scanning Works -- Filters for devices with names starting with one of the known prefixes: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_` +- Filters for devices advertising the Nordic UART Service UUID (so community forks with non-standard names are still found). Known name prefixes used by stock firmware builds for reference: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_`, `NRF52` - Uses low-latency scan mode on Android - Scans for 10 seconds then auto-stops - On iOS/macOS, waits for BLE adapter initialization before starting @@ -65,7 +67,7 @@ Tap a device tile or its Connect button: 3. Requests MTU 185 bytes for optimal throughput 4. Discovers BLE services and locates the Nordic UART Service 5. Subscribes to TX notifications for receiving data -6. On success, automatically navigates to the Contacts screen +6. On success, automatically navigates to the Channels screen 7. On failure, shows a red error snackbar --- @@ -74,7 +76,7 @@ Tap a device tile or its Connect button: ### How to Access -From the Scanner screen, tap the **USB** FAB button. +From the Scanner screen, tap the **USB** icon button in the app bar. ### What the User Sees @@ -82,15 +84,15 @@ From the Scanner screen, tap the **USB** FAB button. - A list of detected USB serial ports, each showing: - Friendly display name - Raw port name (subtitle, only shown when it differs from the display name) - - "Connect" button -- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP) + - Chevron trailing icon (the entire tile is tappable to connect) +- Transport switcher buttons (outlined, not FABs) to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP) ### Key Interactions - On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected) - On mobile: tap the "Scan" FAB to manually refresh -- Tap a port or its Connect button to connect -- On successful connection, navigates to Contacts screen +- Tap a port tile to connect +- On successful connection, navigates to Channels screen - On connection failure, the port list automatically refreshes - Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more) @@ -100,7 +102,7 @@ From the Scanner screen, tap the **USB** FAB button. ### How to Access -From the Scanner screen, tap the **TCP/IP** FAB button. +From the Scanner screen, tap the **TCP/IP** icon button in the app bar. ### What the User Sees @@ -108,7 +110,7 @@ From the Scanner screen, tap the **TCP/IP** FAB button. - **Host address** text field - **Port number** text field - **Connect** button -- FABs at the bottom to switch to USB or BLE +- Transport switcher buttons (outlined, not FABs) to switch to USB or BLE ### Key Interactions @@ -119,6 +121,6 @@ From the Scanner screen, tap the **TCP/IP** FAB button. - Validation errors are shown as red snackbars - The Connect button shows a spinner and "Connecting..." label while in progress - The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000") -- On success, navigates to Contacts screen and saves the host/port to settings +- On success, navigates to Channels screen and saves the host/port to settings - On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000") - Error messages for timeout, unsupported platform, and connection failures diff --git a/documentation/settings.md b/documentation/settings.md index 74e8c933..a2f4192a 100644 --- a/documentation/settings.md +++ b/documentation/settings.md @@ -12,12 +12,13 @@ Settings are only accessible while a device is connected. The settings screen is a scrollable list of cards: 1. [Device Info](#device-info) -2. [App Settings](#app-settings) (link to sub-screen) -3. [Node Settings](#node-settings) -4. [Actions](#actions) -5. [Debug](#debug) +2. [Node Settings](#node-settings) +3. [Location](#location) +4. [App Settings](#app-settings) (link to sub-screen) +5. [Actions](#actions) 6. [Export](#export) -7. [About](#about) +7. [Debug](#debug) +8. [About](#about) --- @@ -40,56 +41,6 @@ Battery shows an alert icon and orange text when at 15% or below. The toggle onl --- -## App Settings - -A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences. - -### Appearance -- **Theme**: System / Light / Dark -- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean) -- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages - -### Notifications -- **Master enable/disable**: Requests OS permission when enabling -- **Message notifications**: New direct message alerts -- **Channel message notifications**: New channel message alerts -- **Advertisement notifications**: New node discovery alerts - -### Messaging -- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail -- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest -- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off): - - Max Route Weight (1–10, default 5, integer steps) - - Initial Route Weight (0.5–5.0, default 3.0) - - Success Increment (0.1–2.0, default 0.5, 0.1 steps) - - Failure Decrement (0.1–2.0, default 0.2, 0.1 steps) - - Max Message Retries (2–10, default 5) - -### Battery -- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage) - -### Translation -Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model: -- **Enable Translation**: Translates incoming messages into the selected target language -- **Translate Composer**: Translates outgoing messages from the target language back before sending -- **Target Language**: Language to translate into (searchable list; defaults to the app language) -- **Downloaded Model**: Dropdown to select among already-downloaded translation models -- **Preset Model**: Download a curated preset model with one tap -- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button - -### Map Display -- **Show Repeaters**: Toggle repeater markers on map -- **Show Chat Nodes**: Toggle chat node markers -- **Show Other Nodes**: Toggle room/sensor markers -- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week -- **Units**: Metric / Imperial -- **Offline Map Cache**: Navigate to tile download screen - -### Debug -- **App Debug Logging**: Enable the in-app debug log - ---- - ## Node Settings These settings are sent directly to the connected device firmware. @@ -101,7 +52,7 @@ These settings are sent directly to the connected device firmware. ### Radio Settings Opens a dialog pre-populated with the device's current radio settings. Contains: -- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA, WA, QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918 +- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam - **Frequency** (MHz): Free text, validated 300–2500 MHz - **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz) - **Spreading Factor**: SF5–SF12 @@ -109,6 +60,13 @@ Opens a dialog pre-populated with the device's current radio settings. Contains: - **TX Power** (dBm): Validated 0 to device max (typically 22 dBm) - **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies +### Companion Radio Stats +Opens the RF statistics screen (RSSI, SNR, packet counts) for the paired radio. Only enabled when connected to a device that supports companion radio stats. + +--- + +## Location + ### Location Opens a dialog pre-populated with the device's current coordinates (if known): - Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value @@ -125,8 +83,68 @@ Five toggles controlling which node types are auto-added when heard: - Auto-add Sensors - Overwrite Oldest (when contact list is full) -### Privacy Mode -Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements. +### Privacy +Opens a dialog with controls for how the node shares telemetry and location data: +- **Advert Location**: Toggle whether the node broadcasts its location in advertisements +- **Multi-Ack**: Toggle multi-ack delivery confirmations +- **Telemetry Base Mode**: Deny All / Allow by Contact / Allow All +- **Telemetry Location Mode**: Deny All / Allow by Contact / Allow All +- **Telemetry Environment Mode**: Deny All / Allow by Contact / Allow All + +Settings take effect when saved. A snackbar confirms the update. + +--- + +## App Settings + +A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences. + +### Appearance +- **Theme**: System / Light / Dark +- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean) + +### Notifications +- **Master enable/disable**: Requests OS permission when enabling +- **Message notifications**: New direct message alerts +- **Channel message notifications**: New channel message alerts +- **Advertisement notifications**: New node discovery alerts + +### Messaging +- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail +- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest +- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off): + - Max Route Weight (1–10, default 5, integer steps) + - Initial Route Weight (0.5–5.0, default 3.0) + - Success Increment (0.1–2.0, default 0.5, 0.1 steps) + - Failure Decrement (0.1–2.0, default 0.2, 0.1 steps) + - Max Message Retries (2–10, default 5) +- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages + +### Battery +- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage) + +### Map Display +- **Show Repeaters**: Toggle repeater markers on map +- **Show Chat Nodes**: Toggle chat node markers +- **Show Other Nodes**: Toggle room/sensor markers +- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week +- **Units**: Metric / Imperial +- **Offline Map Cache**: Navigate to tile download screen + +### Translation +Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model: +- **Enable Translation**: Translates incoming messages into the selected target language +- **Translate Composer**: Translates outgoing messages from the target language back before sending +- **Target Language**: Language to translate into (searchable list; defaults to the app language) +- **Downloaded Model**: Dropdown to select among already-downloaded translation models +- **Preset Model**: Download a curated preset model with one tap +- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button + +### Cyrillic-to-Latin (Cyr2Lat) +Controls character substitution profiles used to render Cyrillic text in Latin characters. A dropdown selects the active profile; Add, Edit, and Delete buttons manage the profile list (the last remaining profile cannot be deleted). Each profile stores a JSON character map. + +### Debug +- **App Debug Logging**: Enable the in-app debug log --- @@ -136,10 +154,24 @@ One-tap device operations: | Action | Description | |---|---| -| Send Advertisement | Floods the mesh with your node's advertisement | | Sync Time | Sends current Unix timestamp to the device | | Refresh Contacts | Re-requests the full contact list | -| Reboot Device | Confirmation dialog → reboots the device (shown in orange) | +| Reboot Device | Confirmation dialog → reboots the device (shown in warning color) | +| Delete All Paths | Confirmation dialog → clears all stored routing paths (shown in alert color) | + +--- + +## Export + +Three GPX export options (not available on web): + +| Option | Exports | +|---|---| +| Export Repeaters | Repeaters and Rooms with GPS coordinates | +| Export Contacts | Chat contacts with GPS coordinates | +| Export All | All contacts with GPS coordinates | + +Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error. --- @@ -160,20 +192,6 @@ Structured log entries (Info / Warning / Error), with tag, message, and timestam --- -## Export - -Three GPX export options (not available on web): - -| Option | Exports | -|---|---| -| Export Repeaters | Repeaters and Rooms with GPS coordinates | -| Export Contacts | Chat contacts with GPS coordinates | -| Export All | All contacts with GPS coordinates | - -Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error. - ---- - ## About Shows the standard Flutter about dialog with app name, version, and legal notice. From e38d03a32e4fae4e122dff5c52d2dc426bb14e88 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 13 Jun 2026 02:13:34 -0700 Subject: [PATCH 15/16] Fix battery voltage null check in RepeaterStatusScreen --- lib/screens/repeater_status_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 42c97fa5..e7d1fbe6 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -605,8 +605,9 @@ class _RepeaterStatusScreenState extends State { final batteryMv = connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? _batteryMv; - if (batteryMv == null) + if (batteryMv == null) { return Theme.of(context).colorScheme.onSurfaceVariant; + } final percent = estimateBatteryPercentFromMillivolts( batteryMv, _batteryChemistry(), From 43f3d439ba5833095319eb3d36d8352cf01bcab7 Mon Sep 17 00:00:00 2001 From: Zach Date: Mon, 15 Jun 2026 13:12:08 -0700 Subject: [PATCH 16/16] Add jni to FFI plugin list for Linux and Windows --- lib/screens/channel_chat_screen.dart | 17 ++++++++++------- linux/flutter/generated_plugins.cmake | 1 + windows/flutter/generated_plugins.cmake | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 94101ef3..bea4ac47 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -658,6 +658,7 @@ class _ChannelChatScreenState extends State { : EdgeInsets.zero, child: Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ RouteChip( isDirect: (message.pathLength ?? -1) >= 0, @@ -666,13 +667,15 @@ class _ChannelChatScreenState extends State { : null, ), const SizedBox(width: 4), - Text( - context.l10n.channels_via( - _formatPathPrefixes(displayPath), - ), - style: MeshTheme.mono( - fontSize: 9.5 * textScale, - color: metaColor, + Flexible( + child: Text( + context.l10n.channels_via( + _formatPathPrefixes(displayPath), + ), + style: MeshTheme.mono( + fontSize: 9.5 * textScale, + color: metaColor, + ), ), ), ], diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36fa..93e46829 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f4..533a1712 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES)