mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(shared): align non-Apple UX with Apple
This commit is contained in:
@@ -828,6 +828,20 @@
|
|||||||
"ru": "Назад"
|
"ru": "Назад"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"button_more_actions": {
|
||||||
|
"context": "Accessibility label for a button that opens more actions for an item.",
|
||||||
|
"translations": {
|
||||||
|
"en": "More actions",
|
||||||
|
"fr": "Plus d’actions",
|
||||||
|
"es": "Más acciones",
|
||||||
|
"it": "Altre azioni",
|
||||||
|
"de": "Weitere Aktionen",
|
||||||
|
"pt": "Mais ações",
|
||||||
|
"pl": "Więcej działań",
|
||||||
|
"nl": "Meer acties",
|
||||||
|
"ru": "Другие действия"
|
||||||
|
}
|
||||||
|
},
|
||||||
"button_cancel": {
|
"button_cancel": {
|
||||||
"context": "Button: cancel the current action or dialog.",
|
"context": "Button: cancel the current action or dialog.",
|
||||||
"translations": {
|
"translations": {
|
||||||
@@ -1826,9 +1840,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receive_completed_body": {
|
"notifications_receive_completed_body": {
|
||||||
"context": "Notification body shown when an incoming transfer finishes downloading. {transferName} = transfer name.",
|
"context": "Notification body shown when an incoming transfer finishes downloading. {transferName} = transfer name.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "transferName",
|
"name": "transferName",
|
||||||
@@ -1849,9 +1860,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receive_completed_title": {
|
"notifications_receive_completed_title": {
|
||||||
"context": "Notification title shown when an incoming transfer finishes downloading.",
|
"context": "Notification title shown when an incoming transfer finishes downloading.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Download complete",
|
"en": "Download complete",
|
||||||
"fr": "Téléchargement terminé",
|
"fr": "Téléchargement terminé",
|
||||||
@@ -1866,9 +1874,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receive_failed_body": {
|
"notifications_receive_failed_body": {
|
||||||
"context": "Notification body shown when an incoming transfer fails. {transferName} = transfer name.",
|
"context": "Notification body shown when an incoming transfer fails. {transferName} = transfer name.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "transferName",
|
"name": "transferName",
|
||||||
@@ -1889,9 +1894,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receive_failed_title": {
|
"notifications_receive_failed_title": {
|
||||||
"context": "Notification title shown when an incoming transfer fails.",
|
"context": "Notification title shown when an incoming transfer fails.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Download failed",
|
"en": "Download failed",
|
||||||
"fr": "Échec du téléchargement",
|
"fr": "Échec du téléchargement",
|
||||||
@@ -1906,9 +1908,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receiver_completed_body": {
|
"notifications_receiver_completed_body": {
|
||||||
"context": "Notification body shown to the sender when a receiver finishes downloading a shared transfer. {receiver} = receiver name, {transferName} = transfer name.",
|
"context": "Notification body shown to the sender when a receiver finishes downloading a shared transfer. {receiver} = receiver name, {transferName} = transfer name.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "receiver",
|
"name": "receiver",
|
||||||
@@ -1933,9 +1932,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receiver_completed_title": {
|
"notifications_receiver_completed_title": {
|
||||||
"context": "Notification title shown to the sender when a receiver finishes downloading a shared transfer.",
|
"context": "Notification title shown to the sender when a receiver finishes downloading a shared transfer.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Transfer received",
|
"en": "Transfer received",
|
||||||
"fr": "Transfert reçu",
|
"fr": "Transfert reçu",
|
||||||
@@ -1950,9 +1946,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receiver_failed_body": {
|
"notifications_receiver_failed_body": {
|
||||||
"context": "Notification body shown to the sender when a receiver's download fails. {receiver} = receiver name, {transferName} = transfer name.",
|
"context": "Notification body shown to the sender when a receiver's download fails. {receiver} = receiver name, {transferName} = transfer name.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "receiver",
|
"name": "receiver",
|
||||||
@@ -1977,9 +1970,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_receiver_failed_title": {
|
"notifications_receiver_failed_title": {
|
||||||
"context": "Notification title shown to the sender when a receiver's download fails.",
|
"context": "Notification title shown to the sender when a receiver's download fails.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Delivery failed",
|
"en": "Delivery failed",
|
||||||
"fr": "Échec de l'envoi",
|
"fr": "Échec de l'envoi",
|
||||||
@@ -1994,9 +1984,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_send_failed_body": {
|
"notifications_send_failed_body": {
|
||||||
"context": "Notification body shown to the sender when a shared transfer fails. {transferName} = transfer name.",
|
"context": "Notification body shown to the sender when a shared transfer fails. {transferName} = transfer name.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "transferName",
|
"name": "transferName",
|
||||||
@@ -2017,9 +2004,6 @@
|
|||||||
},
|
},
|
||||||
"notifications_send_failed_title": {
|
"notifications_send_failed_title": {
|
||||||
"context": "Notification title shown to the sender when a shared transfer fails.",
|
"context": "Notification title shown to the sender when a shared transfer fails.",
|
||||||
"targets": [
|
|
||||||
"apple"
|
|
||||||
],
|
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Sharing failed",
|
"en": "Sharing failed",
|
||||||
"fr": "Échec du partage",
|
"fr": "Échec du partage",
|
||||||
|
|||||||
@@ -105,6 +105,30 @@ private class AndroidFileSystemService(
|
|||||||
|
|
||||||
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = directorySize(context.cacheDir)
|
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = directorySize(context.cacheDir)
|
||||||
|
|
||||||
|
override suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong {
|
||||||
|
var reclaimed = 0UL
|
||||||
|
context.cacheDir.listFiles().orEmpty().forEach { entry ->
|
||||||
|
val size = if (entry.isDirectory) directorySize(entry) else entry.length().coerceAtLeast(0L).toULong()
|
||||||
|
if (entry.deleteRecursively()) reclaimed += size
|
||||||
|
}
|
||||||
|
val appDataRoot = File(appDataDir)
|
||||||
|
val appDataIsOwned = runCatching {
|
||||||
|
val appDataPath = appDataRoot.canonicalPath
|
||||||
|
val filesPath = context.filesDir.canonicalPath
|
||||||
|
appDataPath == filesPath || appDataPath.startsWith(filesPath + File.separator)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
if (appDataIsOwned) {
|
||||||
|
appDataRoot.walkTopDown()
|
||||||
|
.filter { it.isDirectory && it.name == ".Trash" }
|
||||||
|
.toList()
|
||||||
|
.forEach { trash ->
|
||||||
|
val size = directorySize(trash)
|
||||||
|
if (trash.deleteRecursively()) reclaimed += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reclaimed
|
||||||
|
}
|
||||||
|
|
||||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? =
|
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? =
|
||||||
when (folder.kind) {
|
when (folder.kind) {
|
||||||
ReceiveFolderKind.AndroidPublicDownloads -> AndroidMediaStoreDownloadsSink(context)
|
ReceiveFolderKind.AndroidPublicDownloads -> AndroidMediaStoreDownloadsSink(context)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">Was ist passiert?</string>
|
<string name="bug_report_what_label">Was ist passiert?</string>
|
||||||
<string name="button_approve">Genehmigen</string>
|
<string name="button_approve">Genehmigen</string>
|
||||||
<string name="button_back">Zurück</string>
|
<string name="button_back">Zurück</string>
|
||||||
|
<string name="button_more_actions">Weitere Aktionen</string>
|
||||||
<string name="button_cancel">Abbrechen</string>
|
<string name="button_cancel">Abbrechen</string>
|
||||||
<string name="button_cancel_receive">Abbrechen</string>
|
<string name="button_cancel_receive">Abbrechen</string>
|
||||||
<string name="button_change_files">Dateien ändern</string>
|
<string name="button_change_files">Dateien ändern</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Mitteilungen aktiviert.</string>
|
<string name="notifications_enabled_message">Mitteilungen aktiviert.</string>
|
||||||
<string name="notifications_local_title">Mitteilungen erlauben</string>
|
<string name="notifications_local_title">Mitteilungen erlauben</string>
|
||||||
<string name="notifications_permission_denied">Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren.</string>
|
<string name="notifications_permission_denied">Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren.</string>
|
||||||
|
<string name="notifications_receive_completed_body">„%1$s“ wurde vollständig heruntergeladen.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Download abgeschlossen</string>
|
||||||
|
<string name="notifications_receive_failed_body">„%1$s“ konnte nicht empfangen werden.</string>
|
||||||
|
<string name="notifications_receive_failed_title">Download fehlgeschlagen</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s hat „%2$s“ vollständig empfangen.</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Übertragung empfangen</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s konnte „%2$s“ nicht empfangen</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Übertragung fehlgeschlagen</string>
|
||||||
|
<string name="notifications_send_failed_body">„%1$s“ konnte nicht geteilt werden.</string>
|
||||||
|
<string name="notifications_send_failed_title">Freigabe fehlgeschlagen</string>
|
||||||
<string name="notifications_settings_open_failed">Die Mitteilungseinstellungen konnten nicht geöffnet werden.</string>
|
<string name="notifications_settings_open_failed">Die Mitteilungseinstellungen konnten nicht geöffnet werden.</string>
|
||||||
<string name="notifications_title">Mitteilungen</string>
|
<string name="notifications_title">Mitteilungen</string>
|
||||||
<string name="notifications_unsupported">Mitteilungen sind auf diesem Gerät nicht verfügbar.</string>
|
<string name="notifications_unsupported">Mitteilungen sind auf diesem Gerät nicht verfügbar.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">¿Qué ocurrió?</string>
|
<string name="bug_report_what_label">¿Qué ocurrió?</string>
|
||||||
<string name="button_approve">Aprobar</string>
|
<string name="button_approve">Aprobar</string>
|
||||||
<string name="button_back">Atrás</string>
|
<string name="button_back">Atrás</string>
|
||||||
|
<string name="button_more_actions">Más acciones</string>
|
||||||
<string name="button_cancel">Cancelar</string>
|
<string name="button_cancel">Cancelar</string>
|
||||||
<string name="button_cancel_receive">Cancelar</string>
|
<string name="button_cancel_receive">Cancelar</string>
|
||||||
<string name="button_change_files">Cambiar archivos</string>
|
<string name="button_change_files">Cambiar archivos</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Notificaciones activadas.</string>
|
<string name="notifications_enabled_message">Notificaciones activadas.</string>
|
||||||
<string name="notifications_local_title">Permitir notificaciones</string>
|
<string name="notifications_local_title">Permitir notificaciones</string>
|
||||||
<string name="notifications_permission_denied">Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes.</string>
|
<string name="notifications_permission_denied">Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes.</string>
|
||||||
|
<string name="notifications_receive_completed_body">«%1$s» terminó de descargarse.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Descarga completada</string>
|
||||||
|
<string name="notifications_receive_failed_body">No se pudo recibir «%1$s».</string>
|
||||||
|
<string name="notifications_receive_failed_title">Error en la descarga</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s terminó de recibir «%2$s».</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Transferencia recibida</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s no pudo recibir «%2$s»</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Error en la entrega</string>
|
||||||
|
<string name="notifications_send_failed_body">No se pudo compartir «%1$s».</string>
|
||||||
|
<string name="notifications_send_failed_title">Error al compartir</string>
|
||||||
<string name="notifications_settings_open_failed">No se pudieron abrir los ajustes de notificaciones.</string>
|
<string name="notifications_settings_open_failed">No se pudieron abrir los ajustes de notificaciones.</string>
|
||||||
<string name="notifications_title">Notificaciones</string>
|
<string name="notifications_title">Notificaciones</string>
|
||||||
<string name="notifications_unsupported">Las notificaciones no están disponibles en este dispositivo.</string>
|
<string name="notifications_unsupported">Las notificaciones no están disponibles en este dispositivo.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">Que s’est-il passé ?</string>
|
<string name="bug_report_what_label">Que s’est-il passé ?</string>
|
||||||
<string name="button_approve">Approuver</string>
|
<string name="button_approve">Approuver</string>
|
||||||
<string name="button_back">Retour</string>
|
<string name="button_back">Retour</string>
|
||||||
|
<string name="button_more_actions">Plus d’actions</string>
|
||||||
<string name="button_cancel">Annuler</string>
|
<string name="button_cancel">Annuler</string>
|
||||||
<string name="button_cancel_receive">Annuler</string>
|
<string name="button_cancel_receive">Annuler</string>
|
||||||
<string name="button_change_files">Modifier les fichiers</string>
|
<string name="button_change_files">Modifier les fichiers</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Notifications activées.</string>
|
<string name="notifications_enabled_message">Notifications activées.</string>
|
||||||
<string name="notifications_local_title">Autoriser les notifications</string>
|
<string name="notifications_local_title">Autoriser les notifications</string>
|
||||||
<string name="notifications_permission_denied">Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages.</string>
|
<string name="notifications_permission_denied">Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages.</string>
|
||||||
|
<string name="notifications_receive_completed_body">« %1$s » a fini de se télécharger.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Téléchargement terminé</string>
|
||||||
|
<string name="notifications_receive_failed_body">« %1$s » n’a pas pu être reçu.</string>
|
||||||
|
<string name="notifications_receive_failed_title">Échec du téléchargement</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s a fini de recevoir « %2$s ».</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Transfert reçu</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s n\'a pas pu recevoir « %2$s »</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Échec de l\'envoi</string>
|
||||||
|
<string name="notifications_send_failed_body">« %1$s » n’a pas pu être partagé.</string>
|
||||||
|
<string name="notifications_send_failed_title">Échec du partage</string>
|
||||||
<string name="notifications_settings_open_failed">Impossible d’ouvrir les réglages de notifications.</string>
|
<string name="notifications_settings_open_failed">Impossible d’ouvrir les réglages de notifications.</string>
|
||||||
<string name="notifications_title">Notifications</string>
|
<string name="notifications_title">Notifications</string>
|
||||||
<string name="notifications_unsupported">Les notifications ne sont pas disponibles sur cet appareil.</string>
|
<string name="notifications_unsupported">Les notifications ne sont pas disponibles sur cet appareil.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">Cosa è accaduto?</string>
|
<string name="bug_report_what_label">Cosa è accaduto?</string>
|
||||||
<string name="button_approve">Approva</string>
|
<string name="button_approve">Approva</string>
|
||||||
<string name="button_back">Indietro</string>
|
<string name="button_back">Indietro</string>
|
||||||
|
<string name="button_more_actions">Altre azioni</string>
|
||||||
<string name="button_cancel">Annulla</string>
|
<string name="button_cancel">Annulla</string>
|
||||||
<string name="button_cancel_receive">Annulla</string>
|
<string name="button_cancel_receive">Annulla</string>
|
||||||
<string name="button_change_files">Cambia file</string>
|
<string name="button_change_files">Cambia file</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Notifiche attivate.</string>
|
<string name="notifications_enabled_message">Notifiche attivate.</string>
|
||||||
<string name="notifications_local_title">Consenti le notifiche</string>
|
<string name="notifications_local_title">Consenti le notifiche</string>
|
||||||
<string name="notifications_permission_denied">Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni.</string>
|
<string name="notifications_permission_denied">Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni.</string>
|
||||||
|
<string name="notifications_receive_completed_body">«%1$s» è stato scaricato.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Download completato</string>
|
||||||
|
<string name="notifications_receive_failed_body">Impossibile ricevere «%1$s».</string>
|
||||||
|
<string name="notifications_receive_failed_title">Download non riuscito</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s ha finito di ricevere «%2$s».</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Trasferimento ricevuto</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s non ha potuto ricevere “%2$s”</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Consegna non riuscita</string>
|
||||||
|
<string name="notifications_send_failed_body">Impossibile condividere «%1$s».</string>
|
||||||
|
<string name="notifications_send_failed_title">Condivisione non riuscita</string>
|
||||||
<string name="notifications_settings_open_failed">Impossibile aprire le impostazioni delle notifiche.</string>
|
<string name="notifications_settings_open_failed">Impossibile aprire le impostazioni delle notifiche.</string>
|
||||||
<string name="notifications_title">Notifiche</string>
|
<string name="notifications_title">Notifiche</string>
|
||||||
<string name="notifications_unsupported">Le notifiche non sono disponibili su questo dispositivo.</string>
|
<string name="notifications_unsupported">Le notifiche non sono disponibili su questo dispositivo.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">Wat is er gebeurd?</string>
|
<string name="bug_report_what_label">Wat is er gebeurd?</string>
|
||||||
<string name="button_approve">Goedkeuren</string>
|
<string name="button_approve">Goedkeuren</string>
|
||||||
<string name="button_back">Terug</string>
|
<string name="button_back">Terug</string>
|
||||||
|
<string name="button_more_actions">Meer acties</string>
|
||||||
<string name="button_cancel">Annuleren</string>
|
<string name="button_cancel">Annuleren</string>
|
||||||
<string name="button_cancel_receive">Annuleren</string>
|
<string name="button_cancel_receive">Annuleren</string>
|
||||||
<string name="button_change_files">Bestanden wijzigen</string>
|
<string name="button_change_files">Bestanden wijzigen</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Meldingen ingeschakeld.</string>
|
<string name="notifications_enabled_message">Meldingen ingeschakeld.</string>
|
||||||
<string name="notifications_local_title">Meldingen toestaan</string>
|
<string name="notifications_local_title">Meldingen toestaan</string>
|
||||||
<string name="notifications_permission_denied">Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen.</string>
|
<string name="notifications_permission_denied">Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen.</string>
|
||||||
|
<string name="notifications_receive_completed_body">‘%1$s’ is volledig gedownload.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Download voltooid</string>
|
||||||
|
<string name="notifications_receive_failed_body">‘%1$s’ kon niet worden ontvangen.</string>
|
||||||
|
<string name="notifications_receive_failed_title">Download mislukt</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s heeft ‘%2$s’ volledig ontvangen.</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Overdracht ontvangen</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s kon “%2$s” niet ontvangen</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Levering mislukt</string>
|
||||||
|
<string name="notifications_send_failed_body">‘%1$s’ kon niet worden gedeeld.</string>
|
||||||
|
<string name="notifications_send_failed_title">Delen mislukt</string>
|
||||||
<string name="notifications_settings_open_failed">De meldingsinstellingen konden niet worden geopend.</string>
|
<string name="notifications_settings_open_failed">De meldingsinstellingen konden niet worden geopend.</string>
|
||||||
<string name="notifications_title">Meldingen</string>
|
<string name="notifications_title">Meldingen</string>
|
||||||
<string name="notifications_unsupported">Meldingen zijn niet beschikbaar op dit apparaat.</string>
|
<string name="notifications_unsupported">Meldingen zijn niet beschikbaar op dit apparaat.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">Co się stało?</string>
|
<string name="bug_report_what_label">Co się stało?</string>
|
||||||
<string name="button_approve">Zatwierdź</string>
|
<string name="button_approve">Zatwierdź</string>
|
||||||
<string name="button_back">Wstecz</string>
|
<string name="button_back">Wstecz</string>
|
||||||
|
<string name="button_more_actions">Więcej działań</string>
|
||||||
<string name="button_cancel">Anuluj</string>
|
<string name="button_cancel">Anuluj</string>
|
||||||
<string name="button_cancel_receive">Anuluj</string>
|
<string name="button_cancel_receive">Anuluj</string>
|
||||||
<string name="button_change_files">Zmień pliki</string>
|
<string name="button_change_files">Zmień pliki</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Powiadomienia włączone.</string>
|
<string name="notifications_enabled_message">Powiadomienia włączone.</string>
|
||||||
<string name="notifications_local_title">Zezwól na powiadomienia</string>
|
<string name="notifications_local_title">Zezwól na powiadomienia</string>
|
||||||
<string name="notifications_permission_denied">Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach.</string>
|
<string name="notifications_permission_denied">Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach.</string>
|
||||||
|
<string name="notifications_receive_completed_body">Zakończono pobieranie „%1$s”.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Pobieranie zakończone</string>
|
||||||
|
<string name="notifications_receive_failed_body">Nie udało się odebrać „%1$s”.</string>
|
||||||
|
<string name="notifications_receive_failed_title">Pobieranie nie powiodło się</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s zakończył odbieranie „%2$s”.</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Transfer odebrany</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s nie mógł odebrać „%2$s”</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Dostarczenie nie powiodło się</string>
|
||||||
|
<string name="notifications_send_failed_body">Nie udało się udostępnić „%1$s”.</string>
|
||||||
|
<string name="notifications_send_failed_title">Udostępnianie nie powiodło się</string>
|
||||||
<string name="notifications_settings_open_failed">Nie udało się otworzyć ustawień powiadomień.</string>
|
<string name="notifications_settings_open_failed">Nie udało się otworzyć ustawień powiadomień.</string>
|
||||||
<string name="notifications_title">Powiadomienia</string>
|
<string name="notifications_title">Powiadomienia</string>
|
||||||
<string name="notifications_unsupported">Powiadomienia nie są dostępne na tym urządzeniu.</string>
|
<string name="notifications_unsupported">Powiadomienia nie są dostępne na tym urządzeniu.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">O que aconteceu?</string>
|
<string name="bug_report_what_label">O que aconteceu?</string>
|
||||||
<string name="button_approve">Aprovar</string>
|
<string name="button_approve">Aprovar</string>
|
||||||
<string name="button_back">Voltar</string>
|
<string name="button_back">Voltar</string>
|
||||||
|
<string name="button_more_actions">Mais ações</string>
|
||||||
<string name="button_cancel">Cancelar</string>
|
<string name="button_cancel">Cancelar</string>
|
||||||
<string name="button_cancel_receive">Cancelar</string>
|
<string name="button_cancel_receive">Cancelar</string>
|
||||||
<string name="button_change_files">Alterar ficheiros</string>
|
<string name="button_change_files">Alterar ficheiros</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Notificações ativadas.</string>
|
<string name="notifications_enabled_message">Notificações ativadas.</string>
|
||||||
<string name="notifications_local_title">Permitir notificações</string>
|
<string name="notifications_local_title">Permitir notificações</string>
|
||||||
<string name="notifications_permission_denied">As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições.</string>
|
<string name="notifications_permission_denied">As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições.</string>
|
||||||
|
<string name="notifications_receive_completed_body">«%1$s» concluiu a transferência.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Transferência concluída</string>
|
||||||
|
<string name="notifications_receive_failed_body">Não foi possível receber «%1$s».</string>
|
||||||
|
<string name="notifications_receive_failed_title">Falha na transferência</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s terminou de receber «%2$s».</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Transferência recebida</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s não conseguiu receber “%2$s”</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Falha na entrega</string>
|
||||||
|
<string name="notifications_send_failed_body">Não foi possível partilhar «%1$s».</string>
|
||||||
|
<string name="notifications_send_failed_title">Falha na partilha</string>
|
||||||
<string name="notifications_settings_open_failed">Não foi possível abrir as definições de notificações.</string>
|
<string name="notifications_settings_open_failed">Não foi possível abrir as definições de notificações.</string>
|
||||||
<string name="notifications_title">Notificações</string>
|
<string name="notifications_title">Notificações</string>
|
||||||
<string name="notifications_unsupported">As notificações não estão disponíveis neste dispositivo.</string>
|
<string name="notifications_unsupported">As notificações não estão disponíveis neste dispositivo.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">Что произошло?</string>
|
<string name="bug_report_what_label">Что произошло?</string>
|
||||||
<string name="button_approve">Одобрить</string>
|
<string name="button_approve">Одобрить</string>
|
||||||
<string name="button_back">Назад</string>
|
<string name="button_back">Назад</string>
|
||||||
|
<string name="button_more_actions">Другие действия</string>
|
||||||
<string name="button_cancel">Отмена</string>
|
<string name="button_cancel">Отмена</string>
|
||||||
<string name="button_cancel_receive">Отмена</string>
|
<string name="button_cancel_receive">Отмена</string>
|
||||||
<string name="button_change_files">Изменить файлы</string>
|
<string name="button_change_files">Изменить файлы</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Уведомления включены.</string>
|
<string name="notifications_enabled_message">Уведомления включены.</string>
|
||||||
<string name="notifications_local_title">Разрешить уведомления</string>
|
<string name="notifications_local_title">Разрешить уведомления</string>
|
||||||
<string name="notifications_permission_denied">Уведомления отключены для VniDrop. Вы можете включить их в Настройках.</string>
|
<string name="notifications_permission_denied">Уведомления отключены для VniDrop. Вы можете включить их в Настройках.</string>
|
||||||
|
<string name="notifications_receive_completed_body">«%1$s» завершил загрузку.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Загрузка завершена</string>
|
||||||
|
<string name="notifications_receive_failed_body">Не удалось получить «%1$s».</string>
|
||||||
|
<string name="notifications_receive_failed_title">Ошибка загрузки</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s завершил получение «%2$s».</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Передача получена</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s не удалось получить «%2$s»</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Ошибка доставки</string>
|
||||||
|
<string name="notifications_send_failed_body">Не удалось поделиться «%1$s».</string>
|
||||||
|
<string name="notifications_send_failed_title">Не удалось поделиться</string>
|
||||||
<string name="notifications_settings_open_failed">Не удалось открыть настройки уведомлений.</string>
|
<string name="notifications_settings_open_failed">Не удалось открыть настройки уведомлений.</string>
|
||||||
<string name="notifications_title">Уведомления</string>
|
<string name="notifications_title">Уведомления</string>
|
||||||
<string name="notifications_unsupported">Уведомления недоступны на этом устройстве.</string>
|
<string name="notifications_unsupported">Уведомления недоступны на этом устройстве.</string>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<string name="bug_report_what_label">What happened?</string>
|
<string name="bug_report_what_label">What happened?</string>
|
||||||
<string name="button_approve">Approve</string>
|
<string name="button_approve">Approve</string>
|
||||||
<string name="button_back">Back</string>
|
<string name="button_back">Back</string>
|
||||||
|
<string name="button_more_actions">More actions</string>
|
||||||
<string name="button_cancel">Cancel</string>
|
<string name="button_cancel">Cancel</string>
|
||||||
<string name="button_cancel_receive">Cancel</string>
|
<string name="button_cancel_receive">Cancel</string>
|
||||||
<string name="button_change_files">Change files</string>
|
<string name="button_change_files">Change files</string>
|
||||||
@@ -122,6 +123,16 @@
|
|||||||
<string name="notifications_enabled_message">Notifications enabled.</string>
|
<string name="notifications_enabled_message">Notifications enabled.</string>
|
||||||
<string name="notifications_local_title">Allow notifications</string>
|
<string name="notifications_local_title">Allow notifications</string>
|
||||||
<string name="notifications_permission_denied">Notifications are turned off for VniDrop. You can enable them in Settings.</string>
|
<string name="notifications_permission_denied">Notifications are turned off for VniDrop. You can enable them in Settings.</string>
|
||||||
|
<string name="notifications_receive_completed_body">“%1$s” finished downloading.</string>
|
||||||
|
<string name="notifications_receive_completed_title">Download complete</string>
|
||||||
|
<string name="notifications_receive_failed_body">“%1$s” couldn’t be received.</string>
|
||||||
|
<string name="notifications_receive_failed_title">Download failed</string>
|
||||||
|
<string name="notifications_receiver_completed_body">%1$s finished receiving “%2$s”.</string>
|
||||||
|
<string name="notifications_receiver_completed_title">Transfer received</string>
|
||||||
|
<string name="notifications_receiver_failed_body">%1$s couldn\'t receive “%2$s”</string>
|
||||||
|
<string name="notifications_receiver_failed_title">Delivery failed</string>
|
||||||
|
<string name="notifications_send_failed_body">“%1$s” couldn’t be shared.</string>
|
||||||
|
<string name="notifications_send_failed_title">Sharing failed</string>
|
||||||
<string name="notifications_settings_open_failed">Could not open notification settings.</string>
|
<string name="notifications_settings_open_failed">Could not open notification settings.</string>
|
||||||
<string name="notifications_title">Notifications</string>
|
<string name="notifications_title">Notifications</string>
|
||||||
<string name="notifications_unsupported">Notifications are not available on this device.</string>
|
<string name="notifications_unsupported">Notifications are not available on this device.</string>
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -14,6 +21,8 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -49,6 +58,9 @@ import com.vnidrop.app.ui.theme.rememberResolvedDarkTheme
|
|||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import vnidrop.shared.generated.resources.Res
|
||||||
|
import vnidrop.shared.generated.resources.app_starting
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App(
|
fun App(
|
||||||
@@ -217,6 +229,32 @@ fun App(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
windowChrome?.invoke()
|
windowChrome?.invoke()
|
||||||
|
val startingLabel = stringResource(Res.string.app_starting)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !sendCoreState.isInitialized,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(LocalVniDropColors.current.backgroundSurface100)
|
||||||
|
.semantics { contentDescription = startingLabel },
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Text(
|
||||||
|
startingLabel,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = LocalVniDropColors.current.foregroundLighter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.vnidrop.app.feature.approvals.ApprovalCoordinator
|
|||||||
import com.vnidrop.app.feature.send.AppFilePreviewRepository
|
import com.vnidrop.app.feature.send.AppFilePreviewRepository
|
||||||
import com.vnidrop.app.feature.send.createPlatformPreviewStore
|
import com.vnidrop.app.feature.send.createPlatformPreviewStore
|
||||||
import com.vnidrop.app.logging.AppLogger
|
import com.vnidrop.app.logging.AppLogger
|
||||||
|
import com.vnidrop.app.notifications.TransferNotificationCoordinator
|
||||||
import com.vnidrop.app.platform.AppVisibility
|
import com.vnidrop.app.platform.AppVisibility
|
||||||
import com.vnidrop.app.preferences.AppPreferencesDefaults
|
import com.vnidrop.app.preferences.AppPreferencesDefaults
|
||||||
import com.vnidrop.app.preferences.AppPreferencesRepository
|
import com.vnidrop.app.preferences.AppPreferencesRepository
|
||||||
@@ -62,6 +63,14 @@ class AppGraph(
|
|||||||
messages = messages,
|
messages = messages,
|
||||||
scope = applicationScope,
|
scope = applicationScope,
|
||||||
)
|
)
|
||||||
|
val transferNotificationCoordinator = TransferNotificationCoordinator(
|
||||||
|
repository = coreRepository,
|
||||||
|
preferencesRepository = preferencesRepository,
|
||||||
|
notifications = dependencies.localNotificationService,
|
||||||
|
visibility = visibility,
|
||||||
|
messages = messages,
|
||||||
|
scope = applicationScope,
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppLogger.initialize(dependencies.environment.defaultCoreDataDir)
|
AppLogger.initialize(dependencies.environment.defaultCoreDataDir)
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ interface FileSystemService {
|
|||||||
suspend fun validateReceiveFolder(folder: ReceiveFolder): FolderAccessStatus
|
suspend fun validateReceiveFolder(folder: ReceiveFolder): FolderAccessStatus
|
||||||
suspend fun inspectReceivedArtifacts(artifacts: List<ReceivedArtifactModel>): ReceivedStorageInspection
|
suspend fun inspectReceivedArtifacts(artifacts: List<ReceivedArtifactModel>): ReceivedStorageInspection
|
||||||
suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong
|
suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong
|
||||||
|
/** Reclaims only app-owned temporary files and returns the number of bytes removed. */
|
||||||
|
suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong
|
||||||
fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2?
|
fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2?
|
||||||
fun canRevealReceiveFolder(folder: ReceiveFolder): Boolean = false
|
fun canRevealReceiveFolder(folder: ReceiveFolder): Boolean = false
|
||||||
suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> =
|
suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> =
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ private fun InvitationReviewPanel(
|
|||||||
Text(
|
Text(
|
||||||
when (error) {
|
when (error) {
|
||||||
is UiText.Dynamic -> error.value
|
is UiText.Dynamic -> error.value
|
||||||
is UiText.Resource -> stringResource(error.resource)
|
is UiText.Resource -> stringResource(error.resource, *error.formatArgs.toTypedArray())
|
||||||
},
|
},
|
||||||
color = LocalVniDropColors.current.destructiveDefault,
|
color = LocalVniDropColors.current.destructiveDefault,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|||||||
@@ -19,17 +19,25 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -58,12 +66,16 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
import org.jetbrains.compose.resources.decodeToImageBitmap
|
import org.jetbrains.compose.resources.decodeToImageBitmap
|
||||||
import vnidrop.shared.generated.resources.Res
|
import vnidrop.shared.generated.resources.Res
|
||||||
import vnidrop.shared.generated.resources.button_create_new_transfer
|
import vnidrop.shared.generated.resources.button_create_new_transfer
|
||||||
|
import vnidrop.shared.generated.resources.button_delete_transfer
|
||||||
|
import vnidrop.shared.generated.resources.button_more_actions
|
||||||
import vnidrop.shared.generated.resources.send_empty_body
|
import vnidrop.shared.generated.resources.send_empty_body
|
||||||
import vnidrop.shared.generated.resources.send_empty_title
|
import vnidrop.shared.generated.resources.send_empty_title
|
||||||
import vnidrop.shared.generated.resources.send_new_transfer_description
|
import vnidrop.shared.generated.resources.send_new_transfer_description
|
||||||
import vnidrop.shared.generated.resources.send_new_transfer_title
|
import vnidrop.shared.generated.resources.send_new_transfer_title
|
||||||
import vnidrop.shared.generated.resources.send_title
|
import vnidrop.shared.generated.resources.send_title
|
||||||
|
import vnidrop.shared.generated.resources.send_stop_sharing
|
||||||
import vnidrop.shared.generated.resources.send_transfers_title
|
import vnidrop.shared.generated.resources.send_transfers_title
|
||||||
|
import vnidrop.shared.generated.resources.transfer_share_title
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SendFloatingAction(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
internal fun SendFloatingAction(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
@@ -86,6 +98,9 @@ internal fun TransferCatalog(
|
|||||||
windowClass: WindowClass,
|
windowClass: WindowClass,
|
||||||
onOpenComposer: () -> Unit,
|
onOpenComposer: () -> Unit,
|
||||||
onTransferSelected: (ULong) -> Unit,
|
onTransferSelected: (ULong) -> Unit,
|
||||||
|
onShare: (ULong) -> Unit = {},
|
||||||
|
onStopSharing: (ULong) -> Unit = {},
|
||||||
|
onDelete: (ULong) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val usesFloatingAction = usesMobilePresentation(LocalUiPlatform.current, windowClass)
|
val usesFloatingAction = usesMobilePresentation(LocalUiPlatform.current, windowClass)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -129,6 +144,9 @@ internal fun TransferCatalog(
|
|||||||
thumbnailBytes = transferThumbnails[transfer.transferId],
|
thumbnailBytes = transferThumbnails[transfer.transferId],
|
||||||
progress = progress,
|
progress = progress,
|
||||||
onClick = { onTransferSelected(transfer.transferId) },
|
onClick = { onTransferSelected(transfer.transferId) },
|
||||||
|
onShare = { onShare(transfer.transferId) },
|
||||||
|
onStopSharing = { onStopSharing(transfer.transferId) },
|
||||||
|
onDelete = { onDelete(transfer.transferId) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +210,9 @@ private fun TransferListItem(
|
|||||||
thumbnailBytes: ByteArray?,
|
thumbnailBytes: ByteArray?,
|
||||||
progress: TransferProgress?,
|
progress: TransferProgress?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onShare: () -> Unit,
|
||||||
|
onStopSharing: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = LocalVniDropColors.current
|
val colors = LocalVniDropColors.current
|
||||||
Surface(onClick = onClick, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), color = colors.backgroundSurface200) {
|
Surface(onClick = onClick, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), color = colors.backgroundSurface200) {
|
||||||
@@ -227,8 +248,68 @@ private fun TransferListItem(
|
|||||||
ProgressRow(label = progress.label, progress = progress.progress, detail = progress.detail)
|
ProgressRow(label = progress.label, progress = progress.progress, detail = progress.detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
TransferActionsMenu(transfer, onShare, onStopSharing, onDelete)
|
||||||
PlatformIcon(AppIcon.ChevronRight, contentDescription = null, tint = colors.foregroundLighter, modifier = Modifier.size(18.dp))
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TransferActionsMenu(
|
||||||
|
transfer: Transfer,
|
||||||
|
onShare: () -> Unit,
|
||||||
|
onStopSharing: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val moreActionsLabel = stringResource(Res.string.button_more_actions)
|
||||||
|
Box {
|
||||||
|
IconButton(
|
||||||
|
onClick = { expanded = true },
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
contentDescription = moreActionsLabel
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"⋮",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = LocalVniDropColors.current.foregroundLighter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
if (transfer.ticket != null) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(Res.string.transfer_share_title)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onShare()
|
||||||
|
},
|
||||||
|
leadingIcon = { PlatformIcon(AppIcon.Send, contentDescription = null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (transfer.status == TransferStatus.Sharing) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(Res.string.send_stop_sharing)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onStopSharing()
|
||||||
|
},
|
||||||
|
leadingIcon = { PlatformIcon(AppIcon.Close, contentDescription = null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(Res.string.button_delete_transfer)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onDelete()
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
PlatformIcon(
|
||||||
|
AppIcon.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LocalVniDropColors.current.destructiveDefault,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ fun SendRoute(
|
|||||||
onAccessPolicyChanged = viewModel::setAccessPolicy,
|
onAccessPolicyChanged = viewModel::setAccessPolicy,
|
||||||
onCreateShare = viewModel::createShare,
|
onCreateShare = viewModel::createShare,
|
||||||
onTransferSelected = viewModel::openTransfer,
|
onTransferSelected = viewModel::openTransfer,
|
||||||
|
onShareTransfer = { transferId ->
|
||||||
|
viewModel.openTransfer(transferId)
|
||||||
|
viewModel.openShare()
|
||||||
|
},
|
||||||
|
onStopSharing = viewModel::stopSharing,
|
||||||
onCloseTransferDetails = viewModel::closeTransferDetails,
|
onCloseTransferDetails = viewModel::closeTransferDetails,
|
||||||
onCopyTicket = viewModel::copyTicket,
|
onCopyTicket = viewModel::copyTicket,
|
||||||
onActivity = viewModel::openActivity,
|
onActivity = viewModel::openActivity,
|
||||||
@@ -53,7 +58,8 @@ fun SendRoute(
|
|||||||
onShare = viewModel::openShare,
|
onShare = viewModel::openShare,
|
||||||
onCloseDetailPanel = viewModel::closeDetailPanel,
|
onCloseDetailPanel = viewModel::closeDetailPanel,
|
||||||
onInvitationResult = viewModel::onInvitationResult,
|
onInvitationResult = viewModel::onInvitationResult,
|
||||||
onRequestDelete = viewModel::requestDeleteTransfer,
|
onRequestDelete = { viewModel.requestDeleteTransfer() },
|
||||||
|
onRequestDeleteTransfer = { viewModel.requestDeleteTransfer(it) },
|
||||||
onDismissDelete = viewModel::dismissDeleteTransfer,
|
onDismissDelete = viewModel::dismissDeleteTransfer,
|
||||||
onConfirmDelete = viewModel::confirmDeleteTransfer,
|
onConfirmDelete = viewModel::confirmDeleteTransfer,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ fun SendScreen(
|
|||||||
onAccessPolicyChanged: (ShareAccessPolicy) -> Unit,
|
onAccessPolicyChanged: (ShareAccessPolicy) -> Unit,
|
||||||
onCreateShare: () -> Unit,
|
onCreateShare: () -> Unit,
|
||||||
onTransferSelected: (ULong) -> Unit,
|
onTransferSelected: (ULong) -> Unit,
|
||||||
|
onShareTransfer: (ULong) -> Unit = {},
|
||||||
|
onStopSharing: (ULong) -> Unit = {},
|
||||||
onCloseTransferDetails: () -> Unit,
|
onCloseTransferDetails: () -> Unit,
|
||||||
onCopyTicket: (String) -> Unit,
|
onCopyTicket: (String) -> Unit,
|
||||||
onActivity: () -> Unit = {},
|
onActivity: () -> Unit = {},
|
||||||
@@ -41,11 +43,13 @@ fun SendScreen(
|
|||||||
onCloseDetailPanel: () -> Unit = {},
|
onCloseDetailPanel: () -> Unit = {},
|
||||||
onInvitationResult: (InvitationAction, Result<Unit>) -> Unit = { _, _ -> },
|
onInvitationResult: (InvitationAction, Result<Unit>) -> Unit = { _, _ -> },
|
||||||
onRequestDelete: () -> Unit = {},
|
onRequestDelete: () -> Unit = {},
|
||||||
|
onRequestDeleteTransfer: (ULong) -> Unit = {},
|
||||||
onDismissDelete: () -> Unit = {},
|
onDismissDelete: () -> Unit = {},
|
||||||
onConfirmDelete: () -> Unit = {},
|
onConfirmDelete: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val outgoingTransfers = coreState.transfers.filter { it.direction == TransferDirection.Send }
|
val outgoingTransfers = coreState.transfers.filter { it.direction == TransferDirection.Send }
|
||||||
val selectedTransfer = state.selectedTransferId?.let { id -> outgoingTransfers.firstOrNull { it.transferId == id } }
|
val selectedTransfer = state.selectedTransferId?.let { id -> outgoingTransfers.firstOrNull { it.transferId == id } }
|
||||||
|
val deleteTarget = state.deleteTargetTransferId?.let { id -> outgoingTransfers.firstOrNull { it.transferId == id } }
|
||||||
val qrCache = remember { mutableStateMapOf<String, ImageBitmap>() }
|
val qrCache = remember { mutableStateMapOf<String, ImageBitmap>() }
|
||||||
LaunchedEffect(outgoingTransfers.mapNotNull { it.ticket }) {
|
LaunchedEffect(outgoingTransfers.mapNotNull { it.ticket }) {
|
||||||
qrCache.keys.retainAll(outgoingTransfers.mapNotNull { it.ticket }.toSet())
|
qrCache.keys.retainAll(outgoingTransfers.mapNotNull { it.ticket }.toSet())
|
||||||
@@ -64,6 +68,7 @@ fun SendScreen(
|
|||||||
onActivity = onActivity,
|
onActivity = onActivity,
|
||||||
onReceivers = onReceivers,
|
onReceivers = onReceivers,
|
||||||
onShare = onShare,
|
onShare = onShare,
|
||||||
|
onStopSharing = { onStopSharing(selectedTransfer.transferId) },
|
||||||
onDelete = onRequestDelete,
|
onDelete = onRequestDelete,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -75,6 +80,9 @@ fun SendScreen(
|
|||||||
windowClass = windowClass,
|
windowClass = windowClass,
|
||||||
onOpenComposer = onOpenComposer,
|
onOpenComposer = onOpenComposer,
|
||||||
onTransferSelected = onTransferSelected,
|
onTransferSelected = onTransferSelected,
|
||||||
|
onShare = onShareTransfer,
|
||||||
|
onStopSharing = onStopSharing,
|
||||||
|
onDelete = onRequestDeleteTransfer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,10 +131,10 @@ fun SendScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTransfer != null && state.isDeleteConfirmationOpen) {
|
if (deleteTarget != null && state.isDeleteConfirmationOpen) {
|
||||||
AdaptiveDrawer(windowClass = windowClass, onDismissRequest = onDismissDelete) {
|
AdaptiveDrawer(windowClass = windowClass, onDismissRequest = onDismissDelete) {
|
||||||
DeleteTransferPanel(
|
DeleteTransferPanel(
|
||||||
transferName = selectedTransfer.transferName,
|
transferName = deleteTarget.transferName,
|
||||||
isDeleting = state.isDeleting,
|
isDeleting = state.isDeleting,
|
||||||
onCancel = onDismissDelete,
|
onCancel = onDismissDelete,
|
||||||
onConfirm = onConfirmDelete,
|
onConfirm = onConfirmDelete,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ data class SendState(
|
|||||||
val receiversByTransfer: Map<ULong, List<ReceiverRequestModel>> = emptyMap(),
|
val receiversByTransfer: Map<ULong, List<ReceiverRequestModel>> = emptyMap(),
|
||||||
val isLoadingReceivers: Boolean = false,
|
val isLoadingReceivers: Boolean = false,
|
||||||
val isDeleteConfirmationOpen: Boolean = false,
|
val isDeleteConfirmationOpen: Boolean = false,
|
||||||
|
val deleteTargetTransferId: ULong? = null,
|
||||||
val isDeleting: Boolean = false,
|
val isDeleting: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val selectedFile: PickedShareFile? get() = selectedFiles.singleOrNull()
|
val selectedFile: PickedShareFile? get() = selectedFiles.singleOrNull()
|
||||||
@@ -221,12 +222,19 @@ class SendViewModel(
|
|||||||
refreshReceivers(transferId)
|
refreshReceivers(transferId)
|
||||||
}
|
}
|
||||||
fun closeDetailPanel() = _state.update { it.copy(detailPanel = null) }
|
fun closeDetailPanel() = _state.update { it.copy(detailPanel = null) }
|
||||||
fun requestDeleteTransfer() = _state.update { it.copy(isDeleteConfirmationOpen = true) }
|
fun requestDeleteTransfer(transferId: ULong? = null) = _state.update {
|
||||||
|
it.copy(
|
||||||
|
isDeleteConfirmationOpen = true,
|
||||||
|
deleteTargetTransferId = transferId ?: it.selectedTransferId,
|
||||||
|
)
|
||||||
|
}
|
||||||
fun dismissDeleteTransfer() {
|
fun dismissDeleteTransfer() {
|
||||||
if (!_state.value.isDeleting) _state.update { it.copy(isDeleteConfirmationOpen = false) }
|
if (!_state.value.isDeleting) {
|
||||||
|
_state.update { it.copy(isDeleteConfirmationOpen = false, deleteTargetTransferId = null) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun confirmDeleteTransfer() {
|
fun confirmDeleteTransfer() {
|
||||||
val transferId = _state.value.selectedTransferId ?: return
|
val transferId = _state.value.deleteTargetTransferId ?: return
|
||||||
if (_state.value.isDeleting) return
|
if (_state.value.isDeleting) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isDeleting = true) }
|
_state.update { it.copy(isDeleting = true) }
|
||||||
@@ -239,6 +247,7 @@ class SendViewModel(
|
|||||||
detailPanel = null,
|
detailPanel = null,
|
||||||
receiverHistory = emptyList(),
|
receiverHistory = emptyList(),
|
||||||
isDeleteConfirmationOpen = false,
|
isDeleteConfirmationOpen = false,
|
||||||
|
deleteTargetTransferId = null,
|
||||||
isDeleting = false,
|
isDeleting = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -285,6 +294,7 @@ class SendViewModel(
|
|||||||
onSuccess = { share ->
|
onSuccess = { share ->
|
||||||
current.selectedFiles.firstNotNullOfOrNull { it.thumbnailBytes }
|
current.selectedFiles.firstNotNullOfOrNull { it.thumbnailBytes }
|
||||||
?.let { filePreviewRepository.save(share.transferId, it) }
|
?.let { filePreviewRepository.save(share.transferId, it) }
|
||||||
|
repository.refresh()
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isComposerOpen = false,
|
isComposerOpen = false,
|
||||||
@@ -292,8 +302,11 @@ class SendViewModel(
|
|||||||
transferName = "",
|
transferName = "",
|
||||||
accessPolicy = ShareAccessPolicy.RequireApproval,
|
accessPolicy = ShareAccessPolicy.RequireApproval,
|
||||||
isSharing = false,
|
isSharing = false,
|
||||||
|
selectedTransferId = share.transferId,
|
||||||
|
detailPanel = TransferDetailPanel.Share,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
refreshReceivers(share.transferId)
|
||||||
messages.show(UiMessage(UiText.Resource(Res.string.send_transfer_created), UiMessageTone.Success))
|
messages.show(UiMessage(UiText.Resource(Res.string.send_transfer_created), UiMessageTone.Success))
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
@@ -304,6 +317,15 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun stopSharing(transferId: ULong) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.cancel(transferId).fold(
|
||||||
|
onSuccess = { repository.refresh() },
|
||||||
|
onFailure = messages::error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun defaultTransferName(files: List<PickedShareFile>): String = when {
|
private fun defaultTransferName(files: List<PickedShareFile>): String = when {
|
||||||
files.isEmpty() -> ""
|
files.isEmpty() -> ""
|
||||||
files.size == 1 && files.first().isDirectory -> files.first().displayName
|
files.size == 1 && files.first().isDirectory -> files.first().displayName
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -174,22 +175,51 @@ private fun ReviewFileStep(
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (windowClass == WindowClass.Phone) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
ShareButton(state, coreInitialized, onCreateShare, Modifier.fillMaxWidth())
|
||||||
ShareButton(state, coreInitialized, onCreateShare, Modifier.fillMaxWidth())
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
QuietButton(stringResource(Res.string.button_change_files), onClick = onSelectFile, modifier = Modifier.fillMaxWidth(), enabled = !state.isSharing)
|
SourceButton(
|
||||||
QuietButton(stringResource(Res.string.button_choose_folder), onClick = onSelectFolder, modifier = Modifier.fillMaxWidth(), enabled = !state.isSharing)
|
text = stringResource(Res.string.button_change_files),
|
||||||
}
|
icon = AppIcon.File,
|
||||||
} else {
|
onClick = onSelectFile,
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
modifier = Modifier.weight(1f),
|
||||||
ShareButton(state, coreInitialized, onCreateShare)
|
enabled = !state.isSharing,
|
||||||
QuietButton(stringResource(Res.string.button_change_files), onClick = onSelectFile, enabled = !state.isSharing)
|
)
|
||||||
QuietButton(stringResource(Res.string.button_choose_folder), onClick = onSelectFolder, enabled = !state.isSharing)
|
SourceButton(
|
||||||
QuietButton(stringResource(Res.string.button_clear), onClick = onClearFile, enabled = !state.isSharing)
|
text = stringResource(Res.string.button_choose_folder),
|
||||||
|
icon = AppIcon.Folder,
|
||||||
|
onClick = onSelectFolder,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = !state.isSharing,
|
||||||
|
)
|
||||||
|
if (windowClass != WindowClass.Phone) {
|
||||||
|
SourceButton(
|
||||||
|
text = stringResource(Res.string.button_clear),
|
||||||
|
icon = AppIcon.Close,
|
||||||
|
onClick = onClearFile,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = !state.isSharing,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SourceButton(
|
||||||
|
text: String,
|
||||||
|
icon: AppIcon,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean,
|
||||||
|
) {
|
||||||
|
OutlinedButton(onClick = onClick, modifier = modifier, enabled = enabled) {
|
||||||
|
PlatformIcon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShareButton(state: SendState, coreInitialized: Boolean, onCreateShare: () -> Unit, modifier: Modifier = Modifier) {
|
private fun ShareButton(state: SendState, coreInitialized: Boolean, onCreateShare: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ internal fun TransferDetails(
|
|||||||
onActivity: () -> Unit,
|
onActivity: () -> Unit,
|
||||||
onReceivers: () -> Unit,
|
onReceivers: () -> Unit,
|
||||||
onShare: () -> Unit,
|
onShare: () -> Unit,
|
||||||
|
onStopSharing: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -100,8 +101,14 @@ internal fun TransferDetails(
|
|||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
IconButton(onClick = onDelete) {
|
if (transfer.status in setOf(TransferStatus.Importing, TransferStatus.Sharing)) {
|
||||||
PlatformIcon(AppIcon.Delete, stringResource(Res.string.button_delete_transfer), tint = LocalVniDropColors.current.destructiveDefault)
|
IconButton(onClick = onShare) {
|
||||||
|
PlatformIcon(
|
||||||
|
AppIcon.Send,
|
||||||
|
stringResource(Res.string.transfer_share_title),
|
||||||
|
tint = LocalVniDropColors.current.brandLink,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,32 +137,25 @@ internal fun TransferDetails(
|
|||||||
count = pendingReceivers + completedReceivers,
|
count = pendingReceivers + completedReceivers,
|
||||||
onClick = onReceivers,
|
onClick = onReceivers,
|
||||||
)
|
)
|
||||||
when (transfer.status) {
|
|
||||||
TransferStatus.Sharing -> {
|
|
||||||
HorizontalDivider(color = LocalVniDropColors.current.borderDefault)
|
|
||||||
DetailDestination(
|
|
||||||
title = stringResource(Res.string.transfer_share_title),
|
|
||||||
description = stringResource(Res.string.transfer_share_description),
|
|
||||||
onClick = onShare,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TransferStatus.Importing -> {
|
|
||||||
HorizontalDivider(color = LocalVniDropColors.current.borderDefault)
|
|
||||||
DetailDestination(
|
|
||||||
title = stringResource(Res.string.transfer_share_title),
|
|
||||||
description = stringResource(Res.string.transfer_event_preparing),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TransferStatus.Receiving,
|
|
||||||
TransferStatus.Done,
|
|
||||||
TransferStatus.Failed,
|
|
||||||
TransferStatus.Cancelled,
|
|
||||||
TransferStatus.Stopped,
|
|
||||||
-> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
if (transfer.status == TransferStatus.Sharing) {
|
||||||
|
DestructiveButton(
|
||||||
|
stringResource(Res.string.send_stop_sharing),
|
||||||
|
onClick = onStopSharing,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DestructiveButton(
|
||||||
|
stringResource(Res.string.button_delete_transfer),
|
||||||
|
onClick = onDelete,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,5 +42,7 @@ fun SettingsRoute(viewModel: SettingsViewModel, windowClass: WindowClass) {
|
|||||||
onSubmitBugReport = viewModel::submitBugReport,
|
onSubmitBugReport = viewModel::submitBugReport,
|
||||||
onDeleteAllTransfers = viewModel::deleteAllTransfers,
|
onDeleteAllTransfers = viewModel::deleteAllTransfers,
|
||||||
onClearTransferCache = viewModel::clearTransferCache,
|
onClearTransferCache = viewModel::clearTransferCache,
|
||||||
|
onFreeUpSpace = viewModel::freeUpSpace,
|
||||||
|
onRefreshStorage = viewModel::loadStorageUsage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ fun SettingsScreen(
|
|||||||
onSubmitBugReport: () -> Unit,
|
onSubmitBugReport: () -> Unit,
|
||||||
onDeleteAllTransfers: () -> Unit = {},
|
onDeleteAllTransfers: () -> Unit = {},
|
||||||
onClearTransferCache: () -> Unit = {},
|
onClearTransferCache: () -> Unit = {},
|
||||||
|
onFreeUpSpace: () -> Unit = {},
|
||||||
|
onRefreshStorage: () -> Unit = {},
|
||||||
onRelayModeChanged: (RelayMode) -> Unit = {},
|
onRelayModeChanged: (RelayMode) -> Unit = {},
|
||||||
onRelayUrlChanged: (Int, String) -> Unit = { _, _ -> },
|
onRelayUrlChanged: (Int, String) -> Unit = { _, _ -> },
|
||||||
onAddRelayUrl: () -> Unit = {},
|
onAddRelayUrl: () -> Unit = {},
|
||||||
@@ -69,6 +71,8 @@ fun SettingsScreen(
|
|||||||
onSubmitBugReport = onSubmitBugReport,
|
onSubmitBugReport = onSubmitBugReport,
|
||||||
onDeleteAllTransfers = onDeleteAllTransfers,
|
onDeleteAllTransfers = onDeleteAllTransfers,
|
||||||
onClearTransferCache = onClearTransferCache,
|
onClearTransferCache = onClearTransferCache,
|
||||||
|
onFreeUpSpace = onFreeUpSpace,
|
||||||
|
onRefreshStorage = onRefreshStorage,
|
||||||
onRelayModeChanged = onRelayModeChanged,
|
onRelayModeChanged = onRelayModeChanged,
|
||||||
onRelayUrlChanged = onRelayUrlChanged,
|
onRelayUrlChanged = onRelayUrlChanged,
|
||||||
onAddRelayUrl = onAddRelayUrl,
|
onAddRelayUrl = onAddRelayUrl,
|
||||||
@@ -110,6 +114,8 @@ fun SettingsScreen(
|
|||||||
onSubmitBugReport = onSubmitBugReport,
|
onSubmitBugReport = onSubmitBugReport,
|
||||||
onDeleteAllTransfers = onDeleteAllTransfers,
|
onDeleteAllTransfers = onDeleteAllTransfers,
|
||||||
onClearTransferCache = onClearTransferCache,
|
onClearTransferCache = onClearTransferCache,
|
||||||
|
onFreeUpSpace = onFreeUpSpace,
|
||||||
|
onRefreshStorage = onRefreshStorage,
|
||||||
onRelayModeChanged = onRelayModeChanged,
|
onRelayModeChanged = onRelayModeChanged,
|
||||||
onRelayUrlChanged = onRelayUrlChanged,
|
onRelayUrlChanged = onRelayUrlChanged,
|
||||||
onAddRelayUrl = onAddRelayUrl,
|
onAddRelayUrl = onAddRelayUrl,
|
||||||
@@ -143,6 +149,8 @@ private fun SettingsSectionContent(
|
|||||||
onSubmitBugReport: () -> Unit,
|
onSubmitBugReport: () -> Unit,
|
||||||
onDeleteAllTransfers: () -> Unit,
|
onDeleteAllTransfers: () -> Unit,
|
||||||
onClearTransferCache: () -> Unit,
|
onClearTransferCache: () -> Unit,
|
||||||
|
onFreeUpSpace: () -> Unit,
|
||||||
|
onRefreshStorage: () -> Unit,
|
||||||
onRelayModeChanged: (RelayMode) -> Unit,
|
onRelayModeChanged: (RelayMode) -> Unit,
|
||||||
onRelayUrlChanged: (Int, String) -> Unit,
|
onRelayUrlChanged: (Int, String) -> Unit,
|
||||||
onAddRelayUrl: () -> Unit,
|
onAddRelayUrl: () -> Unit,
|
||||||
@@ -169,6 +177,8 @@ private fun SettingsSectionContent(
|
|||||||
windowClass,
|
windowClass,
|
||||||
onDeleteAllTransfers,
|
onDeleteAllTransfers,
|
||||||
onClearTransferCache,
|
onClearTransferCache,
|
||||||
|
onFreeUpSpace,
|
||||||
|
onRefreshStorage,
|
||||||
onBack,
|
onBack,
|
||||||
showBack,
|
showBack,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.vnidrop.app.ui.feedback.UiMessage
|
|||||||
import com.vnidrop.app.ui.feedback.UiMessageController
|
import com.vnidrop.app.ui.feedback.UiMessageController
|
||||||
import com.vnidrop.app.ui.feedback.UiMessageTone
|
import com.vnidrop.app.ui.feedback.UiMessageTone
|
||||||
import com.vnidrop.app.ui.feedback.UiText
|
import com.vnidrop.app.ui.feedback.UiText
|
||||||
|
import com.vnidrop.app.ui.state.formatBytes
|
||||||
import com.vnidrop.app.ui.theme.ThemeMode
|
import com.vnidrop.app.ui.theme.ThemeMode
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -51,6 +52,8 @@ import vnidrop.shared.generated.resources.notifications_unsupported
|
|||||||
import vnidrop.shared.generated.resources.relay_settings_applied
|
import vnidrop.shared.generated.resources.relay_settings_applied
|
||||||
import vnidrop.shared.generated.resources.storage_transfer_cache_cleared
|
import vnidrop.shared.generated.resources.storage_transfer_cache_cleared
|
||||||
import vnidrop.shared.generated.resources.storage_transfers_deleted
|
import vnidrop.shared.generated.resources.storage_transfers_deleted
|
||||||
|
import vnidrop.shared.generated.resources.storage_cleanup_busy
|
||||||
|
import vnidrop.shared.generated.resources.storage_cleanup_freed
|
||||||
|
|
||||||
enum class SettingsSection {
|
enum class SettingsSection {
|
||||||
Overview,
|
Overview,
|
||||||
@@ -112,8 +115,10 @@ data class SettingsState(
|
|||||||
val bugLogPreviewBytes: Int = 0,
|
val bugLogPreviewBytes: Int = 0,
|
||||||
val storage: StorageBreakdown? = null,
|
val storage: StorageBreakdown? = null,
|
||||||
val isCalculatingStorage: Boolean = false,
|
val isCalculatingStorage: Boolean = false,
|
||||||
|
val storageLoadFailed: Boolean = false,
|
||||||
val isDeletingTransfers: Boolean = false,
|
val isDeletingTransfers: Boolean = false,
|
||||||
val isClearingTransferCache: Boolean = false,
|
val isClearingTransferCache: Boolean = false,
|
||||||
|
val isCleaningStorage: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val hasRelaySettingsChanges: Boolean
|
val hasRelaySettingsChanges: Boolean
|
||||||
get() = relayMode != savedRelaySettings.mode ||
|
get() = relayMode != savedRelaySettings.mode ||
|
||||||
@@ -188,6 +193,14 @@ class SettingsViewModel(
|
|||||||
endpointId = coreState.status?.endpointId,
|
endpointId = coreState.status?.endpointId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
coreState.isInitialized &&
|
||||||
|
_state.value.selectedSection == SettingsSection.Storage &&
|
||||||
|
_state.value.storage == null &&
|
||||||
|
_state.value.storageLoadFailed
|
||||||
|
) {
|
||||||
|
loadStorageUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshNotificationPermission()
|
refreshNotificationPermission()
|
||||||
@@ -209,7 +222,7 @@ class SettingsViewModel(
|
|||||||
fun loadStorageUsage() {
|
fun loadStorageUsage() {
|
||||||
if (_state.value.isCalculatingStorage) return
|
if (_state.value.isCalculatingStorage) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isCalculatingStorage = true) }
|
_state.update { it.copy(isCalculatingStorage = true, storageLoadFailed = false) }
|
||||||
try {
|
try {
|
||||||
val receiveFolder = _state.value.receiveFolder ?: fileSystemService.defaultReceiveFolder()
|
val receiveFolder = _state.value.receiveFolder ?: fileSystemService.defaultReceiveFolder()
|
||||||
val coreUsage = repository.storageUsage().getOrThrow()
|
val coreUsage = repository.storageUsage().getOrThrow()
|
||||||
@@ -227,12 +240,42 @@ class SettingsViewModel(
|
|||||||
inaccessibleReceivedFileCount = received.inaccessibleCount,
|
inaccessibleReceivedFileCount = received.inaccessibleCount,
|
||||||
),
|
),
|
||||||
isCalculatingStorage = false,
|
isCalculatingStorage = false,
|
||||||
|
storageLoadFailed = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error: CancellationException) {
|
} catch (error: CancellationException) {
|
||||||
throw error
|
throw error
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
_state.update { it.copy(isCalculatingStorage = false) }
|
_state.update { it.copy(isCalculatingStorage = false, storageLoadFailed = true) }
|
||||||
|
messages.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun freeUpSpace() {
|
||||||
|
val current = _state.value
|
||||||
|
if (current.isCleaningStorage) return
|
||||||
|
if (current.hasActiveNetworkWork) {
|
||||||
|
messages.tryShow(UiMessage(UiText.Resource(Res.string.storage_cleanup_busy), UiMessageTone.Warning))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isCleaningStorage = true) }
|
||||||
|
try {
|
||||||
|
val receiveFolder = _state.value.receiveFolder ?: fileSystemService.defaultReceiveFolder()
|
||||||
|
val reclaimed = fileSystemService.reclaimTemporaryStorage(environment.defaultCoreDataDir, receiveFolder)
|
||||||
|
_state.update { it.copy(isCleaningStorage = false) }
|
||||||
|
loadStorageUsage()
|
||||||
|
messages.tryShow(
|
||||||
|
UiMessage(
|
||||||
|
UiText.Resource(Res.string.storage_cleanup_freed, formatArgs = listOf(formatBytes(reclaimed))),
|
||||||
|
UiMessageTone.Success,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (error: CancellationException) {
|
||||||
|
throw error
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
_state.update { it.copy(isCleaningStorage = false) }
|
||||||
messages.error(error)
|
messages.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,16 @@ import vnidrop.shared.generated.resources.storage_calculating
|
|||||||
import vnidrop.shared.generated.resources.storage_clear_transfer_cache
|
import vnidrop.shared.generated.resources.storage_clear_transfer_cache
|
||||||
import vnidrop.shared.generated.resources.storage_clear_transfer_cache_description
|
import vnidrop.shared.generated.resources.storage_clear_transfer_cache_description
|
||||||
import vnidrop.shared.generated.resources.storage_clearing_transfer_cache
|
import vnidrop.shared.generated.resources.storage_clearing_transfer_cache
|
||||||
|
import vnidrop.shared.generated.resources.storage_cleaning
|
||||||
|
import vnidrop.shared.generated.resources.storage_delete_transfers_caption
|
||||||
import vnidrop.shared.generated.resources.storage_delete_transfers
|
import vnidrop.shared.generated.resources.storage_delete_transfers
|
||||||
import vnidrop.shared.generated.resources.storage_delete_transfers_description
|
import vnidrop.shared.generated.resources.storage_delete_transfers_description
|
||||||
import vnidrop.shared.generated.resources.storage_deleting
|
import vnidrop.shared.generated.resources.storage_deleting
|
||||||
|
import vnidrop.shared.generated.resources.storage_free_up_space
|
||||||
|
import vnidrop.shared.generated.resources.storage_free_up_space_caption
|
||||||
|
import vnidrop.shared.generated.resources.storage_refresh
|
||||||
|
import vnidrop.shared.generated.resources.storage_unavailable
|
||||||
|
import vnidrop.shared.generated.resources.storage_usage_header
|
||||||
import vnidrop.shared.generated.resources.storage_total
|
import vnidrop.shared.generated.resources.storage_total
|
||||||
import vnidrop.shared.generated.resources.storage_received_files
|
import vnidrop.shared.generated.resources.storage_received_files
|
||||||
import vnidrop.shared.generated.resources.storage_temporary
|
import vnidrop.shared.generated.resources.storage_temporary
|
||||||
@@ -52,6 +59,8 @@ internal fun StorageSettings(
|
|||||||
windowClass: WindowClass,
|
windowClass: WindowClass,
|
||||||
onDeleteAllTransfers: () -> Unit,
|
onDeleteAllTransfers: () -> Unit,
|
||||||
onClearTransferCache: () -> Unit,
|
onClearTransferCache: () -> Unit,
|
||||||
|
onFreeUpSpace: () -> Unit,
|
||||||
|
onRefreshStorage: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
showBack: Boolean,
|
showBack: Boolean,
|
||||||
) {
|
) {
|
||||||
@@ -59,8 +68,30 @@ internal fun StorageSettings(
|
|||||||
var showClearCacheConfirmation by rememberSaveable { mutableStateOf(false) }
|
var showClearCacheConfirmation by rememberSaveable { mutableStateOf(false) }
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
SettingsTopBar(stringResource(Res.string.storage_title), onBack, showBack)
|
SettingsTopBar(stringResource(Res.string.storage_title), onBack, showBack)
|
||||||
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.storage_usage_header),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
SecondaryButton(
|
||||||
|
stringResource(Res.string.storage_refresh),
|
||||||
|
onClick = onRefreshStorage,
|
||||||
|
enabled = !state.isCalculatingStorage &&
|
||||||
|
!state.isCleaningStorage &&
|
||||||
|
!state.isDeletingTransfers &&
|
||||||
|
!state.isClearingTransferCache,
|
||||||
|
)
|
||||||
|
}
|
||||||
val storage = state.storage
|
val storage = state.storage
|
||||||
if (storage == null || state.isCalculatingStorage) {
|
if (storage == null && state.storageLoadFailed && !state.isCalculatingStorage) {
|
||||||
|
SecondaryButton(
|
||||||
|
stringResource(Res.string.storage_unavailable),
|
||||||
|
onClick = onRefreshStorage,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
} else if (storage == null || state.isCalculatingStorage) {
|
||||||
SettingsGroup {
|
SettingsGroup {
|
||||||
StorageRow(
|
StorageRow(
|
||||||
title = stringResource(Res.string.storage_calculating),
|
title = stringResource(Res.string.storage_calculating),
|
||||||
@@ -84,6 +115,23 @@ internal fun StorageSettings(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(
|
||||||
|
if (state.isCleaningStorage) Res.string.storage_cleaning else Res.string.storage_free_up_space,
|
||||||
|
),
|
||||||
|
onClick = onFreeUpSpace,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !state.isCleaningStorage &&
|
||||||
|
!state.isDeletingTransfers &&
|
||||||
|
!state.isClearingTransferCache &&
|
||||||
|
!state.isCalculatingStorage &&
|
||||||
|
!state.hasActiveNetworkWork,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.storage_free_up_space_caption),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalVniDropColors.current.foregroundLighter,
|
||||||
|
)
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
if (state.isClearingTransferCache) {
|
if (state.isClearingTransferCache) {
|
||||||
@@ -107,6 +155,11 @@ internal fun StorageSettings(
|
|||||||
) {
|
) {
|
||||||
Text(stringResource(if (state.isDeletingTransfers) Res.string.storage_deleting else Res.string.storage_delete_transfers))
|
Text(stringResource(if (state.isDeletingTransfers) Res.string.storage_deleting else Res.string.storage_delete_transfers))
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.storage_delete_transfers_caption),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalVniDropColors.current.foregroundLighter,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(Res.string.storage_footer),
|
stringResource(Res.string.storage_footer),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.vnidrop.app.notifications
|
||||||
|
|
||||||
|
import com.vnidrop.app.core.CoreGateway
|
||||||
|
import com.vnidrop.app.core.CoreSignal
|
||||||
|
import com.vnidrop.app.core.ReceiverDeliveryStatus
|
||||||
|
import com.vnidrop.app.core.ReceiverRequestModel
|
||||||
|
import com.vnidrop.app.core.Transfer
|
||||||
|
import com.vnidrop.app.core.TransferDirection
|
||||||
|
import com.vnidrop.app.core.TransferStatus
|
||||||
|
import com.vnidrop.app.platform.AppVisibility
|
||||||
|
import com.vnidrop.app.preferences.PreferencesRepository
|
||||||
|
import com.vnidrop.app.ui.feedback.UiMessageController
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.compose.resources.getString
|
||||||
|
import vnidrop.shared.generated.resources.Res
|
||||||
|
import vnidrop.shared.generated.resources.approval_nearby_device
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receive_completed_body
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receive_completed_title
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receive_failed_body
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receive_failed_title
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receiver_completed_body
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receiver_completed_title
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receiver_failed_body
|
||||||
|
import vnidrop.shared.generated.resources.notifications_receiver_failed_title
|
||||||
|
import vnidrop.shared.generated.resources.notifications_send_failed_body
|
||||||
|
import vnidrop.shared.generated.resources.notifications_send_failed_title
|
||||||
|
import vnidrop.shared.generated.resources.receive_unknown_transfer
|
||||||
|
|
||||||
|
internal enum class TransferNotificationKind {
|
||||||
|
SendFailed,
|
||||||
|
ReceiveCompleted,
|
||||||
|
ReceiveFailed,
|
||||||
|
ReceiverCompleted,
|
||||||
|
ReceiverFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class PlannedTransferNotification(
|
||||||
|
val id: String,
|
||||||
|
val kind: TransferNotificationKind,
|
||||||
|
val transferName: String?,
|
||||||
|
val receiver: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun plannedTransferNotifications(
|
||||||
|
transfers: List<Transfer>,
|
||||||
|
published: Set<String>,
|
||||||
|
): List<PlannedTransferNotification> = transfers.mapNotNull { transfer ->
|
||||||
|
val kind = when {
|
||||||
|
transfer.direction == TransferDirection.Send && transfer.status == TransferStatus.Failed ->
|
||||||
|
TransferNotificationKind.SendFailed
|
||||||
|
transfer.direction == TransferDirection.Receive && transfer.status == TransferStatus.Done ->
|
||||||
|
TransferNotificationKind.ReceiveCompleted
|
||||||
|
transfer.direction == TransferDirection.Receive && transfer.status == TransferStatus.Failed ->
|
||||||
|
TransferNotificationKind.ReceiveFailed
|
||||||
|
else -> return@mapNotNull null
|
||||||
|
}
|
||||||
|
val id = "${kind.idPrefix}-${transfer.transferId}"
|
||||||
|
PlannedTransferNotification(id, kind, transfer.transferName).takeUnless { id in published }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun plannedReceiverNotifications(
|
||||||
|
requests: List<ReceiverRequestModel>,
|
||||||
|
published: Set<String>,
|
||||||
|
): List<PlannedTransferNotification> = requests.mapNotNull { request ->
|
||||||
|
val kind = when (request.status) {
|
||||||
|
ReceiverDeliveryStatus.Completed -> TransferNotificationKind.ReceiverCompleted
|
||||||
|
ReceiverDeliveryStatus.Failed -> TransferNotificationKind.ReceiverFailed
|
||||||
|
else -> return@mapNotNull null
|
||||||
|
}
|
||||||
|
val id = "${kind.idPrefix}-${request.id}"
|
||||||
|
PlannedTransferNotification(
|
||||||
|
id = id,
|
||||||
|
kind = kind,
|
||||||
|
transferName = request.transferName,
|
||||||
|
receiver = request.receiverName ?: request.receiverDeviceName,
|
||||||
|
).takeUnless { id in published }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransferNotificationCoordinator(
|
||||||
|
private val repository: CoreGateway,
|
||||||
|
private val preferencesRepository: PreferencesRepository,
|
||||||
|
private val notifications: LocalNotificationService,
|
||||||
|
private val visibility: AppVisibility,
|
||||||
|
private val messages: UiMessageController,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
private val published = mutableSetOf<String>()
|
||||||
|
private var transfersPrimed = false
|
||||||
|
private var notificationsEnabled = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
preferencesRepository.preferences.collectLatest { preferences ->
|
||||||
|
notificationsEnabled = preferences.notificationsEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
repository.state.collect { core ->
|
||||||
|
if (core.isInitialized) syncTransfers(core.transfers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
repository.signals.collect { signal ->
|
||||||
|
when (signal) {
|
||||||
|
is CoreSignal.ReceiverHistoryChanged -> syncReceivers(signal.transferId)
|
||||||
|
is CoreSignal.TransfersChanged -> syncReceivers(signal.transferId)
|
||||||
|
is CoreSignal.ApprovalChanged -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncTransfers(transfers: List<Transfer>) {
|
||||||
|
val planned = plannedTransferNotifications(transfers, published)
|
||||||
|
if (!transfersPrimed) {
|
||||||
|
transfersPrimed = true
|
||||||
|
published += planned.map(PlannedTransferNotification::id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
planned.forEach { deliver(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncReceivers(transferId: ULong) {
|
||||||
|
val isOutgoing = repository.state.value.transfers.any {
|
||||||
|
it.transferId == transferId && it.direction == TransferDirection.Send
|
||||||
|
}
|
||||||
|
if (!isOutgoing) return
|
||||||
|
repository.receiverRequests(transferId).fold(
|
||||||
|
onSuccess = { requests ->
|
||||||
|
plannedReceiverNotifications(requests, published).forEach { deliver(it) }
|
||||||
|
},
|
||||||
|
onFailure = messages::error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deliver(plan: PlannedTransferNotification) {
|
||||||
|
published += plan.id
|
||||||
|
if (
|
||||||
|
!notificationsEnabled ||
|
||||||
|
visibility.isForeground.value ||
|
||||||
|
notifications.permission.value != NotificationPermission.Granted
|
||||||
|
) return
|
||||||
|
val transferName = plan.transferName ?: getString(Res.string.receive_unknown_transfer)
|
||||||
|
val notification = when (plan.kind) {
|
||||||
|
TransferNotificationKind.SendFailed -> LocalNotification(
|
||||||
|
plan.id,
|
||||||
|
getString(Res.string.notifications_send_failed_title),
|
||||||
|
getString(Res.string.notifications_send_failed_body, transferName),
|
||||||
|
)
|
||||||
|
TransferNotificationKind.ReceiveCompleted -> LocalNotification(
|
||||||
|
plan.id,
|
||||||
|
getString(Res.string.notifications_receive_completed_title),
|
||||||
|
getString(Res.string.notifications_receive_completed_body, transferName),
|
||||||
|
)
|
||||||
|
TransferNotificationKind.ReceiveFailed -> LocalNotification(
|
||||||
|
plan.id,
|
||||||
|
getString(Res.string.notifications_receive_failed_title),
|
||||||
|
getString(Res.string.notifications_receive_failed_body, transferName),
|
||||||
|
)
|
||||||
|
TransferNotificationKind.ReceiverCompleted -> {
|
||||||
|
val receiver = plan.receiver ?: getString(Res.string.approval_nearby_device)
|
||||||
|
LocalNotification(
|
||||||
|
plan.id,
|
||||||
|
getString(Res.string.notifications_receiver_completed_title),
|
||||||
|
getString(Res.string.notifications_receiver_completed_body, receiver, transferName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TransferNotificationKind.ReceiverFailed -> {
|
||||||
|
val receiver = plan.receiver ?: getString(Res.string.approval_nearby_device)
|
||||||
|
LocalNotification(
|
||||||
|
plan.id,
|
||||||
|
getString(Res.string.notifications_receiver_failed_title),
|
||||||
|
getString(Res.string.notifications_receiver_failed_body, receiver, transferName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifications.publish(notification).onFailure(messages::error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TransferNotificationKind.idPrefix: String
|
||||||
|
get() = when (this) {
|
||||||
|
TransferNotificationKind.SendFailed -> "send-failed"
|
||||||
|
TransferNotificationKind.ReceiveCompleted -> "receive-completed"
|
||||||
|
TransferNotificationKind.ReceiveFailed -> "receive-failed"
|
||||||
|
TransferNotificationKind.ReceiverCompleted -> "receiver-completed"
|
||||||
|
TransferNotificationKind.ReceiverFailed -> "receiver-failed"
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||||||
import org.jetbrains.compose.resources.StringResource
|
import org.jetbrains.compose.resources.StringResource
|
||||||
|
|
||||||
sealed interface UiText {
|
sealed interface UiText {
|
||||||
data class Resource(val resource: StringResource) : UiText
|
data class Resource(val resource: StringResource, val formatArgs: List<Any> = emptyList()) : UiText
|
||||||
data class Dynamic(val value: String) : UiText
|
data class Dynamic(val value: String) : UiText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,5 +138,5 @@ private fun DismissButton(onClick: () -> Unit) {
|
|||||||
|
|
||||||
private suspend fun UiText.resolve(): String = when (this) {
|
private suspend fun UiText.resolve(): String = when (this) {
|
||||||
is UiText.Dynamic -> value
|
is UiText.Dynamic -> value
|
||||||
is UiText.Resource -> getString(resource)
|
is UiText.Resource -> getString(resource, *formatArgs.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.vnidrop.app.PlatformEnvironment
|
|||||||
import com.vnidrop.app.core.CoreState
|
import com.vnidrop.app.core.CoreState
|
||||||
import com.vnidrop.app.core.CoreStatus
|
import com.vnidrop.app.core.CoreStatus
|
||||||
import com.vnidrop.app.core.CoreSignal
|
import com.vnidrop.app.core.CoreSignal
|
||||||
|
import com.vnidrop.app.core.CoreStorageUsageModel
|
||||||
import com.vnidrop.app.core.PickedShareFile
|
import com.vnidrop.app.core.PickedShareFile
|
||||||
import com.vnidrop.app.core.ReceiveFolder
|
import com.vnidrop.app.core.ReceiveFolder
|
||||||
import com.vnidrop.app.core.ReceiveFolderKind
|
import com.vnidrop.app.core.ReceiveFolderKind
|
||||||
@@ -26,6 +27,7 @@ import com.vnidrop.app.feature.app.AppViewModel
|
|||||||
import com.vnidrop.app.feature.receive.ReceiveHistoryDeleteTarget
|
import com.vnidrop.app.feature.receive.ReceiveHistoryDeleteTarget
|
||||||
import com.vnidrop.app.feature.receive.ReceiveViewModel
|
import com.vnidrop.app.feature.receive.ReceiveViewModel
|
||||||
import com.vnidrop.app.feature.send.SendViewModel
|
import com.vnidrop.app.feature.send.SendViewModel
|
||||||
|
import com.vnidrop.app.feature.send.TransferDetailPanel
|
||||||
import com.vnidrop.app.feature.settings.SettingsSection
|
import com.vnidrop.app.feature.settings.SettingsSection
|
||||||
import com.vnidrop.app.feature.settings.RelaySettingsApplyError
|
import com.vnidrop.app.feature.settings.RelaySettingsApplyError
|
||||||
import com.vnidrop.app.feature.settings.RelaySettingsInputError
|
import com.vnidrop.app.feature.settings.RelaySettingsInputError
|
||||||
@@ -168,6 +170,43 @@ class ViewModelsTest {
|
|||||||
assertEquals(0, core.clearTransferCacheCount)
|
assertEquals(0, core.clearTransferCacheCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsFreesOnlyPlatformOwnedTemporaryStorage() = runTest {
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
|
val fileSystem = FakeFileSystemService(folder).apply {
|
||||||
|
reclaimedTemporaryBytes = 12_000UL
|
||||||
|
}
|
||||||
|
val viewModel = settingsViewModel(fileSystem = fileSystem)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.freeUpSpace()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(1, fileSystem.reclaimTemporaryStorageCount)
|
||||||
|
assertFalse(viewModel.state.value.isCleaningStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsRetriesStorageAfterCoreFinishesStarting() = runTest {
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
|
val core = FakeCoreGateway().apply {
|
||||||
|
storageUsageResult = Result.failure(IllegalStateException("not initialized"))
|
||||||
|
}
|
||||||
|
val viewModel = settingsViewModel(repository = core)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.selectSection(SettingsSection.Storage)
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertTrue(viewModel.state.value.storageLoadFailed)
|
||||||
|
|
||||||
|
core.storageUsageResult = Result.success(CoreStorageUsageModel(25UL, 10UL, 5UL, 0UL, 0UL))
|
||||||
|
core.mutableState.value = core.mutableState.value.copy(isInitialized = true)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertFalse(viewModel.state.value.storageLoadFailed)
|
||||||
|
assertEquals(25UL, viewModel.state.value.storage?.transferCacheBytes)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun settingsDeleteAllTransfersImmediatelyClearsUnusedCache() = runTest {
|
fun settingsDeleteAllTransfersImmediatelyClearsUnusedCache() = runTest {
|
||||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
@@ -533,10 +572,33 @@ class ViewModelsTest {
|
|||||||
assertEquals(null, viewModel.state.value.selectedFile)
|
assertEquals(null, viewModel.state.value.selectedFile)
|
||||||
assertEquals(ShareAccessPolicy.AnyoneWithTransfer, core.lastShareAccessPolicy)
|
assertEquals(ShareAccessPolicy.AnyoneWithTransfer, core.lastShareAccessPolicy)
|
||||||
assertEquals(7UL, core.state.value.transfers.first().transferId)
|
assertEquals(7UL, core.state.value.transfers.first().transferId)
|
||||||
|
assertEquals(7UL, viewModel.state.value.selectedTransferId)
|
||||||
|
assertEquals(TransferDetailPanel.Share, viewModel.state.value.detailPanel)
|
||||||
assertContentEquals(thumbnail, previews.previews.value.getValue(7UL))
|
assertContentEquals(thumbnail, previews.previews.value.getValue(7UL))
|
||||||
assertEquals(listOf(selected), fileSystem.discardedPickedFiles)
|
assertEquals(listOf(selected), fileSystem.discardedPickedFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendViewModelStopsSharingFromCatalogAction() = runTest {
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
|
val core = FakeCoreGateway().apply {
|
||||||
|
mutableState.value = CoreState(isInitialized = true, transfers = listOf(sentTransfer(7UL)))
|
||||||
|
}
|
||||||
|
val viewModel = SendViewModel(
|
||||||
|
core,
|
||||||
|
FakeFileSystemService(folder),
|
||||||
|
preferences(),
|
||||||
|
FakeFilePreviewRepository(),
|
||||||
|
UiMessageController(),
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.stopSharing(7UL)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(listOf(7UL), core.cancelledTransfers)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sendComposerStaysOpenWhenShareCreationFails() = runTest {
|
fun sendComposerStaysOpenWhenShareCreationFails() = runTest {
|
||||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.vnidrop.app.notifications
|
||||||
|
|
||||||
|
import com.vnidrop.app.core.ReceiverDeliveryStatus
|
||||||
|
import com.vnidrop.app.core.ReceiverRequestModel
|
||||||
|
import com.vnidrop.app.core.ShareAccessPolicy
|
||||||
|
import com.vnidrop.app.core.Transfer
|
||||||
|
import com.vnidrop.app.core.TransferDirection
|
||||||
|
import com.vnidrop.app.core.TransferStatus
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class TransferNotificationCoordinatorTest {
|
||||||
|
@Test
|
||||||
|
fun plansOnlyNewTerminalTransferOutcomes() {
|
||||||
|
val transfers = listOf(
|
||||||
|
transfer(1UL, TransferDirection.Receive, TransferStatus.Done),
|
||||||
|
transfer(2UL, TransferDirection.Receive, TransferStatus.Failed),
|
||||||
|
transfer(3UL, TransferDirection.Send, TransferStatus.Failed),
|
||||||
|
transfer(4UL, TransferDirection.Send, TransferStatus.Sharing),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("receive-completed-1", "send-failed-3"),
|
||||||
|
plannedTransferNotifications(transfers, setOf("receive-failed-2")).map { it.id },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun plansCompletedAndFailedReceiverOutcomesOnce() {
|
||||||
|
val requests = listOf(
|
||||||
|
request("completed", ReceiverDeliveryStatus.Completed),
|
||||||
|
request("failed", ReceiverDeliveryStatus.Failed),
|
||||||
|
request("accepted", ReceiverDeliveryStatus.Accepted),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("receiver-failed-failed"),
|
||||||
|
plannedReceiverNotifications(requests, setOf("receiver-completed-completed")).map { it.id },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transfer(id: ULong, direction: TransferDirection, status: TransferStatus) = Transfer(
|
||||||
|
localId = "local-$id",
|
||||||
|
transferId = id,
|
||||||
|
direction = direction,
|
||||||
|
status = status,
|
||||||
|
peerId = null,
|
||||||
|
transferName = "Transfer $id",
|
||||||
|
contentHash = null,
|
||||||
|
fileCount = 1UL,
|
||||||
|
totalSize = 10UL,
|
||||||
|
ticket = null,
|
||||||
|
accessPolicy = ShareAccessPolicy.RequireApproval,
|
||||||
|
createdAt = 1L,
|
||||||
|
updatedAt = 1L,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun request(id: String, status: ReceiverDeliveryStatus) = ReceiverRequestModel(
|
||||||
|
id = id,
|
||||||
|
transferId = 1UL,
|
||||||
|
remoteEndpointId = "peer-$id",
|
||||||
|
transferName = "Transfer",
|
||||||
|
receiverName = "Receiver",
|
||||||
|
receiverDeviceName = null,
|
||||||
|
appVersion = "1.0",
|
||||||
|
status = status,
|
||||||
|
reason = null,
|
||||||
|
requestedAt = 1L,
|
||||||
|
respondedAt = 2L,
|
||||||
|
completedAt = 3L,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@ class FakeCoreGateway : CoreGateway {
|
|||||||
private var receiveGate: CompletableDeferred<Unit>? = null
|
private var receiveGate: CompletableDeferred<Unit>? = null
|
||||||
var deleteResult: Result<Unit> = Result.success(Unit)
|
var deleteResult: Result<Unit> = Result.success(Unit)
|
||||||
var clearTransferCacheResult: Result<ULong> = Result.success(0UL)
|
var clearTransferCacheResult: Result<ULong> = Result.success(0UL)
|
||||||
|
var storageUsageResult: Result<CoreStorageUsageModel> = Result.success(
|
||||||
|
CoreStorageUsageModel(0UL, 0UL, 0UL, 0UL, 0UL),
|
||||||
|
)
|
||||||
var clearReceiveHistoryResult: Result<ULong> = Result.success(0UL)
|
var clearReceiveHistoryResult: Result<ULong> = Result.success(0UL)
|
||||||
val deletedTransfers = mutableListOf<ULong>()
|
val deletedTransfers = mutableListOf<ULong>()
|
||||||
val cancelledTransfers = mutableListOf<ULong>()
|
val cancelledTransfers = mutableListOf<ULong>()
|
||||||
@@ -153,9 +156,7 @@ class FakeCoreGateway : CoreGateway {
|
|||||||
awaitReceiveIfNeeded()
|
awaitReceiveIfNeeded()
|
||||||
return receiveResult
|
return receiveResult
|
||||||
}
|
}
|
||||||
override suspend fun storageUsage(): Result<CoreStorageUsageModel> = Result.success(
|
override suspend fun storageUsage(): Result<CoreStorageUsageModel> = storageUsageResult
|
||||||
CoreStorageUsageModel(0UL, 0UL, 0UL, 0UL, 0UL),
|
|
||||||
)
|
|
||||||
override suspend fun clearTransferCache(): Result<ULong> {
|
override suspend fun clearTransferCache(): Result<ULong> {
|
||||||
clearTransferCacheCount += 1
|
clearTransferCacheCount += 1
|
||||||
return clearTransferCacheResult
|
return clearTransferCacheResult
|
||||||
@@ -248,6 +249,8 @@ class FakeFileSystemService(
|
|||||||
var supportsCustomFolders = true
|
var supportsCustomFolders = true
|
||||||
var effectiveFolder: ReceiveFolder? = null
|
var effectiveFolder: ReceiveFolder? = null
|
||||||
var canRevealFolder = false
|
var canRevealFolder = false
|
||||||
|
var reclaimedTemporaryBytes = 0UL
|
||||||
|
var reclaimTemporaryStorageCount = 0
|
||||||
var revealFolderResult: Result<Unit> = Result.success(Unit)
|
var revealFolderResult: Result<Unit> = Result.success(Unit)
|
||||||
val revealedFolders = mutableListOf<ReceiveFolder>()
|
val revealedFolders = mutableListOf<ReceiveFolder>()
|
||||||
val discardedPickedFiles = mutableListOf<PickedShareFile>()
|
val discardedPickedFiles = mutableListOf<PickedShareFile>()
|
||||||
@@ -259,6 +262,10 @@ class FakeFileSystemService(
|
|||||||
override suspend fun inspectReceivedArtifacts(artifacts: List<ReceivedArtifactModel>) =
|
override suspend fun inspectReceivedArtifacts(artifacts: List<ReceivedArtifactModel>) =
|
||||||
ReceivedStorageInspection(artifacts.fold(0UL) { total, item -> total + item.logicalSize }, artifacts.size, 0, 0)
|
ReceivedStorageInspection(artifacts.fold(0UL) { total, item -> total + item.logicalSize }, artifacts.size, 0, 0)
|
||||||
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = 0UL
|
override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = 0UL
|
||||||
|
override suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong {
|
||||||
|
reclaimTemporaryStorageCount += 1
|
||||||
|
return reclaimedTemporaryBytes
|
||||||
|
}
|
||||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null
|
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null
|
||||||
override fun canRevealReceiveFolder(folder: ReceiveFolder) = canRevealFolder
|
override fun canRevealReceiveFolder(folder: ReceiveFolder) = canRevealFolder
|
||||||
override suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> {
|
override suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ private class JvmFileSystemService : FileSystemService {
|
|||||||
return desktopTemporaryUsage(receiveFolder)
|
return desktopTemporaryUsage(receiveFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong =
|
||||||
|
desktopReclaimTemporaryStorage(appDataDir, receiveFolder)
|
||||||
|
|
||||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null
|
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null
|
||||||
|
|
||||||
override suspend fun sharePickedFiles(
|
override suspend fun sharePickedFiles(
|
||||||
@@ -88,3 +91,35 @@ internal fun desktopTemporaryUsage(receiveFolder: ReceiveFolder): ULong {
|
|||||||
}
|
}
|
||||||
}.getOrDefault(0UL)
|
}.getOrDefault(0UL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun desktopReclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong {
|
||||||
|
var reclaimed = 0UL
|
||||||
|
if (receiveFolder.kind == ReceiveFolderKind.FileSystemPath) {
|
||||||
|
val receiveRoot = File(receiveFolder.value)
|
||||||
|
receiveRoot.walkTopDown()
|
||||||
|
.filter { file ->
|
||||||
|
file.isFile &&
|
||||||
|
file.name.startsWith(".") &&
|
||||||
|
file.name.contains(".vnidrop-") &&
|
||||||
|
file.name.endsWith(".part")
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.forEach { file ->
|
||||||
|
val size = file.length().coerceAtLeast(0L).toULong()
|
||||||
|
if (file.delete()) reclaimed += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val appDataRoot = File(appDataDir)
|
||||||
|
if (appDataRoot.isDirectory) {
|
||||||
|
appDataRoot.walkTopDown()
|
||||||
|
.filter { it.isDirectory && it.name == ".Trash" }
|
||||||
|
.toList()
|
||||||
|
.forEach { trash ->
|
||||||
|
val size = trash.walkTopDown()
|
||||||
|
.filter(File::isFile)
|
||||||
|
.fold(0UL) { total, file -> total + file.length().coerceAtLeast(0L).toULong() }
|
||||||
|
if (trash.deleteRecursively()) reclaimed += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reclaimed
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import kotlin.io.path.createDirectories
|
|||||||
import kotlin.io.path.createTempDirectory
|
import kotlin.io.path.createTempDirectory
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class FileSystemServiceTest {
|
class FileSystemServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -26,4 +28,32 @@ class FileSystemServiceTest {
|
|||||||
root.toFile().deleteRecursively()
|
root.toFile().deleteRecursively()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun desktopReclaimTemporaryStorageKeepsUserFiles() {
|
||||||
|
val root = createTempDirectory("vnidrop-storage-cleanup")
|
||||||
|
try {
|
||||||
|
val receive = root.resolve("receive").createDirectories()
|
||||||
|
val appData = root.resolve("app-data").createDirectories()
|
||||||
|
val partial = receive.resolve(".photo.jpg.vnidrop-test.part")
|
||||||
|
val received = receive.resolve("photo.jpg")
|
||||||
|
val trash = appData.resolve("nested/.Trash").createDirectories()
|
||||||
|
Files.write(partial, ByteArray(7))
|
||||||
|
Files.write(received, ByteArray(13))
|
||||||
|
Files.write(trash.resolve("stale.bin"), ByteArray(11))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
18UL,
|
||||||
|
desktopReclaimTemporaryStorage(
|
||||||
|
appData.toString(),
|
||||||
|
ReceiveFolder(ReceiveFolderKind.FileSystemPath, receive.toString(), "Test"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertFalse(Files.exists(partial))
|
||||||
|
assertFalse(Files.exists(trash))
|
||||||
|
assertTrue(Files.exists(received))
|
||||||
|
} finally {
|
||||||
|
root.toFile().deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class FoundationComposeTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onNodeWithText("Notifications").performClick()
|
onNodeWithText("Notifications").performClick()
|
||||||
onNodeWithText("Get notified about new receive requests while VniDrop is in the background.").assertIsDisplayed()
|
onNodeWithText("Get notified about transfer activity while VniDrop is in the background.").assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -212,10 +212,11 @@ class FoundationComposeTest {
|
|||||||
runOnIdle { assertTrue(cacheClearRequested) }
|
runOnIdle { assertTrue(cacheClearRequested) }
|
||||||
|
|
||||||
onNodeWithText("Delete all transfers").performClick()
|
onNodeWithText("Delete all transfers").performClick()
|
||||||
onNodeWithText(
|
onNodeWithText(
|
||||||
"This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. " +
|
"This clears all sent and received transfer records from your history. Your received files are not deleted. " +
|
||||||
"Ongoing transfers and received files are not deleted. This can’t be undone.",
|
"Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. " +
|
||||||
).assertIsDisplayed()
|
"This can’t be undone.",
|
||||||
|
).assertIsDisplayed()
|
||||||
runOnIdle { assertFalse(deleteRequested) }
|
runOnIdle { assertFalse(deleteRequested) }
|
||||||
|
|
||||||
onNodeWithTag("confirm-delete-all-transfers").performClick()
|
onNodeWithTag("confirm-delete-all-transfers").performClick()
|
||||||
@@ -636,10 +637,13 @@ class FoundationComposeTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onNodeWithText("Share").assertIsDisplayed()
|
onNodeWithContentDescription("Share").assertIsDisplayed()
|
||||||
onAllNodesWithText("Scan with VniDrop to receive this transfer").assertCountEquals(0)
|
onAllNodesWithText("Scan with VniDrop to receive this transfer").assertCountEquals(0)
|
||||||
onNode(hasText("Share") and hasClickAction()).performClick()
|
onNodeWithContentDescription("Share").performClick()
|
||||||
runOnIdle { assertEquals(com.vnidrop.app.feature.send.TransferDetailPanel.Share, state.value.detailPanel) }
|
runOnIdle { assertEquals(com.vnidrop.app.feature.send.TransferDetailPanel.Share, state.value.detailPanel) }
|
||||||
|
waitUntil(timeoutMillis = 5_000) {
|
||||||
|
onAllNodesWithText("Scan with VniDrop to receive this transfer").fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
onNodeWithText("Scan with VniDrop to receive this transfer").assertIsDisplayed()
|
onNodeWithText("Scan with VniDrop to receive this transfer").assertIsDisplayed()
|
||||||
onNodeWithText("Save .vnd file").assertIsDisplayed()
|
onNodeWithText("Save .vnd file").assertIsDisplayed()
|
||||||
onNodeWithContentDescription("Close").assertIsDisplayed()
|
onNodeWithContentDescription("Close").assertIsDisplayed()
|
||||||
|
|||||||
Reference in New Issue
Block a user