Skip to content

Commit 621d4cb

Browse files
authored
Merge pull request #2 from gitbrent/add-menu-items
Add menu items
2 parents 9417ff0 + 03f37a1 commit 621d4cb

File tree

7 files changed

+493
-39
lines changed

7 files changed

+493
-39
lines changed

MacImageManager/MacImageManager/ContentView.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import UniformTypeIdentifiers
1111
struct ContentView: View {
1212
// Use EnvironmentObject to access the shared model
1313
@EnvironmentObject private var browserModel: BrowserModel
14-
@State private var selectedFile: FileItem?
14+
@FocusState private var activePane: ActivePane?
15+
16+
enum ActivePane {
17+
case browser, viewer
18+
}
1519

1620
// View selection based on media type
1721
@ViewBuilder
1822
private var mediaViewer: some View {
19-
if let file = selectedFile {
23+
if let file = browserModel.selectedFile {
2024
switch file.mediaType {
2125
case .staticImage, .unknown:
2226
PaneImageViewer(selectedImage: file.url)
@@ -58,11 +62,13 @@ struct ContentView: View {
5862
var body: some View {
5963
HSplitView {
6064
// Left pane - File browser
61-
PaneFileBrowserView(selectedImage: $selectedFile)
65+
PaneFileBrowserView(selectedImage: $browserModel.selectedFile)
6266
.frame(minWidth: 250, maxWidth: 400)
63-
67+
.focused($activePane, equals: .browser)
68+
6469
// Right pane - Media viewer
6570
mediaViewer
71+
.focused($activePane, equals: .viewer)
6672
.fileImporter(
6773
isPresented: $browserModel.showingFileImporter,
6874
allowedContentTypes: [.folder],
@@ -94,7 +100,23 @@ struct ContentView: View {
94100
}
95101
.onChange(of: browserModel.currentDirectory) { _, _ in
96102
// Clear the selected image when navigating to a different directory
97-
selectedFile = nil
103+
browserModel.selectedFile = nil
104+
}
105+
.onKeyPress(.space) {
106+
if activePane == .viewer && browserModel.selectedFileIsVideo {
107+
browserModel.toggleVideoPlayback()
108+
return .handled
109+
}
110+
return .ignored
111+
}
112+
.onKeyPress(phases: .down) { keyPress in
113+
if keyPress.characters == "r" && keyPress.modifiers.contains(.command) {
114+
if browserModel.canRenameSelectedFile {
115+
browserModel.startRenamingSelectedFile()
116+
return .handled
117+
}
118+
}
119+
return .ignored
98120
}
99121
}
100122
}

MacImageManager/MacImageManager/MacImageManagerApp.swift

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,92 @@ struct MacImageManagerApp: App {
1515
WindowGroup {
1616
ContentView()
1717
.frame(minWidth: 1200, minHeight: 800)
18-
// 💡 Add the shared model to the environment so child views can access it
18+
// Add the shared model to the environment so child views can access it
1919
.environmentObject(browserModel)
20+
.onAppear {
21+
NSWindow.allowsAutomaticWindowTabbing = false
22+
}
2023
}
2124
.windowResizability(.contentSize)
2225
.commands {
23-
// Add keyboard shortcuts
26+
// Hide the default Edit menu
27+
CommandGroup(replacing: .textEditing) {}
28+
29+
// Hide the default View menu
30+
CommandGroup(replacing: .toolbar) {}
31+
32+
// Replace the new item commands
2433
CommandGroup(replacing: .newItem) {}
25-
34+
35+
// Remove the (⌘W) "Close" item from `File`
36+
CommandGroup(replacing: .saveItem) {}
37+
38+
// File operations
2639
CommandGroup(after: .newItem) {
2740
Button("Open Folder...") {
2841
browserModel.showingFileImporter = true
2942
}
3043
.keyboardShortcut("o", modifiers: .command)
44+
45+
Divider()
46+
47+
Button("Go Up One Level") {
48+
browserModel.navigateUp()
49+
}
50+
.keyboardShortcut(.upArrow, modifiers: .command)
51+
.disabled(!browserModel.canNavigateUp)
52+
53+
Button("Rename File") {
54+
browserModel.startRenamingSelectedFile()
55+
}
56+
.keyboardShortcut("r", modifiers: .command)
57+
.disabled(!browserModel.canRenameSelectedFile)
58+
59+
Button("Delete File") {
60+
browserModel.deleteSelectedFile()
61+
}
62+
.keyboardShortcut(.delete)
63+
.disabled(!browserModel.hasSelectedFile)
64+
65+
Divider()
66+
67+
Button("Show in Finder") {
68+
browserModel.showSelectedFileInFinder()
69+
}
70+
.keyboardShortcut("r", modifiers: [.command, .shift])
71+
.disabled(!browserModel.hasSelectedFile)
72+
}
73+
74+
// Playback menu for video controls (inserted explicitly after our File operations)
75+
CommandMenu("Playback") {
76+
Button("Play/Pause") {
77+
browserModel.toggleVideoPlayback()
78+
}
79+
.keyboardShortcut(.space, modifiers: [])
80+
.disabled(!browserModel.selectedFileIsVideo)
81+
82+
Divider()
83+
84+
Button("Jump Backward 10s") {
85+
browserModel.jumpVideoBackward()
86+
}
87+
.keyboardShortcut(.leftArrow, modifiers: .command)
88+
.disabled(!browserModel.selectedFileIsVideo)
89+
90+
Button("Jump Forward 10s") {
91+
browserModel.jumpVideoForward()
92+
}
93+
.keyboardShortcut(.rightArrow, modifiers: .command)
94+
.disabled(!browserModel.selectedFileIsVideo)
95+
96+
Divider()
97+
98+
Button("Restart Video") {
99+
browserModel.restartVideo()
100+
}
101+
.keyboardShortcut("r", modifiers: [.command, .option])
102+
.disabled(!browserModel.selectedFileIsVideo)
31103
}
32-
33104
}
34105
}
35106
}

MacImageManager/MacImageManager/Models/BrowserModel.swift

Lines changed: 198 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ import Foundation
99
import SwiftUI
1010
import UniformTypeIdentifiers
1111
import Combine
12+
import AVKit
1213

1314
class BrowserModel: ObservableObject {
1415
@Published var items: [FileItem] = []
1516
@Published var currentDirectory: URL
1617
@Published var canNavigateUp: Bool = false
1718
@Published var showingFileImporter: Bool = false
19+
@Published var selectedFile: FileItem?
20+
@Published var isRenamingFile = false
21+
@Published var renamingText = ""
22+
@Published var currentVideoPlayer: AVPlayer?
23+
24+
enum VideoAction {
25+
case play, pause, toggle, jumpForward, jumpBackward, restart
26+
}
1827

1928
// Cache to speed up metadata recomputation in large directories
2029
private var fileItemCache: [URL: FileItem] = [:]
@@ -42,6 +51,8 @@ class BrowserModel: ObservableObject {
4251
updateNavigationState()
4352
}
4453

54+
let videoActionPublisher = PassthroughSubject<VideoAction, Never>()
55+
4556
var currentDirectoryName: String {
4657
currentDirectory.lastPathComponent
4758
}
@@ -72,8 +83,6 @@ class BrowserModel: ObservableObject {
7283

7384
let uti = resourceValues.contentType
7485
let isDir = resourceValues.isDirectory ?? false
75-
let isAnimatedGif = uti?.conforms(to: UTType.gif) ?? false
76-
let isVideo = uti?.conforms(to: UTType.movie) ?? false
7786
let fileSize = resourceValues.fileSize ?? 0
7887
let modDate = resourceValues.contentModificationDate ?? Date()
7988

@@ -198,4 +207,191 @@ class BrowserModel: ObservableObject {
198207

199208
return imageItems.last
200209
}
210+
211+
// MARK: - Menu Actions
212+
213+
/// Computed property to check if a file is selected for menu state
214+
var hasSelectedFile: Bool {
215+
selectedFile != nil
216+
}
217+
218+
/// Computed property to check if selected file is renameable
219+
var canRenameSelectedFile: Bool {
220+
guard let file = selectedFile else { return false }
221+
return !file.isDirectory // For now, only allow renaming files, not directories
222+
}
223+
224+
/// Start renaming the currently selected file
225+
func startRenamingSelectedFile() {
226+
guard let file = selectedFile else { return }
227+
renamingText = file.name
228+
isRenamingFile = true
229+
}
230+
231+
/// Complete the rename operation
232+
func completeRename() {
233+
guard let file = selectedFile, !renamingText.isEmpty else {
234+
cancelRename()
235+
return
236+
}
237+
238+
// Validate the filename
239+
guard isValidFilename(renamingText) else {
240+
cancelRename()
241+
return
242+
}
243+
244+
// Check if the name hasn't actually changed
245+
guard renamingText != file.name else {
246+
cancelRename()
247+
return
248+
}
249+
250+
let newURL = file.url.deletingLastPathComponent().appendingPathComponent(renamingText)
251+
252+
do {
253+
try FileManager.default.moveItem(at: file.url, to: newURL)
254+
255+
// Update the file item in our list
256+
if let index = items.firstIndex(where: { $0.url == file.url }) {
257+
Task {
258+
let updatedItem = await FileItem(
259+
url: newURL,
260+
name: renamingText,
261+
isDirectory: file.isDirectory,
262+
fileSize: file.fileSize,
263+
modificationDate: file.modificationDate,
264+
uti: file.uti
265+
)
266+
await MainActor.run {
267+
items[index] = updatedItem
268+
selectedFile = updatedItem
269+
270+
// Update cache
271+
fileItemCache.removeValue(forKey: file.url)
272+
fileItemCache[newURL] = updatedItem
273+
274+
// Cancel rename mode after UI is updated
275+
cancelRename()
276+
}
277+
}
278+
} else {
279+
// If item not found in list, still cancel rename mode
280+
cancelRename()
281+
}
282+
283+
print("Successfully renamed \(file.name) to \(renamingText)")
284+
} catch {
285+
print("Failed to rename file: \(error.localizedDescription)")
286+
cancelRename()
287+
}
288+
}
289+
290+
/// Cancel the rename operation
291+
func cancelRename() {
292+
isRenamingFile = false
293+
renamingText = ""
294+
}
295+
296+
/// Delete the currently selected file
297+
func deleteSelectedFile() {
298+
guard let file = selectedFile else { return }
299+
300+
do {
301+
try FileManager.default.trashItem(at: file.url, resultingItemURL: nil)
302+
303+
// Remove from our list
304+
items.removeAll { $0.url == file.url }
305+
fileItemCache.removeValue(forKey: file.url)
306+
selectedFile = nil
307+
308+
print("Successfully moved \(file.name) to trash")
309+
} catch {
310+
print("Failed to delete file: \(error.localizedDescription)")
311+
}
312+
}
313+
314+
/// Show selected file in Finder
315+
func showSelectedFileInFinder() {
316+
guard let file = selectedFile else { return }
317+
NSWorkspace.shared.selectFile(file.url.path, inFileViewerRootedAtPath: "")
318+
}
319+
320+
// MARK: - Video Control Actions
321+
322+
/// Toggle video playback (play/pause)
323+
func toggleVideoPlayback() {
324+
videoActionPublisher.send(.toggle)
325+
}
326+
327+
/// Play video
328+
func playVideo() {
329+
videoActionPublisher.send(.play)
330+
}
331+
332+
/// Pause video
333+
func pauseVideo() {
334+
videoActionPublisher.send(.pause)
335+
}
336+
337+
/// Jump forward 10 seconds
338+
func jumpVideoForward() {
339+
videoActionPublisher.send(.jumpForward)
340+
}
341+
342+
/// Jump backward 10 seconds
343+
func jumpVideoBackward() {
344+
videoActionPublisher.send(.jumpBackward)
345+
}
346+
347+
/// Restart video from beginning
348+
func restartVideo() {
349+
videoActionPublisher.send(.restart)
350+
}
351+
352+
/// Set the current video player reference
353+
func setVideoPlayer(_ player: AVPlayer?) {
354+
currentVideoPlayer = player
355+
}
356+
357+
/// Check if we currently have a video playing
358+
var hasVideoPlayer: Bool {
359+
currentVideoPlayer != nil
360+
}
361+
362+
/// Check if current selection is a video file
363+
var selectedFileIsVideo: Bool {
364+
selectedFile?.mediaType == .video
365+
}
366+
367+
/// Validate filename for macOS compatibility
368+
private func isValidFilename(_ filename: String) -> Bool {
369+
let trimmedName = filename.trimmingCharacters(in: .whitespacesAndNewlines)
370+
371+
// Check for empty or whitespace-only names
372+
guard !trimmedName.isEmpty else { return false }
373+
374+
// Check if it's just an extension (starts with .)
375+
guard !trimmedName.hasPrefix(".") else { return false }
376+
377+
// Check filename length (macOS limit is 255 bytes, but we'll use a conservative limit)
378+
guard trimmedName.count <= 255 else { return false }
379+
380+
// Check for invalid characters on macOS
381+
// macOS is more permissive than Windows, but these are still problematic
382+
let invalidCharacters = CharacterSet(charactersIn: ":\0")
383+
guard trimmedName.rangeOfCharacter(from: invalidCharacters) == nil else { return false }
384+
385+
// Check for names that are just dots
386+
guard trimmedName != "." && trimmedName != ".." else { return false }
387+
388+
// Check for control characters (0x00-0x1F and 0x7F)
389+
for char in trimmedName.unicodeScalars {
390+
if char.value <= 0x1F || char.value == 0x7F {
391+
return false
392+
}
393+
}
394+
395+
return true
396+
}
201397
}

0 commit comments

Comments
 (0)