mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Merge branch 'dev' of https://github.com/50n50/Sora into dev
This commit is contained in:
commit
a1cc56adc0
19 changed files with 504 additions and 33 deletions
|
|
@ -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." = "هل أنت متأكد أنك تريد حذف الملفات اليتيمة المحددة؟ لا يمكن التراجع عن هذا الإجراء.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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á.";
|
||||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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." = "Таңдалған жетім файлдарды жойғыңыз келетініне сенімдісіз бе? Бұл әрекет қайтарылмайды.";
|
||||
|
|
|
|||
|
|
@ -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." = "Сонгосон орхигдсон файлуудыг устгахдаа итгэлтэй байна уу? Энэ үйлдлийг буцаах боломжгүй.";
|
||||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
@ -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." = "Вы уверены, что хотите удалить выбранные осиротевшие файлы? Это действие необратимо.";
|
||||
|
|
|
|||
|
|
@ -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á.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -124,6 +124,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 */; };
|
||||
|
|
@ -248,6 +249,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>"; };
|
||||
|
|
@ -607,6 +609,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0414ED042E32D90000A7E76A /* SettingsViewLibrary.swift */,
|
||||
7260B66C2E32A8CB00365CDA /* OrphanedDownloadsView.swift */,
|
||||
041E9D712E11D71F0025F150 /* SettingsViewBackup.swift */,
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */,
|
||||
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */,
|
||||
|
|
@ -1104,6 +1107,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 */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue