native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Stabilize sidebar drag payload handling

onevcat bad7a46f 7400ec0f

+81 -63
+2 -4
supacode/Features/Repositories/Views/RepositorySectionView.swift
··· 36 36 } 37 37 } 38 38 } 39 - let isDragging = isDragActive 40 - 41 39 let appearance = repositoryAppearances[repository.id] ?? .empty 42 40 let header = HStack { 43 41 // Inner HStack groups the name row and the tab-count badge so they ··· 73 71 } 74 72 } 75 73 } 76 - if isRemovingRepository && !isDragging { 74 + if isRemovingRepository { 77 75 ProgressView() 78 76 .controlSize(.small) 79 77 .background { ··· 94 92 .help(color.displayName) 95 93 .accessibilityLabel(Text("Repo color: \(color.displayName)")) 96 94 } 97 - if isHovering && !isDragging { 95 + if isHovering { 98 96 Menu { 99 97 Button("Repo Settings") { 100 98 openRepoSettings()
+63 -51
supacode/Features/Repositories/Views/SidebarDragSupport.swift
··· 2 2 import UniformTypeIdentifiers 3 3 4 4 extension UTType { 5 - static let prowlSidebarRepositoryID = UTType(exportedAs: "com.onevcat.prowl.sidebar.repository-id") 6 - static let prowlSidebarWorktreeID = UTType(exportedAs: "com.onevcat.prowl.sidebar.worktree-id") 5 + nonisolated static let prowlSidebarDragPayload = UTType.plainText 7 6 } 8 7 9 8 enum SidebarDragProvider { 10 - static func repository(id: Repository.ID) -> NSItemProvider { 11 - itemProvider(id: id, type: .prowlSidebarRepositoryID) 9 + private nonisolated static let repositoryPrefix = "prowl-sidebar-repository:" 10 + private nonisolated static let worktreePrefix = "prowl-sidebar-worktree:" 11 + 12 + nonisolated static func repository(id: Repository.ID) -> NSItemProvider { 13 + itemProvider(payload: repositoryPrefix + id) 14 + } 15 + 16 + nonisolated static func worktree(id: Worktree.ID) -> NSItemProvider { 17 + itemProvider(payload: worktreePrefix + id) 18 + } 19 + 20 + nonisolated static func repositoryID(from data: Data) -> Repository.ID? { 21 + payload(from: data, prefix: repositoryPrefix) 12 22 } 13 23 14 - static func worktree(id: Worktree.ID) -> NSItemProvider { 15 - itemProvider(id: id, type: .prowlSidebarWorktreeID) 24 + nonisolated static func worktreeID(from data: Data) -> Worktree.ID? { 25 + payload(from: data, prefix: worktreePrefix) 16 26 } 17 27 18 - private static func itemProvider(id: String, type: UTType) -> NSItemProvider { 28 + private nonisolated static func itemProvider(payload: String) -> NSItemProvider { 19 29 let provider = NSItemProvider() 20 - provider.registerDataRepresentation(forTypeIdentifier: type.identifier, visibility: .all) { completion in 21 - completion(Data(id.utf8), nil) 30 + let loadHandler: (@escaping (Data?, (any Error)?) -> Void) -> Progress? = { completion in 31 + completion(Data(payload.utf8), nil) 22 32 return nil 23 33 } 34 + provider.registerDataRepresentation( 35 + forTypeIdentifier: UTType.prowlSidebarDragPayload.identifier, 36 + visibility: .all, 37 + loadHandler: loadHandler 38 + ) 24 39 return provider 40 + } 41 + 42 + private nonisolated static func payload(from data: Data, prefix: String) -> String? { 43 + guard let payload = String(data: data, encoding: .utf8), 44 + payload.hasPrefix(prefix) 45 + else { 46 + return nil 47 + } 48 + return String(payload.dropFirst(prefix.count)) 25 49 } 26 50 } 27 51 ··· 48 72 func performDrop(info: DropInfo) -> Bool { 49 73 let dropDestination = destination(info) 50 74 targetedDestination = nil 51 - guard let provider = info.itemProviders(for: [.prowlSidebarRepositoryID]).first else { 75 + guard let provider = info.itemProviders(for: [.prowlSidebarDragPayload]).first else { 52 76 onDragEnded() 53 77 return false 54 78 } 55 - provider.loadDataRepresentation(forTypeIdentifier: UTType.prowlSidebarRepositoryID.identifier) { data, _ in 79 + provider.loadDataRepresentation(forTypeIdentifier: UTType.prowlSidebarDragPayload.identifier) { data, _ in 56 80 guard let data, 57 - let repositoryID = String(data: data, encoding: .utf8), 81 + let repositoryID = SidebarDragProvider.repositoryID(from: data), 58 82 let source = repositoryOrderIDs.firstIndex(of: repositoryID), 59 83 source != dropDestination, 60 84 source + 1 != dropDestination ··· 94 118 func performDrop(info: DropInfo) -> Bool { 95 119 let dropDestination = destination(info) 96 120 targetedDestination = nil 97 - guard let provider = info.itemProviders(for: [.prowlSidebarWorktreeID]).first else { 121 + guard let provider = info.itemProviders(for: [.prowlSidebarDragPayload]).first else { 98 122 onDragEnded() 99 123 return false 100 124 } 101 - provider.loadDataRepresentation(forTypeIdentifier: UTType.prowlSidebarWorktreeID.identifier) { data, _ in 125 + provider.loadDataRepresentation(forTypeIdentifier: UTType.prowlSidebarDragPayload.identifier) { data, _ in 102 126 guard let data, 103 - let worktreeID = String(data: data, encoding: .utf8), 127 + let worktreeID = SidebarDragProvider.worktreeID(from: data), 104 128 let source = sectionIDs.firstIndex(of: worktreeID), 105 129 source != dropDestination, 106 130 source + 1 != dropDestination ··· 152 176 .overlay(alignment: .bottom) { 153 177 SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == index + 1) 154 178 } 155 - .background { 156 - GeometryReader { proxy in 157 - Color.clear 158 - .onDrop( 159 - of: [.prowlSidebarRepositoryID], 160 - delegate: SidebarRepositoryDropDelegate( 161 - destination: { info in 162 - info.location.y < proxy.size.height / 2 ? index : index + 1 163 - }, 164 - repositoryOrderIDs: repositoryOrderIDs, 165 - targetedDestination: targetedDestination, 166 - onDrop: onDrop, 167 - onDragEnded: onDragEnded 168 - ) 169 - ) 170 - .accessibilityHidden(true) 171 - } 172 - } 179 + .onDrop( 180 + of: [.prowlSidebarDragPayload], 181 + delegate: SidebarRepositoryDropDelegate( 182 + destination: { info in 183 + info.location.y < 24 ? index : index + 1 184 + }, 185 + repositoryOrderIDs: repositoryOrderIDs, 186 + targetedDestination: targetedDestination, 187 + onDrop: onDrop, 188 + onDragEnded: onDragEnded 189 + ) 190 + ) 173 191 } 174 192 175 193 func worktreeDropTarget( ··· 186 204 .overlay(alignment: .bottom) { 187 205 SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == index + 1, horizontalPadding: 28) 188 206 } 189 - .background { 190 - GeometryReader { proxy in 191 - Color.clear 192 - .onDrop( 193 - of: [.prowlSidebarWorktreeID], 194 - delegate: SidebarWorktreeDropDelegate( 195 - destination: { info in 196 - info.location.y < proxy.size.height / 2 ? index : index + 1 197 - }, 198 - sectionIDs: rowIDs, 199 - targetedDestination: targetedDestination, 200 - onDrop: onDrop, 201 - onDragEnded: onDragEnded 202 - ) 203 - ) 204 - .accessibilityHidden(true) 205 - } 206 - } 207 + .onDrop( 208 + of: [.prowlSidebarDragPayload], 209 + delegate: SidebarWorktreeDropDelegate( 210 + destination: { info in 211 + info.location.y < 18 ? index : index + 1 212 + }, 213 + sectionIDs: rowIDs, 214 + targetedDestination: targetedDestination, 215 + onDrop: onDrop, 216 + onDragEnded: onDragEnded 217 + ) 218 + ) 207 219 } 208 220 209 221 @ViewBuilder
+9 -1
supacode/Features/Repositories/Views/SidebarListView.swift
··· 118 118 return true 119 119 } 120 120 .focused($isSidebarFocused) 121 + .onAppear { 122 + resetSidebarDrag() 123 + } 121 124 .task(id: pendingSidebarReveal?.id) { 122 125 await revealPendingSidebarWorktree(pendingSidebarReveal, with: scrollProxy) 123 126 } ··· 256 259 257 260 private func endSidebarDrag() { 258 261 targetedRepositoryDropDestination = nil 259 - guard isDragActive else { return } 262 + isDragActive = false 263 + store.send(.worktreeOrdering(.setSidebarDragActive(false))) 264 + } 265 + 266 + private func resetSidebarDrag() { 267 + targetedRepositoryDropDestination = nil 260 268 isDragActive = false 261 269 store.send(.worktreeOrdering(.setSidebarDragActive(false))) 262 270 }
+7 -7
supacode/Features/Repositories/Views/WorktreeRowsView.swift
··· 115 115 moveDisabled: Bool, 116 116 shortcutHint: String? 117 117 ) -> some View { 118 - let isSidebarDragActive = store.state.isSidebarDragActive 118 + let isWorktreeDragActive = !draggingWorktreeIDs.isEmpty 119 119 let showsNotificationIndicator = terminalManager.hasUnseenNotifications(for: row.id) 120 120 let displayName = 121 121 if row.isDeleting { ··· 125 125 } else { 126 126 row.name 127 127 } 128 - let canShowRowActions = row.isRemovable && !isRepositoryRemoving && !isSidebarDragActive 128 + let canShowRowActions = row.isRemovable && !isRepositoryRemoving && !isWorktreeDragActive 129 129 let pinAction: (() -> Void)? = 130 130 canShowRowActions && !row.isMainWorktree 131 131 ? { togglePin(for: row.id, isPinned: row.isPinned) } ··· 156 156 let config = WorktreeRowViewConfig( 157 157 displayName: displayName, 158 158 worktreeName: worktreeName(for: row), 159 - isHovered: !isSidebarDragActive && hoveredWorktreeID == row.id, 160 - showsNotificationIndicator: !isSidebarDragActive && showsNotificationIndicator, 161 - notifications: isSidebarDragActive ? [] : notifications, 159 + isHovered: !isWorktreeDragActive && hoveredWorktreeID == row.id, 160 + showsNotificationIndicator: !isWorktreeDragActive && showsNotificationIndicator, 161 + notifications: isWorktreeDragActive ? [] : notifications, 162 162 onFocusNotification: onFocusNotification, 163 163 shortcutHint: shortcutHint, 164 164 pinAction: pinAction, ··· 304 304 let isSelected = selectedWorktreeIDs.contains(row.id) 305 305 let taskStatus = terminalManager.taskStatus(for: row.id) 306 306 let isRunScriptRunning = terminalManager.isRunScriptRunning(for: row.id) 307 - let isSidebarDragActive = store.state.isSidebarDragActive 307 + let isWorktreeDragActive = !draggingWorktreeIDs.isEmpty 308 308 return WorktreeRow( 309 309 name: config.displayName, 310 310 worktreeName: config.worktreeName, 311 311 info: row.info, 312 - showsPullRequestInfo: !isSidebarDragActive && !draggingWorktreeIDs.contains(row.id), 312 + showsPullRequestInfo: !isWorktreeDragActive, 313 313 isHovered: config.isHovered, 314 314 isPinned: row.isPinned, 315 315 isMainWorktree: row.isMainWorktree,