@@ -9,12 +9,21 @@ import Foundation
99import SwiftUI
1010import UniformTypeIdentifiers
1111import Combine
12+ import AVKit
1213
1314class 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