From 2ac96a95166728d08a107c8f524fa38772efe55e Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:22:11 -0500 Subject: [PATCH 01/18] New Crowdin updates (#2987) --- app/src/main/res/values-ar-rSA/strings.xml | 4 +++- app/src/main/res/values-b+sr+Latn/strings.xml | 4 +++- app/src/main/res/values-bg-rBG/strings.xml | 4 +++- app/src/main/res/values-ca-rES/strings.xml | 4 +++- app/src/main/res/values-cs-rCZ/strings.xml | 4 +++- app/src/main/res/values-de-rDE/strings.xml | 4 +++- app/src/main/res/values-el-rGR/strings.xml | 4 +++- app/src/main/res/values-es-rES/strings.xml | 4 +++- app/src/main/res/values-et-rEE/strings.xml | 4 +++- app/src/main/res/values-fi-rFI/strings.xml | 4 +++- app/src/main/res/values-fr-rFR/strings.xml | 4 +++- app/src/main/res/values-ga-rIE/strings.xml | 4 +++- app/src/main/res/values-gl-rES/strings.xml | 4 +++- app/src/main/res/values-hr-rHR/strings.xml | 4 +++- app/src/main/res/values-ht-rHT/strings.xml | 4 +++- app/src/main/res/values-hu-rHU/strings.xml | 4 +++- app/src/main/res/values-is-rIS/strings.xml | 4 +++- app/src/main/res/values-it-rIT/strings.xml | 4 +++- app/src/main/res/values-iw-rIL/strings.xml | 4 +++- app/src/main/res/values-ja-rJP/strings.xml | 4 +++- app/src/main/res/values-ko-rKR/strings.xml | 4 +++- app/src/main/res/values-lt-rLT/strings.xml | 4 +++- app/src/main/res/values-nl-rNL/strings.xml | 4 +++- app/src/main/res/values-no-rNO/strings.xml | 4 +++- app/src/main/res/values-pl-rPL/strings.xml | 4 +++- app/src/main/res/values-pt-rBR/strings.xml | 4 +++- app/src/main/res/values-pt-rPT/strings.xml | 4 +++- app/src/main/res/values-ro-rRO/strings.xml | 4 +++- app/src/main/res/values-ru-rRU/strings.xml | 4 +++- app/src/main/res/values-sk-rSK/strings.xml | 4 +++- app/src/main/res/values-sl-rSI/strings.xml | 4 +++- app/src/main/res/values-sq-rAL/strings.xml | 4 +++- app/src/main/res/values-srp/strings.xml | 4 +++- app/src/main/res/values-sv-rSE/strings.xml | 4 +++- app/src/main/res/values-tr-rTR/strings.xml | 4 +++- app/src/main/res/values-uk-rUA/strings.xml | 4 +++- app/src/main/res/values-zh-rCN/strings.xml | 4 +++- app/src/main/res/values-zh-rTW/strings.xml | 4 +++- 38 files changed, 114 insertions(+), 38 deletions(-) diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index 67e4f1501..c4c96e62e 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -644,6 +644,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -781,7 +782,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index ece294f7b..b2655a5d6 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -638,6 +638,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -775,7 +776,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index 983850e96..41539f2f8 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -636,6 +636,7 @@ Експортиране на ключовете Експортира публичния и частния ключове във файл. Моля, съхранявайте го на сигурно място. Modules unlocked + Modules already unlocked Отдалечен (%1$d онлайн / %2$d общо) React @@ -773,7 +774,8 @@ URL must contain placeholders. Шаблон за URL track point - Настройки на телефона + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index 9664b376a..0e8ec2ecf 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index ed0a0bbcc..a54af2e36 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -640,6 +640,7 @@ Exportovat klíče Exportuje veřejné a soukromé klíče do souboru. Uložte je prosím bezpečně. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d celkem) Odpovědět @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index e8391a0b4..2f61eaa41 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -636,6 +636,7 @@ Schlüssel exportieren Exportiert den öffentlichen und privaten Schlüssel in eine Datei. Bitte speichern Sie diese an einem sicheren Ort. Entsperrte Module + Modules already unlocked Entfernt (%1$d Online / %2$d Gesamt) Reagieren @@ -773,7 +774,8 @@ URL muss Platzhalter enthalten. URL Vorlage Verlaufspunkt - Telefoneinstellungen + App + Version Kanalfunktionen Standortfreigabe Regelmäßige Standortübertragung diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 1c031dc77..481fa4f39 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 343697f8f..7d45c47e2 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -637,6 +637,7 @@ Rango de Valores 0 - 500. Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) Reaccionar @@ -774,7 +775,8 @@ Rango de Valores 0 - 500. URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index 70770206d..19ba99643 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -636,6 +636,7 @@ Salvesta võtmed Ekspordib avalikud- ja privaatvõtmed faili. Palun hoidke kuskil turvalises kohas. Moodulid on lukustamata + Modules already unlocked Kaugjuhtimine (%1$d võrgus / %2$d kokku) Reageeri @@ -773,7 +774,8 @@ URL peab sisaldama vahesümboleid. URL mall jälgimispunkt - Telefoni seaded + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 59429bb5c..b870fcc8d 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -636,6 +636,7 @@ Vie avaimet Vie julkiset ja yksityiset avaimet tiedostoon. Säilytä tiedosto turvallisessa paikassa. Lukitsemattomat moduulit + Modules already unlocked Etäyhteys (%1$d yhdistetty / %2$d yhteensä) Reagoi @@ -773,7 +774,8 @@ URL-osoitteessa on oltava paikkamerkkejä. URL-mallipohja seurantapiste - Puhelimen asetukset + App + Version Kanavan ominaisuudet Sijainnin jakaminen Sijainnin toistuva lähetys diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 22af876ff..7807634db 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -636,6 +636,7 @@ Exporter les clés Exporte les clés publiques et privées vers un fichier. Veuillez stocker quelque part en toute sécurité. Modules déverrouillés + Modules already unlocked Distant (%1$d en ligne / %2$d total) Réagir @@ -770,7 +771,8 @@ L\'URL doit contenir des espaces réservés. Modèle d\'URL Point de suivi - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ga-rIE/strings.xml b/app/src/main/res/values-ga-rIE/strings.xml index 01b1d6eab..19e005f60 100644 --- a/app/src/main/res/values-ga-rIE/strings.xml +++ b/app/src/main/res/values-ga-rIE/strings.xml @@ -642,6 +642,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -779,7 +780,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml index b14d960ba..1881a6806 100644 --- a/app/src/main/res/values-gl-rES/strings.xml +++ b/app/src/main/res/values-gl-rES/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-hr-rHR/strings.xml b/app/src/main/res/values-hr-rHR/strings.xml index 7bf3fd8f9..dab40122a 100644 --- a/app/src/main/res/values-hr-rHR/strings.xml +++ b/app/src/main/res/values-hr-rHR/strings.xml @@ -638,6 +638,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -775,7 +776,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ht-rHT/strings.xml b/app/src/main/res/values-ht-rHT/strings.xml index df2554214..167b57ac3 100644 --- a/app/src/main/res/values-ht-rHT/strings.xml +++ b/app/src/main/res/values-ht-rHT/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 322f49125..2c1578edc 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-is-rIS/strings.xml b/app/src/main/res/values-is-rIS/strings.xml index 92e4c059a..2dd71282e 100644 --- a/app/src/main/res/values-is-rIS/strings.xml +++ b/app/src/main/res/values-is-rIS/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index bb8980165..f72c09217 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -636,6 +636,7 @@ Esporta Chiavi Esporta le chiavi pubbliche e private in un file. Si prega di memorizzarlo da qualche parte in modo sicuro. Moduli sbloccati + Modules already unlocked Controllo remoto (%1$d online / %2$d in totale) Rispondi @@ -773,7 +774,8 @@ L\'URL deve contenere dei placeholder. Template dell\'URL punto di interesse - Impostazioni telefono + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index 036af49c6..e4326af5f 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -640,6 +640,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 31876ef99..4cb2d46f0 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -635,6 +635,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -772,7 +773,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 7e8a377ad..f88cfeb71 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -634,6 +634,7 @@ 키 내보내기 공개 및 개인 키를 파일로 내 보냅니다. 안전하게 보관하십시오. 모듈 잠금해제 + Modules already unlocked 원격 (%1$d 온라인 / 총 %2$d ) 반응 @@ -771,7 +772,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml index fd3277832..d1ee9c871 100644 --- a/app/src/main/res/values-lt-rLT/strings.xml +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -640,6 +640,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index dc061c427..4e820e14e 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index 9b6e0a0c4..777bc49a9 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 86ba7fa63..031323e61 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -640,6 +640,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fe00a5cd4..fd2dbdd7a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -636,6 +636,7 @@ Exportar chaves Exporta as chaves públicas e privadas para um arquivo. Por favor, armazene em algum lugar com segurança. Módulos desbloqueados + Modules already unlocked Remoto (%1$d online / %2$d total) Reagir @@ -773,7 +774,8 @@ A URL deve conter espaços reservados. Modelo de URL ponto de rastreamento - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 91af48882..f4cf551c3 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index 1d2c5c0b7..28be5bd7c 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -638,6 +638,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -775,7 +776,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 11369fc32..78581c027 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -640,6 +640,7 @@ Экспортировать ключи Экспортирует публичный и приватный ключи в файл. Пожалуйста, храните их где-нибудь в безопасности. Модули разблокированы + Modules already unlocked Удаленные (%1$d в сети / всего %2$d) Среагировать @@ -774,7 +775,8 @@ URL должен содержать placeholders. Шаблон URL track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 730a5795f..9473e2707 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -640,6 +640,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml index 4b4d8761a..5e62a383e 100644 --- a/app/src/main/res/values-sl-rSI/strings.xml +++ b/app/src/main/res/values-sl-rSI/strings.xml @@ -640,6 +640,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index 66d299f0e..db447f4cc 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-srp/strings.xml b/app/src/main/res/values-srp/strings.xml index f63ea5839..e456bed4f 100644 --- a/app/src/main/res/values-srp/strings.xml +++ b/app/src/main/res/values-srp/strings.xml @@ -638,6 +638,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -775,7 +776,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index a217aa3cc..5625c9a95 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 99de1de98..520367e71 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -636,6 +636,7 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -773,7 +774,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index b567fef4d..dd069a2dc 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -640,6 +640,7 @@ Експортувати ключі Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Modules already unlocked Remote (%1$d online / %2$d total) React @@ -777,7 +778,8 @@ URL must contain placeholders. URL Template track point - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0a711def8..d3bc406f3 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -636,6 +636,7 @@ 导出密钥 导出公钥和私钥到文件。请安全地存储某处。 模块已解锁 + Modules already unlocked 远程 (%1$d 在线 / %2$d 总计) 互动 @@ -772,7 +773,8 @@ URL 必须包含占位符。 URL 模板 轨迹点 - Phone Settings + App + Version Channel Features Location Sharing Periodic position broadcast diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c719c11f6..8aa7cab04 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -634,6 +634,7 @@ 匯出金鑰 請將匯出後的私鑰及公鑰妥善保存。 模組已解鎖 + Modules already unlocked 遠端 (%1$d 個上線 / 共計 %2$d 個) 回應 @@ -771,7 +772,8 @@ 網址必須包含佔位符。 URL 範本 軌跡點 - 手機設定 + App + Version Channel Features Location Sharing Periodic position broadcast From 0f349469413efc79b55f2cdd17702119fb8375f7 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:27:20 -0500 Subject: [PATCH 02/18] New Crowdin updates (#2988) --- app/src/main/res/values-fi-rFI/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index b870fcc8d..4e6c1da3b 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -636,7 +636,7 @@ Vie avaimet Vie julkiset ja yksityiset avaimet tiedostoon. Säilytä tiedosto turvallisessa paikassa. Lukitsemattomat moduulit - Modules already unlocked + Moduulit ovat jo käytettävissä Etäyhteys (%1$d yhdistetty / %2$d yhteensä) Reagoi @@ -774,8 +774,8 @@ URL-osoitteessa on oltava paikkamerkkejä. URL-mallipohja seurantapiste - App - Version + Sovellus + Versio Kanavan ominaisuudet Sijainnin jakaminen Sijainnin toistuva lähetys From 2f1a3fabb925a06df36fc14a6c9d3764493bd523 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Sat, 6 Sep 2025 14:54:05 +1000 Subject: [PATCH 03/18] align strategies for display, add missing entries, clean up display when everything is present, --- .../mesh/ui/metrics/EnvironmentMetrics.kt | 206 ++++++++++++------ 1 file changed, 135 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt index 3a4308998..7d7db006a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -45,6 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -120,35 +120,45 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) { } @Composable -private fun TemperatureDisplay(temperature: Float, environmentDisplayFahrenheit: Boolean) { - if (!temperature.isNaN()) { - val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" - Text( - text = textFormat.format(stringResource(id = R.string.temperature), temperature), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) +private fun TemperatureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, environmentDisplayFahrenheit: Boolean) { + envMetrics.temperature?.let { temperature -> + if (!temperature.isNaN()) { + val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" + Text( + text = textFormat.format(stringResource(id = R.string.temperature), temperature), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + } } } @Composable private fun HumidityAndBarometricPressureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - envMetrics.relativeHumidity?.let { humidity -> - if (!humidity.isNaN()) { + val hasHumidity = envMetrics.relativeHumidity?.let { !it.isNaN() } == true + val hasPressure = envMetrics.barometricPressure?.let { !it.isNaN() && it > 0 } == true + + if (hasHumidity || hasPressure) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + if (hasHumidity) { + val humidity = envMetrics.relativeHumidity!! Text( text = "%s %.2f%%".format(stringResource(id = R.string.humidity), humidity), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, + modifier = Modifier.padding(vertical = 0.dp), ) } - } - envMetrics.barometricPressure?.let { pressure -> - if (!pressure.isNaN() && pressure > 0) { // Keep pressure > 0 check + if (hasPressure) { + val pressure = envMetrics.barometricPressure!! Text( text = "%.2f hPa".format(pressure), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, + modifier = Modifier.padding(vertical = 0.dp), ) } } @@ -161,7 +171,6 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e envMetrics.soilTemperature != null || (envMetrics.soilMoisture != null && envMetrics.soilMoisture != Int.MIN_VALUE) ) { - Spacer(modifier = Modifier.height(4.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" val soilMoistureTextFormat = "%s %d%%" @@ -191,41 +200,23 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e } } -@Composable -private fun IaqDisplay(iaqValue: Int) { - if (iaqValue != Int.MIN_VALUE) { - Spacer(modifier = Modifier.height(4.dp)) - /* Air Quality */ - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.iaq), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - Spacer(modifier = Modifier.width(4.dp)) - IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot) - } - } -} - @Composable private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - envMetrics.lux?.let { luxValue -> - if (!luxValue.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + val hasLux = envMetrics.lux != null && !envMetrics.lux.isNaN() + val hasUvLux = envMetrics.uvLux != null && !envMetrics.uvLux.isNaN() + + if (hasLux || hasUvLux) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasLux) { + val luxValue = envMetrics.lux!! Text( text = "%s %.0f lx".format(stringResource(R.string.lux), luxValue), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - } - } - envMetrics.uvLux?.let { uvLuxValue -> - if (!uvLuxValue.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasUvLux) { + val uvLuxValue = envMetrics.uvLux!! Text( text = "%s %.0f UVlx".format(stringResource(R.string.uv_lux), uvLuxValue), color = MaterialTheme.colorScheme.onSurface, @@ -238,23 +229,21 @@ private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { @Composable private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - envMetrics.voltage?.let { voltage -> - if (!voltage.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage.isNaN() + val hasCurrent = envMetrics.current != null && !envMetrics.current.isNaN() + + if (hasVoltage || hasCurrent) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasVoltage) { + val voltage = envMetrics.voltage!! Text( text = "%s %.2f V".format(stringResource(R.string.voltage), voltage), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - } - } - - envMetrics.current?.let { current -> - if (!current.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasCurrent) { + val current = envMetrics.current!! Text( text = "%s %.2f mA".format(stringResource(R.string.current), current), color = MaterialTheme.colorScheme.onSurface, @@ -266,15 +255,66 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics } @Composable -private fun GasResistanceDisplay(gasResistance: Float) { - if (!gasResistance.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) +private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { + val iaqValue = envMetrics.iaq + val gasResistance = envMetrics.gasResistance + + if ((iaqValue != null && iaqValue != Int.MIN_VALUE) || (gasResistance != null && !gasResistance.isNaN())) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) + if (iaqValue != null && iaqValue != Int.MIN_VALUE) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.iaq), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + Spacer(modifier = Modifier.width(4.dp)) + IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot) + } + } + if (gasResistance != null && !gasResistance.isNaN()) { + Text( + text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + } + } + } + // These are in a differnt proto ... + // envMetrics.co2?.let { co2 -> + // Spacer(modifier = Modifier.height(4.dp)) + // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + // Text( + // text = "%s %.0f ppm".format(stringResource(R.string.co2), co2), + // color = MaterialTheme.colorScheme.onSurface, + // fontSize = MaterialTheme.typography.labelLarge.fontSize, + // ) + // } + // } + // envMetrics.tvoc?.let { tvoc -> + // Spacer(modifier = Modifier.height(4.dp)) + // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + // Text( + // text = "%s %.0f ppb".format(stringResource(R.string.tvoc), tvoc), + // color = MaterialTheme.colorScheme.onSurface, + // fontSize = MaterialTheme.typography.labelLarge.fontSize, + // ) + // } + // } +} + +@Composable +private fun RadiationDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { + envMetrics.radiation?.let { radiation -> + if (!radiation.isNaN() && radiation > 0f) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + text = "%s %.2f µSv/h".format(stringResource(R.string.radiation), radiation), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + } } } } @@ -292,7 +332,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) { val envMetrics = telemetry.environmentMetrics val time = telemetry.time * MS_PER_SEC - Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp)) { /* Time and Temperature */ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( @@ -300,23 +340,47 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa style = TextStyle(fontWeight = FontWeight.Bold), fontSize = MaterialTheme.typography.labelLarge.fontSize, ) - envMetrics.temperature?.let { temperature -> TemperatureDisplay(temperature, environmentDisplayFahrenheit) } + TemperatureDisplay(envMetrics, environmentDisplayFahrenheit) } - Spacer(modifier = Modifier.height(4.dp)) - - /* Humidity and Barometric Pressure */ HumidityAndBarometricPressureDisplay(envMetrics) - /* Soil Moisture and Soil Temperature */ SoilMetricsDisplay(envMetrics, environmentDisplayFahrenheit) - envMetrics.iaq?.let { iaqValue -> IaqDisplay(iaqValue) } + GasCompositionDisplay(envMetrics) LuxUVLuxDisplay(envMetrics) VoltageCurrentDisplay(envMetrics) - - envMetrics.gasResistance?.let { gasResistance -> GasResistanceDisplay(gasResistance) } + RadiationDisplay(envMetrics) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewEnvironmentMetricsContent() { + // Build a fake EnvironmentMetrics using the generated proto builder APIs + val fakeEnvMetrics = + TelemetryProtos.EnvironmentMetrics.newBuilder() + .setTemperature(22.5f) + .setRelativeHumidity(55.0f) + .setBarometricPressure(1013.25f) + .setSoilMoisture(33) + .setSoilTemperature(18.0f) + .setLux(100.0f) + .setUvLux(100.0f) + .setVoltage(3.7f) + .setCurrent(0.12f) + .setIaq(100) + .setRadiation(0.15f) + .setGasResistance(1200.0f) + .build() + val fakeTelemetry = + TelemetryProtos.Telemetry.newBuilder() + .setTime((System.currentTimeMillis() / 1000).toInt()) + .setEnvironmentMetrics(fakeEnvMetrics) + .build() + MaterialTheme { + Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) } } } From 4dd519456bcecd195e22c2438b4ba0977a3ba2d5 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Sat, 6 Sep 2025 15:03:15 +1000 Subject: [PATCH 04/18] Revert "align strategies for display, add missing entries, clean up display when everything is present," This reverts commit 2f1a3fabb925a06df36fc14a6c9d3764493bd523. --- .../mesh/ui/metrics/EnvironmentMetrics.kt | 204 ++++++------------ 1 file changed, 70 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt index 7d7db006a..3a4308998 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -44,7 +45,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -120,45 +120,35 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) { } @Composable -private fun TemperatureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, environmentDisplayFahrenheit: Boolean) { - envMetrics.temperature?.let { temperature -> - if (!temperature.isNaN()) { - val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" - Text( - text = textFormat.format(stringResource(id = R.string.temperature), temperature), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - } +private fun TemperatureDisplay(temperature: Float, environmentDisplayFahrenheit: Boolean) { + if (!temperature.isNaN()) { + val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" + Text( + text = textFormat.format(stringResource(id = R.string.temperature), temperature), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) } } @Composable private fun HumidityAndBarometricPressureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - val hasHumidity = envMetrics.relativeHumidity?.let { !it.isNaN() } == true - val hasPressure = envMetrics.barometricPressure?.let { !it.isNaN() && it > 0 } == true - - if (hasHumidity || hasPressure) { - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - if (hasHumidity) { - val humidity = envMetrics.relativeHumidity!! + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + envMetrics.relativeHumidity?.let { humidity -> + if (!humidity.isNaN()) { Text( text = "%s %.2f%%".format(stringResource(id = R.string.humidity), humidity), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, - modifier = Modifier.padding(vertical = 0.dp), ) } - if (hasPressure) { - val pressure = envMetrics.barometricPressure!! + } + envMetrics.barometricPressure?.let { pressure -> + if (!pressure.isNaN() && pressure > 0) { // Keep pressure > 0 check Text( text = "%.2f hPa".format(pressure), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, - modifier = Modifier.padding(vertical = 0.dp), ) } } @@ -171,6 +161,7 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e envMetrics.soilTemperature != null || (envMetrics.soilMoisture != null && envMetrics.soilMoisture != Int.MIN_VALUE) ) { + Spacer(modifier = Modifier.height(4.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" val soilMoistureTextFormat = "%s %d%%" @@ -201,22 +192,40 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e } @Composable -private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - val hasLux = envMetrics.lux != null && !envMetrics.lux.isNaN() - val hasUvLux = envMetrics.uvLux != null && !envMetrics.uvLux.isNaN() +private fun IaqDisplay(iaqValue: Int) { + if (iaqValue != Int.MIN_VALUE) { + Spacer(modifier = Modifier.height(4.dp)) + /* Air Quality */ + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.iaq), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + Spacer(modifier = Modifier.width(4.dp)) + IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot) + } + } +} - if (hasLux || hasUvLux) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (hasLux) { - val luxValue = envMetrics.lux!! +@Composable +private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { + envMetrics.lux?.let { luxValue -> + if (!luxValue.isNaN()) { + Spacer(modifier = Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( text = "%s %.0f lx".format(stringResource(R.string.lux), luxValue), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - if (hasUvLux) { - val uvLuxValue = envMetrics.uvLux!! + } + } + envMetrics.uvLux?.let { uvLuxValue -> + if (!uvLuxValue.isNaN()) { + Spacer(modifier = Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( text = "%s %.0f UVlx".format(stringResource(R.string.uv_lux), uvLuxValue), color = MaterialTheme.colorScheme.onSurface, @@ -229,21 +238,23 @@ private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { @Composable private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage.isNaN() - val hasCurrent = envMetrics.current != null && !envMetrics.current.isNaN() - - if (hasVoltage || hasCurrent) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (hasVoltage) { - val voltage = envMetrics.voltage!! + envMetrics.voltage?.let { voltage -> + if (!voltage.isNaN()) { + Spacer(modifier = Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( text = "%s %.2f V".format(stringResource(R.string.voltage), voltage), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - if (hasCurrent) { - val current = envMetrics.current!! + } + } + + envMetrics.current?.let { current -> + if (!current.isNaN()) { + Spacer(modifier = Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( text = "%s %.2f mA".format(stringResource(R.string.current), current), color = MaterialTheme.colorScheme.onSurface, @@ -255,66 +266,15 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics } @Composable -private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - val iaqValue = envMetrics.iaq - val gasResistance = envMetrics.gasResistance - - if ((iaqValue != null && iaqValue != Int.MIN_VALUE) || (gasResistance != null && !gasResistance.isNaN())) { +private fun GasResistanceDisplay(gasResistance: Float) { + if (!gasResistance.isNaN()) { + Spacer(modifier = Modifier.height(4.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (iaqValue != null && iaqValue != Int.MIN_VALUE) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.iaq), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - Spacer(modifier = Modifier.width(4.dp)) - IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot) - } - } - if (gasResistance != null && !gasResistance.isNaN()) { - Text( - text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - } - } - } - // These are in a differnt proto ... - // envMetrics.co2?.let { co2 -> - // Spacer(modifier = Modifier.height(4.dp)) - // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - // Text( - // text = "%s %.0f ppm".format(stringResource(R.string.co2), co2), - // color = MaterialTheme.colorScheme.onSurface, - // fontSize = MaterialTheme.typography.labelLarge.fontSize, - // ) - // } - // } - // envMetrics.tvoc?.let { tvoc -> - // Spacer(modifier = Modifier.height(4.dp)) - // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - // Text( - // text = "%s %.0f ppb".format(stringResource(R.string.tvoc), tvoc), - // color = MaterialTheme.colorScheme.onSurface, - // fontSize = MaterialTheme.typography.labelLarge.fontSize, - // ) - // } - // } -} - -@Composable -private fun RadiationDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - envMetrics.radiation?.let { radiation -> - if (!radiation.isNaN() && radiation > 0f) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - text = "%s %.2f µSv/h".format(stringResource(R.string.radiation), radiation), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - } + Text( + text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) } } } @@ -332,7 +292,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) { val envMetrics = telemetry.environmentMetrics val time = telemetry.time * MS_PER_SEC - Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp)) { + Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { /* Time and Temperature */ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( @@ -340,47 +300,23 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa style = TextStyle(fontWeight = FontWeight.Bold), fontSize = MaterialTheme.typography.labelLarge.fontSize, ) - TemperatureDisplay(envMetrics, environmentDisplayFahrenheit) + envMetrics.temperature?.let { temperature -> TemperatureDisplay(temperature, environmentDisplayFahrenheit) } } + Spacer(modifier = Modifier.height(4.dp)) + + /* Humidity and Barometric Pressure */ HumidityAndBarometricPressureDisplay(envMetrics) + /* Soil Moisture and Soil Temperature */ SoilMetricsDisplay(envMetrics, environmentDisplayFahrenheit) - GasCompositionDisplay(envMetrics) + envMetrics.iaq?.let { iaqValue -> IaqDisplay(iaqValue) } LuxUVLuxDisplay(envMetrics) VoltageCurrentDisplay(envMetrics) - RadiationDisplay(envMetrics) - } -} -@Preview(showBackground = true) -@Composable -private fun PreviewEnvironmentMetricsContent() { - // Build a fake EnvironmentMetrics using the generated proto builder APIs - val fakeEnvMetrics = - TelemetryProtos.EnvironmentMetrics.newBuilder() - .setTemperature(22.5f) - .setRelativeHumidity(55.0f) - .setBarometricPressure(1013.25f) - .setSoilMoisture(33) - .setSoilTemperature(18.0f) - .setLux(100.0f) - .setUvLux(100.0f) - .setVoltage(3.7f) - .setCurrent(0.12f) - .setIaq(100) - .setRadiation(0.15f) - .setGasResistance(1200.0f) - .build() - val fakeTelemetry = - TelemetryProtos.Telemetry.newBuilder() - .setTime((System.currentTimeMillis() / 1000).toInt()) - .setEnvironmentMetrics(fakeEnvMetrics) - .build() - MaterialTheme { - Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) } + envMetrics.gasResistance?.let { gasResistance -> GasResistanceDisplay(gasResistance) } } } From 82b6266f0e06fbceb1e0e7891981165a98f4e326 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 15:16:53 +1000 Subject: [PATCH 05/18] feat #2570, Add ExportAll to csv (#2989) --- .../java/com/geeksville/mesh/model/UIState.kt | 21 ++++++++++---- .../mesh/ui/settings/SettingsScreen.kt | 28 +++++++++++++++++-- app/src/main/res/values/strings.xml | 3 +- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 4f677308f..65985983f 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -845,13 +845,21 @@ constructor( } } - /** Write the persisted packet data out to a CSV file in the specified location. */ + /** + * Export all persisted packet data to a CSV file at the given URI. + * + * The CSV will include all packets, or only those matching the given port number if specified. Each row contains: + * date, time, sender node number, sender name, sender latitude, sender longitude, receiver latitude, receiver + * longitude, receiver elevation, received SNR, distance, hop limit, and payload. + * + * @param uri The destination URI for the CSV file. + * @param filterPortnum If provided, only packets with this port number will be exported. + */ @Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod") - fun saveRangeTestCsv(uri: Uri) { + fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) { viewModelScope.launch(Dispatchers.Main) { // Extract distances to this device from position messages and put (node,SNR,distance) - // in - // the file_uri + // in the file_uri val myNodeNum = myNodeNum ?: return@launch // Capture the current node value while we're still on main thread @@ -888,9 +896,10 @@ constructor( } } - // Only look at range test messages, with SNR reported. + // packets must have rxSNR, and optionally match the filter given as a param. if ( - proto.decoded.portnumValue == Portnums.PortNum.RANGE_TEST_APP_VALUE && proto.rxSnr != 0.0f + (filterPortnum == null || proto.decoded.portnumValue == filterPortnum) && + proto.rxSnr != 0.0f ) { val rxDateTime = dateFormat.format(packet.received_date) val rxFrom = proto.from.toUInt() diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index eede0aafc..b7fcb9ad3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -66,6 +66,9 @@ import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog import com.geeksville.mesh.util.LanguageUtils import kotlinx.coroutines.delay +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import kotlin.time.Duration.Companion.seconds @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -222,11 +225,12 @@ fun SettingsScreen( choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } }, ) } + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) val exportRangeTestLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { - it.data?.data?.let { uri -> uiViewModel.saveRangeTestCsv(uri) } + it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) } } } SettingsItem( @@ -238,11 +242,31 @@ fun SettingsScreen( Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/csv" - putExtra(Intent.EXTRA_TITLE, "rangetest.csv") + putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv") } exportRangeTestLauncher.launch(intent) } + val exportDataLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) } + } + } + SettingsItem( + text = stringResource(R.string.export_data_csv), + leadingIcon = Icons.Rounded.Output, + trailingIcon = null, + ) { + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/csv" + putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv") + } + exportDataLauncher.launch(intent) + } + SettingsItem( text = stringResource(R.string.intro_show), leadingIcon = Icons.Rounded.WavingHand, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51faa4391..35ff4cd95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,7 +156,8 @@ OK You must set a region! Couldn\'t change channel, because radio is not yet connected. Please try again. - Export rangetest.csv + Export rangetest packets + Export all packets Reset Scan Add From 3a9e5ffbbe96572d6d85d8e354272eea0e1fa0db Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 15:17:04 +1000 Subject: [PATCH 06/18] move debug export to using URI (#2991) --- .../com/geeksville/mesh/ui/debug/Debug.kt | 105 ++++++++++-------- app/src/main/res/values/strings.xml | 3 + 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt index 62c486d84..9ba603edf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt @@ -18,8 +18,10 @@ package com.geeksville.mesh.ui.debug import android.content.Context -import android.os.Environment +import android.net.Uri import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable @@ -88,8 +90,6 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream import java.io.OutputStreamWriter import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat @@ -136,6 +136,14 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) { listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex) } } + // Prepare a document creator for exporting logs via SAF + val exportLogsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { createdUri -> + if (createdUri != null) { + scope.launch { exportAllLogsToUri(context, createdUri, filteredLogs) } + } + } + Column(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) { stickyHeader { @@ -149,7 +157,11 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) { logs = logs, filterMode = filterMode, onFilterModeChange = { filterMode = it }, - onExportLogs = { scope.launch { exportAllLogs(context, filteredLogs) } }, + onExportLogs = { + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val fileName = "meshtastic_debug_$timestamp.txt" + exportLogsLauncher.launch(fileName) + }, ) } items(filteredLogs, key = { it.uuid }) { log -> @@ -338,57 +350,54 @@ fun DebugMenuActions(viewModel: DebugViewModel = hiltViewModel(), modifier: Modi } } -private suspend fun exportAllLogs(context: Context, logs: List) = withContext(Dispatchers.IO) { - try { - val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) - val fileName = "meshtastic_debug_$timestamp.txt" - - val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - val logFile = File(downloadsDir, fileName) - - OutputStreamWriter(FileOutputStream(logFile), StandardCharsets.UTF_8).use { writer -> - logs.forEach { log -> - writer.write("${log.formattedReceivedDate} [${log.messageType}]\n") - writer.write(log.logMessage) - if (!log.decodedPayload.isNullOrBlank()) { - writer.write("\n\nDecoded Payload:\n{") - writer.write("\n") - // Redact Decoded keys. - log.decodedPayload.lineSequence().forEach { line -> - var outputLine = line - val redacted = redactedKeys.firstOrNull { line.contains(it) } - if (redacted != null) { - val idx = line.indexOf(':') - if (idx != -1) { - outputLine = line.substring(0, idx + 1) - outputLine += "" +private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List) = + withContext(Dispatchers.IO) { + try { + context.contentResolver.openOutputStream(targetUri)?.use { os -> + OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer -> + logs.forEach { log -> + writer.write("${log.formattedReceivedDate} [${log.messageType}]\n") + writer.write(log.logMessage) + if (!log.decodedPayload.isNullOrBlank()) { + writer.write("\n\nDecoded Payload:\n{") + writer.write("\n") + // Redact Decoded keys. + log.decodedPayload.lineSequence().forEach { line -> + var outputLine = line + val redacted = redactedKeys.firstOrNull { line.contains(it) } + if (redacted != null) { + val idx = line.indexOf(':') + if (idx != -1) { + outputLine = line.substring(0, idx + 1) + outputLine += "" + } + } + writer.write(outputLine) + writer.write("\n") } + writer.write("\n}") } - writer.write(outputLine) - writer.write("\n") + writer.write("\n\n") } - writer.write("\n}") } - writer.write("\n\n") - } - } + } ?: run { throw IOException("Unable to open output stream for URI: $targetUri") } - withContext(Dispatchers.Main) { - Toast.makeText(context, "${logs.size} logs exported to ${logFile.absolutePath}", Toast.LENGTH_LONG) - .show() + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.debug_export_success, logs.size), Toast.LENGTH_LONG) + .show() + } + } catch (e: IOException) { + withContext(Dispatchers.Main) { + Toast.makeText( + context, + context.getString(R.string.debug_export_failed, e.message ?: ""), + Toast.LENGTH_LONG, + ) + .show() + } + warn("Error:IOException: " + e.toString()) } - } catch (e: SecurityException) { - withContext(Dispatchers.Main) { - Toast.makeText(context, "Permission denied: Cannot write to Downloads folder", Toast.LENGTH_LONG).show() - warn("Error:SecurityException: " + e.toString()) - } - } catch (e: IOException) { - withContext(Dispatchers.Main) { - Toast.makeText(context, "Failed to write log file: ${e.message}", Toast.LENGTH_LONG).show() - } - warn("Error:IOException: " + e.toString()) } -} @Composable private fun DecodedPayloadBlock( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35ff4cd95..b4010469b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,6 +133,9 @@ Debug Panel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… From c0dc9fdf3e3e5104913afa2484758d3714da2429 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 01:01:44 -0500 Subject: [PATCH 07/18] New Crowdin updates (#2994) --- app/src/main/res/values-ar-rSA/strings.xml | 6 +++++- app/src/main/res/values-b+sr+Latn/strings.xml | 6 +++++- app/src/main/res/values-bg-rBG/strings.xml | 6 +++++- app/src/main/res/values-ca-rES/strings.xml | 6 +++++- app/src/main/res/values-cs-rCZ/strings.xml | 6 +++++- app/src/main/res/values-de-rDE/strings.xml | 6 +++++- app/src/main/res/values-el-rGR/strings.xml | 6 +++++- app/src/main/res/values-es-rES/strings.xml | 6 +++++- app/src/main/res/values-et-rEE/strings.xml | 6 +++++- app/src/main/res/values-fi-rFI/strings.xml | 6 +++++- app/src/main/res/values-fr-rFR/strings.xml | 6 +++++- app/src/main/res/values-ga-rIE/strings.xml | 6 +++++- app/src/main/res/values-gl-rES/strings.xml | 6 +++++- app/src/main/res/values-hr-rHR/strings.xml | 6 +++++- app/src/main/res/values-ht-rHT/strings.xml | 6 +++++- app/src/main/res/values-hu-rHU/strings.xml | 6 +++++- app/src/main/res/values-is-rIS/strings.xml | 6 +++++- app/src/main/res/values-it-rIT/strings.xml | 6 +++++- app/src/main/res/values-iw-rIL/strings.xml | 6 +++++- app/src/main/res/values-ja-rJP/strings.xml | 6 +++++- app/src/main/res/values-ko-rKR/strings.xml | 6 +++++- app/src/main/res/values-lt-rLT/strings.xml | 6 +++++- app/src/main/res/values-nl-rNL/strings.xml | 6 +++++- app/src/main/res/values-no-rNO/strings.xml | 6 +++++- app/src/main/res/values-pl-rPL/strings.xml | 6 +++++- app/src/main/res/values-pt-rBR/strings.xml | 6 +++++- app/src/main/res/values-pt-rPT/strings.xml | 6 +++++- app/src/main/res/values-ro-rRO/strings.xml | 6 +++++- app/src/main/res/values-ru-rRU/strings.xml | 6 +++++- app/src/main/res/values-sk-rSK/strings.xml | 6 +++++- app/src/main/res/values-sl-rSI/strings.xml | 6 +++++- app/src/main/res/values-sq-rAL/strings.xml | 6 +++++- app/src/main/res/values-srp/strings.xml | 6 +++++- app/src/main/res/values-sv-rSE/strings.xml | 6 +++++- app/src/main/res/values-tr-rTR/strings.xml | 6 +++++- app/src/main/res/values-uk-rUA/strings.xml | 6 +++++- app/src/main/res/values-zh-rCN/strings.xml | 6 +++++- app/src/main/res/values-zh-rTW/strings.xml | 6 +++++- 38 files changed, 190 insertions(+), 38 deletions(-) diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index c4c96e62e..043117027 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -116,6 +116,9 @@ Debug Panel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ حسنا واجب إدخال المنطقة! Couldn\'t change channel, because radio is not yet connected. Please try again. - Export rangetest.csv + Export rangetest packets + Export all packets Reset البحث أضف diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index b2655a5d6..50ad8d48f 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -116,6 +116,9 @@ Panel za otklanjanje grešaka Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Океј Мораш одабрати регион! Није било могуће променити канал, јер радио још није повезан. Молимо покушајте поново. - Извези rangetest.csv + Export rangetest packets + Export all packets Поново покрени Скенирај Додај diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index 41539f2f8..7e883def2 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -116,6 +116,9 @@ Панел за отстраняване на грешки Decoded Payload: Експортиране на журнали + Export canceled + %1$d logs exported + Failed to write log file: %1$s Филтри Активни филтри Търсене в журналите… @@ -139,7 +142,8 @@ Добре Трябва да зададете регион! Каналът не може да бъде сменен, тъй като радиото все още не е свързано. Моля, опитайте отново. - Експорт на rangetest.csv + Export rangetest packets + Export all packets Нулиране Сканиране Добавяне diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index 0e8ec2ecf..2ff65c6fc 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -116,6 +116,9 @@ Panell de depuració Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Acceptar Has de configurar la regió! No s\'ha pogut canviar el canal perquè la ràdio no està configurada correctament. Si us plau torna-ho a provar. - Exportat rangetest.csv + Export rangetest packets + Export all packets Restablir Escanejar Afegir diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index a54af2e36..bfc076389 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -116,6 +116,9 @@ Panel pro ladění Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtry Aktivní filtry Hledat v protokolech… @@ -139,7 +142,8 @@ OK Musíte specifikovat region! Kanál nelze změnit, protože rádio ještě není připojeno. Zkuste to znovu. - Exportovat rangetest.csv + Export rangetest packets + Export all packets Reset Skenovat Přidat diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 2f61eaa41..351a5d1aa 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -116,6 +116,9 @@ Debug-Ausgaben Dekodiertes Payload: Protokolle exportieren + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filter Aktive Filter In Protokollen suchen @@ -139,7 +142,8 @@ OK Sie müssen eine Region festlegen! Konnte den Kanal nicht ändern, da das Funkgerät noch nicht verbunden ist. Bitte versuchen Sie es erneut. - Exportiere rangetest.csv + Export rangetest packets + Export all packets Zurücksetzen Scannen Hinzufügen diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 481fa4f39..030542214 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -116,6 +116,9 @@ Πίνακας αποσφαλμάτωσης Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Εντάξει Πρέπει να ορίσετε μια περιοχή! Couldn\'t change channel, because radio is not yet connected. Please try again. - Εξαγωγή rangetest.csv + Export rangetest packets + Export all packets Επαναφορά Σάρωση Προσθήκη diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 7d45c47e2..0fd1b5508 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -116,6 +116,9 @@ Panel de depuración Decoded Payload: Exportar registros + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtros Filtros activos Buscar en registros… @@ -139,7 +142,8 @@ Vale ¡Debe establecer una región! No se puede cambiar de canal porque la radio aún no está conectada. Por favor inténtelo de nuevo. - Guardar rangetest.csv + Export rangetest packets + Export all packets Reiniciar Escanear Añadir diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index 19ba99643..77ca390f0 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -116,6 +116,9 @@ Arendaja paneel Dekodeeritud andmed: Salvesta logi + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtrid Aktiivsed filtrid Otsi logist… @@ -139,7 +142,8 @@ Olgu Pead valima regiooni! Kanalit ei saanud vahetada, kuna raadio pole veel ühendatud. Proovi uuesti. - Lae alla rangetest.csv + Export rangetest packets + Export all packets Taasta Otsi Lisa diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 4e6c1da3b..ae1c76cc8 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -116,6 +116,9 @@ Vianetsintäpaneeli Dekoodattu data: Vie lokitiedot + Export canceled + %1$d logs exported + Failed to write log file: %1$s Suodattimet Aktiiviset suodattimet Hae lokitiedoista… @@ -139,7 +142,8 @@ OK Sinun täytyy määrittää alue! Kanavaa ei voitu vaihtaa, koska radiota ei ole vielä yhdistetty. Yritä uudelleen. - Vie rangetest.csv + Export rangetest packets + Export all packets Palauta Etsi Lisää diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 7807634db..757f409d7 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -116,6 +116,9 @@ Panneau de débogage Contenu décodé : Exporter les logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtres Filtres actifs Rechercher dans les journaux… @@ -139,7 +142,8 @@ D\'accord Vous devez définir une région ! Impossible de modifier le canal, car la radio n\'est pas encore connectée. Veuillez réessayer. - Exporter rangetest.csv + Export rangetest packets + Export all packets Réinitialiser Scanner Ajouter diff --git a/app/src/main/res/values-ga-rIE/strings.xml b/app/src/main/res/values-ga-rIE/strings.xml index 19e005f60..5e9f76e8d 100644 --- a/app/src/main/res/values-ga-rIE/strings.xml +++ b/app/src/main/res/values-ga-rIE/strings.xml @@ -116,6 +116,9 @@ Painéal Laige Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Ceadaigh Caithfidh tú réigiún a shocrú! Ní féidir an cainéal a athrú, toisc nach bhfuil an raidió nasctha fós. Déan iarracht arís. - Onnmhairigh rangetest.csv + Export rangetest packets + Export all packets Athshocraigh Scanadh Cuir leis diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml index 1881a6806..9bd0106f3 100644 --- a/app/src/main/res/values-gl-rES/strings.xml +++ b/app/src/main/res/values-gl-rES/strings.xml @@ -116,6 +116,9 @@ Panel de depuración Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ OK Tes que seleccionar rexión! Non se puido cambiar de canle, porque a radio aínda non está conectada. Por favor inténteo de novo. - Exportar rangetest.csv + Export rangetest packets + Export all packets Restablecer Escanear Engadir diff --git a/app/src/main/res/values-hr-rHR/strings.xml b/app/src/main/res/values-hr-rHR/strings.xml index dab40122a..e6f645312 100644 --- a/app/src/main/res/values-hr-rHR/strings.xml +++ b/app/src/main/res/values-hr-rHR/strings.xml @@ -116,6 +116,9 @@ Otklanjanje pogrešaka Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ U redu Potrebno je postaviti regiju! Nije moguće promijeniti kanal jer radio još nije povezan. Molim pokušajte ponovno. - Izvezi rangetest.csv + Export rangetest packets + Export all packets Resetiraj Pretraži Dodaj diff --git a/app/src/main/res/values-ht-rHT/strings.xml b/app/src/main/res/values-ht-rHT/strings.xml index 167b57ac3..e2f5953d5 100644 --- a/app/src/main/res/values-ht-rHT/strings.xml +++ b/app/src/main/res/values-ht-rHT/strings.xml @@ -116,6 +116,9 @@ Panno Debug Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Dakò Ou dwe mete yon rejyon! Nou pa t kapab chanje kanal la paske radyo a poko konekte. Tanpri eseye ankò. - Eksporte rangetest.csv + Export rangetest packets + Export all packets Reyajiste Eskane Ajoute diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 2c1578edc..f4b40f7ec 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -116,6 +116,9 @@ Hibakereső panel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ OK Be kell állítania egy régiót Nem lehet csatornát váltani, mert a rádió nincs csatlakoztatva. Kérem próbálja meg újra. - Rangetest.csv exportálása + Export rangetest packets + Export all packets Újraindítás Keresés Új hozzáadása diff --git a/app/src/main/res/values-is-rIS/strings.xml b/app/src/main/res/values-is-rIS/strings.xml index 2dd71282e..d86efa1a8 100644 --- a/app/src/main/res/values-is-rIS/strings.xml +++ b/app/src/main/res/values-is-rIS/strings.xml @@ -116,6 +116,9 @@ Villuleitarborð Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Í lagi Þú verður að velja svæði! Gat ekki skipt um rás vegna þess að radíó er ekki enn tengt. Vinsamlegast reyndu aftur. - Flytja út skránna rangetest.csv + Export rangetest packets + Export all packets Endurræsa Leita Bæta við diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index f72c09217..2789bcaf3 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -116,6 +116,9 @@ Pannello Di Debug Payload decodificato: Esporta i logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtri Filtri attivi Cerca nei log… @@ -139,7 +142,8 @@ Ok Devi impostare una regione! Impossibile cambiare il canale, perché la radio non è ancora connessa. Riprova. - Esporta rangetest.csv + Export rangetest packets + Export all packets Reset Scan Aggiungere diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index e4326af5f..c7c9c31f1 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -116,6 +116,9 @@ פאנל דיבאג Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ אישור חובה לבחור אזור! לא ניתן לשנות ערוץ כי אין מכשיר מחובר. בבקשה נסה שנית. - ייצא rangetest.csv + Export rangetest packets + Export all packets איפוס סריקה הוסף diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 4cb2d46f0..04171dded 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -117,6 +117,9 @@ デバッグ Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -140,7 +143,8 @@ OK リージョンを指定する必要があります。 デバイスが未接続のため、チャンネルが変更できませんでした。もう一度やり直してください。 - rangetest.csv をエクスポート + Export rangetest packets + Export all packets リセット スキャン 追加 diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index f88cfeb71..21f3ec623 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -116,6 +116,9 @@ 디버그 패널 Decoded Payload: 로그 내보내기 + Export canceled + %1$d logs exported + Failed to write log file: %1$s 필터 Active filters Search in logs… @@ -139,7 +142,8 @@ 확인 지역을 설정해 주세요! 장치가 연결되지않아 채널을 변경할 수 없습니다. 다시 시도해주세요. - rangetest.csv 내보내기 + Export rangetest packets + Export all packets 초기화 스캔 추가 diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml index d1ee9c871..3229c2cb3 100644 --- a/app/src/main/res/values-lt-rLT/strings.xml +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -116,6 +116,9 @@ Derinimo skydelis Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Gerai Turite nustatyti regioną! Nepavyko pakeisti kanalo, nes radijas dar nėra prisijungęs. Bandykite dar kartą. - Eksportuoti rangetest.csv + Export rangetest packets + Export all packets Nustatyti iš naujo Skenuoti Pridėti diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 4e820e14e..aa4bc30df 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -116,6 +116,9 @@ Debug-paneel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ OK Je moet een regio instellen! Kon kanaal niet wijzigen, omdat de radio nog niet is aangesloten. Probeer het opnieuw. - Exporteer rangetest.csv + Export rangetest packets + Export all packets Reset Scan Voeg toe diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index 777bc49a9..c9b3bbc1c 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -116,6 +116,9 @@ Feilsøkningspanel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Ok Du må angi en region! Kunne ikke endre kanalen, fordi radio ikke er tilkoblet enda. Vennligst prøv på nytt. - Eksporter rekkeviddetest.csv + Export rangetest packets + Export all packets Nullstill Søk Legg til diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 031323e61..e70e8c946 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -116,6 +116,9 @@ Panel debugowania Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ OK Musisz ustawić region! Nie można zmienić kanału, ponieważ urządzenie nie jest jeszcze podłączone. Proszę, spróbuj ponownie. - Eksport rangetest.csv + Export rangetest packets + Export all packets Zresetuj Skanowanie Dodaj diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fd2dbdd7a..78af3af21 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -116,6 +116,9 @@ Painel de depuração Pacote Decodificado: Exportar Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtros Filtros ativos Pesquisar nos logs… @@ -139,7 +142,8 @@ Ok Você deve informar uma região! Não foi possível mudar de canal, rádio não conectado. Tente novamente. - Exportar rangetest.csv + Export rangetest packets + Export all packets Redefinir Escanear Adicionar diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index f4cf551c3..214a10a9f 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -116,6 +116,9 @@ Painel de depuração Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Okay Você deve informar uma região! Não foi possível mudar de canal, rádio desligado. Tente novamente. - Exportar rangetest.csv + Export rangetest packets + Export all packets Redefinir Digitalizar Adicionar diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index 28be5bd7c..d807d39d3 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -116,6 +116,9 @@ Panou debug Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Ok Trebuie să alegeți o regiune! Nu s-a putut schimba canalul, deoarece radioul nu este conectat încă. Vă rugăm să încercați din nou. - Export rangetest.csv + Export rangetest packets + Export all packets Resetare Scanare Adaugă diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 78581c027..bd20f76aa 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -116,6 +116,9 @@ Панель отладки Decoded Payload: Экспортировать логи + Export canceled + %1$d logs exported + Failed to write log file: %1$s Фильтры Активные фильтры Искать в журнале… @@ -139,7 +142,8 @@ Лады Вы должны задать регион! Не удалось изменить канал, потому что радио еще не подключено. Пожалуйста, попробуйте еще раз. - Экспортировать rangetest.csv + Export rangetest packets + Export all packets Сброс Сканирования Добавить diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 9473e2707..d1f5c597b 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -116,6 +116,9 @@ Debug okno Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ OK Musíte nastaviť región! Nie je možné zmeniť kanál, pretože vysielač ešte nie je pripojený. Skúste to neskôr. - Exportovať rangetest.csv + Export rangetest packets + Export all packets Obnoviť Skenovať Pridať diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml index 5e62a383e..826af98e9 100644 --- a/app/src/main/res/values-sl-rSI/strings.xml +++ b/app/src/main/res/values-sl-rSI/strings.xml @@ -116,6 +116,9 @@ Plošča za odpravljanje napak Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ V redu Nastavitev regije! Menjava ni možna ni radia. - Izvozi rangetest.csv + Export rangetest packets + Export all packets Ponastavi Skeniraj Dodaj diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index db447f4cc..fdba19216 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -116,6 +116,9 @@ Paneli i debug-ut Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Mirë Duhet të vendosni një rajon! Nuk mund të ndryshoni kanalin, sepse radioja ende nuk është lidhur. Ju lutemi provoni përsëri. - Eksporto rangetest.csv + Export rangetest packets + Export all packets Rivendos Skano Shto diff --git a/app/src/main/res/values-srp/strings.xml b/app/src/main/res/values-srp/strings.xml index e456bed4f..40ff5b777 100644 --- a/app/src/main/res/values-srp/strings.xml +++ b/app/src/main/res/values-srp/strings.xml @@ -116,6 +116,9 @@ Панел за отклањање грешака Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Океј Мораш одабрати регион! Није било могуће променити канал, јер радио још није повезан. Молимо покушајте поново. - Извези rangetest.csv + Export rangetest packets + Export all packets Поново покрени Скенирај Додај diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 5625c9a95..838219931 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -116,6 +116,9 @@ Felsökningspanel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs… @@ -139,7 +142,8 @@ Okej Du måste ställa in en region! Det gick inte att byta kanal, eftersom radiomodulen ännu inte är ansluten. Försök igen. - Exportera rangetest.csv + Export rangetest packets + Export all packets Nollställ Sök Lägg till diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 520367e71..7f687bc87 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -116,6 +116,9 @@ Hata Ayıklama Paneli Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filtreler Aktif Filtreler Loglarda ara… @@ -139,7 +142,8 @@ Tamam Bölge seçmelisin! Radyo bağlı olmadığından, kanal değiştirilemedi. Lütfen tekrar deneyin. - Dışa aktar: rangetest.csv + Export rangetest packets + Export all packets Sıfırla Tara Ekle diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index dd069a2dc..aa1436a59 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -116,6 +116,9 @@ Панель налагодження Decoded Payload: Експортувати журнали + Export canceled + %1$d logs exported + Failed to write log file: %1$s Фільтри Active filters Search in logs… @@ -139,7 +142,8 @@ Гаразд Ви повинні встановити регіон! Неможливо змінити канал, тому що радіо поки що не підключені. Будь ласка, спробуйте ще раз. - Експортувати rangetest.csv + Export rangetest packets + Export all packets Скинути Сканувати Додати diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d3bc406f3..40d8b6250 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -116,6 +116,9 @@ 调试面板 解码Payload: 导出程序日志 + Export canceled + %1$d logs exported + Failed to write log file: %1$s 筛选器 启用的过滤器 搜索日志… @@ -139,7 +142,8 @@ 确定 您必须先选择一个地区 无法更改频道,因为装置尚未连接。请再试一次。 - 导出信号测试数据.csv + Export rangetest packets + Export all packets 重置 扫描 新增 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8aa7cab04..e8a187620 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -116,6 +116,9 @@ 除錯面板 解析封包: 匯出日誌 + Export canceled + %1$d logs exported + Failed to write log file: %1$s 篩選 啟動篩選功能 在日誌中搜尋… @@ -139,7 +142,8 @@ 好的 您必須設定一個區域! 無法更改頻道,因為裝置尚未連接。請再試一次。 - 匯出範圍測試資料.csv + Export rangetest packets + Export all packets 重設 掃描 新增 From 6123f5de248160e06bbeb51062fa1032aa3bb967 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 16:26:38 +1000 Subject: [PATCH 08/18] repo: update label check so it doesn't need help (#2995) --- .github/workflows/pr_enforce_labels.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index bd4072304..ce382f21d 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -16,10 +16,17 @@ jobs: uses: actions/github-script@v8 with: script: | - const labels = context.payload.pull_request.labels.map(label => label.name); + // Always fetch the latest labels from the GitHub API to avoid stale context + const prNumber = context.payload.pull_request.number; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + const latestLabels = pr.labels.map(label => label.name); const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release']; - const hasRequiredLabel = labels.some(label => requiredLabels.includes(label)); - console.log(labels); + const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label)); + console.log('Latest labels:', latestLabels); if (!hasRequiredLabel) { core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`); } \ No newline at end of file From e37cc5112d99588a1fe2112b4041821f38621196 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 04:03:00 -0500 Subject: [PATCH 09/18] New Crowdin updates (#2997) --- app/src/main/res/values-de-rDE/strings.xml | 14 +++++++------- app/src/main/res/values-fi-rFI/strings.xml | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 351a5d1aa..bb68a533e 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -116,9 +116,9 @@ Debug-Ausgaben Dekodiertes Payload: Protokolle exportieren - Export canceled - %1$d logs exported - Failed to write log file: %1$s + Export abgebrochen + %1$d Protokolle exportiert + Fehler beim Scheiben der Protokolldatei: %1$s Filter Aktive Filter In Protokollen suchen @@ -142,8 +142,8 @@ OK Sie müssen eine Region festlegen! Konnte den Kanal nicht ändern, da das Funkgerät noch nicht verbunden ist. Bitte versuchen Sie es erneut. - Export rangetest packets - Export all packets + Reichweitentest exportieren + Alle Pakete exportieren Zurücksetzen Scannen Hinzufügen @@ -640,7 +640,7 @@ Schlüssel exportieren Exportiert den öffentlichen und privaten Schlüssel in eine Datei. Bitte speichern Sie diese an einem sicheren Ort. Entsperrte Module - Modules already unlocked + Module sind bereits freigeschaltet Entfernt (%1$d Online / %2$d Gesamt) Reagieren @@ -778,7 +778,7 @@ URL muss Platzhalter enthalten. URL Vorlage Verlaufspunkt - App + Anwendung Version Kanalfunktionen Standortfreigabe diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index ae1c76cc8..75ac4510a 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -116,9 +116,9 @@ Vianetsintäpaneeli Dekoodattu data: Vie lokitiedot - Export canceled - %1$d logs exported - Failed to write log file: %1$s + Vienti peruutettu + %1$d lokitietoa viety + Lokitiedoston kirjoittaminen epäonnistui: %1$s Suodattimet Aktiiviset suodattimet Hae lokitiedoista… @@ -142,8 +142,8 @@ OK Sinun täytyy määrittää alue! Kanavaa ei voitu vaihtaa, koska radiota ei ole vielä yhdistetty. Yritä uudelleen. - Export rangetest packets - Export all packets + Vie kuuluvuustestin paketit + Vie kaikki paketit Palauta Etsi Lisää From 80a7b9e081fdae366e403bc47ca5baeff50cf15b Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 08:06:14 -0500 Subject: [PATCH 10/18] chore: Scheduled updates (Firmware, Hardware) (#2998) Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> --- app/src/main/assets/firmware_releases.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json index a7d6e8002..6193440df 100644 --- a/app/src/main/assets/firmware_releases.json +++ b/app/src/main/assets/firmware_releases.json @@ -193,6 +193,12 @@ "title": "the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS", "page_url": "https://github.com/meshtastic/firmware/pull/7658", "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17074730483" + }, + { + "id": "7583", + "title": "chore(deps): update meshtastic/web to v2.6.6", + "page_url": "https://github.com/meshtastic/firmware/pull/7583", + "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17070663764" } ] } \ No newline at end of file From 99938e97bd481e261c2edfd16bba1f09d5e73b10 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 23:34:03 +1000 Subject: [PATCH 11/18] add times to traceroute displays. (#2999) --- .../geeksville/mesh/model/MetricsViewModel.kt | 4 +- .../geeksville/mesh/model/RouteDiscovery.kt | 77 ++++++++++--------- .../geeksville/mesh/service/MeshService.kt | 21 ++++- .../mesh/ui/metrics/TracerouteLog.kt | 15 +++- 4 files changed, 75 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt index c88ee607f..a5f8d2216 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt @@ -84,7 +84,7 @@ data class MetricsState( val powerMetrics: List = emptyList(), val hostMetrics: List = emptyList(), val tracerouteRequests: List = emptyList(), - val tracerouteResults: List = emptyList(), + val tracerouteResults: List = emptyList(), val positionLogs: List = emptyList(), val deviceHardware: DeviceHardware? = null, val isLocalDevice: Boolean = false, @@ -321,7 +321,7 @@ constructor( combine( meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE), - meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE), + meshLogRepository.getLogsFrom(destNum ?: 0, PortNum.TRACEROUTE_APP_VALUE), ) { request, response -> _state.update { state -> state.copy( diff --git a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt b/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt index 2497e8dcc..59a74881a 100644 --- a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt +++ b/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt @@ -22,48 +22,51 @@ import com.geeksville.mesh.MeshProtos.RouteDiscovery import com.geeksville.mesh.Portnums val MeshProtos.MeshPacket.fullRouteDiscovery: RouteDiscovery? - get() = with(decoded) { - if (hasDecoded() && !wantResponse && portnum == Portnums.PortNum.TRACEROUTE_APP) { - runCatching { RouteDiscovery.parseFrom(payload).toBuilder() }.getOrNull()?.apply { - val fullRoute = listOf(to) + routeList + from - clearRoute() - addAllRoute(fullRoute) + get() = + with(decoded) { + if (hasDecoded() && !wantResponse && portnum == Portnums.PortNum.TRACEROUTE_APP) { + runCatching { RouteDiscovery.parseFrom(payload).toBuilder() } + .getOrNull() + ?.apply { + val fullRoute = listOf(to) + routeList + from + clearRoute() + addAllRoute(fullRoute) - val fullRouteBack = listOf(from) + routeBackList + to - clearRouteBack() - if (hopStart > 0 && snrBackCount > 0) { // otherwise back route is invalid - addAllRouteBack(fullRouteBack) - } - }?.build() - } else { - null + val fullRouteBack = listOf(from) + routeBackList + to + clearRouteBack() + if (hopStart > 0 && snrBackCount > 0) { // otherwise back route is invalid + addAllRouteBack(fullRouteBack) + } + } + ?.build() + } else { + null + } } - } @Suppress("MagicNumber") private fun formatTraceroutePath(nodesList: List, snrList: List): String { // nodesList should include both origin and destination nodes // origin will not have an SNR value, but destination should - val snrStr = if (snrList.size == nodesList.size - 1) { - snrList - } else { - // use unknown SNR for entire route if snrList has invalid size - List(nodesList.size - 1) { -128 } - }.map { snr -> - val str = if (snr == -128) "?" else "${snr / 4f}" - "⇊ $str dB" - } + val snrStr = + if (snrList.size == nodesList.size - 1) { + snrList + } else { + // use unknown SNR for entire route if snrList has invalid size + List(nodesList.size - 1) { -128 } + } + .map { snr -> + val str = if (snr == -128) "?" else "${snr / 4f}" + "⇊ $str dB" + } - return nodesList.map { userName -> - "■ $userName" - }.flatMapIndexed { i, nodeStr -> - if (i == 0) listOf(nodeStr) else listOf(snrStr[i - 1], nodeStr) - }.joinToString("\n") + return nodesList + .map { userName -> "■ $userName" } + .flatMapIndexed { i, nodeStr -> if (i == 0) listOf(nodeStr) else listOf(snrStr[i - 1], nodeStr) } + .joinToString("\n") } -private fun RouteDiscovery.getTracerouteResponse( - getUser: (nodeNum: Int) -> String, -): String = buildString { +private fun RouteDiscovery.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String = buildString { if (routeList.isNotEmpty()) { append("Route traced toward destination:\n\n") append(formatTraceroutePath(routeList.map(getUser), snrTowardsList)) @@ -75,6 +78,10 @@ private fun RouteDiscovery.getTracerouteResponse( } } -fun MeshProtos.MeshPacket.getTracerouteResponse( - getUser: (nodeNum: Int) -> String, -): String? = fullRouteDiscovery?.getTracerouteResponse(getUser) +fun MeshProtos.MeshPacket.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = + fullRouteDiscovery?.getTracerouteResponse(getUser) + +/** Returns a traceroute response string only when the result is complete (both directions). */ +fun MeshProtos.MeshPacket.getFullTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = fullRouteDiscovery + ?.takeIf { it.routeList.isNotEmpty() && it.routeBackList.isNotEmpty() } + ?.getTracerouteResponse(getUser) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index fe6d4151c..3874b2e25 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -73,7 +73,7 @@ import com.geeksville.mesh.fromRadio import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.NO_DEVICE_SELECTED import com.geeksville.mesh.model.Node -import com.geeksville.mesh.model.getTracerouteResponse +import com.geeksville.mesh.model.getFullTracerouteResponse import com.geeksville.mesh.position import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.location.LocationRepository @@ -148,6 +148,8 @@ class MeshService : @Inject lateinit var meshPrefs: MeshPrefs + private val tracerouteStartTimes = ConcurrentHashMap() + companion object : Logging { // Intents broadcast by MeshService @@ -848,7 +850,21 @@ class MeshService : } Portnums.PortNum.TRACEROUTE_APP_VALUE -> { - radioConfigRepository.setTracerouteResponse(packet.getTracerouteResponse(::getUserName)) + val full = packet.getFullTracerouteResponse(::getUserName) + if (full != null) { + val requestId = packet.decoded.requestId + val start = tracerouteStartTimes.remove(requestId) + val response = + if (start != null) { + val elapsedMs = System.currentTimeMillis() - start + val seconds = elapsedMs / 1000.0 + info("Traceroute $requestId complete in $seconds s") + "$full\n\nDuration: ${"%.1f".format(seconds)} s" + } else { + full + } + radioConfigRepository.setTracerouteResponse(response) + } } else -> debug("No custom processing needed for ${data.portnumValue}") @@ -2376,6 +2392,7 @@ class MeshService : } override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions { + tracerouteStartTimes[requestId] = System.currentTimeMillis() packetHandler.sendToRadio( newMeshPacketTo(destNum).buildMeshPacket( wantAck = true, diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt index 24577db71..8947d40ca 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt @@ -63,6 +63,7 @@ import com.geeksville.mesh.model.fullRouteDiscovery import com.geeksville.mesh.model.getTracerouteResponse import com.geeksville.mesh.ui.common.components.SimpleAlertDialog import com.geeksville.mesh.ui.common.theme.AppTheme +import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import java.text.DateFormat @OptIn(ExperimentalFoundationApi::class) @@ -88,9 +89,9 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod items(state.tracerouteRequests, key = { it.uuid }) { log -> val result = remember(state.tracerouteRequests) { - state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id } + state.tracerouteResults.find { it.fromRadio.packet.decoded.requestId == log.fromRadio.packet.id } } - val route = remember(result) { result?.fullRouteDiscovery } + val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery } val time = dateFormat.format(log.received_date) val (text, icon) = route.getTextAndIcon() @@ -103,7 +104,15 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod modifier = Modifier.combinedClickable(onLongClick = { expanded = true }) { if (result != null) { - showDialog = result.getTracerouteResponse(::getUsername) + val full = route + if (full != null && full.routeList.isNotEmpty() && full.routeBackList.isNotEmpty()) { + val elapsedMs = (result.received_date - log.received_date).coerceAtLeast(0) + val seconds = elapsedMs.toDouble() / MS_PER_SEC + val base = result.fromRadio.packet.getTracerouteResponse(::getUsername) + showDialog = "$base\n\nDuration: ${"%.1f".format(seconds)} s" + } else { + showDialog = result.fromRadio.packet.getTracerouteResponse(::getUsername) + } } }, ) From 266379c979c1738d685aeb5b3c7026ef28f141df Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 23:34:15 +1000 Subject: [PATCH 12/18] Feat/2932 env metrics radiation (#2993) --- .../mesh/ui/metrics/EnvironmentMetrics.kt | 207 ++++++++++++------ 1 file changed, 136 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt index 3a4308998..3625ee4f3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -45,6 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -120,35 +120,45 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) { } @Composable -private fun TemperatureDisplay(temperature: Float, environmentDisplayFahrenheit: Boolean) { - if (!temperature.isNaN()) { - val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" - Text( - text = textFormat.format(stringResource(id = R.string.temperature), temperature), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) +private fun TemperatureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, environmentDisplayFahrenheit: Boolean) { + envMetrics.temperature?.let { temperature -> + if (!temperature.isNaN()) { + val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" + Text( + text = textFormat.format(stringResource(id = R.string.temperature), temperature), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + } } } @Composable private fun HumidityAndBarometricPressureDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - envMetrics.relativeHumidity?.let { humidity -> - if (!humidity.isNaN()) { + val hasHumidity = envMetrics.relativeHumidity?.let { !it.isNaN() } == true + val hasPressure = envMetrics.barometricPressure?.let { !it.isNaN() && it > 0 } == true + + if (hasHumidity || hasPressure) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + if (hasHumidity) { + val humidity = envMetrics.relativeHumidity!! Text( text = "%s %.2f%%".format(stringResource(id = R.string.humidity), humidity), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, + modifier = Modifier.padding(vertical = 0.dp), ) } - } - envMetrics.barometricPressure?.let { pressure -> - if (!pressure.isNaN() && pressure > 0) { // Keep pressure > 0 check + if (hasPressure) { + val pressure = envMetrics.barometricPressure!! Text( text = "%.2f hPa".format(pressure), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, + modifier = Modifier.padding(vertical = 0.dp), ) } } @@ -161,7 +171,6 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e envMetrics.soilTemperature != null || (envMetrics.soilMoisture != null && envMetrics.soilMoisture != Int.MIN_VALUE) ) { - Spacer(modifier = Modifier.height(4.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" val soilMoistureTextFormat = "%s %d%%" @@ -191,41 +200,23 @@ private fun SoilMetricsDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics, e } } -@Composable -private fun IaqDisplay(iaqValue: Int) { - if (iaqValue != Int.MIN_VALUE) { - Spacer(modifier = Modifier.height(4.dp)) - /* Air Quality */ - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.iaq), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - Spacer(modifier = Modifier.width(4.dp)) - IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot) - } - } -} - @Composable private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - envMetrics.lux?.let { luxValue -> - if (!luxValue.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + val hasLux = envMetrics.lux != null && !envMetrics.lux.isNaN() + val hasUvLux = envMetrics.uvLux != null && !envMetrics.uvLux.isNaN() + + if (hasLux || hasUvLux) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasLux) { + val luxValue = envMetrics.lux!! Text( text = "%s %.0f lx".format(stringResource(R.string.lux), luxValue), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - } - } - envMetrics.uvLux?.let { uvLuxValue -> - if (!uvLuxValue.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasUvLux) { + val uvLuxValue = envMetrics.uvLux!! Text( text = "%s %.0f UVlx".format(stringResource(R.string.uv_lux), uvLuxValue), color = MaterialTheme.colorScheme.onSurface, @@ -238,23 +229,21 @@ private fun LuxUVLuxDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { @Composable private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { - envMetrics.voltage?.let { voltage -> - if (!voltage.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage.isNaN() + val hasCurrent = envMetrics.current != null && !envMetrics.current.isNaN() + + if (hasVoltage || hasCurrent) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasVoltage) { + val voltage = envMetrics.voltage!! Text( text = "%s %.2f V".format(stringResource(R.string.voltage), voltage), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - } - } - - envMetrics.current?.let { current -> - if (!current.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (hasCurrent) { + val current = envMetrics.current!! Text( text = "%s %.2f mA".format(stringResource(R.string.current), current), color = MaterialTheme.colorScheme.onSurface, @@ -266,15 +255,66 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics } @Composable -private fun GasResistanceDisplay(gasResistance: Float) { - if (!gasResistance.isNaN()) { - Spacer(modifier = Modifier.height(4.dp)) +private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { + val iaqValue = envMetrics.iaq + val gasResistance = envMetrics.gasResistance + + if ((iaqValue != null && iaqValue != Int.MIN_VALUE) || (gasResistance?.isFinite() == true)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) + if (iaqValue != null && iaqValue != Int.MIN_VALUE) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.iaq), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + Spacer(modifier = Modifier.width(4.dp)) + IndoorAirQuality(iaq = iaqValue, displayMode = IaqDisplayMode.Dot) + } + } + if (gasResistance != null && !gasResistance.isNaN()) { + Text( + text = "%s %.2f Ohm".format(stringResource(R.string.gas_resistance), gasResistance), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + } + } + } + // These are in a differnt proto ... + // envMetrics.co2?.let { co2 -> + // Spacer(modifier = Modifier.height(4.dp)) + // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + // Text( + // text = "%s %.0f ppm".format(stringResource(R.string.co2), co2), + // color = MaterialTheme.colorScheme.onSurface, + // fontSize = MaterialTheme.typography.labelLarge.fontSize, + // ) + // } + // } + // envMetrics.tvoc?.let { tvoc -> + // Spacer(modifier = Modifier.height(4.dp)) + // Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + // Text( + // text = "%s %.0f ppb".format(stringResource(R.string.tvoc), tvoc), + // color = MaterialTheme.colorScheme.onSurface, + // fontSize = MaterialTheme.typography.labelLarge.fontSize, + // ) + // } + // } +} + +@Composable +private fun RadiationDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics) { + envMetrics.radiation?.let { radiation -> + if (!radiation.isNaN() && radiation > 0f) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + text = "%s %.2f µSv/h".format(stringResource(R.string.radiation), radiation), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + ) + } } } } @@ -292,7 +332,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) { val envMetrics = telemetry.environmentMetrics val time = telemetry.time * MS_PER_SEC - Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp)) { /* Time and Temperature */ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( @@ -300,23 +340,48 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa style = TextStyle(fontWeight = FontWeight.Bold), fontSize = MaterialTheme.typography.labelLarge.fontSize, ) - envMetrics.temperature?.let { temperature -> TemperatureDisplay(temperature, environmentDisplayFahrenheit) } + TemperatureDisplay(envMetrics, environmentDisplayFahrenheit) } - Spacer(modifier = Modifier.height(4.dp)) - - /* Humidity and Barometric Pressure */ HumidityAndBarometricPressureDisplay(envMetrics) - /* Soil Moisture and Soil Temperature */ SoilMetricsDisplay(envMetrics, environmentDisplayFahrenheit) - envMetrics.iaq?.let { iaqValue -> IaqDisplay(iaqValue) } + GasCompositionDisplay(envMetrics) LuxUVLuxDisplay(envMetrics) VoltageCurrentDisplay(envMetrics) - - envMetrics.gasResistance?.let { gasResistance -> GasResistanceDisplay(gasResistance) } + RadiationDisplay(envMetrics) + } +} + +@Suppress("MagicNumber") // preview data +@Preview(showBackground = true) +@Composable +private fun PreviewEnvironmentMetricsContent() { + // Build a fake EnvironmentMetrics using the generated proto builder APIs + val fakeEnvMetrics = + TelemetryProtos.EnvironmentMetrics.newBuilder() + .setTemperature(22.5f) + .setRelativeHumidity(55.0f) + .setBarometricPressure(1013.25f) + .setSoilMoisture(33) + .setSoilTemperature(18.0f) + .setLux(100.0f) + .setUvLux(100.0f) + .setVoltage(3.7f) + .setCurrent(0.12f) + .setIaq(100) + .setRadiation(0.15f) + .setGasResistance(1200.0f) + .build() + val fakeTelemetry = + TelemetryProtos.Telemetry.newBuilder() + .setTime((System.currentTimeMillis() / 1000).toInt()) + .setEnvironmentMetrics(fakeEnvMetrics) + .build() + MaterialTheme { + Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) } } } From 93e7eb3aa06a53a39aa6c47d4b6180420b1c91d8 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sun, 7 Sep 2025 00:40:37 +1000 Subject: [PATCH 13/18] feat #2810 - fix export config file name (#3000) --- .../java/com/geeksville/mesh/ui/settings/SettingsScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index b7fcb9ad3..771113e7b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -135,11 +135,15 @@ fun SettingsScreen( viewModel.installProfile(it) } else { deviceProfile = it + val nodeName = it.shortName.ifBlank { "node" } + val dateFormat = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.getDefault()) + val dateStr = dateFormat.format(java.util.Date()) + val fileName = "Meshtastic_${nodeName}_${dateStr}_nodeConfig.cfg" val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/*" - putExtra(Intent.EXTRA_TITLE, "device_profile.cfg") + putExtra(Intent.EXTRA_TITLE, fileName) } exportConfigLauncher.launch(intent) } From 91ce6c5b93982203eddec73546510266e3e54c31 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 13:24:06 -0500 Subject: [PATCH 14/18] feat(map): allow map to follow phone bearing (#3002) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/ui/map/MapView.kt | 76 +++++++++---------- .../ui/map/components/MapControlsOverlay.kt | 12 +-- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index 4e8066e6f..e80741ebb 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -190,6 +190,7 @@ fun MapView( // Location tracking state var isLocationTrackingEnabled by remember { mutableStateOf(false) } + var followPhoneBearing by remember { mutableStateOf(false) } LocationPermissionsHandler { isGranted -> hasLocationPermission = isGranted } @@ -224,11 +225,27 @@ fun MapView( if (isLocationTrackingEnabled) { locationResult.lastLocation?.let { location -> val latLng = LatLng(location.latitude, location.longitude) + val cameraUpdate = + if (followPhoneBearing) { + val bearing = + if (location.hasBearing()) { + location.bearing + } else { + cameraPositionState.position.bearing + } + CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder() + .target(latLng) + .zoom(cameraPositionState.position.zoom) + .bearing(bearing) + .build(), + ) + } else { + CameraUpdateFactory.newLatLngZoom(latLng, cameraPositionState.position.zoom) + } coroutineScope.launch { try { - cameraPositionState.animate( - CameraUpdateFactory.newLatLngZoom(latLng, cameraPositionState.position.zoom), - ) + cameraPositionState.animate(cameraUpdate) } catch (e: IllegalStateException) { debug("Error animating camera to location: ${e.message}") } @@ -260,7 +277,6 @@ fun MapView( } } - // Clean up location tracking on disposal DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } } val allNodes by @@ -344,7 +360,7 @@ fun MapView( zoomControlsEnabled = true, mapToolbarEnabled = true, compassEnabled = false, - myLocationButtonEnabled = false, // Disabled - we use custom location button + myLocationButtonEnabled = false, rotationGesturesEnabled = true, scrollGesturesEnabled = true, tiltGesturesEnabled = true, @@ -574,46 +590,30 @@ fun MapView( isLocationTrackingEnabled = isLocationTrackingEnabled, onToggleLocationTracking = { if (hasLocationPermission) { - if (!isLocationTrackingEnabled) { - // When enabling tracking, get current location and center on it - try { - fusedLocationClient.lastLocation.addOnSuccessListener { location -> - location?.let { - val latLng = LatLng(it.latitude, it.longitude) - coroutineScope.launch { - try { - cameraPositionState.animate( - CameraUpdateFactory.newLatLngZoom(latLng, 16f), - ) - } catch (e: IllegalStateException) { - debug("Error centering camera on location: ${e.message}") - } - } - } - } - } catch (e: SecurityException) { - debug("Location permission not available: ${e.message}") - } - } isLocationTrackingEnabled = !isLocationTrackingEnabled + if (!isLocationTrackingEnabled) { + followPhoneBearing = false + } } }, bearing = cameraPositionState.position.bearing, - onOrientNorth = { - coroutineScope.launch { - try { - val currentPosition = cameraPositionState.position - val newCameraPosition = - CameraPosition.Builder(currentPosition) - .bearing(0f) // Orient to north - .build() - cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition)) - debug("Oriented map to north") - } catch (e: IllegalStateException) { - debug("Error orienting map to north: ${e.message}") + onCompassClick = { + if (isLocationTrackingEnabled) { + followPhoneBearing = !followPhoneBearing + } else { + coroutineScope.launch { + try { + val currentPosition = cameraPositionState.position + val newCameraPosition = CameraPosition.Builder(currentPosition).bearing(0f).build() + cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition)) + debug("Oriented map to north") + } catch (e: IllegalStateException) { + debug("Error orienting map to north: ${e.message}") + } } } }, + followPhoneBearing = followPhoneBearing, ) } if (showLayersBottomSheet) { diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt b/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt index 34ca2bf70..faf4b6c0f 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt @@ -20,6 +20,7 @@ package com.geeksville.mesh.ui.map.components import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.LocationDisabled +import androidx.compose.material.icons.filled.Navigation import androidx.compose.material.icons.outlined.Layers import androidx.compose.material.icons.outlined.Map import androidx.compose.material.icons.outlined.MyLocation @@ -55,7 +56,8 @@ fun MapControlsOverlay( isLocationTrackingEnabled: Boolean = false, onToggleLocationTracking: () -> Unit = {}, bearing: Float = 0f, - onOrientNorth: () -> Unit = {}, + onCompassClick: () -> Unit = {}, + followPhoneBearing: Boolean, ) { HorizontalFloatingToolbar( modifier = modifier, @@ -63,7 +65,7 @@ fun MapControlsOverlay( leadingContent = {}, trailingContent = {}, content = { - CompassButton(onOrientNorth = onOrientNorth, bearing = bearing) + CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing) if (showFilterButton) { Box { MapButton( @@ -117,14 +119,14 @@ fun MapControlsOverlay( } @Composable -private fun CompassButton(onOrientNorth: () -> Unit, bearing: Float) { - val icon = Icons.Outlined.Navigation +private fun CompassButton(onClick: () -> Unit, bearing: Float, isFollowing: Boolean) { + val icon = if (isFollowing) Icons.Filled.Navigation else Icons.Outlined.Navigation MapButton( modifier = Modifier.rotate(-bearing), icon = icon, iconTint = MaterialTheme.colorScheme.StatusRed.takeIf { bearing == 0f }, contentDescription = stringResource(id = R.string.orient_north), - onClick = onOrientNorth, + onClick = onClick, ) } From d5e53e26399fdc8f23d30c61d2106a4cbdac1175 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:06:02 -0500 Subject: [PATCH 15/18] feat(map): keep screen on when location tracking is enabled (#3003) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../java/com/geeksville/mesh/ui/map/MapView.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index e80741ebb..f0f557002 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -19,11 +19,13 @@ package com.geeksville.mesh.ui.map +import android.app.Activity import android.content.Intent import android.graphics.Canvas import android.graphics.Paint import android.location.Location import android.net.Uri +import android.view.WindowManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatDelegate @@ -349,6 +351,17 @@ fun MapView( var showClusterItemsDialog by remember { mutableStateOf?>(null) } + LaunchedEffect(isLocationTrackingEnabled) { + val activity = context as? Activity ?: return@LaunchedEffect + val window = activity.window + + if (isLocationTrackingEnabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + Scaffold { paddingValues -> Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { GoogleMap( From 50545e1c1af5b4658031d02f97e48a479de54ef9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:06:30 -0500 Subject: [PATCH 16/18] chore(deps): update meshtastic protobufs to a84657c (#3001) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/src/main/proto | 2 +- mesh_service_example/src/main/proto | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/proto b/app/src/main/proto index 07d6573e1..a84657c22 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 07d6573e1065344e80845de704885f011e515233 +Subproject commit a84657c220421536f18d11fc5edf680efadbceeb diff --git a/mesh_service_example/src/main/proto b/mesh_service_example/src/main/proto index 07d6573e1..a84657c22 160000 --- a/mesh_service_example/src/main/proto +++ b/mesh_service_example/src/main/proto @@ -1 +1 @@ -Subproject commit 07d6573e1065344e80845de704885f011e515233 +Subproject commit a84657c220421536f18d11fc5edf680efadbceeb From 5e462c9fd72a7af99c9493c98ad0fc5d4d1704c3 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:09:33 -0500 Subject: [PATCH 17/18] chore: Scheduled updates (Firmware, Hardware) (#3005) Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> --- app/src/main/assets/firmware_releases.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json index 6193440df..a7d6e8002 100644 --- a/app/src/main/assets/firmware_releases.json +++ b/app/src/main/assets/firmware_releases.json @@ -193,12 +193,6 @@ "title": "the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS", "page_url": "https://github.com/meshtastic/firmware/pull/7658", "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17074730483" - }, - { - "id": "7583", - "title": "chore(deps): update meshtastic/web to v2.6.6", - "page_url": "https://github.com/meshtastic/firmware/pull/7583", - "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17070663764" } ] } \ No newline at end of file From ce60d490b78a48af9a0d21ad2967695ec4c8086f Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:33:06 -0500 Subject: [PATCH 18/18] fix: map regressions (#3004) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/android/prefs/GoogleMapsPrefs.kt | 2 + .../com/geeksville/mesh/ui/map/MapView.kt | 18 ++- .../geeksville/mesh/ui/map/MapViewModel.kt | 110 ++++++++++++------ .../android/prefs/StringSetPrefDelegate.kt | 35 ++++++ 4 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt diff --git a/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt b/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt index 3365f8129..bf39bc344 100644 --- a/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt +++ b/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt @@ -24,10 +24,12 @@ import com.google.maps.android.compose.MapType interface GoogleMapsPrefs { var selectedGoogleMapType: String? var selectedCustomTileUrl: String? + var hiddenLayerUrls: Set } class GoogleMapsPrefsImpl(prefs: SharedPreferences) : GoogleMapsPrefs { override var selectedGoogleMapType: String? by NullableStringPrefDelegate(prefs, "selected_google_map_type", MapType.NORMAL.name) override var selectedCustomTileUrl: String? by NullableStringPrefDelegate(prefs, "selected_custom_tile_url", null) + override var hiddenLayerUrls: Set by StringSetPrefDelegate(prefs, "hidden_layer_urls", emptySet()) } diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index f0f557002..9a2b4eb6a 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -217,7 +217,16 @@ fun MapView( var mapTypeMenuExpanded by remember { mutableStateOf(false) } var showCustomTileManagerSheet by remember { mutableStateOf(false) } - val cameraPositionState = rememberCameraPositionState {} + val cameraPositionState = rememberCameraPositionState { + position = + CameraPosition.fromLatLngZoom( + LatLng( + ourNodeInfo?.position?.latitudeI?.times(DEG_D) ?: 0.0, + ourNodeInfo?.position?.longitudeI?.times(DEG_D) ?: 0.0, + ), + 7f, + ) + } // Location tracking functionality val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } @@ -279,7 +288,12 @@ fun MapView( } } - DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } } + DisposableEffect(Unit) { + onDispose { + fusedLocationClient.removeLocationUpdates(locationCallback) + mapViewModel.clearLoadedLayerData() + } + } val allNodes by mapViewModel.nodes diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt index a0beb072c..30b1a557e 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -231,8 +232,11 @@ constructor( val mapLayers: StateFlow> = _mapLayers.asStateFlow() init { + viewModelScope.launch { + customTileProviderRepository.getCustomTileProviders().first() + loadPersistedMapType() + } loadPersistedLayers() - loadPersistedMapType() } private fun loadPersistedMapType() { @@ -271,6 +275,7 @@ constructor( val persistedLayerFiles = layersDir.listFiles() if (persistedLayerFiles != null) { + val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls val loadedItems = persistedLayerFiles.mapNotNull { file -> if (file.isFile) { @@ -286,10 +291,11 @@ constructor( } layerType?.let { + val uri = Uri.fromFile(file) MapLayerItem( name = file.nameWithoutExtension, - uri = Uri.fromFile(file), - isVisible = true, + uri = uri, + isVisible = !hiddenLayerUrls.contains(uri.toString()), layerType = it, ) } @@ -372,7 +378,25 @@ constructor( } fun toggleLayerVisibility(layerId: String) { - _mapLayers.value = _mapLayers.value.map { if (it.id == layerId) it.copy(isVisible = !it.isVisible) else it } + var toggledLayer: MapLayerItem? = null + val updatedLayers = + _mapLayers.value.map { + if (it.id == layerId) { + toggledLayer = it.copy(isVisible = !it.isVisible) + toggledLayer + } else { + it + } + } + _mapLayers.value = updatedLayers + + toggledLayer?.let { + if (it.isVisible) { + googleMapsPrefs.hiddenLayerUrls -= it.uri.toString() + } else { + googleMapsPrefs.hiddenLayerUrls += it.uri.toString() + } + } } fun removeMapLayer(layerId: String) { @@ -383,7 +407,10 @@ constructor( LayerType.GEOJSON -> layerToRemove.geoJsonLayerData?.removeLayerFromMap() null -> {} } - layerToRemove?.uri?.let { uri -> deleteFileToInternalStorage(uri) } + layerToRemove?.uri?.let { uri -> + deleteFileToInternalStorage(uri) + googleMapsPrefs.hiddenLayerUrls -= uri.toString() + } _mapLayers.value = _mapLayers.value.filterNot { it.id == layerId } } } @@ -418,40 +445,55 @@ constructor( if (layerItem.kmlLayerData != null || layerItem.geoJsonLayerData != null) return try { when (layerItem.layerType) { - LayerType.KML -> { - val kmlLayer = - getInputStreamFromUri(layerItem)?.use { KmlLayer(map, it, application.applicationContext) } - _mapLayers.update { currentLayers -> - currentLayers.map { - if (it.id == layerItem.id) { - it.copy(kmlLayerData = kmlLayer) - } else { - it - } - } - } - } - LayerType.GEOJSON -> { - val geoJsonLayer = - getInputStreamFromUri(layerItem)?.use { inputStream -> - val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) - GeoJsonLayer(map, jsonObject) - } - _mapLayers.update { currentLayers -> - currentLayers.map { - if (it.id == layerItem.id) { - it.copy(geoJsonLayerData = geoJsonLayer) - } else { - it - } - } - } - } + LayerType.KML -> loadKmlLayerIfNeeded(layerItem, map) + + LayerType.GEOJSON -> loadGeoJsonLayerIfNeeded(layerItem, map) } } catch (e: Exception) { Timber.tag("MapViewModel").e(e, "Error loading map layer for ${layerItem.uri}") } } + + private suspend fun loadKmlLayerIfNeeded(layerItem: MapLayerItem, map: GoogleMap) { + val kmlLayer = + getInputStreamFromUri(layerItem)?.use { + KmlLayer(map, it, application.applicationContext).apply { + if (!layerItem.isVisible) removeLayerFromMap() + } + } + _mapLayers.update { currentLayers -> + currentLayers.map { + if (it.id == layerItem.id) { + it.copy(kmlLayerData = kmlLayer) + } else { + it + } + } + } + } + + private suspend fun loadGeoJsonLayerIfNeeded(layerItem: MapLayerItem, map: GoogleMap) { + val geoJsonLayer = + getInputStreamFromUri(layerItem)?.use { inputStream -> + val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) + GeoJsonLayer(map, jsonObject).apply { if (!layerItem.isVisible) removeLayerFromMap() } + } + _mapLayers.update { currentLayers -> + currentLayers.map { + if (it.id == layerItem.id) { + it.copy(geoJsonLayerData = geoJsonLayer) + } else { + it + } + } + } + } + + fun clearLoadedLayerData() { + _mapLayers.update { currentLayers -> + currentLayers.map { it.copy(kmlLayerData = null, geoJsonLayerData = null) } + } + } } enum class LayerType { diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt new file mode 100644 index 000000000..714b4936b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences +import androidx.core.content.edit +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class StringSetPrefDelegate( + private val prefs: SharedPreferences, + private val key: String, + private val defaultValue: Set, +) : ReadWriteProperty> { + override fun getValue(thisRef: Any?, property: KProperty<*>): Set = + prefs.getStringSet(key, defaultValue) ?: emptySet() + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) = + prefs.edit { putStringSet(key, value) } +}