@@ -50,7 +50,7 @@ struct SelectiveFolderView: View {
5050
5151 List ( selection: $listSelection) {
5252 Section ( self . prefix. isEmpty ? " Files kept on device " : " Files in ' \( self . prefix) ' kept on this device " ) {
53- PathsOutlineGroup ( paths: self . selectedFilteredPaths, disableIntermediateSelection: true ) { item, isIntermediate in
53+ PathsOutlineGroup ( paths: self . selectedFilteredPaths, disableIntermediateSelection: false ) { item, isIntermediate in
5454 if item. isEmpty {
5555 EmptyView ( )
5656 }
@@ -60,7 +60,13 @@ struct SelectiveFolderView: View {
6060 folder: folder,
6161 deselect: {
6262 Task {
63- await self . deselectItems ( [ item] )
63+ let entry = try folder. getFileInformation ( item)
64+ if entry. isExplicitlySelected ( ) {
65+ await self . deselectItems ( [ item] )
66+ }
67+ else {
68+ await self . deselectPrefix ( item)
69+ }
6470 }
6571 }
6672 )
@@ -334,14 +340,22 @@ struct SelectiveFolderView: View {
334340
335341 private nonisolated func deselect( paths: Set < String > , includingLastCopy: Bool ) async throws {
336342 var paths = paths
337- // If we should not delete if our copy is the only one available, check availability for each file first
338- if !includingLastCopy {
339- for path in paths {
340- let entry = try folder. getFileInformation ( path)
343+
344+ for path in paths {
345+ let entry = try folder. getFileInformation ( path)
346+ if !entry. isExplicitlySelected ( ) {
347+ Log . info ( " Not deselecting \( path) , it is not explicitly selected " )
348+ paths. remove ( path)
349+ continue
350+ }
351+
352+ // If we should not delete if our copy is the only one available, check availability for each file first
353+ if !includingLastCopy {
341354 let peersWithFullCopy = try entry. peersWithFullCopy ( )
342355 if peersWithFullCopy. count ( ) == 0 {
343356 Log . info ( " Not removing \( path) , it is the last copy " )
344357 paths. remove ( path)
358+ continue
345359 }
346360 }
347361 }
@@ -358,6 +372,18 @@ struct SelectiveFolderView: View {
358372 }
359373 }
360374
375+ // Deselects all items contained here that are explicitly selected, and available on at least one other device
376+ private func deselectPrefix( _ path: String ) async {
377+ do {
378+ let prefix = path. withoutEndingSlash + " / "
379+ let items = Set ( self . selectedFilteredPaths. filter { $0. starts ( with: prefix) } )
380+ try await self . deselect ( paths: items, includingLastCopy: false )
381+ }
382+ catch {
383+ Log . warn ( " Could not deselect prefix \( path) : \( error. localizedDescription) " )
384+ }
385+ }
386+
361387 private func deselectItems( _ paths: [ String ] ) async {
362388 do {
363389 let verdicts = paths. reduce ( into: [ : ] ) { dict, p in
@@ -393,7 +419,7 @@ private struct SelectiveFileView: View {
393419 Label ( entry. fileName ( ) , systemImage: entry. systemImage) . strikethrough ( )
394420 }
395421 else if !entry. isExplicitlySelected ( ) {
396- Label ( entry. fileName ( ) , systemImage : entry . systemImage ) . opacity ( 0.8 )
422+ IntermediateSelectiveFileView ( entry: entry , deselect : deselect )
397423 }
398424 else {
399425 SelectedFileView ( entry: entry, folder: folder, deselect: deselect)
@@ -416,6 +442,29 @@ private struct SelectiveFileView: View {
416442 }
417443}
418444
445+ /** Entry in the list that is not explicitly selected itself, but can deselect a whole prefix */
446+ private struct IntermediateSelectiveFileView : View {
447+ let entry : SushitrainEntry
448+ let deselect : ( ) -> Void
449+
450+ var body : some View {
451+ HStack {
452+ Label ( entry. fileName ( ) , systemImage: entry. systemImage) . opacity ( 0.8 )
453+ Spacer ( )
454+ Menu {
455+ Section ( " For all files in here " ) {
456+ Button ( " Remove from this device if available on other devices " ) {
457+ self . deselect ( )
458+ }
459+ }
460+ } label: {
461+ Label ( " " , systemImage: " pin.slash.fill " ) . accessibilityLabel ( " Actions " )
462+ } . buttonStyle ( . borderless)
463+ }
464+ }
465+ }
466+
467+ /** Entry in the list that is explicitly selected itself */
419468private struct SelectedFileView : View {
420469 @Environment ( AppState . self) private var appState
421470 let entry : SushitrainEntry
0 commit comments