diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift
index 6ccdb96..af29633 100644
--- a/apple/VniDrop/Features/Settings/SettingsModel.swift
+++ b/apple/VniDrop/Features/Settings/SettingsModel.swift
@@ -55,6 +55,7 @@ struct SettingsState: Equatable {
var bugLogPreviewBytes = 0
var storage: StorageBreakdown?
var isCalculatingStorage = false
+ var storageLoadFailed = false
var isDeletingTransfers = false
var isCleaningStorage = false
@@ -72,6 +73,7 @@ struct SettingsState: Equatable {
&& lhs.bugIncludeLogs == rhs.bugIncludeLogs && lhs.isSubmittingBugReport == rhs.isSubmittingBugReport
&& lhs.bugLogPreviewBytes == rhs.bugLogPreviewBytes
&& lhs.storage == rhs.storage && lhs.isCalculatingStorage == rhs.isCalculatingStorage
+ && lhs.storageLoadFailed == rhs.storageLoadFailed
&& lhs.isDeletingTransfers == rhs.isDeletingTransfers
&& lhs.isCleaningStorage == rhs.isCleaningStorage
&& lhs.deviceInfo?.operatingSystem == rhs.deviceInfo?.operatingSystem
@@ -265,17 +267,28 @@ final class SettingsModel: ObservableObject {
// MARK: - Storage
- /// Recomputes the on-disk usage breakdown off the main actor.
+ /// Recomputes the on-disk usage breakdown off the main actor. Safe to call
+ /// before the core is ready: it keeps the spinner up and waits for the core to
+ /// finish initializing (it starts asynchronously at launch) rather than bailing.
func loadStorageUsage() {
if state.isCalculatingStorage { return }
state.isCalculatingStorage = true
+ state.storageLoadFailed = false
let tempDir = NSTemporaryDirectory()
Task {
+ // The core initializes asynchronously at launch; poll briefly so opening
+ // Storage early doesn't leave the summary stuck.
+ var attempts = 0
+ while !repository.state.isInitialized && attempts < 100 {
+ try? await Task.sleep(nanoseconds: 100_000_000)
+ attempts += 1
+ }
let coreResult = await repository.storageUsage()
let artifactsResult = await repository.receivedArtifacts()
guard case .success(let core) = coreResult,
case .success(let artifacts) = artifactsResult else {
state.isCalculatingStorage = false
+ state.storageLoadFailed = true
return
}
let diskSizes = await Task.detached {
diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift
index 5b639b0..603b632 100644
--- a/apple/VniDrop/Features/Settings/SettingsSections.swift
+++ b/apple/VniDrop/Features/Settings/SettingsSections.swift
@@ -78,61 +78,71 @@ struct StorageSettings: View {
@ObservedObject var model: SettingsModel
@State private var showDeleteConfirmation = false
+ private var isBusy: Bool {
+ model.state.isCalculatingStorage || model.state.isCleaningStorage || model.state.isDeletingTransfers
+ }
+
var body: some View {
Section {
- if let storage = model.state.storage {
- LabeledContent(String(localized: L10n.Storage.receivedFiles), value: formatBytes(storage.receivedFiles))
- LabeledContent(String(localized: L10n.Storage.transferData), value: formatBytes(storage.transferCache))
- LabeledContent(String(localized: L10n.Storage.appData), value: formatBytes(storage.appData))
- LabeledContent(String(localized: L10n.Storage.temporary), value: formatBytes(storage.temporary))
- LabeledContent(String(localized: L10n.Storage.total)) {
- Text(formatBytes(storage.total)).fontWeight(.semibold)
- }
- } else {
- HStack {
- Text(String(localized: L10n.Storage.calculating)).foregroundStyle(.secondary)
- Spacer()
- ProgressView()
+ usageContent
+ } header: {
+ HStack {
+ Text(String(localized: L10n.Storage.usageHeader))
+ Spacer()
+ if model.state.isCalculatingStorage {
+ ProgressView().controlSize(.small)
+ } else {
+ Button(action: model.loadStorageUsage) {
+ Label(String(localized: L10n.Storage.refresh), systemSymbol: .arrowClockwise)
+ .labelStyle(.iconOnly)
+ }
+ .buttonStyle(.borderless)
+ .disabled(isBusy)
+ .help(String(localized: L10n.Storage.refresh))
}
}
} footer: {
Text(String(localized: L10n.Storage.footer))
}
+ // Reclaim reversible junk (temp + trash) — non-destructive to history.
Section {
- Button {
- model.freeUpSpace()
- } label: {
- HStack {
- Text(model.state.isCleaningStorage
- ? String(localized: L10n.Storage.cleaning)
- : String(localized: L10n.Storage.freeUpSpace))
- if model.state.isCleaningStorage {
- Spacer()
- ProgressView()
- }
- }
+ Button(action: model.freeUpSpace) {
+ actionLabel(
+ title: L10n.Storage.freeUpSpace,
+ busyTitle: L10n.Storage.cleaning,
+ isBusy: model.state.isCleaningStorage,
+ symbol: .sparkles,
+ tint: .accentColor
+ )
}
- .disabled(model.state.isCleaningStorage)
+ // `.plain` so pressing the row dims the label instead of flipping it to
+ // the white selection-highlight that the default form button style uses.
+ .buttonStyle(.plain)
+ .disabled(isBusy)
+ } footer: {
+ Text(String(localized: L10n.Storage.freeUpSpaceCaption))
}
+ // Destructive: clears transfer history + cached share content.
Section {
- Button(role: .destructive) {
+ Button {
showDeleteConfirmation = true
} label: {
- HStack {
- Text(model.state.isDeletingTransfers
- ? String(localized: L10n.Storage.deleting)
- : String(localized: L10n.Storage.deleteTransfers))
- if model.state.isDeletingTransfers {
- Spacer()
- ProgressView()
- }
- }
+ actionLabel(
+ title: L10n.Storage.deleteTransfers,
+ busyTitle: L10n.Storage.deleting,
+ isBusy: model.state.isDeletingTransfers,
+ symbol: .trash,
+ tint: .red
+ )
}
- .disabled(model.state.isDeletingTransfers)
+ .buttonStyle(.plain)
+ .disabled(isBusy)
+ } footer: {
+ Text(String(localized: L10n.Storage.deleteTransfersCaption))
}
- .onAppear { model.loadStorageUsage() }
+ .task { model.loadStorageUsage() }
.confirmationDialog(
Text(String(localized: L10n.Storage.deleteTransfers)),
isPresented: $showDeleteConfirmation,
@@ -146,6 +156,53 @@ struct StorageSettings: View {
Text(String(localized: L10n.Storage.deleteTransfersDescription))
}
}
+
+ @ViewBuilder
+ private var usageContent: some View {
+ if let storage = model.state.storage {
+ LabeledContent(String(localized: L10n.Storage.receivedFiles), value: formatBytes(storage.receivedFiles))
+ LabeledContent(String(localized: L10n.Storage.transferData), value: formatBytes(storage.transferCache))
+ LabeledContent(String(localized: L10n.Storage.appData), value: formatBytes(storage.appData))
+ LabeledContent(String(localized: L10n.Storage.temporary), value: formatBytes(storage.temporary))
+ LabeledContent(String(localized: L10n.Storage.total)) {
+ Text(formatBytes(storage.total)).fontWeight(.semibold)
+ }
+ } else if model.state.storageLoadFailed {
+ // Genuine failure (core reported an error) — offer a retry.
+ Button(action: model.loadStorageUsage) {
+ Label(String(localized: L10n.Storage.unavailable), systemSymbol: .arrowClockwise)
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.plain)
+ } else {
+ // Loading, or waiting for the core to finish starting.
+ HStack {
+ Text(String(localized: L10n.Storage.calculating)).foregroundStyle(.secondary)
+ Spacer()
+ ProgressView().controlSize(.small)
+ }
+ }
+ }
+
+ /// A tinted, full-width button label with a leading symbol and a trailing
+ /// spinner while busy. `.contentShape` keeps the whole row tappable.
+ private func actionLabel(
+ title: String.LocalizationValue,
+ busyTitle: String.LocalizationValue,
+ isBusy: Bool,
+ symbol: SFSymbol,
+ tint: Color
+ ) -> some View {
+ HStack {
+ Label(String(localized: isBusy ? busyTitle : title), systemSymbol: symbol)
+ Spacer()
+ if isBusy {
+ ProgressView().controlSize(.small)
+ }
+ }
+ .foregroundStyle(tint)
+ .contentShape(Rectangle())
+ }
}
struct AboutSettings: View {
diff --git a/localization/strings.json b/localization/strings.json
index f8bed6c..4c826e4 100644
--- a/localization/strings.json
+++ b/localization/strings.json
@@ -3188,6 +3188,76 @@
"ru": "Освобождено {size}"
}
},
+ "storage_delete_transfers_caption": {
+ "context": "Settings > Storage: caption under the destructive delete-all button.",
+ "translations": {
+ "en": "Clears your send and receive history and the app's cached share content. Received files on disk are kept.",
+ "fr": "Efface votre historique d'envois et de réceptions ainsi que le contenu de partage mis en cache par l'app. Les fichiers reçus sur le disque sont conservés.",
+ "es": "Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan.",
+ "it": "Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall'app. I file ricevuti sul disco vengono mantenuti.",
+ "de": "Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten.",
+ "pt": "Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos.",
+ "pl": "Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane.",
+ "nl": "Wist je verzend- en ontvangstgeschiedenis en de gecachte deelinhoud van de app. Ontvangen bestanden op schijf blijven behouden.",
+ "ru": "Очищает историю отправки и получения и кэшированное содержимое общих ресурсов. Полученные файлы на диске сохраняются."
+ }
+ },
+ "storage_free_up_space_caption": {
+ "context": "Settings > Storage: caption under the free-up-space button.",
+ "translations": {
+ "en": "Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept.",
+ "fr": "Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés.",
+ "es": "Elimina los archivos temporales y los restos de transferencias anteriores. Tus transferencias y archivos recibidos se conservan.",
+ "it": "Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti.",
+ "de": "Entfernt temporäre Dateien und Reste früherer Übertragungen. Deine Übertragungen und empfangenen Dateien bleiben erhalten.",
+ "pt": "Remove ficheiros temporários e resíduos de transferências anteriores. As tuas transferências e ficheiros recebidos são mantidos.",
+ "pl": "Usuwa pliki tymczasowe i pozostałości po wcześniejszych transferach. Twoje transfery i odebrane pliki zostają zachowane.",
+ "nl": "Verwijdert tijdelijke bestanden en resten van eerdere overdrachten. Je overdrachten en ontvangen bestanden blijven behouden.",
+ "ru": "Удаляет временные файлы и остатки прошлых передач. Ваши передачи и полученные файлы сохраняются."
+ }
+ },
+ "storage_refresh": {
+ "context": "Settings > Storage: label for the button that recalculates usage.",
+ "translations": {
+ "en": "Refresh",
+ "fr": "Actualiser",
+ "es": "Actualizar",
+ "it": "Aggiorna",
+ "de": "Aktualisieren",
+ "pt": "Atualizar",
+ "pl": "Odśwież",
+ "nl": "Vernieuwen",
+ "ru": "Обновить"
+ }
+ },
+ "storage_unavailable": {
+ "context": "Settings > Storage: shown when usage couldn't be calculated yet.",
+ "translations": {
+ "en": "Storage usage isn't available yet",
+ "fr": "L'utilisation du stockage n'est pas encore disponible",
+ "es": "El uso de almacenamiento aún no está disponible",
+ "it": "L'utilizzo dello spazio non è ancora disponibile",
+ "de": "Die Speichernutzung ist noch nicht verfügbar",
+ "pt": "A utilização do armazenamento ainda não está disponível",
+ "pl": "Wykorzystanie pamięci nie jest jeszcze dostępne",
+ "nl": "Opslaggebruik is nog niet beschikbaar",
+ "ru": "Данные об использовании хранилища пока недоступны"
+ }
+ },
+ "storage_usage_header": {
+ "context": "Settings > Storage: header above the usage breakdown.",
+ "translations": {
+ "en": "On this device",
+ "fr": "Sur cet appareil",
+ "es": "En este dispositivo",
+ "it": "Su questo dispositivo",
+ "de": "Auf diesem Gerät",
+ "pt": "Neste dispositivo",
+ "pl": "Na tym urządzeniu",
+ "nl": "Op dit apparaat",
+ "ru": "На этом устройстве"
+ }
+ },
"storage_free_up_space": {
"context": "Settings > Storage: button that clears temporary files and stray trash.",
"translations": {
diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml
index b85e8d5..32660c6 100644
--- a/shared/src/commonMain/composeResources/values-de/strings.xml
+++ b/shared/src/commonMain/composeResources/values-de/strings.xml
@@ -206,6 +206,11 @@
Wird bereinigt…
Beende aktive Übertragungen, bevor du Speicher freigibst
%1$s freigegeben
+ Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten.
+ Entfernt temporäre Dateien und Reste früherer Übertragungen. Deine Übertragungen und empfangenen Dateien bleiben erhalten.
+ Aktualisieren
+ Die Speichernutzung ist noch nicht verfügbar
+ Auf diesem Gerät
Speicher freigeben
Alle Übertragungen löschen
Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden.
diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml
index a1dcb0e..551f8e1 100644
--- a/shared/src/commonMain/composeResources/values-es/strings.xml
+++ b/shared/src/commonMain/composeResources/values-es/strings.xml
@@ -206,6 +206,11 @@
Limpiando…
Finaliza las transferencias activas antes de liberar espacio
Se liberó %1$s
+ Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan.
+ Elimina los archivos temporales y los restos de transferencias anteriores. Tus transferencias y archivos recibidos se conservan.
+ Actualizar
+ El uso de almacenamiento aún no está disponible
+ En este dispositivo
Liberar espacio
Eliminar todas las transferencias
Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer.
diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml
index a49c270..0e86280 100644
--- a/shared/src/commonMain/composeResources/values-fr/strings.xml
+++ b/shared/src/commonMain/composeResources/values-fr/strings.xml
@@ -206,6 +206,11 @@
Nettoyage…
Terminez les transferts en cours avant de libérer de l\'espace
%1$s libéré
+ Efface votre historique d\'envois et de réceptions ainsi que le contenu de partage mis en cache par l\'app. Les fichiers reçus sur le disque sont conservés.
+ Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés.
+ Actualiser
+ L\'utilisation du stockage n\'est pas encore disponible
+ Sur cet appareil
Libérer de l\'espace
Supprimer tous les transferts
Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible.
diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml
index bb26acb..4394347 100644
--- a/shared/src/commonMain/composeResources/values-it/strings.xml
+++ b/shared/src/commonMain/composeResources/values-it/strings.xml
@@ -206,6 +206,11 @@
Pulizia…
Completa i trasferimenti attivi prima di liberare spazio
Liberati %1$s
+ Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall\'app. I file ricevuti sul disco vengono mantenuti.
+ Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti.
+ Aggiorna
+ L\'utilizzo dello spazio non è ancora disponibile
+ Su questo dispositivo
Libera spazio
Elimina tutti i trasferimenti
Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata.
diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml
index 60d3791..58930d7 100644
--- a/shared/src/commonMain/composeResources/values-nl/strings.xml
+++ b/shared/src/commonMain/composeResources/values-nl/strings.xml
@@ -206,6 +206,11 @@
Opschonen…
Voltooi actieve overdrachten voordat je ruimte vrijmaakt
%1$s vrijgemaakt
+ Wist je verzend- en ontvangstgeschiedenis en de gecachte deelinhoud van de app. Ontvangen bestanden op schijf blijven behouden.
+ Verwijdert tijdelijke bestanden en resten van eerdere overdrachten. Je overdrachten en ontvangen bestanden blijven behouden.
+ Vernieuwen
+ Opslaggebruik is nog niet beschikbaar
+ Op dit apparaat
Ruimte vrijmaken
Alle overdrachten verwijderen
Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt.
diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml
index 9de29f4..391b3e4 100644
--- a/shared/src/commonMain/composeResources/values-pl/strings.xml
+++ b/shared/src/commonMain/composeResources/values-pl/strings.xml
@@ -206,6 +206,11 @@
Czyszczenie…
Zakończ aktywne transfery przed zwolnieniem miejsca
Zwolniono %1$s
+ Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane.
+ Usuwa pliki tymczasowe i pozostałości po wcześniejszych transferach. Twoje transfery i odebrane pliki zostają zachowane.
+ Odśwież
+ Wykorzystanie pamięci nie jest jeszcze dostępne
+ Na tym urządzeniu
Zwolnij miejsce
Usuń wszystkie transfery
Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć.
diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml
index b6d0c35..54a990d 100644
--- a/shared/src/commonMain/composeResources/values-pt/strings.xml
+++ b/shared/src/commonMain/composeResources/values-pt/strings.xml
@@ -206,6 +206,11 @@
A limpar…
Conclui as transferências ativas antes de libertar espaço
Libertado %1$s
+ Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos.
+ Remove ficheiros temporários e resíduos de transferências anteriores. As tuas transferências e ficheiros recebidos são mantidos.
+ Atualizar
+ A utilização do armazenamento ainda não está disponível
+ Neste dispositivo
Libertar espaço
Eliminar todas as transferências
Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada.
diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml
index f8894e5..8c9ed33 100644
--- a/shared/src/commonMain/composeResources/values-ru/strings.xml
+++ b/shared/src/commonMain/composeResources/values-ru/strings.xml
@@ -206,6 +206,11 @@
Очистка…
Завершите активные передачи перед освобождением места
Освобождено %1$s
+ Очищает историю отправки и получения и кэшированное содержимое общих ресурсов. Полученные файлы на диске сохраняются.
+ Удаляет временные файлы и остатки прошлых передач. Ваши передачи и полученные файлы сохраняются.
+ Обновить
+ Данные об использовании хранилища пока недоступны
+ На этом устройстве
Освободить место
Удалить все передачи
Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить.
diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml
index 4daed58..4dc463a 100644
--- a/shared/src/commonMain/composeResources/values/strings.xml
+++ b/shared/src/commonMain/composeResources/values/strings.xml
@@ -206,6 +206,11 @@
Cleaning up…
Finish active transfers before freeing up space
Freed %1$s
+ Clears your send and receive history and the app\'s cached share content. Received files on disk are kept.
+ Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept.
+ Refresh
+ Storage usage isn\'t available yet
+ On this device
Free up space
Delete all transfers
This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone.