Add orphaned file management features (#231)
Some checks failed
Build and Release / Build IPA (push) Has been cancelled
Build and Release / Build Mac Catalyst (push) Has been cancelled

This commit is contained in:
realdoomsboygaming 2025-07-26 06:55:25 -05:00 committed by GitHub
parent 067c485d64
commit b3933f0da4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 504 additions and 33 deletions

View file

@ -490,3 +490,12 @@
"Import Backup" = "استيراد النسخة الاحتياطية";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "تنبيه: هذه الميزة لا تزال تجريبية. يرجى التحقق من بياناتك بعد التصدير/الاستيراد.";
"Backup" = "نسخة احتياطية";
"Total Orphaned File Size" = "إجمالي حجم الملفات اليتيمة";
"Total Orphaned Files" = "إجمالي عدد الملفات اليتيمة";
"Orphaned Downloads" = "التنزيلات اليتيمة";
"No orphaned files found." = "لم يتم العثور على ملفات يتيمة.";
"Delete All Orphaned Files" = "حذف جميع الملفات اليتيمة";
"Delete Selected Files?" = "حذف الملفات المحددة؟";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "هل أنت متأكد أنك تريد حذف الملفات اليتيمة المحددة؟ لا يمكن التراجع عن هذا الإجراء.";

View file

@ -508,3 +508,12 @@ Za metapodatke epizode, odnosi se na sličicu i naslov epizode, jer ponekad mogu
"Import Backup" = "Uvezi sigurnosnu kopiju";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Napomena: Ova funkcija je još uvijek eksperimentalna. Molimo provjerite svoje podatke nakon izvoza/uvoza.";
"Backup" = "Sigurnosna kopija";
"Total Orphaned File Size" = "Ukupna veličina siročadnih datoteka";
"Total Orphaned Files" = "Ukupan broj siročadnih datoteka";
"Orphaned Downloads" = "Siročadne preuzimanja";
"No orphaned files found." = "Nema pronađenih siročadnih datoteka.";
"Delete All Orphaned Files" = "Obriši sve siročadne datoteke";
"Delete Selected Files?" = "Obrisati odabrane datoteke?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Jeste li sigurni da želite obrisati odabrane siročadne datoteke? Ova radnja se ne može poništiti.";

View file

@ -508,4 +508,13 @@ Metadata epizody se týkají náhledu a názvu epizody, které mohou někdy obsa
"Export Backup" = "Exportovat zálohu";
"Import Backup" = "Importovat zálohu";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Upozornění: Tato funkce je stále experimentální. Po exportu/importu si prosím zkontrolujte svá data.";
"Backup" = "Záloha";
"Backup" = "Záloha";
"Total Orphaned File Size" = "Celková velikost osiřelých souborů";
"Total Orphaned Files" = "Celkový počet osiřelých souborů";
"Orphaned Downloads" = "Osiřelé stahování";
"No orphaned files found." = "Nebyly nalezeny žádné osiřelé soubory.";
"Delete All Orphaned Files" = "Smazat všechny osiřelé soubory";
"Delete Selected Files?" = "Smazat vybrané soubory?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Opravdu chcete smazat vybrané osiřelé soubory? Tato akce je nevratná.";

View file

@ -497,3 +497,12 @@
"Import Backup" = "Backup importieren";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Hinweis: Diese Funktion ist noch experimentell. Bitte überprüfe deine Daten nach dem Export/Import.";
"Backup" = "Backup";
"Total Orphaned File Size" = "Gesamte Größe verwaister Dateien";
"Total Orphaned Files" = "Anzahl verwaister Dateien";
"Orphaned Downloads" = "Verwaiste Downloads";
"No orphaned files found." = "Keine verwaisten Dateien gefunden.";
"Delete All Orphaned Files" = "Alle verwaisten Dateien löschen";
"Delete Selected Files?" = "Ausgewählte Dateien löschen?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Möchten Sie die ausgewählten verwaisten Dateien wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.";

View file

@ -459,3 +459,12 @@
"Translators" = "Translators";
"Paste URL" = "Paste URL";
"Total Orphaned File Size" = "Total Orphaned File Size";
"Total Orphaned Files" = "Total Orphaned Files";
"Orphaned Downloads" = "Orphaned Downloads";
"No orphaned files found." = "No orphaned files found.";
"Delete All Orphaned Files" = "Delete All Orphaned Files";
"Delete Selected Files?" = "Delete Selected Files?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Are you sure you want to delete the selected orphaned files? This action cannot be undone.";

View file

@ -504,3 +504,12 @@ Para los metadatos del episodio, se refiere a la miniatura y el título del epis
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Aviso: Esta función aún es experimental. Por favor, verifica tus datos después de exportar/importar.";
"Backup" = "Copia de seguridad";
"Total Orphaned File Size" = "Tamaño total de archivos huérfanos";
"Total Orphaned Files" = "Total de archivos huérfanos";
"Orphaned Downloads" = "Descargas huérfanas";
"No orphaned files found." = "No se encontraron archivos huérfanos.";
"Delete All Orphaned Files" = "Eliminar todos los archivos huérfanos";
"Delete Selected Files?" = "¿Eliminar archivos seleccionados?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "¿Está seguro de que desea eliminar los archivos huérfanos seleccionados? Esta acción no se puede deshacer.";

View file

@ -492,3 +492,12 @@
"Import Backup" = "Importer la sauvegarde";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Remarque : Cette fonctionnalité est encore expérimentale. Veuillez vérifier vos données après l'export/import.";
"Backup" = "Sauvegarde";
"Total Orphaned File Size" = "Taille totale des fichiers orphelins";
"Total Orphaned Files" = "Nombre total de fichiers orphelins";
"Orphaned Downloads" = "Téléchargements orphelins";
"No orphaned files found." = "Aucun fichier orphelin trouvé.";
"Delete All Orphaned Files" = "Supprimer tous les fichiers orphelins";
"Delete Selected Files?" = "Supprimer les fichiers sélectionnés ?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Êtes-vous sûr de vouloir supprimer les fichiers orphelins sélectionnés ? Cette action est irréversible.";

View file

@ -423,3 +423,12 @@
"Import Backup" = "Importa backup";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Nota: Questa funzione è ancora sperimentale. Si prega di ricontrollare i dati dopo esportazione/importazione.";
"Backup" = "Backup";
"Total Orphaned File Size" = "Dimensione totale dei file orfani";
"Total Orphaned Files" = "Numero totale di file orfani";
"Orphaned Downloads" = "Download orfani";
"No orphaned files found." = "Nessun file orfano trovato.";
"Delete All Orphaned Files" = "Elimina tutti i file orfani";
"Delete Selected Files?" = "Eliminare i file selezionati?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Sei sicuro di voler eliminare i file orfani selezionati? Questa azione non può essere annullata.";

View file

@ -503,4 +503,13 @@ For episode metadata, it refers to the episode thumbnail and title, since someti
"Export Backup" = "Сақтық көшірмені экспорттау";
"Import Backup" = "Сақтық көшірмені импорттау";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Ескерту: Бұл мүмкіндік әлі де тәжірибелік. Экспорт/импорттан кейін деректеріңізді тексеріңіз.";
"Backup" = "Сақтық көшірме";
"Backup" = "Сақтық көшірме";
"Total Orphaned File Size" = "Жалпы жетім файлдардың өлшемі";
"Total Orphaned Files" = "Жетім файлдардың жалпы саны";
"Orphaned Downloads" = "Жетім жүктеулер";
"No orphaned files found." = "Жетім файлдар табылмады.";
"Delete All Orphaned Files" = "Барлық жетім файлдарды жою";
"Delete Selected Files?" = "Таңдалған файлдарды жою керек пе?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Таңдалған жетім файлдарды жойғыңыз келетініне сенімдісіз бе? Бұл әрекет қайтарылмайды.";

View file

@ -464,4 +464,13 @@ Sora ба cranci1 нь AniList эсвэл Trakt-тэй ямар ч хамаар
"Watch Progress" = "Үзсэн явц";
"Recent searches" = "Саяхны хайлт";
"Collections" = "Цуглуулгууд";
"Continue Reading" = "Унших үргэлжлүүлэх";
"Continue Reading" = "Унших үргэлжлүүлэх";
"Total Orphaned File Size" = "Орхигдсон файлуудын нийт хэмжээ";
"Total Orphaned Files" = "Орхигдсон файлуудын нийт тоо";
"Orphaned Downloads" = "Орхигдсон таталтууд";
"No orphaned files found." = "Орхигдсон файл олдсонгүй.";
"Delete All Orphaned Files" = "Бүх орхигдсон файлуудыг устгах";
"Delete Selected Files?" = "Сонгосон файлуудыг устгах уу?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Сонгосон орхигдсон файлуудыг устгахдаа итгэлтэй байна уу? Энэ үйлдлийг буцаах боломжгүй.";

View file

@ -485,4 +485,13 @@
"Export Backup" = "Back-up exporteren";
"Import Backup" = "Back-up importeren";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Let op: Deze functie is nog experimenteel. Controleer je gegevens na export/import.";
"Backup" = "Back-up";
"Backup" = "Back-up";
"Total Orphaned File Size" = "Totale grootte van verweesde bestanden";
"Total Orphaned Files" = "Totaal aantal verweesde bestanden";
"Orphaned Downloads" = "Verweesde downloads";
"No orphaned files found." = "Geen verweesde bestanden gevonden.";
"Delete All Orphaned Files" = "Alle verweesde bestanden verwijderen";
"Delete Selected Files?" = "Geselecteerde bestanden verwijderen?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Weet u zeker dat u de geselecteerde verweesde bestanden wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.";

View file

@ -485,4 +485,13 @@
"Export Backup" = "Eksporter sikkerhetskopi";
"Import Backup" = "Importer sikkerhetskopi";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Merk: Denne funksjonen er fortsatt eksperimentell. Vennligst dobbeltsjekk dataene dine etter eksport/import.";
"Backup" = "Sikkerhetskopi";
"Backup" = "Sikkerhetskopi";
"Total Orphaned File Size" = "Total storleik på foreldrelause filer";
"Total Orphaned Files" = "Totalt tal på foreldrelause filer";
"Orphaned Downloads" = "Foreldrelause nedlastingar";
"No orphaned files found." = "Ingen foreldrelause filer funne.";
"Delete All Orphaned Files" = "Slett alle foreldrelause filer";
"Delete Selected Files?" = "Slette valde filer?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Er du sikker på at du vil slette dei valde foreldrelause filene? Denne handlinga kan ikkje angrast.";

View file

@ -509,4 +509,13 @@ For episode metadata, it refers to the episode thumbnail and title, since someti
"Export Backup" = "Экспорт резервной копии";
"Import Backup" = "Импорт резервной копии";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Внимание: Эта функция все еще экспериментальная. Пожалуйста, проверьте свои данные после экспорта/импорта.";
"Backup" = "Резервная копия";
"Backup" = "Резервная копия";
"Total Orphaned File Size" = "Общий размер осиротевших файлов";
"Total Orphaned Files" = "Общее количество осиротевших файлов";
"Orphaned Downloads" = "Осиротевшие загрузки";
"No orphaned files found." = "Осиротевшие файлы не найдены.";
"Delete All Orphaned Files" = "Удалить все осиротевшие файлы";
"Delete Selected Files?" = "Удалить выбранные файлы?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Вы уверены, что хотите удалить выбранные осиротевшие файлы? Это действие необратимо.";

View file

@ -506,3 +506,12 @@ For episode metadata, it refers to the episode thumbnail and title, since someti
"Import Backup" = "Importovať zálohu";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Upozornenie: Táto funkcia je stále experimentálna. Po exporte/importe si prosím skontrolujte svoje údaje.";
"Backup" = "Záloha";
"Total Orphaned File Size" = "Celková veľkosť osirelých súborov";
"Total Orphaned Files" = "Celkový počet osirelých súborov";
"Orphaned Downloads" = "Osirelé sťahovania";
"No orphaned files found." = "Nenašli sa žiadne osirelé súbory.";
"Delete All Orphaned Files" = "Vymazať všetky osirelé súbory";
"Delete Selected Files?" = "Vymazať vybrané súbory?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Ste si istí, že chcete vymazať vybrané osirelé súbory? Táto akcia je nevratná.";

View file

@ -484,4 +484,13 @@
"Export Backup" = "Exportera säkerhetskopia";
"Import Backup" = "Importera säkerhetskopia";
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Observera: Denna funktion är fortfarande experimentell. Kontrollera dina data efter export/import.";
"Backup" = "Säkerhetskopia";
"Backup" = "Säkerhetskopia";
"Total Orphaned File Size" = "Total storlek för föräldralösa filer";
"Total Orphaned Files" = "Totalt antal föräldralösa filer";
"Orphaned Downloads" = "Föräldralösa nedladdningar";
"No orphaned files found." = "Inga föräldralösa filer hittades.";
"Delete All Orphaned Files" = "Ta bort alla föräldralösa filer";
"Delete Selected Files?" = "Ta bort valda filer?";
"Are you sure you want to delete the selected orphaned files? This action cannot be undone." = "Är du säker på att du vill ta bort de valda föräldralösa filerna? Denna åtgärd kan inte ångras.";

View file

@ -43,6 +43,27 @@ enum DownloadPersistence {
save(assets)
}
static func orphanedFiles() -> [URL] {
let fileManager = FileManager.default
let downloadsDir = documentsDirectory
let jsonFile = downloadsDir.appendingPathComponent(jsonFileName)
let persistedAssets = load()
let referencedPaths = Set(persistedAssets.compactMap { [$0.localURL.lastPathComponent] + ($0.localSubtitleURL != nil ? [$0.localSubtitleURL!.lastPathComponent] : []) }.flatMap { $0 })
var orphaned: [URL] = []
do {
let files = try fileManager.contentsOfDirectory(at: downloadsDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
for file in files {
let name = file.lastPathComponent
if name == jsonFileName { continue }
if !referencedPaths.contains(name) {
orphaned.append(file)
}
}
} catch {
}
return orphaned
}
private static func readStore() -> DiskStore {
let url = documentsDirectory.appendingPathComponent(jsonFileName)
guard FileManager.default.fileExists(atPath: url.path),

View file

@ -0,0 +1,248 @@
import SwiftUI
import Drops
struct OrphanedDownloadsView: View {
@State private var orphanedFiles: [URL] = []
@State private var selectedFiles: Set<URL> = []
@State private var showDeleteConfirmation = false
@State private var isLoading = false
@Environment(\.presentationMode) private var presentationMode
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 24) {
if isLoading {
loadingView
} else if orphanedFiles.isEmpty {
emptyStateView
} else {
orphanedFilesListView
if !selectedFiles.isEmpty {
deleteSelectedButton
}
}
}
.padding(.vertical, 20)
.scrollViewBottomPadding()
}
.navigationTitle("Orphaned Downloads")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "xmark")
.foregroundColor(.primary)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
if !orphanedFiles.isEmpty && !isLoading {
Button(action: {
loadOrphanedFiles()
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(.primary)
}
} else {
// Empty spacer to maintain layout when refresh button is hidden
Color.clear.frame(width: 20, height: 20)
}
}
}
}
.onAppear(perform: loadOrphanedFiles)
.alert(NSLocalizedString("Delete Selected Files?", comment: ""), isPresented: $showDeleteConfirmation) {
Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) {}
Button(NSLocalizedString("Delete", comment: ""), role: .destructive) {
deleteSelectedFiles()
}
} message: {
Text(NSLocalizedString("Are you sure you want to delete the selected orphaned files? This action cannot be undone.", comment: ""))
}
}
// MARK: - Extracted Views
private var loadingView: some View {
VStack {
Spacer()
ProgressView()
.padding()
Text(NSLocalizedString("Loading orphaned files...", comment: ""))
.foregroundColor(.secondary)
Spacer()
}
.frame(minHeight: 300)
}
private var emptyStateView: some View {
VStack {
Spacer()
Image(systemName: "checkmark.circle")
.font(.system(size: 50))
.foregroundColor(.green)
.padding()
Text(NSLocalizedString("No orphaned files found", comment: ""))
.font(.headline)
.foregroundColor(.primary)
Text(NSLocalizedString("Your downloads are well-organized", comment: ""))
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
}
.frame(minHeight: 300)
}
private var orphanedFilesListView: some View {
VStack(alignment: .leading, spacing: 4) {
Text(NSLocalizedString("ORPHANED FILES", comment: ""))
.font(.footnote)
.foregroundColor(.gray)
.padding(.horizontal, 20)
orphanedFilesContainer
}
}
private var orphanedFilesContainer: some View {
VStack(spacing: 0) {
deleteAllButton
Divider()
.padding(.horizontal, 16)
ForEach(orphanedFiles, id: \.self) { file in
fileRow(for: file)
if file != orphanedFiles.last {
Divider()
.padding(.horizontal, 16)
}
}
}
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.strokeBorder(
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color.accentColor.opacity(0.3), location: 0),
.init(color: Color.accentColor.opacity(0), location: 1)
]),
startPoint: .top,
endPoint: .bottom
),
lineWidth: 0.5
)
)
.padding(.horizontal, 20)
}
private var deleteAllButton: some View {
Button(action: {
selectedFiles = Set(orphanedFiles)
showDeleteConfirmation = true
}) {
HStack {
Image(systemName: "trash")
.frame(width: 24, height: 24)
.foregroundColor(.red)
Text(NSLocalizedString("Delete All Orphaned Files", comment: ""))
.foregroundColor(.red)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
private func fileRow(for file: URL) -> some View {
Button(action: {
if selectedFiles.contains(file) {
selectedFiles.remove(file)
} else {
selectedFiles.insert(file)
}
}) {
HStack {
Image(systemName: selectedFiles.contains(file) ? "checkmark.circle.fill" : "circle")
.frame(width: 24, height: 24)
.foregroundColor(selectedFiles.contains(file) ? .accentColor : .gray)
VStack(alignment: .leading, spacing: 2) {
Text(file.lastPathComponent)
.foregroundColor(.primary)
.lineLimit(1)
Text(fileSizeString(for: file))
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
private var deleteSelectedButton: some View {
Button(action: {
showDeleteConfirmation = true
}) {
Text(String(format: NSLocalizedString("Delete Selected (%d)", comment: "Button to delete selected orphaned files with count"), selectedFiles.count))
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 20)
}
}
// MARK: - Helper Methods
private func loadOrphanedFiles() {
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
let files = DownloadPersistence.orphanedFiles()
DispatchQueue.main.async {
self.orphanedFiles = files
self.selectedFiles = []
self.isLoading = false
}
}
}
private func fileSizeString(for url: URL) -> String {
let resourceValues = try? url.resourceValues(forKeys: [.fileSizeKey])
let size = resourceValues?.fileSize ?? 0
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useKB, .useMB, .useGB]
formatter.countStyle = .file
return formatter.string(fromByteCount: Int64(size))
}
private func deleteSelectedFiles() {
let jsonFileName = "downloads.json"
var deletedCount = 0
for file in selectedFiles {
if file.lastPathComponent == jsonFileName { continue }
if (try? FileManager.default.removeItem(at: file)) != nil {
deletedCount += 1
}
}
loadOrphanedFiles()
if deletedCount > 0 {
DropManager.shared.success(String(format: NSLocalizedString("%d file(s) deleted successfully", comment: "Success message for deleted orphaned files"), deletedCount))
}
}
}

View file

@ -159,6 +159,9 @@ struct SettingsViewDownloads: View {
@State private var totalStorageSize: Int64 = 0
@State private var existingDownloadCount: Int = 0
@State private var isCalculating: Bool = false
@State private var showOrphanedDownloads = false
@State private var orphanedStorageSize: Int64 = 0
@State private var orphanedFileCount: Int = 0
var body: some View {
ScrollView(showsIndicators: false) {
@ -229,42 +232,64 @@ struct SettingsViewDownloads: View {
Image(systemName: "externaldrive")
.frame(width: 24, height: 24)
.foregroundStyle(.primary)
Text(String(localized: "Storage Used"))
Text(String(localized: "Total Storage Used"))
.foregroundStyle(.primary)
Spacer()
if isCalculating {
ProgressView()
.scaleEffect(0.7)
.padding(.trailing, 5)
}
Text(formatFileSize(totalStorageSize))
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
Divider()
.padding(.horizontal, 16)
HStack {
Image(systemName: "doc.text")
Image(systemName: "doc.text" )
.frame(width: 24, height: 24)
.foregroundStyle(.primary)
Text(String(localized: "Files Downloaded"))
Text(String(localized: "Total Orphaned File Size"))
.foregroundStyle(.primary)
Spacer()
Text("\(existingDownloadCount) of \(jsController.savedAssets.count)")
Text(formatFileSize(orphanedStorageSize))
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
Divider()
.padding(.horizontal, 16)
HStack {
Image(systemName: "doc.text")
.frame(width: 24, height: 24)
.foregroundStyle(.primary)
Text(String(localized: "Total Orphaned Files"))
.foregroundStyle(.primary)
Spacer()
Text("\(orphanedFileCount)")
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
Divider()
.padding(.horizontal, 16)
Button(action: {
showOrphanedDownloads = true
}) {
HStack {
Image(systemName: "questionmark.folder")
.frame(width: 24, height: 24)
.foregroundStyle(.yellow)
Text(String(localized: "Orphaned Downloads"))
.foregroundStyle(.yellow)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
Divider()
.padding(.horizontal, 16)
@ -327,31 +352,69 @@ struct SettingsViewDownloads: View {
calculateTotalStorage()
jsController.updateMaxConcurrentDownloads(maxConcurrentDownloads)
}
.sheet(isPresented: $showOrphanedDownloads) {
OrphanedDownloadsView()
}
}
private func calculateTotalStorage() {
guard !jsController.savedAssets.isEmpty else {
totalStorageSize = 0
existingDownloadCount = 0
return
}
let downloadsDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("SoraDownloads")
isCalculating = true
DownloadedAsset.clearFileSizeCache()
DownloadGroup.clearFileSizeCache()
DispatchQueue.global(qos: .userInitiated).async {
let total = jsController.savedAssets.reduce(0) { $0 + $1.fileSize }
let existing = jsController.savedAssets.filter { $0.fileExists }.count
// Calculate total size of all files in SoraDownloads
let allFiles: [URL]
do {
allFiles = try FileManager.default.contentsOfDirectory(at: downloadsDir, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: .skipsHiddenFiles)
} catch {
allFiles = []
}
var totalDiskSize: Int64 = 0
for url in allFiles {
if let resourceValues = try? url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]),
let isDirectory = resourceValues.isDirectory, isDirectory {
totalDiskSize += calculateDirectorySize(url)
} else {
totalDiskSize += Int64((try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0)
}
}
// Calculate orphaned files and their size
let orphanedFiles = DownloadPersistence.orphanedFiles()
var orphanedSize: Int64 = 0
for url in orphanedFiles {
if let resourceValues = try? url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]),
let isDirectory = resourceValues.isDirectory, isDirectory {
orphanedSize += calculateDirectorySize(url)
} else {
orphanedSize += Int64((try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0)
}
}
DispatchQueue.main.async {
self.totalStorageSize = total
self.existingDownloadCount = existing
self.totalStorageSize = totalDiskSize
self.orphanedStorageSize = orphanedSize
self.orphanedFileCount = orphanedFiles.count
self.isCalculating = false
}
}
}
private func calculateDirectorySize(_ directoryURL: URL) -> Int64 {
let fileManager = FileManager.default
var totalSize: Int64 = 0
do {
let contents = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: .skipsHiddenFiles)
for url in contents {
let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey])
if resourceValues.isDirectory == true {
totalSize += calculateDirectorySize(url)
} else {
totalSize += Int64(resourceValues.fileSize ?? 0)
}
}
} catch {}
return totalSize
}
private func clearAllDownloads(preservePersistentDownloads: Bool = false) {
let assetsToDelete = jsController.savedAssets

View file

@ -119,6 +119,7 @@
722248662DCBC13E00CABE2D /* JSController-Downloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722248652DCBC13E00CABE2D /* JSController-Downloads.swift */; };
72443C7D2DC8036500A61321 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7C2DC8036500A61321 /* DownloadView.swift */; };
72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */; };
7260B66D2E32A8CB00365CDA /* OrphanedDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7260B66C2E32A8CB00365CDA /* OrphanedDownloadsView.swift */; };
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */; };
7273F0402E26B19700DF083D /* DownloadPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7273F03F2E26B19700DF083D /* DownloadPersistence.swift */; };
72D546DB2DFC5E950044C567 /* JSController+Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D546DA2DFC5E950044C567 /* JSController+Downloader.swift */; };
@ -238,6 +239,7 @@
722248652DCBC13E00CABE2D /* JSController-Downloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Downloads.swift"; sourceTree = "<group>"; };
72443C7C2DC8036500A61321 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = "<group>"; };
72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDownloads.swift; sourceTree = "<group>"; };
7260B66C2E32A8CB00365CDA /* OrphanedDownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrphanedDownloadsView.swift; sourceTree = "<group>"; };
7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-StreamTypeDownload.swift"; sourceTree = "<group>"; };
7273F03F2E26B19700DF083D /* DownloadPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadPersistence.swift; sourceTree = "<group>"; };
72D546DA2DFC5E950044C567 /* JSController+Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController+Downloader.swift"; sourceTree = "<group>"; };
@ -580,6 +582,7 @@
133D7C832D2BE2630075467E /* SettingsSubViews */ = {
isa = PBXGroup;
children = (
7260B66C2E32A8CB00365CDA /* OrphanedDownloadsView.swift */,
041E9D712E11D71F0025F150 /* SettingsViewBackup.swift */,
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */,
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */,
@ -1066,6 +1069,7 @@
722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */,
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */,
13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */,
7260B66D2E32A8CB00365CDA /* OrphanedDownloadsView.swift in Sources */,
0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */,
0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */,
0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */,