Skip to content

Commit 4edce44

Browse files
author
Iakov Senatov
committed
Refactor archive workflow: PackDialog → performCompress → CompressService; support archive name, destination path and move-to-archive; fix dialog autofocus
1 parent cba9593 commit 4edce44

File tree

9 files changed

+1862
-1833
lines changed

9 files changed

+1862
-1833
lines changed

GUI/Sources/ContextMenu/Dialogs/ConfirmationDialog.swift

Lines changed: 353 additions & 353 deletions
Large diffs are not rendered by default.

GUI/Sources/ContextMenu/Dialogs/ContextMenuDialogModifier.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@
103103
Task {
104104
await coordinator.performCompress(
105105
files: files,
106+
archiveName: archiveName,
107+
destination: finalDestination,
108+
moveToArchive: deleteSource,
106109
appState: appState
107110
)
108111
}

GUI/Sources/ContextMenu/Dialogs/PackDialog.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
/// Callback: (archiveName, format, destination, deleteSourceFiles)
2323
let onPack: (String, ArchiveFormat, URL, Bool) -> Void
2424
let onCancel: () -> Void
25+
private static let lastArchiveDirectoryKey = "LastArchiveDirectory"
2526

2627
@State private var archiveName: String
2728
@State private var destinationPath: String
@@ -49,8 +50,14 @@
4950
} else {
5051
defaultName = "Archive"
5152
}
52-
self._archiveName = State(initialValue: defaultName)
53-
self._destinationPath = State(initialValue: destinationPath.path)
53+
54+
if mode == .compress {
55+
self._archiveName = State(initialValue: defaultName + ".zip")
56+
} else {
57+
self._archiveName = State(initialValue: defaultName)
58+
}
59+
let lastDir = UserDefaults.standard.string(forKey: Self.lastArchiveDirectoryKey)
60+
self._destinationPath = State(initialValue: lastDir ?? destinationPath.path)
5461
}
5562

5663
private var isValidName: Bool {
@@ -191,6 +198,8 @@
191198
}
192199

193200
private func performPack() {
201+
UserDefaults.standard.set(destinationPath, forKey: Self.lastArchiveDirectoryKey)
202+
194203
let format: ArchiveFormat = mode == .compress ? .zip : selectedFormat
195204
onPack(archiveName, format, URL(fileURLWithPath: destinationPath), deleteSourceFiles)
196205
}
@@ -202,6 +211,11 @@
202211
panel.allowsMultipleSelection = false
203212
panel.canCreateDirectories = true
204213
panel.prompt = L10n.Button.select
214+
215+
if FileManager.default.fileExists(atPath: destinationPath) {
216+
panel.directoryURL = URL(fileURLWithPath: destinationPath)
217+
}
218+
205219
if panel.runModal() == .OK, let url = panel.url {
206220
destinationPath = url.path
207221
}

