@@ -29,10 +29,47 @@ struct ThumbnailGridView: View {
2929 let onSelect : ( CustomFile ) -> Void
3030 let onDoubleClick : ( CustomFile ) -> Void
3131
32+ @State private var selectedIDs : Set < CustomFile . ID > = [ ]
33+
3234 private var columns : [ GridItem ] {
3335 [ GridItem ( . adaptive( minimum: cellSize, maximum: cellSize + 20 ) , spacing: 8 ) ]
3436 }
3537
38+ private func handleSelection( for file: CustomFile , modifiers: NSEvent . ModifierFlags ) {
39+ if modifiers. contains ( . command) {
40+ if selectedIDs. contains ( file. id) {
41+ selectedIDs. remove ( file. id)
42+ } else {
43+ selectedIDs. insert ( file. id)
44+ }
45+
46+ // Update anchor selection
47+ selectedID = selectedIDs. isEmpty ? nil : file. id
48+
49+ onSelect ( file)
50+ return
51+ }
52+
53+ if modifiers. contains ( . shift) ,
54+ let anchor = selectedID,
55+ let anchorIndex = files. firstIndex ( where: { $0. id == anchor } ) ,
56+ let targetIndex = files. firstIndex ( where: { $0. id == file. id } )
57+ {
58+ let lower = min ( anchorIndex, targetIndex)
59+ let upper = max ( anchorIndex, targetIndex)
60+ selectedIDs. removeAll ( )
61+ for item in files [ lower... upper] {
62+ selectedIDs. insert ( item. id)
63+ }
64+ selectedID = file. id
65+ onSelect ( file)
66+ return
67+ }
68+ selectedIDs = [ file. id]
69+ selectedID = file. id
70+ onSelect ( file)
71+ }
72+
3673 // MARK: - Body
3774 var body : some View {
3875 ScrollView {
@@ -41,16 +78,19 @@ struct ThumbnailGridView: View {
4178 ThumbnailCellView (
4279 file: file,
4380 cellSize: cellSize,
44- isSelected: selectedID == file. id,
81+ isSelected: selectedIDs . contains ( file. id) ,
4582 panelSide: panelSide,
4683 dragFiles: dragFilesFor ( file) ,
47- onSelect: { onSelect ( file) } ,
84+ onSelect: { modifiers in
85+ handleSelection ( for: file, modifiers: modifiers)
86+ } ,
4887 onDoubleClick: { onDoubleClick ( file) } ,
4988 onFileAction: { action in
5089 ContextMenuCoordinator . shared. handleFileAction ( action, for: file, panel: panelSide, appState: appState)
5190 } ,
5291 onDirectoryAction: { action in
53- ContextMenuCoordinator . shared. handleDirectoryAction ( action, for: file, panel: panelSide, appState: appState)
92+ ContextMenuCoordinator . shared. handleDirectoryAction (
93+ action, for: file, panel: panelSide, appState: appState)
5494 }
5595 )
5696 }
@@ -61,8 +101,9 @@ struct ThumbnailGridView: View {
61101
62102 // MARK: - Drag helpers
63103 private func dragFilesFor( _ file: CustomFile ) -> [ CustomFile ] {
64- let marked = appState. markedCustomFiles ( for: panelSide)
65- if !marked. isEmpty && marked. contains ( where: { $0. id == file. id } ) { return marked }
104+ if selectedIDs. contains ( file. id) {
105+ return files. filter { selectedIDs. contains ( $0. id) }
106+ }
66107 return [ file]
67108 }
68109}
@@ -76,7 +117,7 @@ private struct ThumbnailCellView: View {
76117 let isSelected : Bool
77118 let panelSide : PanelSide
78119 let dragFiles : [ CustomFile ]
79- let onSelect : ( ) -> Void
120+ let onSelect : ( NSEvent . ModifierFlags ) -> Void
80121 let onDoubleClick : ( ) -> Void
81122 let onFileAction : ( FileAction ) -> Void
82123 let onDirectoryAction : ( DirectoryAction ) -> Void
@@ -95,9 +136,11 @@ private struct ThumbnailCellView: View {
95136 // Thumbnail or icon
96137 ZStack {
97138 RoundedRectangle ( cornerRadius: 6 , style: . continuous)
98- . fill ( isSelected
99- ? Color . accentColor. opacity ( 0.18 )
100- : ( isHovered ? Color . primary. opacity ( 0.06 ) : Color . clear) )
139+ . fill (
140+ isSelected
141+ ? Color . accentColor. opacity ( 0.18 )
142+ : ( isHovered ? Color . primary. opacity ( 0.06 ) : Color . clear)
143+ )
101144 . frame ( width: cellSize, height: cellSize)
102145
103146 if let img = thumbnail {
@@ -141,7 +184,13 @@ private struct ThumbnailCellView: View {
141184 . contentShape ( Rectangle ( ) )
142185 . onHover { isHovered = $0 }
143186 . onTapGesture ( count: 2 ) { onDoubleClick ( ) }
144- . onTapGesture ( count: 1 ) { onSelect ( ) }
187+ . simultaneousGesture (
188+ TapGesture ( count: 1 )
189+ . onEnded {
190+ let modifiers = NSApp . currentEvent? . modifierFlags ?? [ ]
191+ onSelect ( modifiers)
192+ }
193+ )
145194 // MARK: Context menu
146195 . contextMenu { contextMenuContent }
147196 // MARK: Drag
@@ -161,7 +210,8 @@ private struct ThumbnailCellView: View {
161210 provider. registerObject ( first. urlValue as NSURL , visibility: . all)
162211 }
163212
164- let allDraggedPaths = dragFiles
213+ let allDraggedPaths =
214+ dragFiles
165215 . map { $0. urlValue. absoluteString }
166216 . joined ( separator: " \n " )
167217
@@ -201,24 +251,24 @@ private struct ThumbnailCellView: View {
201251 private func sfSymbol( for name: String ) -> String {
202252 let ext = ( name as NSString ) . pathExtension. lowercased ( )
203253 switch ext {
204- case " jpg " , " jpeg " , " png " , " gif " , " webp " , " heic " , " heif " , " bmp " , " tiff " :
205- return " photo "
206- case " mp4 " , " mov " , " avi " , " mkv " , " m4v " , " wmv " :
207- return " film "
208- case " mp3 " , " aac " , " flac " , " wav " , " m4a " , " ogg " :
209- return " music.note "
210- case " pdf " :
211- return " doc.richtext "
212- case " zip " , " tar " , " gz " , " 7z " , " rar " , " bz2 " :
213- return " archivebox "
214- case " swift " , " py " , " js " , " ts " , " java " , " kt " , " cpp " , " c " , " h " , " m " , " rb " , " go " , " rs " :
215- return " chevron.left.forwardslash.chevron.right "
216- case " txt " , " md " , " rtf " :
217- return " doc.text "
218- case " app " :
219- return " app.badge "
220- default :
221- return " doc "
254+ case " jpg " , " jpeg " , " png " , " gif " , " webp " , " heic " , " heif " , " bmp " , " tiff " :
255+ return " photo "
256+ case " mp4 " , " mov " , " avi " , " mkv " , " m4v " , " wmv " :
257+ return " film "
258+ case " mp3 " , " aac " , " flac " , " wav " , " m4a " , " ogg " :
259+ return " music.note "
260+ case " pdf " :
261+ return " doc.richtext "
262+ case " zip " , " tar " , " gz " , " 7z " , " rar " , " bz2 " :
263+ return " archivebox "
264+ case " swift " , " py " , " js " , " ts " , " java " , " kt " , " cpp " , " c " , " h " , " m " , " rb " , " go " , " rs " :
265+ return " chevron.left.forwardslash.chevron.right "
266+ case " txt " , " md " , " rtf " :
267+ return " doc.text "
268+ case " app " :
269+ return " app.badge "
270+ default :
271+ return " doc "
222272 }
223273 }
224274
@@ -229,7 +279,7 @@ private struct ThumbnailCellView: View {
229279 if file. isDirectory { return }
230280
231281 let url = file. urlValue
232- let size = CGSize ( width: imageSize * 2 , height: imageSize * 2 ) // 2x for retina
282+ let size = CGSize ( width: imageSize * 2 , height: imageSize * 2 ) // 2x for retina
233283 let scale = NSScreen . main? . backingScaleFactor ?? 2.0
234284 let request = QLThumbnailGenerator . Request (
235285 fileAt: url,
0 commit comments