88import CocoaLumberjackSwift
99import SwiftUI
1010import SwiftUIIntrospect
11+ import ZIPFoundation
1112
1213struct EjectListView : View {
1314 @StateObject var searchViewModel = AppListSearchModel ( )
1415 @StateObject var ejectList : EjectListModel
1516
1617 @State var quickLookExport : URL ?
1718 @State var isDeletingAll = false
19+ @State var isExportingAll = false
1820 @State var isErrorOccurred : Bool = false
1921 @State var lastError : Error ?
2022
@@ -33,6 +35,23 @@ struct EjectListView: View {
3335
3436 var body : some View {
3537 refreshableListView
38+ . toolbar {
39+ ToolbarItem ( placement: . topBarTrailing) {
40+ Button {
41+ exportAll ( )
42+ } label: {
43+ if isExportingAll {
44+ ProgressView ( )
45+ . progressViewStyle ( CircularProgressViewStyle ( ) )
46+ . transition ( . opacity)
47+ } else {
48+ Label ( NSLocalizedString ( " Export All " , comment: " " ) , systemImage: " square.and.arrow.up " )
49+ . transition ( . opacity)
50+ }
51+ }
52+ }
53+ }
54+ . animation ( . easeOut, value: isExportingAll)
3655 . quickLookPreview ( $quickLookExport)
3756 }
3857
@@ -256,8 +275,6 @@ struct EjectListView: View {
256275 try injector. ejectAll ( )
257276 } catch {
258277 DispatchQueue . main. async {
259- isDeletingAll = false
260-
261278 DDLogError ( " \( error) " , ddlog: InjectorV3 . main. logger)
262279
263280 lastError = error
@@ -271,6 +288,58 @@ struct EjectListView: View {
271288 }
272289 }
273290
291+ private func exportAll( ) {
292+ let view = viewControllerHost. viewController?
293+ . navigationController? . view
294+
295+ view? . isUserInteractionEnabled = false
296+
297+ isExportingAll = true
298+
299+ DispatchQueue . global ( qos: . userInteractive) . async {
300+ defer {
301+ DispatchQueue . main. async {
302+ isExportingAll = false
303+ view? . isUserInteractionEnabled = true
304+ }
305+ }
306+
307+ do {
308+ try _exportAll ( )
309+ } catch {
310+ DispatchQueue . main. async {
311+ DDLogError ( " \( error) " , ddlog: InjectorV3 . main. logger)
312+
313+ lastError = error
314+ isErrorOccurred = true
315+ }
316+ }
317+ }
318+ }
319+
320+ private func _exportAll( ) throws {
321+ let exportURL = InjectorV3 . temporaryRoot
322+ . appendingPathComponent ( " Exports_ \( UUID ( ) . uuidString) " , isDirectory: true )
323+
324+ let fileMgr = FileManager . default
325+ try fileMgr. createDirectory ( at: exportURL, withIntermediateDirectories: true )
326+
327+ for plugin in ejectList. filteredPlugIns {
328+ let exportURL = exportURL. appendingPathComponent ( plugin. url. lastPathComponent)
329+ try fileMgr. copyItem ( at: plugin. url, to: exportURL)
330+ }
331+
332+ let zipURL = InjectorV3 . temporaryRoot
333+ . appendingPathComponent (
334+ " \( ejectList. app. name) _ \( ejectList. app. id) _ \( UUID ( ) . uuidString. components ( separatedBy: " - " ) . last ?? " " ) .zip " )
335+
336+ try fileMgr. zipItem ( at: exportURL, to: zipURL)
337+
338+ DispatchQueue . main. async {
339+ quickLookExport = zipURL
340+ }
341+ }
342+
274343 @ViewBuilder
275344 private func paddedHeaderFooterText( _ content: String ) -> some View {
276345 if #available( iOS 15 , * ) {
0 commit comments