diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift
index a6a3f3c..6ccdb96 100644
--- a/apple/VniDrop/Features/Settings/SettingsModel.swift
+++ b/apple/VniDrop/Features/Settings/SettingsModel.swift
@@ -56,6 +56,7 @@ struct SettingsState: Equatable {
var storage: StorageBreakdown?
var isCalculatingStorage = false
var isDeletingTransfers = false
+ var isCleaningStorage = false
static func == (lhs: SettingsState, rhs: SettingsState) -> Bool {
lhs.selectedSection == rhs.selectedSection && lhs.username == rhs.username
@@ -72,6 +73,7 @@ struct SettingsState: Equatable {
&& lhs.bugLogPreviewBytes == rhs.bugLogPreviewBytes
&& lhs.storage == rhs.storage && lhs.isCalculatingStorage == rhs.isCalculatingStorage
&& lhs.isDeletingTransfers == rhs.isDeletingTransfers
+ && lhs.isCleaningStorage == rhs.isCleaningStorage
&& lhs.deviceInfo?.operatingSystem == rhs.deviceInfo?.operatingSystem
}
}
@@ -315,6 +317,83 @@ final class SettingsModel: ObservableObject {
}
}
+ /// Reclaims disk space the core's transfer deletion doesn't touch: the app's
+ /// temporary directory (leftover picker/staging copies) and any stray `.Trash`
+ /// folders that accumulate inside app-owned directories. Never touches received
+ /// files, the core database, or user-chosen receive folders.
+ func freeUpSpace() {
+ if state.isCleaningStorage { return }
+ // Purging staging while a transfer is mid-flight could break it.
+ let hasActive = repository.state.transfers.contains {
+ $0.status == .sharing || $0.status == .importing || $0.status == .receiving
+ }
+ if hasActive {
+ messages.tryShow(UiMessage(text: .resource(L10n.Storage.cleanupBusy), tone: .warning))
+ return
+ }
+ state.isCleaningStorage = true
+ let tempDir = NSTemporaryDirectory()
+ let dataDir = environment.defaultCoreDataDir
+ // Only clean the receive folder's trash when it is app-owned (iOS fixed
+ // Documents), never a user-chosen macOS folder like ~/Downloads.
+ let receiveTrashRoot = fileSystemService.supportsCustomReceiveFolders ? nil : state.receiveFolder?.value
+ Task {
+ let freed = await Task.detached {
+ SettingsModel.reclaimJunk(tempDir: tempDir, dataDir: dataDir, receiveTrashRoot: receiveTrashRoot)
+ }.value
+ state.isCleaningStorage = false
+ loadStorageUsage()
+ messages.show(UiMessage(
+ text: .dynamic(L10n.Storage.cleanupFreed(size: formatBytes(freed))),
+ tone: .success
+ ))
+ }
+ }
+
+ /// Deletes temp-directory contents and `.Trash` folders under the given roots,
+ /// returning the number of bytes reclaimed. Runs off the main actor.
+ nonisolated static func reclaimJunk(tempDir: String, dataDir: String, receiveTrashRoot: String?) -> UInt64 {
+ let fm = FileManager.default
+ var freed: UInt64 = 0
+ // Empty the temporary directory.
+ if let entries = try? fm.contentsOfDirectory(atPath: tempDir) {
+ for name in entries {
+ let path = (tempDir as NSString).appendingPathComponent(name)
+ freed += itemSize(path)
+ try? fm.removeItem(atPath: path)
+ }
+ }
+ // Remove stray `.Trash` folders inside app-owned directories.
+ for root in [dataDir, receiveTrashRoot].compactMap({ $0 }) {
+ for trash in trashDirectories(under: root) {
+ freed += directorySize(trash)
+ try? fm.removeItem(atPath: trash)
+ }
+ }
+ return freed
+ }
+
+ /// Paths of every directory named `.Trash` under `root` (not descending into them).
+ private nonisolated static func trashDirectories(under root: String) -> [String] {
+ let url = URL(fileURLWithPath: root, isDirectory: true)
+ guard let enumerator = FileManager.default.enumerator(
+ at: url, includingPropertiesForKeys: [.isDirectoryKey]
+ ) else { return [] }
+ var result: [String] = []
+ for case let fileURL as URL in enumerator where fileURL.lastPathComponent == ".Trash" {
+ result.append(fileURL.path)
+ enumerator.skipDescendants()
+ }
+ return result
+ }
+
+ /// Allocated size of a file or directory (0 if missing).
+ private nonisolated static func itemSize(_ path: String) -> UInt64 {
+ var isDirectory: ObjCBool = false
+ guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { return 0 }
+ return isDirectory.boolValue ? directorySize(path) : fileSize(path)
+ }
+
nonisolated static func fileSize(_ path: String) -> UInt64 {
let values = try? URL(fileURLWithPath: path).resourceValues(
forKeys: [.isRegularFileKey, .totalFileAllocatedSizeKey, .fileSizeKey]
diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift
index 8457dd4..5b639b0 100644
--- a/apple/VniDrop/Features/Settings/SettingsSections.swift
+++ b/apple/VniDrop/Features/Settings/SettingsSections.swift
@@ -99,6 +99,23 @@ struct StorageSettings: View {
Text(String(localized: L10n.Storage.footer))
}
+ 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()
+ }
+ }
+ }
+ .disabled(model.state.isCleaningStorage)
+ }
+
Section {
Button(role: .destructive) {
showDeleteConfirmation = true
diff --git a/localization/strings.json b/localization/strings.json
index 02cf6c7..f8bed6c 100644
--- a/localization/strings.json
+++ b/localization/strings.json
@@ -3140,6 +3140,68 @@
"ru": "Вычисление…"
}
},
+ "storage_cleaning": {
+ "context": "Settings > Storage: free-up-space button while cleanup runs.",
+ "translations": {
+ "en": "Cleaning up…",
+ "fr": "Nettoyage…",
+ "es": "Limpiando…",
+ "it": "Pulizia…",
+ "de": "Wird bereinigt…",
+ "pt": "A limpar…",
+ "pl": "Czyszczenie…",
+ "nl": "Opschonen…",
+ "ru": "Очистка…"
+ }
+ },
+ "storage_cleanup_busy": {
+ "context": "Settings > Storage: shown when cleanup is blocked by in-flight transfers.",
+ "translations": {
+ "en": "Finish active transfers before freeing up space",
+ "fr": "Terminez les transferts en cours avant de libérer de l'espace",
+ "es": "Finaliza las transferencias activas antes de liberar espacio",
+ "it": "Completa i trasferimenti attivi prima di liberare spazio",
+ "de": "Beende aktive Übertragungen, bevor du Speicher freigibst",
+ "pt": "Conclui as transferências ativas antes de libertar espaço",
+ "pl": "Zakończ aktywne transfery przed zwolnieniem miejsca",
+ "nl": "Voltooi actieve overdrachten voordat je ruimte vrijmaakt",
+ "ru": "Завершите активные передачи перед освобождением места"
+ }
+ },
+ "storage_cleanup_freed": {
+ "context": "Settings > Storage: cleanup success. {size} = amount freed.",
+ "args": [
+ {
+ "name": "size",
+ "type": "string"
+ }
+ ],
+ "translations": {
+ "en": "Freed {size}",
+ "fr": "{size} libéré",
+ "es": "Se liberó {size}",
+ "it": "Liberati {size}",
+ "de": "{size} freigegeben",
+ "pt": "Libertado {size}",
+ "pl": "Zwolniono {size}",
+ "nl": "{size} vrijgemaakt",
+ "ru": "Освобождено {size}"
+ }
+ },
+ "storage_free_up_space": {
+ "context": "Settings > Storage: button that clears temporary files and stray trash.",
+ "translations": {
+ "en": "Free up space",
+ "fr": "Libérer de l'espace",
+ "es": "Liberar espacio",
+ "it": "Libera spazio",
+ "de": "Speicher freigeben",
+ "pt": "Libertar espaço",
+ "pl": "Zwolnij miejsce",
+ "nl": "Ruimte vrijmaken",
+ "ru": "Освободить место"
+ }
+ },
"storage_delete_transfers": {
"context": "Settings > Storage: button to delete all transfer records.",
"translations": {
diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml
index da1a6ad..b85e8d5 100644
--- a/shared/src/commonMain/composeResources/values-de/strings.xml
+++ b/shared/src/commonMain/composeResources/values-de/strings.xml
@@ -203,6 +203,10 @@
Wird empfangen
Beendet
Wird berechnet…
+ Wird bereinigt…
+ Beende aktive Übertragungen, bevor du Speicher freigibst
+ %1$s freigegeben
+ 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.
App-Daten
diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml
index b7ff17e..a1dcb0e 100644
--- a/shared/src/commonMain/composeResources/values-es/strings.xml
+++ b/shared/src/commonMain/composeResources/values-es/strings.xml
@@ -203,6 +203,10 @@
Recibiendo
Detenido
Calculando…
+ Limpiando…
+ Finaliza las transferencias activas antes de liberar espacio
+ Se liberó %1$s
+ 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.
Datos de la aplicación
diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml
index 126bb38..a49c270 100644
--- a/shared/src/commonMain/composeResources/values-fr/strings.xml
+++ b/shared/src/commonMain/composeResources/values-fr/strings.xml
@@ -203,6 +203,10 @@
Réception
Arrêté
Calcul…
+ Nettoyage…
+ Terminez les transferts en cours avant de libérer de l\'espace
+ %1$s libéré
+ 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.
Données de l’app
diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml
index 10f1aad..bb26acb 100644
--- a/shared/src/commonMain/composeResources/values-it/strings.xml
+++ b/shared/src/commonMain/composeResources/values-it/strings.xml
@@ -203,6 +203,10 @@
Ricezione
Interrotto
Calcolo…
+ Pulizia…
+ Completa i trasferimenti attivi prima di liberare spazio
+ Liberati %1$s
+ 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.
Dati dell’app
diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml
index b281934..60d3791 100644
--- a/shared/src/commonMain/composeResources/values-nl/strings.xml
+++ b/shared/src/commonMain/composeResources/values-nl/strings.xml
@@ -203,6 +203,10 @@
Ontvangen
Gestopt
Berekenen…
+ Opschonen…
+ Voltooi actieve overdrachten voordat je ruimte vrijmaakt
+ %1$s vrijgemaakt
+ 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.
Appgegevens
diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml
index e19c520..9de29f4 100644
--- a/shared/src/commonMain/composeResources/values-pl/strings.xml
+++ b/shared/src/commonMain/composeResources/values-pl/strings.xml
@@ -203,6 +203,10 @@
Odbieranie
Zatrzymany
Obliczanie…
+ Czyszczenie…
+ Zakończ aktywne transfery przed zwolnieniem miejsca
+ Zwolniono %1$s
+ 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ąć.
Dane aplikacji
diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml
index 1233976..b6d0c35 100644
--- a/shared/src/commonMain/composeResources/values-pt/strings.xml
+++ b/shared/src/commonMain/composeResources/values-pt/strings.xml
@@ -203,6 +203,10 @@
A receber
Parada
A calcular…
+ A limpar…
+ Conclui as transferências ativas antes de libertar espaço
+ Libertado %1$s
+ 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.
Dados da aplicação
diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml
index 513bb0c..f8894e5 100644
--- a/shared/src/commonMain/composeResources/values-ru/strings.xml
+++ b/shared/src/commonMain/composeResources/values-ru/strings.xml
@@ -203,6 +203,10 @@
Получение
Остановлено
Вычисление…
+ Очистка…
+ Завершите активные передачи перед освобождением места
+ Освобождено %1$s
+ Освободить место
Удалить все передачи
Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить.
Данные приложения
diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml
index b4da6ac..4daed58 100644
--- a/shared/src/commonMain/composeResources/values/strings.xml
+++ b/shared/src/commonMain/composeResources/values/strings.xml
@@ -203,6 +203,10 @@
Receiving
Stopped
Calculating…
+ Cleaning up…
+ Finish active transfers before freeing up space
+ Freed %1$s
+ 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.
App data