mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-14 14:19:57 +02:00
feat(shared): refine saved devices experience
This commit is contained in:
@@ -110,6 +110,7 @@ Use one deep, session-scoped composition module for Invitation and Targeted crea
|
||||
Build quiet, intentional product interfaces. Establish hierarchy with typography, alignment, spacing, and native controls before adding containers or decoration.
|
||||
|
||||
- Give each screen one clear primary task and scanning order.
|
||||
- Use title-only headers for familiar, populated screens. Put explanatory copy in genuine empty/onboarding states or beside the specific control that needs clarification.
|
||||
- Use cards only when a real object or boundary needs containment. Prefer native lists, grouped rows, dividers, and whitespace for ordinary collections.
|
||||
- Use count badges only when the count changes a decision. Use icon tiles only when the icon is meaningful content or a native convention.
|
||||
- Keep accent color scarce. Let status, selection, or the primary action earn it.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { STRINGS_JSON } from "../config";
|
||||
import type { StringsFile } from "../types";
|
||||
|
||||
const TOKEN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
||||
const PLATFORM_PLACEHOLDER = /%(?:\d+\$)?(?:l{0,2})?[@sdif]/;
|
||||
|
||||
export async function validate() {
|
||||
const doc = JSON.parse(await Bun.file(STRINGS_JSON).text()) as StringsFile;
|
||||
@@ -33,6 +34,9 @@ export async function validate() {
|
||||
}
|
||||
|
||||
for (const text of texts) {
|
||||
if (argNames.size > 0 && PLATFORM_PLACEHOLDER.test(text)) {
|
||||
errors.push(`${where}: uses a platform printf placeholder; use declared {name} placeholders in strings.json.`);
|
||||
}
|
||||
for (const m of text.matchAll(TOKEN)) {
|
||||
if (!argNames.has(m[1]!)) {
|
||||
errors.push(`${where}: uses {${m[1]}} but no matching arg is declared.`);
|
||||
|
||||
@@ -4843,21 +4843,6 @@
|
||||
},
|
||||
"targets": ["kmp"]
|
||||
},
|
||||
"saved_devices_devices_title": {
|
||||
"context": "Saved devices screen: heading for mutually trusted saved devices.",
|
||||
"translations": {
|
||||
"en": "Your devices",
|
||||
"fr": "Vos appareils",
|
||||
"es": "Tus dispositivos",
|
||||
"it": "I tuoi dispositivi",
|
||||
"de": "Deine Geräte",
|
||||
"pt": "Os seus dispositivos",
|
||||
"pl": "Twoje urządzenia",
|
||||
"nl": "Je apparaten",
|
||||
"ru": "Ваши устройства"
|
||||
},
|
||||
"targets": ["kmp"]
|
||||
},
|
||||
"saved_devices_transfers_title": {
|
||||
"context": "Saved devices screen: heading for durable targeted-transfer history.",
|
||||
"translations": {
|
||||
@@ -4892,15 +4877,15 @@
|
||||
"context": "Targeted transfer card: incoming direction followed by the saved-device display name.",
|
||||
"args": [{"name": "device", "type": "string"}],
|
||||
"translations": {
|
||||
"en": "From %1$s",
|
||||
"fr": "De %1$s",
|
||||
"es": "De %1$s",
|
||||
"it": "Da %1$s",
|
||||
"de": "Von %1$s",
|
||||
"pt": "De %1$s",
|
||||
"pl": "Od %1$s",
|
||||
"nl": "Van %1$s",
|
||||
"ru": "От %1$s"
|
||||
"en": "From {device}",
|
||||
"fr": "De {device}",
|
||||
"es": "De {device}",
|
||||
"it": "Da {device}",
|
||||
"de": "Von {device}",
|
||||
"pt": "De {device}",
|
||||
"pl": "Od {device}",
|
||||
"nl": "Van {device}",
|
||||
"ru": "От {device}"
|
||||
},
|
||||
"targets": ["kmp"]
|
||||
},
|
||||
@@ -4908,15 +4893,15 @@
|
||||
"context": "Targeted transfer card: outgoing direction followed by the saved-device display name.",
|
||||
"args": [{"name": "device", "type": "string"}],
|
||||
"translations": {
|
||||
"en": "To %1$s",
|
||||
"fr": "Vers %1$s",
|
||||
"es": "A %1$s",
|
||||
"it": "A %1$s",
|
||||
"de": "An %1$s",
|
||||
"pt": "Para %1$s",
|
||||
"pl": "Do %1$s",
|
||||
"nl": "Naar %1$s",
|
||||
"ru": "Для %1$s"
|
||||
"en": "To {device}",
|
||||
"fr": "Vers {device}",
|
||||
"es": "A {device}",
|
||||
"it": "A {device}",
|
||||
"de": "An {device}",
|
||||
"pt": "Para {device}",
|
||||
"pl": "Do {device}",
|
||||
"nl": "Naar {device}",
|
||||
"ru": "Для {device}"
|
||||
},
|
||||
"targets": ["kmp"]
|
||||
},
|
||||
@@ -4924,15 +4909,15 @@
|
||||
"context": "Targeted transfer card metadata. First placeholder is file count, second is formatted total size.",
|
||||
"args": [{"name": "count", "type": "string"}, {"name": "size", "type": "string"}],
|
||||
"translations": {
|
||||
"en": "%1$s files · %2$s",
|
||||
"fr": "%1$s fichiers · %2$s",
|
||||
"es": "%1$s archivos · %2$s",
|
||||
"it": "%1$s file · %2$s",
|
||||
"de": "%1$s Dateien · %2$s",
|
||||
"pt": "%1$s ficheiros · %2$s",
|
||||
"pl": "%1$s plików · %2$s",
|
||||
"nl": "%1$s bestanden · %2$s",
|
||||
"ru": "%1$s файлов · %2$s"
|
||||
"en": "{count} files · {size}",
|
||||
"fr": "{count} fichiers · {size}",
|
||||
"es": "{count} archivos · {size}",
|
||||
"it": "{count} file · {size}",
|
||||
"de": "{count} Dateien · {size}",
|
||||
"pt": "{count} ficheiros · {size}",
|
||||
"pl": "{count} plików · {size}",
|
||||
"nl": "{count} bestanden · {size}",
|
||||
"ru": "{count} файлов · {size}"
|
||||
},
|
||||
"targets": ["kmp"]
|
||||
},
|
||||
@@ -4940,15 +4925,15 @@
|
||||
"context": "Targeted transfer card progress. First placeholder is verified bytes, second is total size.",
|
||||
"args": [{"name": "verified", "type": "string"}, {"name": "total", "type": "string"}],
|
||||
"translations": {
|
||||
"en": "%1$s of %2$s",
|
||||
"fr": "%1$s sur %2$s",
|
||||
"es": "%1$s de %2$s",
|
||||
"it": "%1$s di %2$s",
|
||||
"de": "%1$s von %2$s",
|
||||
"pt": "%1$s de %2$s",
|
||||
"pl": "%1$s z %2$s",
|
||||
"nl": "%1$s van %2$s",
|
||||
"ru": "%1$s из %2$s"
|
||||
"en": "{verified} of {total}",
|
||||
"fr": "{verified} sur {total}",
|
||||
"es": "{verified} de {total}",
|
||||
"it": "{verified} di {total}",
|
||||
"de": "{verified} von {total}",
|
||||
"pt": "{verified} de {total}",
|
||||
"pl": "{verified} z {total}",
|
||||
"nl": "{verified} van {total}",
|
||||
"ru": "{verified} из {total}"
|
||||
},
|
||||
"targets": ["kmp"]
|
||||
},
|
||||
@@ -5058,23 +5043,6 @@
|
||||
"kmp"
|
||||
]
|
||||
},
|
||||
"saved_devices_description": {
|
||||
"context": "Saved devices screen: short explanation below the title.",
|
||||
"translations": {
|
||||
"en": "Send directly to devices you trust, without sharing another invitation.",
|
||||
"fr": "Envoyez directement aux appareils de confiance, sans partager une nouvelle invitation.",
|
||||
"es": "Envía directamente a dispositivos de confianza sin compartir otra invitación.",
|
||||
"it": "Invia direttamente ai dispositivi attendibili senza condividere un altro invito.",
|
||||
"de": "Direkt an vertrauenswürdige Geräte senden, ohne eine neue Einladung zu teilen.",
|
||||
"pt": "Envie diretamente para dispositivos de confiança sem partilhar outro convite.",
|
||||
"pl": "Wysyłaj bezpośrednio do zaufanych urządzeń bez udostępniania kolejnego zaproszenia.",
|
||||
"nl": "Stuur rechtstreeks naar vertrouwde apparaten zonder opnieuw een uitnodiging te delen.",
|
||||
"ru": "Отправляйте напрямую доверенным устройствам без новой ссылки-приглашения."
|
||||
},
|
||||
"targets": [
|
||||
"kmp"
|
||||
]
|
||||
},
|
||||
"saved_devices_endpoint": {
|
||||
"context": "Saved-device screen: secondary diagnostic endpoint identity. Placeholder is a shortened endpoint ID.",
|
||||
"translations": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<!-- fluent icon imported by shared/tools/import_platform_icons.py. -->
|
||||
<path android:pathData="M6.75 22.0004C6.33579 22.0004 6 21.6647 6 21.2504C6 20.8707 6.28215 20.557 6.64823 20.5073L6.75 20.5004L8.499 20.5V18.002L4.25 18.0023C3.05914 18.0023 2.08436 17.0771 2.00519 15.9063L2 15.7523V5.25C2 4.05914 2.92516 3.08436 4.09595 3.00519L4.25 3H19.7488C20.9397 3 21.9145 3.92516 21.9936 5.09595L21.9988 5.25V15.7523C21.9988 16.9431 21.0737 17.9179 19.9029 17.9971L19.7488 18.0023L15.499 18.002V20.5L17.25 20.5004C17.6642 20.5004 18 20.8362 18 21.2504C18 21.6301 17.7178 21.9439 17.3518 21.9936L17.25 22.0004H6.75ZM13.998 18.002H9.998L9.999 20.5004H13.999L13.998 18.002ZM19.7488 4.5H4.25C3.8703 4.5 3.55651 4.78215 3.50685 5.14823L3.5 5.25V15.7523C3.5 16.132 3.78215 16.4458 4.14823 16.4954L4.25 16.5023H19.7488C20.1285 16.5023 20.4423 16.2201 20.492 15.854L20.4988 15.7523V5.25C20.4988 4.8703 20.2167 4.55651 19.8506 4.50685L19.7488 4.5Z" android:fillColor="#FF000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<!-- lucide icon imported by shared/tools/import_platform_icons.py. -->
|
||||
<path android:pathData="M 4 3 H 20 A 2 2 0 0 1 22 5 V 15 A 2 2 0 0 1 20 17 H 4 A 2 2 0 0 1 2 15 V 5 A 2 2 0 0 1 4 3 Z" android:fillColor="#00000000" android:strokeColor="#FF000000" android:strokeWidth="2" android:strokeLineCap="round" android:strokeLineJoin="round" />
|
||||
<path android:pathData="M 8 21 L 16 21" android:fillColor="#00000000" android:strokeColor="#FF000000" android:strokeWidth="2" android:strokeLineCap="round" android:strokeLineJoin="round" />
|
||||
<path android:pathData="M 12 17 L 12 21" android:fillColor="#00000000" android:strokeColor="#FF000000" android:strokeWidth="2" android:strokeLineCap="round" android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
|
||||
<!-- material icon imported by shared/tools/import_platform_icons.py. -->
|
||||
<path android:fillColor="#FF000000" android:pathData="M140,800Q115,800 97.5,782.5Q80,765 80,740Q80,715 97.5,697.5Q115,680 140,680L160,680L160,240Q160,207 183.5,183.5Q207,160 240,160L800,160Q817,160 828.5,171.5Q840,183 840,200Q840,217 828.5,228.5Q817,240 800,240L240,240Q240,240 240,240Q240,240 240,240L240,680L420,680Q445,680 462.5,697.5Q480,715 480,740Q480,765 462.5,782.5Q445,800 420,800L140,800ZM600,800Q583,800 571.5,788.5Q560,777 560,760L560,360Q560,343 571.5,331.5Q583,320 600,320L840,320Q857,320 868.5,331.5Q880,343 880,360L880,760Q880,777 868.5,788.5Q857,800 840,800L600,800ZM640,680L800,680L800,400L640,400L640,680ZM640,680L640,680L800,680L800,680L640,680Z" />
|
||||
</vector>
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Empfangen</string>
|
||||
<string name="offer_decline">Ablehnen</string>
|
||||
<string name="saved_devices_attention_title">Erfordert Aufmerksamkeit</string>
|
||||
<string name="saved_devices_devices_title">Deine Geräte</string>
|
||||
<string name="saved_devices_transfers_title">Direkte Übertragungen</string>
|
||||
<string name="saved_devices_transfer_empty">Noch keine direkten Übertragungen.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Von %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">An %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s Dateien · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s von %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Von %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">An %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s Dateien · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s von %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Empfangen</string>
|
||||
<string name="saved_devices_transfer_resume">Fortsetzen</string>
|
||||
<string name="saved_devices_transfer_cancel">Abbrechen</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Remote-Name: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">%1$s blockieren und künftige Übertragungen gespeicherter Geräte ablehnen?</string>
|
||||
<string name="saved_devices_block_confirm_title">Gerät blockieren?</string>
|
||||
<string name="saved_devices_description">Direkt an vertrauenswürdige Geräte senden, ohne eine neue Einladung zu teilen.</string>
|
||||
<string name="saved_devices_endpoint">Geräte-ID: %1$s</string>
|
||||
<string name="saved_devices_empty">Schließe eine Übertragung ab und speichere anschließend das andere Gerät.</string>
|
||||
<string name="saved_devices_empty_title">Keine gespeicherten Geräte</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Recibir</string>
|
||||
<string name="offer_decline">Rechazar</string>
|
||||
<string name="saved_devices_attention_title">Requiere atención</string>
|
||||
<string name="saved_devices_devices_title">Tus dispositivos</string>
|
||||
<string name="saved_devices_transfers_title">Transferencias directas</string>
|
||||
<string name="saved_devices_transfer_empty">Aún no hay transferencias directas.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">De %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">A %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s archivos · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s de %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">De %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">A %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s archivos · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s de %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Recibir</string>
|
||||
<string name="saved_devices_transfer_resume">Reanudar</string>
|
||||
<string name="saved_devices_transfer_cancel">Cancelar</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Nombre remoto: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">¿Bloquear a %1$s y rechazar futuros envíos de dispositivos guardados?</string>
|
||||
<string name="saved_devices_block_confirm_title">¿Bloquear dispositivo?</string>
|
||||
<string name="saved_devices_description">Envía directamente a dispositivos de confianza sin compartir otra invitación.</string>
|
||||
<string name="saved_devices_endpoint">ID del dispositivo: %1$s</string>
|
||||
<string name="saved_devices_empty">Finaliza una transferencia y elige recordar el otro dispositivo.</string>
|
||||
<string name="saved_devices_empty_title">No hay dispositivos guardados</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Recevoir</string>
|
||||
<string name="offer_decline">Refuser</string>
|
||||
<string name="saved_devices_attention_title">Nécessite votre attention</string>
|
||||
<string name="saved_devices_devices_title">Vos appareils</string>
|
||||
<string name="saved_devices_transfers_title">Transferts directs</string>
|
||||
<string name="saved_devices_transfer_empty">Aucun transfert direct pour le moment.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">De %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Vers %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s fichiers · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s sur %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">De %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Vers %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s fichiers · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s sur %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Recevoir</string>
|
||||
<string name="saved_devices_transfer_resume">Reprendre</string>
|
||||
<string name="saved_devices_transfer_cancel">Annuler</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Nom distant : %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">Bloquer %1$s et refuser les futurs transferts d’appareil enregistré ?</string>
|
||||
<string name="saved_devices_block_confirm_title">Bloquer l’appareil ?</string>
|
||||
<string name="saved_devices_description">Envoyez directement aux appareils de confiance, sans partager une nouvelle invitation.</string>
|
||||
<string name="saved_devices_endpoint">ID de l’appareil : %1$s</string>
|
||||
<string name="saved_devices_empty">Terminez un transfert, puis choisissez de mémoriser l’autre appareil.</string>
|
||||
<string name="saved_devices_empty_title">Aucun appareil enregistré</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Ricevi</string>
|
||||
<string name="offer_decline">Rifiuta</string>
|
||||
<string name="saved_devices_attention_title">Richiede attenzione</string>
|
||||
<string name="saved_devices_devices_title">I tuoi dispositivi</string>
|
||||
<string name="saved_devices_transfers_title">Trasferimenti diretti</string>
|
||||
<string name="saved_devices_transfer_empty">Nessun trasferimento diretto.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Da %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">A %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s file · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s di %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Da %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">A %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s file · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s di %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Ricevi</string>
|
||||
<string name="saved_devices_transfer_resume">Riprendi</string>
|
||||
<string name="saved_devices_transfer_cancel">Annulla</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Nome remoto: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">Bloccare %1$s e rifiutare i futuri trasferimenti da dispositivi salvati?</string>
|
||||
<string name="saved_devices_block_confirm_title">Bloccare il dispositivo?</string>
|
||||
<string name="saved_devices_description">Invia direttamente ai dispositivi attendibili senza condividere un altro invito.</string>
|
||||
<string name="saved_devices_endpoint">ID dispositivo: %1$s</string>
|
||||
<string name="saved_devices_empty">Completa un trasferimento, quindi scegli di ricordare l’altro dispositivo.</string>
|
||||
<string name="saved_devices_empty_title">Nessun dispositivo salvato</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Ontvangen</string>
|
||||
<string name="offer_decline">Weigeren</string>
|
||||
<string name="saved_devices_attention_title">Aandacht vereist</string>
|
||||
<string name="saved_devices_devices_title">Je apparaten</string>
|
||||
<string name="saved_devices_transfers_title">Directe overdrachten</string>
|
||||
<string name="saved_devices_transfer_empty">Nog geen directe overdrachten.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Van %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Naar %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s bestanden · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s van %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Van %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Naar %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s bestanden · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s van %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Ontvangen</string>
|
||||
<string name="saved_devices_transfer_resume">Hervatten</string>
|
||||
<string name="saved_devices_transfer_cancel">Annuleren</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Externe naam: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">%1$s blokkeren en toekomstige overdrachten van opgeslagen apparaten weigeren?</string>
|
||||
<string name="saved_devices_block_confirm_title">Apparaat blokkeren?</string>
|
||||
<string name="saved_devices_description">Stuur rechtstreeks naar vertrouwde apparaten zonder opnieuw een uitnodiging te delen.</string>
|
||||
<string name="saved_devices_endpoint">Apparaat-ID: %1$s</string>
|
||||
<string name="saved_devices_empty">Voltooi een overdracht en kies daarna om het andere apparaat te onthouden.</string>
|
||||
<string name="saved_devices_empty_title">Geen opgeslagen apparaten</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Odbierz</string>
|
||||
<string name="offer_decline">Odrzuć</string>
|
||||
<string name="saved_devices_attention_title">Wymaga uwagi</string>
|
||||
<string name="saved_devices_devices_title">Twoje urządzenia</string>
|
||||
<string name="saved_devices_transfers_title">Transfery bezpośrednie</string>
|
||||
<string name="saved_devices_transfer_empty">Brak transferów bezpośrednich.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Od %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Do %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s plików · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s z %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">Od %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Do %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s plików · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s z %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Odbierz</string>
|
||||
<string name="saved_devices_transfer_resume">Wznów</string>
|
||||
<string name="saved_devices_transfer_cancel">Anuluj</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Nazwa zdalna: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">Zablokować %1$s i odrzucać przyszłe transfery z zapisanych urządzeń?</string>
|
||||
<string name="saved_devices_block_confirm_title">Zablokować urządzenie?</string>
|
||||
<string name="saved_devices_description">Wysyłaj bezpośrednio do zaufanych urządzeń bez udostępniania kolejnego zaproszenia.</string>
|
||||
<string name="saved_devices_endpoint">ID urządzenia: %1$s</string>
|
||||
<string name="saved_devices_empty">Ukończ transfer, a następnie zapisz drugie urządzenie.</string>
|
||||
<string name="saved_devices_empty_title">Brak zapisanych urządzeń</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Receber</string>
|
||||
<string name="offer_decline">Recusar</string>
|
||||
<string name="saved_devices_attention_title">Requer atenção</string>
|
||||
<string name="saved_devices_devices_title">Os seus dispositivos</string>
|
||||
<string name="saved_devices_transfers_title">Transferências diretas</string>
|
||||
<string name="saved_devices_transfer_empty">Ainda não existem transferências diretas.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">De %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Para %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s ficheiros · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s de %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">De %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Para %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s ficheiros · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s de %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Receber</string>
|
||||
<string name="saved_devices_transfer_resume">Retomar</string>
|
||||
<string name="saved_devices_transfer_cancel">Cancelar</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Nome remoto: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">Bloquear %1$s e rejeitar futuras transferências de dispositivos guardados?</string>
|
||||
<string name="saved_devices_block_confirm_title">Bloquear dispositivo?</string>
|
||||
<string name="saved_devices_description">Envie diretamente para dispositivos de confiança sem partilhar outro convite.</string>
|
||||
<string name="saved_devices_endpoint">ID do dispositivo: %1$s</string>
|
||||
<string name="saved_devices_empty">Conclua uma transferência e escolha guardar o outro dispositivo.</string>
|
||||
<string name="saved_devices_empty_title">Nenhum dispositivo guardado</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Получить</string>
|
||||
<string name="offer_decline">Отклонить</string>
|
||||
<string name="saved_devices_attention_title">Требует внимания</string>
|
||||
<string name="saved_devices_devices_title">Ваши устройства</string>
|
||||
<string name="saved_devices_transfers_title">Прямые передачи</string>
|
||||
<string name="saved_devices_transfer_empty">Прямых передач пока нет.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">От %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Для %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s файлов · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s из %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">От %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">Для %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s файлов · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s из %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Получить</string>
|
||||
<string name="saved_devices_transfer_resume">Продолжить</string>
|
||||
<string name="saved_devices_transfer_cancel">Отменить</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Имя устройства: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">Заблокировать %1$s и отклонять будущие передачи с сохранённых устройств?</string>
|
||||
<string name="saved_devices_block_confirm_title">Заблокировать устройство?</string>
|
||||
<string name="saved_devices_description">Отправляйте напрямую доверенным устройствам без новой ссылки-приглашения.</string>
|
||||
<string name="saved_devices_endpoint">ID устройства: %1$s</string>
|
||||
<string name="saved_devices_empty">Завершите передачу, затем сохраните другое устройство.</string>
|
||||
<string name="saved_devices_empty_title">Нет сохранённых устройств</string>
|
||||
|
||||
@@ -320,13 +320,12 @@
|
||||
<string name="offer_accept">Receive</string>
|
||||
<string name="offer_decline">Decline</string>
|
||||
<string name="saved_devices_attention_title">Needs attention</string>
|
||||
<string name="saved_devices_devices_title">Your devices</string>
|
||||
<string name="saved_devices_transfers_title">Direct transfers</string>
|
||||
<string name="saved_devices_transfer_empty">No direct transfers yet.</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">From %%1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">To %%1$s</string>
|
||||
<string name="saved_devices_transfer_files">%%1$s files · %%2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%%1$s of %%2$s</string>
|
||||
<string name="saved_devices_transfer_direction_incoming">From %1$s</string>
|
||||
<string name="saved_devices_transfer_direction_outgoing">To %1$s</string>
|
||||
<string name="saved_devices_transfer_files">%1$s files · %2$s</string>
|
||||
<string name="saved_devices_transfer_progress">%1$s of %2$s</string>
|
||||
<string name="saved_devices_transfer_receive">Receive</string>
|
||||
<string name="saved_devices_transfer_resume">Resume</string>
|
||||
<string name="saved_devices_transfer_cancel">Cancel</string>
|
||||
@@ -341,7 +340,6 @@
|
||||
<string name="saved_devices_authenticated_name">Remote name: %1$s</string>
|
||||
<string name="saved_devices_block_confirm_body">Block %1$s and reject future saved-device transfers?</string>
|
||||
<string name="saved_devices_block_confirm_title">Block device?</string>
|
||||
<string name="saved_devices_description">Send directly to devices you trust, without sharing another invitation.</string>
|
||||
<string name="saved_devices_endpoint">Device ID: %1$s</string>
|
||||
<string name="saved_devices_empty">Finish a transfer, then choose to remember the other device.</string>
|
||||
<string name="saved_devices_empty_title">No saved devices</string>
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.vnidrop.app.feature.saveddevices
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.core.SavedDeviceModel
|
||||
import com.vnidrop.app.ui.components.AdaptiveDrawer
|
||||
import com.vnidrop.app.ui.components.PrimaryButton
|
||||
import com.vnidrop.app.ui.icons.AppIcon
|
||||
import com.vnidrop.app.ui.icons.PlatformIcon
|
||||
import com.vnidrop.app.ui.state.WindowClass
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import vnidrop.shared.generated.resources.Res
|
||||
import vnidrop.shared.generated.resources.button_cancel
|
||||
import vnidrop.shared.generated.resources.saved_devices_authenticated_name
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_confirm_body
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_confirm_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_endpoint
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_confirm_body
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_confirm_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_label_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_more_actions
|
||||
import vnidrop.shared.generated.resources.saved_devices_send_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfer_empty
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfers_title
|
||||
|
||||
private enum class DeviceDestructiveAction { Forget, Block }
|
||||
|
||||
@Composable
|
||||
internal fun SavedDeviceDetailsDrawer(
|
||||
device: SavedDeviceModel,
|
||||
transfers: List<SavedDeviceTransferItem>,
|
||||
busy: Boolean,
|
||||
busyTransferIds: Set<String>,
|
||||
windowClass: WindowClass,
|
||||
onDismiss: () -> Unit,
|
||||
onSend: () -> Unit,
|
||||
onOpenLabel: () -> Unit,
|
||||
onForget: () -> Unit,
|
||||
onBlock: () -> Unit,
|
||||
onTransferAction: (String, SavedDeviceTransferAction) -> Unit,
|
||||
) {
|
||||
var menuExpanded by remember(device.endpointId) { mutableStateOf(false) }
|
||||
var pendingAction by remember(device.endpointId) { mutableStateOf<DeviceDestructiveAction?>(null) }
|
||||
val colors = LocalVniDropColors.current
|
||||
val title = device.displayName()
|
||||
|
||||
AdaptiveDrawer(windowClass = windowClass, onDismissRequest = onDismiss) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 320.dp, max = 720.dp),
|
||||
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 28.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||
) {
|
||||
item(key = "device-header") {
|
||||
Row(Modifier.fillMaxWidth().padding(end = 44.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
PlatformIcon(AppIcon.Device, contentDescription = null, modifier = Modifier.size(30.dp), tint = colors.foregroundLight)
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.semantics { heading() },
|
||||
)
|
||||
device.remoteDisplayName
|
||||
?.takeIf { device.localLabel?.isNotBlank() == true && it.isNotBlank() }
|
||||
?.let {
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_authenticated_name, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colors.foregroundLight,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (busy) CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
item(key = "device-identity") {
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_endpoint, shortDeviceEndpoint(device.endpointId)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colors.foregroundLighter,
|
||||
)
|
||||
}
|
||||
item(key = "send") {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
PrimaryButton(
|
||||
stringResource(Res.string.saved_devices_send_action),
|
||||
onClick = onSend,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !busy,
|
||||
leadingIcon = { PlatformIcon(AppIcon.Send, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box {
|
||||
IconButton(onClick = { menuExpanded = true }, enabled = !busy) {
|
||||
PlatformIcon(
|
||||
AppIcon.MoreVertical,
|
||||
contentDescription = stringResource(Res.string.saved_devices_more_actions, title),
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.saved_devices_label_action)) },
|
||||
onClick = { menuExpanded = false; onOpenLabel() },
|
||||
leadingIcon = { PlatformIcon(AppIcon.User, contentDescription = null) },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.saved_devices_forget_action)) },
|
||||
onClick = { menuExpanded = false; pendingAction = DeviceDestructiveAction.Forget },
|
||||
leadingIcon = { PlatformIcon(AppIcon.UserOff, contentDescription = null) },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.saved_devices_block_action)) },
|
||||
onClick = { menuExpanded = false; pendingAction = DeviceDestructiveAction.Block },
|
||||
leadingIcon = { PlatformIcon(AppIcon.Lock, contentDescription = null) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item(key = "transfer-heading") {
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_transfers_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = 6.dp).semantics { heading() },
|
||||
)
|
||||
}
|
||||
if (transfers.isEmpty()) {
|
||||
item(key = "transfer-empty") {
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_transfer_empty),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = colors.foregroundLight,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetedTransferItems(
|
||||
transfers = transfers,
|
||||
busyTransferIds = busyTransferIds,
|
||||
onAction = onTransferAction,
|
||||
showHeading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingAction?.let { action ->
|
||||
val isBlock = action == DeviceDestructiveAction.Block
|
||||
AlertDialog(
|
||||
onDismissRequest = { pendingAction = null },
|
||||
title = {
|
||||
Text(stringResource(if (isBlock) Res.string.saved_devices_block_confirm_title else Res.string.saved_devices_forget_confirm_title))
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (isBlock) Res.string.saved_devices_block_confirm_body else Res.string.saved_devices_forget_confirm_body,
|
||||
title,
|
||||
),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
pendingAction = null
|
||||
onDismiss()
|
||||
if (isBlock) onBlock() else onForget()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(if (isBlock) Res.string.saved_devices_block_action else Res.string.saved_devices_forget_action))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { pendingAction = null }) { Text(stringResource(Res.string.button_cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vnidrop.app.feature.saveddevices
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -7,7 +8,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
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.padding
|
||||
@@ -16,21 +16,13 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
@@ -52,7 +44,6 @@ import com.vnidrop.app.ui.icons.PlatformIcon
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import vnidrop.shared.generated.resources.Res
|
||||
import vnidrop.shared.generated.resources.button_cancel
|
||||
import vnidrop.shared.generated.resources.button_retry
|
||||
import vnidrop.shared.generated.resources.offer_accept
|
||||
import vnidrop.shared.generated.resources.offer_body
|
||||
@@ -60,28 +51,14 @@ import vnidrop.shared.generated.resources.offer_decline
|
||||
import vnidrop.shared.generated.resources.offer_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_accept_pairing_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_attention_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_authenticated_name
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_confirm_body
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_confirm_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_decline_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_devices_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_eligibility_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_empty
|
||||
import vnidrop.shared.generated.resources.saved_devices_empty_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_endpoint
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_confirm_body
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_confirm_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_label_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_load_failed
|
||||
import vnidrop.shared.generated.resources.saved_devices_more_actions
|
||||
import vnidrop.shared.generated.resources.saved_devices_pending_incoming
|
||||
import vnidrop.shared.generated.resources.saved_devices_pending_outgoing
|
||||
import vnidrop.shared.generated.resources.saved_devices_remember_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_send_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfer_empty
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfers_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_unnamed
|
||||
|
||||
@Composable
|
||||
@@ -94,45 +71,21 @@ internal fun CompactSavedDevicesHub(
|
||||
onDeclineIncoming: (String) -> Unit,
|
||||
onAcceptOffer: (String) -> Unit,
|
||||
onDeclineOffer: (String) -> Unit,
|
||||
onSend: (String) -> Unit,
|
||||
onOpenLabel: (String) -> Unit,
|
||||
onForget: (String) -> Unit,
|
||||
onBlock: (String) -> Unit,
|
||||
onTransferAction: (String, SavedDeviceTransferAction) -> Unit,
|
||||
onOpenDevice: (String) -> Unit,
|
||||
) {
|
||||
val hasAttention = state.attentionCount > 0
|
||||
val isEmpty = !hasAttention && state.savedDevices.isEmpty() && state.targetedTransfers.isEmpty()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
SavedDevicesHub(
|
||||
state = state,
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 28.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
if (state.isLoading) item(key = "loading") { androidx.compose.material3.LinearProgressIndicator(Modifier.fillMaxWidth()) }
|
||||
if (state.loadFailed) item(key = "load-failed") { InlineLoadFailure(onRetry) }
|
||||
if (hasAttention) {
|
||||
item(key = "attention") {
|
||||
AttentionSection(
|
||||
state,
|
||||
onRememberEligible,
|
||||
onDeclineEligible,
|
||||
onAcceptIncoming,
|
||||
onDeclineIncoming,
|
||||
onAcceptOffer,
|
||||
onDeclineOffer,
|
||||
onRetry = onRetry,
|
||||
onRememberEligible = onRememberEligible,
|
||||
onDeclineEligible = onDeclineEligible,
|
||||
onAcceptIncoming = onAcceptIncoming,
|
||||
onDeclineIncoming = onDeclineIncoming,
|
||||
onAcceptOffer = onAcceptOffer,
|
||||
onDeclineOffer = onDeclineOffer,
|
||||
onOpenDevice = onOpenDevice,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.savedDevices.isNotEmpty()) {
|
||||
item(key = "saved-devices") {
|
||||
SavedDeviceSection(state, onSend, onOpenLabel, onForget, onBlock)
|
||||
}
|
||||
}
|
||||
if (state.targetedTransfers.isNotEmpty()) {
|
||||
targetedTransferItems(state.targetedTransfers, state.busyTransferIds, onTransferAction)
|
||||
}
|
||||
if (isEmpty) item(key = "empty") { SavedDevicesEmptyState(Modifier.fillParentMaxHeight(0.72f)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DesktopSavedDevicesHub(
|
||||
@@ -144,30 +97,49 @@ internal fun DesktopSavedDevicesHub(
|
||||
onDeclineIncoming: (String) -> Unit,
|
||||
onAcceptOffer: (String) -> Unit,
|
||||
onDeclineOffer: (String) -> Unit,
|
||||
onSend: (String) -> Unit,
|
||||
onOpenLabel: (String) -> Unit,
|
||||
onForget: (String) -> Unit,
|
||||
onBlock: (String) -> Unit,
|
||||
onTransferAction: (String, SavedDeviceTransferAction) -> Unit,
|
||||
onOpenDevice: (String) -> Unit,
|
||||
) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
SavedDevicesHub(
|
||||
state = state,
|
||||
modifier = Modifier.widthIn(max = 720.dp),
|
||||
contentPadding = PaddingValues(start = 24.dp, end = 24.dp, bottom = 32.dp),
|
||||
onRetry = onRetry,
|
||||
onRememberEligible = onRememberEligible,
|
||||
onDeclineEligible = onDeclineEligible,
|
||||
onAcceptIncoming = onAcceptIncoming,
|
||||
onDeclineIncoming = onDeclineIncoming,
|
||||
onAcceptOffer = onAcceptOffer,
|
||||
onDeclineOffer = onDeclineOffer,
|
||||
onOpenDevice = onOpenDevice,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedDevicesHub(
|
||||
state: SavedDevicesState,
|
||||
contentPadding: PaddingValues,
|
||||
onRetry: () -> Unit,
|
||||
onRememberEligible: (String) -> Unit,
|
||||
onDeclineEligible: (String) -> Unit,
|
||||
onAcceptIncoming: (String) -> Unit,
|
||||
onDeclineIncoming: (String) -> Unit,
|
||||
onAcceptOffer: (String) -> Unit,
|
||||
onDeclineOffer: (String) -> Unit,
|
||||
onOpenDevice: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hasAttention = state.attentionCount > 0
|
||||
val isEmpty = !hasAttention && state.savedDevices.isEmpty() && state.targetedTransfers.isEmpty()
|
||||
if (isEmpty && !state.isLoading && !state.loadFailed) {
|
||||
Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
||||
SavedDevicesEmptyState(Modifier.widthIn(max = 440.dp))
|
||||
}
|
||||
return
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
val showEmptyState = !hasAttention && state.savedDevices.isEmpty()
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(0.9f).fillMaxHeight(),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
contentPadding = PaddingValues(bottom = 24.dp),
|
||||
) {
|
||||
if (state.isLoading) item(key = "loading") { androidx.compose.material3.LinearProgressIndicator(Modifier.fillMaxWidth()) }
|
||||
if (state.isLoading) {
|
||||
item(key = "loading") { androidx.compose.material3.LinearProgressIndicator(Modifier.fillMaxWidth()) }
|
||||
}
|
||||
if (state.loadFailed) item(key = "load-failed") { InlineLoadFailure(onRetry) }
|
||||
if (hasAttention) {
|
||||
item(key = "attention") {
|
||||
@@ -183,33 +155,10 @@ internal fun DesktopSavedDevicesHub(
|
||||
}
|
||||
}
|
||||
if (state.savedDevices.isNotEmpty()) {
|
||||
item(key = "saved-devices") {
|
||||
SavedDeviceSection(state, onSend, onOpenLabel, onForget, onBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1.1f).fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(bottom = 24.dp),
|
||||
) {
|
||||
if (state.targetedTransfers.isNotEmpty()) {
|
||||
targetedTransferItems(
|
||||
state.targetedTransfers,
|
||||
state.busyTransferIds,
|
||||
onTransferAction,
|
||||
presentation = TargetedTransferPresentation.DesktopRow,
|
||||
)
|
||||
} else {
|
||||
item(key = "targeted-transfer-title") { SectionTitle(stringResource(Res.string.saved_devices_transfers_title)) }
|
||||
item(key = "targeted-transfer-empty") {
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_transfer_empty),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = LocalVniDropColors.current.foregroundLight,
|
||||
)
|
||||
}
|
||||
item(key = "saved-devices") { SavedDeviceSection(state, onOpenDevice) }
|
||||
}
|
||||
if (showEmptyState) {
|
||||
item(key = "empty") { SavedDevicesEmptyState(Modifier.fillParentMaxHeight(0.72f)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,10 +178,7 @@ private fun AttentionSection(
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
SectionTitle(stringResource(Res.string.saved_devices_attention_title))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = LocalVniDropColors.current.backgroundSurface200,
|
||||
) {
|
||||
Surface(shape = RoundedCornerShape(12.dp), color = LocalVniDropColors.current.backgroundSurface200) {
|
||||
Column {
|
||||
var hasPrevious = false
|
||||
state.targetedOffers.pending.forEach { offer ->
|
||||
@@ -285,7 +231,6 @@ private fun TargetedOfferRow(
|
||||
icon = AppIcon.Download,
|
||||
title = stringResource(Res.string.offer_title),
|
||||
body = stringResource(Res.string.offer_body, deviceName, offer.transferName),
|
||||
endpointId = offer.senderEndpointId,
|
||||
busy = busy,
|
||||
) {
|
||||
PrimaryButton(stringResource(Res.string.offer_accept), onAccept, enabled = !busy)
|
||||
@@ -301,10 +246,9 @@ private fun EligibilityRow(
|
||||
onDecline: () -> Unit,
|
||||
) {
|
||||
DecisionRow(
|
||||
icon = AppIcon.Shield,
|
||||
icon = AppIcon.Device,
|
||||
title = eligibility.remoteDisplayName?.takeIf(String::isNotBlank) ?: stringResource(Res.string.saved_devices_unnamed),
|
||||
body = stringResource(Res.string.saved_devices_eligibility_title),
|
||||
endpointId = eligibility.peerEndpointId,
|
||||
busy = busy,
|
||||
) {
|
||||
PrimaryButton(stringResource(Res.string.saved_devices_remember_action), onRemember, enabled = !busy)
|
||||
@@ -321,7 +265,7 @@ private fun PendingPairingRow(
|
||||
onDecline: () -> Unit,
|
||||
) {
|
||||
DecisionRow(
|
||||
icon = AppIcon.Shield,
|
||||
icon = AppIcon.Device,
|
||||
title = remoteDisplayName?.takeIf(String::isNotBlank) ?: stringResource(Res.string.saved_devices_unnamed),
|
||||
body = stringResource(
|
||||
if (relationship.state == DeviceRelationshipStateModel.PendingIncoming) {
|
||||
@@ -330,7 +274,6 @@ private fun PendingPairingRow(
|
||||
Res.string.saved_devices_pending_outgoing
|
||||
},
|
||||
),
|
||||
endpointId = relationship.remoteEndpointId,
|
||||
busy = busy,
|
||||
actions = if (relationship.state == DeviceRelationshipStateModel.PendingIncoming) {
|
||||
{
|
||||
@@ -348,18 +291,16 @@ private fun DecisionRow(
|
||||
icon: AppIcon,
|
||||
title: String,
|
||||
body: String,
|
||||
endpointId: String,
|
||||
busy: Boolean,
|
||||
actions: (@Composable RowScope.() -> Unit)?,
|
||||
) {
|
||||
val colors = LocalVniDropColors.current
|
||||
Column(Modifier.padding(horizontal = 16.dp, vertical = 14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
PlatformIcon(icon, contentDescription = null, tint = colors.brandLink, modifier = Modifier.padding(top = 2.dp).size(22.dp))
|
||||
PlatformIcon(icon, contentDescription = null, tint = colors.foregroundLight, modifier = Modifier.padding(top = 2.dp).size(22.dp))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
Text(body, style = MaterialTheme.typography.bodyMedium, color = colors.foregroundLight)
|
||||
DiagnosticEndpoint(endpointId)
|
||||
}
|
||||
if (busy) CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
@@ -368,108 +309,43 @@ private fun DecisionRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedDeviceSection(
|
||||
state: SavedDevicesState,
|
||||
onSend: (String) -> Unit,
|
||||
onOpenLabel: (String) -> Unit,
|
||||
onForget: (String) -> Unit,
|
||||
onBlock: (String) -> Unit,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
SectionTitle(stringResource(Res.string.saved_devices_devices_title))
|
||||
Surface(shape = RoundedCornerShape(12.dp), color = LocalVniDropColors.current.backgroundSurface200) {
|
||||
private fun SavedDeviceSection(state: SavedDevicesState, onOpenDevice: (String) -> Unit) {
|
||||
Column {
|
||||
state.savedDevices.forEachIndexed { index, device ->
|
||||
if (index > 0) GroupDivider()
|
||||
SavedDeviceRow(
|
||||
device = device,
|
||||
busy = device.endpointId in state.busyPeerIds,
|
||||
onSend = { onSend(device.endpointId) },
|
||||
onLabel = { onOpenLabel(device.endpointId) },
|
||||
onForget = { onForget(device.endpointId) },
|
||||
onBlock = { onBlock(device.endpointId) },
|
||||
onOpen = { onOpenDevice(device.endpointId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DeviceDestructiveAction { Forget, Block }
|
||||
|
||||
@Composable
|
||||
private fun SavedDeviceRow(
|
||||
device: SavedDeviceModel,
|
||||
busy: Boolean,
|
||||
onSend: () -> Unit,
|
||||
onLabel: () -> Unit,
|
||||
onForget: () -> Unit,
|
||||
onBlock: () -> Unit,
|
||||
) {
|
||||
private fun SavedDeviceRow(device: SavedDeviceModel, busy: Boolean, onOpen: () -> Unit) {
|
||||
val colors = LocalVniDropColors.current
|
||||
val title = device.displayName()
|
||||
var menuExpanded by remember(device.endpointId) { mutableStateOf(false) }
|
||||
var pendingAction by remember(device.endpointId) { mutableStateOf<DeviceDestructiveAction?>(null) }
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().testTag("saved-device-${device.endpointId}").padding(start = 16.dp, end = 6.dp, top = 12.dp, bottom = 12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag("saved-device-${device.endpointId}")
|
||||
.clickable(onClick = onOpen)
|
||||
.padding(horizontal = 4.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PlatformIcon(AppIcon.ShieldCheck, contentDescription = null, tint = colors.foregroundLight, modifier = Modifier.size(24.dp))
|
||||
PlatformIcon(AppIcon.Device, contentDescription = null, tint = colors.foregroundLight, modifier = Modifier.size(24.dp))
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
device.remoteDisplayName
|
||||
?.takeIf { device.localLabel?.isNotBlank() == true && it.isNotBlank() }
|
||||
?.let { Text(stringResource(Res.string.saved_devices_authenticated_name, it), style = MaterialTheme.typography.bodySmall, color = colors.foregroundLight) }
|
||||
DiagnosticEndpoint(device.endpointId)
|
||||
}
|
||||
if (busy) {
|
||||
CircularProgressIndicator(Modifier.padding(horizontal = 14.dp).size(20.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
val sendLabel = stringResource(Res.string.saved_devices_send_action)
|
||||
IconButton(onClick = onSend) {
|
||||
PlatformIcon(AppIcon.Send, contentDescription = sendLabel)
|
||||
}
|
||||
Box {
|
||||
val moreLabel = stringResource(Res.string.saved_devices_more_actions, title)
|
||||
IconButton(onClick = { menuExpanded = true }) {
|
||||
PlatformIcon(AppIcon.MoreVertical, contentDescription = moreLabel)
|
||||
}
|
||||
DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.saved_devices_label_action)) },
|
||||
onClick = { menuExpanded = false; onLabel() },
|
||||
leadingIcon = { PlatformIcon(AppIcon.User, contentDescription = null) },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.saved_devices_forget_action)) },
|
||||
onClick = { menuExpanded = false; pendingAction = DeviceDestructiveAction.Forget },
|
||||
leadingIcon = { PlatformIcon(AppIcon.UserOff, contentDescription = null) },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.saved_devices_block_action)) },
|
||||
onClick = { menuExpanded = false; pendingAction = DeviceDestructiveAction.Block },
|
||||
leadingIcon = { PlatformIcon(AppIcon.Lock, contentDescription = null) },
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
device.displayName(),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingAction?.let { action ->
|
||||
val isBlock = action == DeviceDestructiveAction.Block
|
||||
AlertDialog(
|
||||
onDismissRequest = { pendingAction = null },
|
||||
title = { Text(stringResource(if (isBlock) Res.string.saved_devices_block_confirm_title else Res.string.saved_devices_forget_confirm_title)) },
|
||||
text = {
|
||||
Text(stringResource(if (isBlock) Res.string.saved_devices_block_confirm_body else Res.string.saved_devices_forget_confirm_body, title))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { pendingAction = null; if (isBlock) onBlock() else onForget() }) {
|
||||
Text(stringResource(if (isBlock) Res.string.saved_devices_block_action else Res.string.saved_devices_forget_action))
|
||||
}
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { pendingAction = null }) { Text(stringResource(Res.string.button_cancel)) } },
|
||||
)
|
||||
if (busy) CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
else PlatformIcon(AppIcon.ChevronRight, contentDescription = null, tint = colors.foregroundLighter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,17 +367,6 @@ private fun GroupDivider() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DiagnosticEndpoint(endpointId: String) {
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_endpoint, shortEndpoint(endpointId)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = LocalVniDropColors.current.foregroundLighter,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedDevicesEmptyState(modifier: Modifier = Modifier) {
|
||||
val colors = LocalVniDropColors.current
|
||||
@@ -511,7 +376,7 @@ private fun SavedDevicesEmptyState(modifier: Modifier = Modifier) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
PlatformIcon(AppIcon.ShieldCheck, contentDescription = null, tint = colors.foregroundLighter, modifier = Modifier.size(36.dp))
|
||||
PlatformIcon(AppIcon.Device, contentDescription = null, tint = colors.foregroundLighter, modifier = Modifier.size(36.dp))
|
||||
Text(stringResource(Res.string.saved_devices_empty_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
Text(
|
||||
stringResource(Res.string.saved_devices_empty),
|
||||
@@ -536,9 +401,9 @@ private fun InlineLoadFailure(onRetry: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedDeviceModel.displayName(): String = localLabel?.takeIf(String::isNotBlank)
|
||||
internal fun SavedDeviceModel.displayName(): String = localLabel?.takeIf(String::isNotBlank)
|
||||
?: remoteDisplayName?.takeIf(String::isNotBlank)
|
||||
?: stringResource(Res.string.saved_devices_unnamed)
|
||||
|
||||
private fun shortEndpoint(endpointId: String): String =
|
||||
internal fun shortDeviceEndpoint(endpointId: String): String =
|
||||
if (endpointId.length <= 20) endpointId else endpointId.take(16) + "…"
|
||||
|
||||
@@ -42,7 +42,6 @@ import vnidrop.shared.generated.resources.saved_devices_transfer_progress
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfer_receive
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfer_resume
|
||||
import vnidrop.shared.generated.resources.saved_devices_transfers_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_endpoint
|
||||
import vnidrop.shared.generated.resources.saved_devices_unnamed
|
||||
import vnidrop.shared.generated.resources.status_approved
|
||||
import vnidrop.shared.generated.resources.status_awaiting_approval
|
||||
@@ -63,17 +62,13 @@ enum class SavedDeviceTransferAction {
|
||||
Delete,
|
||||
}
|
||||
|
||||
internal enum class TargetedTransferPresentation {
|
||||
CompactRow,
|
||||
DesktopRow,
|
||||
}
|
||||
|
||||
internal fun LazyListScope.targetedTransferItems(
|
||||
transfers: List<SavedDeviceTransferItem>,
|
||||
busyTransferIds: Set<String>,
|
||||
onAction: (String, SavedDeviceTransferAction) -> Unit,
|
||||
presentation: TargetedTransferPresentation = TargetedTransferPresentation.CompactRow,
|
||||
showHeading: Boolean = true,
|
||||
) {
|
||||
if (showHeading) {
|
||||
item(key = "targeted-transfer-title") {
|
||||
Text(
|
||||
text = stringResource(Res.string.saved_devices_transfers_title),
|
||||
@@ -82,6 +77,7 @@ internal fun LazyListScope.targetedTransferItems(
|
||||
modifier = Modifier.padding(top = 8.dp).semantics { heading() },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (transfers.isNotEmpty()) {
|
||||
itemsIndexed(transfers, key = { _, transfer -> "targeted-${transfer.id}" }) { index, transfer ->
|
||||
if (index > 0) {
|
||||
@@ -90,7 +86,6 @@ internal fun LazyListScope.targetedTransferItems(
|
||||
TargetedTransferRow(
|
||||
transfer = transfer,
|
||||
busy = transfer.id in busyTransferIds,
|
||||
presentation = presentation,
|
||||
onAction = { action -> onAction(transfer.id, action) },
|
||||
)
|
||||
}
|
||||
@@ -101,7 +96,6 @@ internal fun LazyListScope.targetedTransferItems(
|
||||
private fun TargetedTransferRow(
|
||||
transfer: SavedDeviceTransferItem,
|
||||
busy: Boolean,
|
||||
presentation: TargetedTransferPresentation,
|
||||
onAction: (SavedDeviceTransferAction) -> Unit,
|
||||
) {
|
||||
val colors = LocalVniDropColors.current
|
||||
@@ -109,10 +103,7 @@ private fun TargetedTransferRow(
|
||||
(transfer.verifiedBytes.toDouble() / transfer.totalSize.toDouble()).coerceIn(0.0, 1.0).toFloat()
|
||||
}
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(
|
||||
horizontal = if (presentation == TargetedTransferPresentation.DesktopRow) 4.dp else 0.dp,
|
||||
vertical = 14.dp,
|
||||
),
|
||||
Modifier.fillMaxWidth().padding(vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -163,27 +154,6 @@ private fun TargetedTransferRow(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colors.foregroundLighter,
|
||||
)
|
||||
if (presentation == TargetedTransferPresentation.DesktopRow) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
Res.string.saved_devices_endpoint,
|
||||
shortTransferEndpoint(transfer.peerEndpointId),
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colors.foregroundLighter,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (presentation == TargetedTransferPresentation.CompactRow) {
|
||||
Text(
|
||||
text = stringResource(Res.string.saved_devices_endpoint, shortTransferEndpoint(transfer.peerEndpointId)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colors.foregroundLighter,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (
|
||||
transfer.direction == SavedDeviceTransferDirection.Incoming &&
|
||||
@@ -282,6 +252,3 @@ private fun TargetedTransferStateModel.isDeletable(): Boolean = this in setOf(
|
||||
TargetedTransferStateModel.Cancelled,
|
||||
TargetedTransferStateModel.Failed,
|
||||
)
|
||||
|
||||
private fun shortTransferEndpoint(endpointId: String): String =
|
||||
if (endpointId.length <= 20) endpointId else endpointId.take(16) + "…"
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -17,6 +18,10 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.heading
|
||||
@@ -32,7 +37,6 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import vnidrop.shared.generated.resources.Res
|
||||
import vnidrop.shared.generated.resources.button_cancel
|
||||
import vnidrop.shared.generated.resources.button_retry
|
||||
import vnidrop.shared.generated.resources.saved_devices_description
|
||||
import vnidrop.shared.generated.resources.saved_devices_label_clear
|
||||
import vnidrop.shared.generated.resources.saved_devices_label_placeholder
|
||||
import vnidrop.shared.generated.resources.saved_devices_label_save
|
||||
@@ -63,14 +67,21 @@ internal fun SavedDevicesScreen(
|
||||
onClearLabel: () -> Unit,
|
||||
onDismissLabel: () -> Unit,
|
||||
) {
|
||||
var selectedDeviceId by remember { mutableStateOf<String?>(null) }
|
||||
val selectedDevice = state.savedDevices.firstOrNull { it.endpointId == selectedDeviceId }
|
||||
val hasContent = state.eligibilities.isNotEmpty() || state.pendingRelationships.isNotEmpty() ||
|
||||
state.savedDevices.isNotEmpty() || state.targetedTransfers.isNotEmpty() ||
|
||||
state.targetedOffers.pending.isNotEmpty()
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().statusBarsPadding().padding(top = 16.dp),
|
||||
) {
|
||||
SavedDevicesHeader(Modifier.padding(horizontal = if (windowClass == WindowClass.Desktop) 24.dp else 16.dp))
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
SavedDevicesHeader(
|
||||
Modifier.fillMaxWidth().widthIn(max = 720.dp)
|
||||
.padding(horizontal = if (windowClass == WindowClass.Desktop) 24.dp else 16.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
when {
|
||||
state.isLoading && !hasContent -> SavedDevicesLoading(Modifier.weight(1f))
|
||||
state.loadFailed && !hasContent -> SavedDevicesLoadFailure(onRetry, Modifier.weight(1f))
|
||||
@@ -83,11 +94,7 @@ internal fun SavedDevicesScreen(
|
||||
onDeclineIncoming = onDeclineIncoming,
|
||||
onAcceptOffer = onAcceptOffer,
|
||||
onDeclineOffer = onDeclineOffer,
|
||||
onSend = onSend,
|
||||
onOpenLabel = onOpenLabel,
|
||||
onForget = onForget,
|
||||
onBlock = onBlock,
|
||||
onTransferAction = onTransferAction,
|
||||
onOpenDevice = { selectedDeviceId = it },
|
||||
)
|
||||
else -> CompactSavedDevicesHub(
|
||||
state = state,
|
||||
@@ -98,17 +105,32 @@ internal fun SavedDevicesScreen(
|
||||
onDeclineIncoming = onDeclineIncoming,
|
||||
onAcceptOffer = onAcceptOffer,
|
||||
onDeclineOffer = onDeclineOffer,
|
||||
onSend = onSend,
|
||||
onOpenLabel = onOpenLabel,
|
||||
onForget = onForget,
|
||||
onBlock = onBlock,
|
||||
onTransferAction = onTransferAction,
|
||||
onOpenDevice = { selectedDeviceId = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (selectedDevice != null) {
|
||||
SavedDeviceDetailsDrawer(
|
||||
device = selectedDevice,
|
||||
transfers = state.targetedTransfers.filter { it.peerEndpointId == selectedDevice.endpointId },
|
||||
busy = selectedDevice.endpointId in state.busyPeerIds,
|
||||
busyTransferIds = state.busyTransferIds,
|
||||
windowClass = windowClass,
|
||||
onDismiss = { selectedDeviceId = null },
|
||||
onSend = {
|
||||
selectedDeviceId = null
|
||||
onSend(selectedDevice.endpointId)
|
||||
},
|
||||
onOpenLabel = { onOpenLabel(selectedDevice.endpointId) },
|
||||
onForget = { onForget(selectedDevice.endpointId) },
|
||||
onBlock = { onBlock(selectedDevice.endpointId) },
|
||||
onTransferAction = onTransferAction,
|
||||
)
|
||||
}
|
||||
SavedDeviceLabelDialog(
|
||||
visible = state.labelingPeerId != null,
|
||||
label = state.labelDraft,
|
||||
saving = state.isSavingLabel,
|
||||
onLabelChanged = onLabelDraftChanged,
|
||||
onSave = onSaveLabel,
|
||||
onClear = onClearLabel,
|
||||
@@ -118,19 +140,12 @@ internal fun SavedDevicesScreen(
|
||||
|
||||
@Composable
|
||||
private fun SavedDevicesHeader(modifier: Modifier = Modifier) {
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.saved_devices_list_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.semantics { heading() },
|
||||
modifier = modifier.semantics { heading() },
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.saved_devices_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = LocalVniDropColors.current.foregroundLight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -158,6 +173,7 @@ private fun SavedDevicesLoadFailure(onRetry: () -> Unit, modifier: Modifier = Mo
|
||||
private fun SavedDeviceLabelDialog(
|
||||
visible: Boolean,
|
||||
label: String,
|
||||
saving: Boolean,
|
||||
onLabelChanged: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onClear: () -> Unit,
|
||||
@@ -165,24 +181,28 @@ private fun SavedDeviceLabelDialog(
|
||||
) {
|
||||
if (!visible) return
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
onDismissRequest = { if (!saving) onDismiss() },
|
||||
title = { Text(stringResource(Res.string.saved_devices_label_title)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = label,
|
||||
onValueChange = onLabelChanged,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !saving,
|
||||
singleLine = true,
|
||||
placeholder = { Text(stringResource(Res.string.saved_devices_label_placeholder)) },
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onSave) { Text(stringResource(Res.string.saved_devices_label_save)) }
|
||||
TextButton(onClick = onSave, enabled = !saving) {
|
||||
if (saving) CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
else Text(stringResource(Res.string.saved_devices_label_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
androidx.compose.foundation.layout.Row {
|
||||
TextButton(onClick = onClear) { Text(stringResource(Res.string.saved_devices_label_clear)) }
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(Res.string.button_cancel)) }
|
||||
TextButton(onClick = onClear, enabled = !saving) { Text(stringResource(Res.string.saved_devices_label_clear)) }
|
||||
TextButton(onClick = onDismiss, enabled = !saving) { Text(stringResource(Res.string.button_cancel)) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -48,6 +48,7 @@ data class SavedDevicesState(
|
||||
val busyTransferIds: Set<String> = emptySet(),
|
||||
val labelingPeerId: String? = null,
|
||||
val labelDraft: String = "",
|
||||
val isSavingLabel: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -166,39 +167,53 @@ class SavedDevicesViewModel(
|
||||
}
|
||||
|
||||
fun openLabelEditor(peerEndpointId: String) {
|
||||
if (_state.value.isSavingLabel || peerEndpointId in _state.value.busyPeerIds) return
|
||||
val current = _state.value.savedDevices.firstOrNull { it.endpointId == peerEndpointId }
|
||||
_state.update {
|
||||
it.copy(labelingPeerId = peerEndpointId, labelDraft = current?.localLabel.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun setLabelDraft(value: String) = _state.update { it.copy(labelDraft = value) }
|
||||
fun setLabelDraft(value: String) = _state.update {
|
||||
if (it.isSavingLabel) it else it.copy(labelDraft = value)
|
||||
}
|
||||
|
||||
fun dismissLabelEditor() {
|
||||
if (_state.value.labelingPeerId !in _state.value.busyPeerIds) {
|
||||
if (!_state.value.isSavingLabel) {
|
||||
_state.update { it.copy(labelingPeerId = null, labelDraft = "") }
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLabel() {
|
||||
val peerId = _state.value.labelingPeerId ?: return
|
||||
val label = _state.value.labelDraft.trim().ifBlank { null }
|
||||
mutatePeer(peerId) {
|
||||
repository.setSavedDeviceLabel(peerId, label).onSuccess {
|
||||
messages.tryShow(UiMessage(UiText.Resource(Res.string.saved_devices_labeled), UiMessageTone.Success))
|
||||
}
|
||||
}
|
||||
_state.update { it.copy(labelingPeerId = null, labelDraft = "") }
|
||||
commitLabel(_state.value.labelDraft.trim().ifBlank { null })
|
||||
}
|
||||
|
||||
fun clearLabel() {
|
||||
commitLabel(null)
|
||||
}
|
||||
|
||||
private fun commitLabel(label: String?) {
|
||||
val peerId = _state.value.labelingPeerId ?: return
|
||||
mutatePeer(peerId) {
|
||||
repository.setSavedDeviceLabel(peerId, null).onSuccess {
|
||||
if (_state.value.isSavingLabel || peerId in _state.value.busyPeerIds) return
|
||||
_state.update { it.copy(isSavingLabel = true, busyPeerIds = it.busyPeerIds + peerId) }
|
||||
viewModelScope.launch {
|
||||
val result = repository.setSavedDeviceLabel(peerId, label)
|
||||
if (result.isSuccess) {
|
||||
refresh()
|
||||
messages.tryShow(UiMessage(UiText.Resource(Res.string.saved_devices_labeled), UiMessageTone.Success))
|
||||
} else {
|
||||
result.exceptionOrNull()?.let(messages::error)
|
||||
}
|
||||
_state.update { current ->
|
||||
val editorMatches = current.labelingPeerId == peerId
|
||||
current.copy(
|
||||
busyPeerIds = current.busyPeerIds - peerId,
|
||||
isSavingLabel = false,
|
||||
labelingPeerId = if (result.isSuccess && editorMatches) null else current.labelingPeerId,
|
||||
labelDraft = if (result.isSuccess && editorMatches) "" else current.labelDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
_state.update { it.copy(labelingPeerId = null, labelDraft = "") }
|
||||
}
|
||||
|
||||
fun forget(peerEndpointId: String) = mutatePeer(peerEndpointId) {
|
||||
|
||||
@@ -30,6 +30,7 @@ internal enum class AppIcon(
|
||||
Code(Res.drawable.icon_material_code, Res.drawable.icon_fluent_code, Res.drawable.icon_lucide_code),
|
||||
CloudOff(Res.drawable.icon_material_cloud_off, Res.drawable.icon_fluent_cloud_off, Res.drawable.icon_lucide_cloud_off),
|
||||
Delete(Res.drawable.icon_material_delete, Res.drawable.icon_fluent_delete, Res.drawable.icon_lucide_delete),
|
||||
Device(Res.drawable.icon_material_device, Res.drawable.icon_fluent_device, Res.drawable.icon_lucide_device),
|
||||
Document(Res.drawable.icon_material_document, Res.drawable.icon_fluent_document, Res.drawable.icon_lucide_document),
|
||||
Download(Res.drawable.icon_material_download, Res.drawable.icon_fluent_download, Res.drawable.icon_lucide_download),
|
||||
File(Res.drawable.icon_material_file, Res.drawable.icon_fluent_file, Res.drawable.icon_lucide_file),
|
||||
|
||||
@@ -24,6 +24,6 @@ internal data class NavigationItem(
|
||||
internal val primaryNavigationItems = listOf(
|
||||
NavigationItem(AppDestination.Send, Res.string.nav_send, AppIcon.Send),
|
||||
NavigationItem(AppDestination.Receive, Res.string.nav_receive, AppIcon.Download),
|
||||
NavigationItem(AppDestination.SavedDevices, Res.string.nav_saved_devices, AppIcon.ShieldCheck),
|
||||
NavigationItem(AppDestination.SavedDevices, Res.string.nav_saved_devices, AppIcon.Device),
|
||||
NavigationItem(AppDestination.Settings, Res.string.nav_settings, AppIcon.Settings),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.vnidrop.app.ui.feedback.UiMessageController
|
||||
import com.vnidrop.app.ui.theme.ThemeMode
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
@@ -228,6 +229,55 @@ class SavedDevicesViewModelTest {
|
||||
assertEquals(listOf("peer-2"), core.blockedPeers)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedLabelSaveKeepsTheEditorAndDraftAvailableForRetry() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = initializedCore().apply {
|
||||
savedDevices = listOf(device("peer-1", null, "Kitchen tablet"))
|
||||
setSavedDeviceLabelResult = Result.failure(IllegalStateException("save failed"))
|
||||
}
|
||||
val viewModel = createViewModel(core)
|
||||
runCurrent()
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.openLabelEditor("peer-1")
|
||||
viewModel.setLabelDraft("Kitchen")
|
||||
viewModel.saveLabel()
|
||||
runCurrent()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals("peer-1", viewModel.state.value.labelingPeerId)
|
||||
assertEquals("Kitchen", viewModel.state.value.labelDraft)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun labelEditorCannotDismissOrLoseItsDraftWhileSaveIsInFlight() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val releaseSave = CompletableDeferred<Unit>()
|
||||
val core = initializedCore().apply {
|
||||
savedDevices = listOf(device("peer-1", null, "Kitchen tablet"))
|
||||
beforeSetSavedDeviceLabel = { releaseSave.await() }
|
||||
}
|
||||
val viewModel = createViewModel(core)
|
||||
runCurrent()
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.openLabelEditor("peer-1")
|
||||
viewModel.setLabelDraft("Kitchen")
|
||||
viewModel.saveLabel()
|
||||
runCurrent()
|
||||
viewModel.dismissLabelEditor()
|
||||
|
||||
assertTrue(viewModel.state.value.isSavingLabel)
|
||||
assertEquals("peer-1", viewModel.state.value.labelingPeerId)
|
||||
assertEquals("Kitchen", viewModel.state.value.labelDraft)
|
||||
|
||||
releaseSave.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
assertEquals(false, viewModel.state.value.isSavingLabel)
|
||||
assertNull(viewModel.state.value.labelingPeerId)
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
core: FakeCoreGateway,
|
||||
fileSystemService: FakeFileSystemService = FakeFileSystemService(receiveFolder()),
|
||||
|
||||
@@ -221,6 +221,8 @@ class FakeCoreGateway : CoreGateway {
|
||||
val forgottenDevices = mutableListOf<String>()
|
||||
val blockedPeers = mutableListOf<String>()
|
||||
val labeledDevices = mutableListOf<Pair<String, String?>>()
|
||||
var beforeSetSavedDeviceLabel: suspend () -> Unit = {}
|
||||
var setSavedDeviceLabelResult: Result<Unit> = Result.success(Unit)
|
||||
|
||||
override suspend fun listPairingEligibilities(): Result<List<PairingEligibilityModel>> {
|
||||
listPairingEligibilitiesCount += 1
|
||||
@@ -262,11 +264,13 @@ class FakeCoreGateway : CoreGateway {
|
||||
return Result.success(savedDevices)
|
||||
}
|
||||
override suspend fun setSavedDeviceLabel(peerEndpointId: String, label: String?): Result<Unit> {
|
||||
beforeSetSavedDeviceLabel()
|
||||
labeledDevices += peerEndpointId to label
|
||||
if (setSavedDeviceLabelResult.isFailure) return setSavedDeviceLabelResult
|
||||
savedDevices = savedDevices.map {
|
||||
if (it.endpointId == peerEndpointId) it.copy(localLabel = label) else it
|
||||
}
|
||||
return Result.success(Unit)
|
||||
return setSavedDeviceLabelResult
|
||||
}
|
||||
override suspend fun forgetSavedDevice(peerEndpointId: String): Result<Unit> {
|
||||
forgottenDevices += peerEndpointId
|
||||
|
||||
@@ -6,8 +6,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
@@ -34,7 +35,6 @@ import vnidrop.shared.generated.resources.button_retry
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_action
|
||||
import vnidrop.shared.generated.resources.saved_devices_block_confirm_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_attention_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_devices_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_empty
|
||||
import vnidrop.shared.generated.resources.saved_devices_empty_title
|
||||
import vnidrop.shared.generated.resources.saved_devices_forget_action
|
||||
@@ -64,7 +64,6 @@ class SavedDevicesScreenTest {
|
||||
runOnIdle { state.value = SavedDevicesState(isLoading = false) }
|
||||
onNodeWithText(Res.string.saved_devices_empty_title.value).assertIsDisplayed()
|
||||
onNodeWithText(Res.string.saved_devices_empty.value).assertIsDisplayed()
|
||||
onAllNodesWithText(Res.string.saved_devices_devices_title.value).assertCountEquals(0)
|
||||
onAllNodesWithText(Res.string.saved_devices_transfers_title.value).assertCountEquals(0)
|
||||
}
|
||||
|
||||
@@ -87,8 +86,10 @@ class SavedDevicesScreenTest {
|
||||
}
|
||||
|
||||
onNodeWithText("Office laptop").assertIsDisplayed()
|
||||
onNodeWithText("Remote name: Amira's PC").assertIsDisplayed()
|
||||
onAllNodesWithText("Remote name: Amira's PC").assertCountEquals(0)
|
||||
onAllNodesWithText("Pixel 9", useUnmergedTree = true).assertCountEquals(2)
|
||||
onNodeWithTag("saved-device-saved-peer-long-identifier").performClick()
|
||||
onNodeWithText("Remote name: Amira's PC").assertIsDisplayed()
|
||||
onNodeWithText("Device ID: saved-peer-long-…").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@@ -108,10 +109,11 @@ class SavedDevicesScreenTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
onAllNodesWithContentDescription(Res.string.saved_devices_send_action.value).assertCountEquals(0)
|
||||
onNodeWithTag("saved-device-peer-one").performClick()
|
||||
onNodeWithText(Res.string.saved_devices_send_action.value).assertIsNotEnabled()
|
||||
|
||||
runOnIdle { state.value = state.value.copy(busyPeerIds = emptySet()) }
|
||||
onNodeWithContentDescription(Res.string.saved_devices_send_action.value).assertIsDisplayed()
|
||||
onNodeWithText(Res.string.saved_devices_send_action.value).assertIsEnabled()
|
||||
onNodeWithContentDescription("More actions for Riley's phone").performClick()
|
||||
onNodeWithText(Res.string.saved_devices_block_action.value).performClick()
|
||||
onNodeWithText(Res.string.saved_devices_block_confirm_title.value).assertIsDisplayed()
|
||||
@@ -133,12 +135,13 @@ class SavedDevicesScreenTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
onNodeWithTag("saved-device-peer-two").performClick()
|
||||
onNodeWithContentDescription("More actions for Desktop").performClick()
|
||||
onNodeWithText(Res.string.saved_devices_forget_action.value).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directTransferHistoryShowsProgressAndRoutesResumeById() = runComposeUiTest {
|
||||
fun deviceTransfersStayHiddenUntilTheDeviceDetailsAreOpened() = runComposeUiTest {
|
||||
val actions = mutableListOf<Pair<String, SavedDeviceTransferAction>>()
|
||||
setContent {
|
||||
CompositionLocalProvider(LocalUiPlatform provides UiPlatform.Android) {
|
||||
@@ -146,6 +149,7 @@ class SavedDevicesScreenTest {
|
||||
SavedDevicesScreen(
|
||||
state = SavedDevicesState(
|
||||
isLoading = false,
|
||||
savedDevices = listOf(device("peer", null, "Office PC")),
|
||||
targetedTransfers = listOf(
|
||||
SavedDeviceTransferItem(
|
||||
id = "transfer-resume",
|
||||
@@ -169,9 +173,12 @@ class SavedDevicesScreenTest {
|
||||
}
|
||||
}
|
||||
|
||||
onAllNodesWithText(Res.string.saved_devices_transfers_title.value).assertCountEquals(0)
|
||||
onAllNodesWithText("Project files").assertCountEquals(0)
|
||||
onNodeWithTag("saved-device-peer").performClick()
|
||||
onNodeWithText(Res.string.saved_devices_transfers_title.value).assertIsDisplayed()
|
||||
onNodeWithText("Project files").assertIsDisplayed()
|
||||
onNodeWithText("Office PC", substring = true).assertIsDisplayed()
|
||||
onAllNodesWithText("%", substring = true).assertCountEquals(0)
|
||||
onNodeWithText(Res.string.saved_devices_transfer_resume.value).performClick()
|
||||
runOnIdle {
|
||||
assertEquals(listOf("transfer-resume" to SavedDeviceTransferAction.Resume), actions)
|
||||
@@ -203,7 +210,6 @@ class SavedDevicesScreenTest {
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithText(Res.string.saved_devices_devices_title.value).assertIsDisplayed()
|
||||
onNodeWithText(Res.string.saved_devices_attention_title.value).assertIsDisplayed()
|
||||
onNodeWithText("Amira's phone").assertIsDisplayed()
|
||||
onNodeWithText("Studio PC").assertIsDisplayed()
|
||||
@@ -232,7 +238,7 @@ class SavedDevicesScreenTest {
|
||||
|
||||
val attentionTop = onNodeWithText(Res.string.saved_devices_attention_title.value)
|
||||
.fetchSemanticsNode().boundsInRoot.top
|
||||
val devicesTop = onNodeWithText(Res.string.saved_devices_devices_title.value)
|
||||
val devicesTop = onNodeWithTag("saved-device-peer-one")
|
||||
.fetchSemanticsNode().boundsInRoot.top
|
||||
assertTrue(attentionTop < devicesTop, "compact layouts should surface pending decisions before the device list")
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ ICONS = (
|
||||
IconSource("code", "code", "Code", "code", "code-xml"),
|
||||
IconSource("cloud_off", "cloud_off", "Cloud Off", "cloud_off", "cloud-off"),
|
||||
IconSource("delete", "delete", "Delete", "delete", "trash-2"),
|
||||
IconSource("device", "devices", "Desktop", "desktop", "monitor"),
|
||||
IconSource("document", "description", "Document", "document", "file-text"),
|
||||
IconSource("download", "download", "Arrow Download", "arrow_download", "download"),
|
||||
IconSource("file", "draft", "Document", "document", "file"),
|
||||
|
||||
Reference in New Issue
Block a user