GUI/Sources/ContextMenu/Services/CompressService.swift

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,19 @@
2424
/// Compresses files using Finder-style naming (Archive.zip or item.zip)
2525
/// Returns URL of created archive
2626
@discardableResult
27-
func compress(files: [URL], moveToArchive: Bool = false) async throws -> URL {
27+
func compress(
28+
files: [URL],
29+
archiveName: String,
30+
destination: URL,
31+
moveToArchive: Bool = false
32+
) async throws -> URL {
2833
log.debug("\(#function) files.count=\(files.count) files=\(files.map { $0.lastPathComponent })")
2934
guard !files.isEmpty else {
3035
log.error("\(#function) FAILED: no files to compress")
3136
throw CompressError.noFilesToCompress
3237
}
33-
let parentDirectories = Set(files.map { $0.deletingLastPathComponent().path })
34-
guard parentDirectories.count == 1 else {
35-
let message = "All files must be located in the same directory"
36-
log.error("\(#function) FAILED: \(message)")
37-
throw CompressError.compressionFailed(message)
38-
}
39-
let parentDir = files[0].deletingLastPathComponent()
40-
let archiveName = generateArchiveName(for: files, in: parentDir)
41-
let archiveURL = parentDir.appendingPathComponent(archiveName)
42-
log.info("\(#function) compressing \(files.count) item(s) → '\(archiveName)'")
38+
let archiveURL = destination.appendingPathComponent(archiveName)
39+
log.info("\(#function) compressing \(files.count) item(s) → '\(archiveURL.path)'")
4340
// Use ditto for Finder-compatible compression
4441
let task = Process()
4542
task.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")

GUI/Sources/ContextMenu/Services/Coordinator/FileOperationExecutors.swift renamed to GUI/Sources/ContextMenu/Services/Coordinator/CntMenuCoord+FileOps.swift

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
//
44
// Created by Claude AI on 04.02.2026.
55
// Copyright © 2026 Senatov. All rights reserved.
6-
// Description: Executes file operations (delete, rename, duplicate, compress, pack, etc.)
6+
// Description: Extension of ContextMenuCoordinator that implements file operation executors (delete, rename, duplicate, compress, pack, etc.)
77

88
import AppKit
99
import FileModelKit
1010
import Foundation
1111

12-
// MARK: - File Operation Executors
12+
// MARK: - ContextMenuCoordinator + File Operations
1313
/// Extension containing async file operation executors
1414
extension ContextMenuCoordinator {
1515

@@ -95,61 +95,48 @@
9595

9696
// MARK: - Compress
9797

98-
/// Compress files (Finder-style .zip)
99-
func performCompress(files: [CustomFile], appState: AppState) async {
100-
log.debug("\(#function) files.count=\(files.count)")
101-
102-
guard let moveToArchive = promptMoveToArchiveOption(filesCount: files.count) else {
103-
log.info("\(#function) cancelled by user")
104-
activeDialog = nil
105-
return
106-
}
98+
func performCompress(
99+
files: [CustomFile],
100+
archiveName: String,
101+
destination: URL,
102+
moveToArchive: Bool,
103+
appState: AppState
104+
) async {
105+
log.debug("\(#function) files.count=\(files.count) archiveName='\(archiveName)' dest='\(destination.path)' moveToArchive=\(moveToArchive)")
107106

108107
isProcessing = true
109108
defer { isProcessing = false }
110109

111110
do {
112111
let urls = files.map { $0.urlValue }
113-
let result = try await CompressService.shared.compress(files: urls, moveToArchive: moveToArchive)
112+
113+
let result = try await CompressService.shared.compress(
114+
files: urls,
115+
archiveName: archiveName,
116+
destination: destination,
117+
moveToArchive: moveToArchive
118+
)
119+
114120
refreshPanels(appState: appState)
121+
115122
log.info("\(#function) SUCCESS created '\(result.lastPathComponent)' moveToArchive=\(moveToArchive)")
116123
} catch {
117124
log.error("\(#function) FAILED: \(error.localizedDescription)")
118125
activeDialog = .error(title: "Compress Failed", message: error.localizedDescription)
119126
}
120127
}
121128

122-
@MainActor
123-
private func promptMoveToArchiveOption(filesCount: Int) -> Bool? {
124-
let alert = NSAlert()
125-
alert.alertStyle = .informational
126-
alert.messageText = filesCount == 1 ? "Compress Item" : "Compress Items"
127-
alert.informativeText = filesCount == 1
128-
? "Create a ZIP archive for the selected item."
129-
: "Create a ZIP archive for the selected items."
130-
alert.addButton(withTitle: "Compress")
131-
alert.addButton(withTitle: "Cancel")
132-
133-
let checkbox = NSButton(checkboxWithTitle: "Move originals into archive", target: nil, action: nil)
134-
checkbox.state = .off
135-
checkbox.setButtonType(.switch)
136-
alert.accessoryView = checkbox
137-
138-
let response = alert.runModal()
139-
guard response == .alertFirstButtonReturn else {
140-
return nil
141-
}
142-
143-
return checkbox.state == .on
144-
}
145-
146129
// MARK: - Pack (Archive with options)
147-
148130
/// Pack files into archive with custom options.
149131
/// Creates destination directory if it doesn't exist.
150132
/// Selects the created archive in the appropriate panel.
151-
func performPack(files: [CustomFile], archiveName: String, format: ArchiveFormat, destination: URL, deleteSource: Bool = false, appState: AppState) async {
152-
log.debug("\(#function) files.count=\(files.count) archiveName='\(archiveName)' format=\(format) dest='\(destination.path)' deleteSource=\(deleteSource)")
133+
func performPack(
134+
files: [CustomFile], archiveName: String, format: ArchiveFormat, destination: URL, deleteSource: Bool = false,
135+
appState: AppState
136+
) async {
137+
log.debug(
138+
"\(#function) files.count=\(files.count) archiveName='\(archiveName)' format=\(format) dest='\(destination.path)' deleteSource=\(deleteSource)"
139+
)
153140

154141
isProcessing = true
155142
defer {
Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,66 @@
1-
// MultiSelectionActionsHandler.swift
2-
// MiMiNavigator
3-
//
4-
// Created by Claude on 14.02.2026.
5-
// Copyright © 2026 Senatov. All rights reserved.
6-
// Description: Handles MultiSelectionAction dispatching for batch operations
1+
// MultiSelectionActionsHandler.swift
2+
// MiMiNavigator
3+
//
4+
// Created by Claude on 14.02.2026.
5+
// Copyright © 2026 Senatov. All rights reserved.
6+
// Description: Handles MultiSelectionAction dispatching for batch operations
77

8-
import AppKit
9-
import FileModelKit
10-
import Foundation
8+
import AppKit
9+
import FileModelKit
10+
import Foundation
1111

12-
// MARK: - Multi Selection Actions Handler
13-
/// Extension handling batch operations on multiple marked files
14-
extension ContextMenuCoordinator {
12+
// MARK: - Multi Selection Actions Handler
13+
/// Extension handling batch operations on multiple marked files
14+
extension ContextMenuCoordinator {
1515

16-
/// Handles multi-selection context menu actions
17-
func handleMultiSelectionAction(_ action: MultiSelectionAction, panel: PanelSide, appState: AppState) {
18-
let files = appState.filesForOperation(on: panel)
16+
/// Handles multi-selection context menu actions
17+
func handleMultiSelectionAction(_ action: MultiSelectionAction, panel: PanelSide, appState: AppState) {
18+
let files = appState.filesForOperation(on: panel)
1919

20-
guard !files.isEmpty else {
21-
log.warning("[MultiSelectionActionsHandler] no files for operation")
22-
return
23-
}
20+
guard !files.isEmpty else {
21+
log.warning("[MultiSelectionActionsHandler] no files for operation")
22+
return
23+
}
2424

25-
log.debug("[MultiSelectionActionsHandler] action=\(action.rawValue) files.count=\(files.count) panel=\(panel)")
25+
log.debug("[MultiSelectionActionsHandler] action=\(action.rawValue) files.count=\(files.count) panel=\(panel)")
2626

27-
switch action {
28-
case .cut:
29-
clipboard.cut(files: files, from: panel)
30-
log.info("[MultiSelectionActionsHandler] cut \(files.count) files")
27+
switch action {
28+
case .cut:
29+
clipboard.cut(files: files, from: panel)
30+
log.info("[MultiSelectionActionsHandler] cut \(files.count) files")
3131

32-
case .copy:
33-
clipboard.copy(files: files, from: panel)
34-
log.info("[MultiSelectionActionsHandler] copied \(files.count) files")
32+
case .copy:
33+
clipboard.copy(files: files, from: panel)
34+
log.info("[MultiSelectionActionsHandler] copied \(files.count) files")
3535

36-
case .copyAsPathname:
37-
let paths = files.map { $0.urlValue.path }
38-
let pasteboard = NSPasteboard.general
39-
pasteboard.clearContents()
40-
pasteboard.setString(paths.joined(separator: "\n"), forType: .string)
41-
log.info("[MultiSelectionActionsHandler] copied \(paths.count) pathname(s) to clipboard")
36+
case .copyAsPathname:
37+
let paths = files.map { $0.urlValue.path }
38+
let pasteboard = NSPasteboard.general
39+
pasteboard.clearContents()
40+
pasteboard.setString(paths.joined(separator: "\n"), forType: .string)
41+
log.info("[MultiSelectionActionsHandler] copied \(paths.count) pathname(s) to clipboard")
4242

43-
case .paste:
44-
Task {
45-
await performPaste(to: panel, appState: appState)
46-
}
43+
case .paste:
44+
Task {
45+
await performPaste(to: panel, appState: appState)
46+
}
4747

48-
case .compress:
49-
Task {
50-
await performCompress(files: files, appState: appState)
51-
appState.clearMarksAfterOperation(on: panel)
52-
}
48+
case .compress:
49+
// Default destination: opposite panel directory
50+
let destinationPath = panel == .left ? appState.rightPath : appState.leftPath
51+
let destination = URL(fileURLWithPath: destinationPath)
52+
activeDialog = .compress(files: files, destination: destination)
5353

54-
case .share:
55-
let urls = files.map { $0.urlValue }
56-
ShareService.shared.showSharePicker(for: urls)
54+
case .share:
55+
let urls = files.map { $0.urlValue }
56+
ShareService.shared.showSharePicker(for: urls)
5757

58-
case .revealInFinder:
59-
let urls = files.map { $0.urlValue }
60-
NSWorkspace.shared.activateFileViewerSelecting(urls)
58+
case .revealInFinder:
59+
let urls = files.map { $0.urlValue }
60+
NSWorkspace.shared.activateFileViewerSelecting(urls)
6161

62-
case .delete:
63-
activeDialog = .deleteConfirmation(files: files)
62+
case .delete:
63+
activeDialog = .deleteConfirmation(files: files)
64+
}
6465
}
6566
}
66-
}

0 commit comments

Comments
 (0)