not working, claude is ass
This commit is contained in:
@@ -328,9 +328,16 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
/// The item currently being dragged (nil when no drag active)
|
/// The item currently being dragged (nil when no drag active)
|
||||||
private var draggingItem: ItineraryRowItem?
|
private var draggingItem: ItineraryRowItem?
|
||||||
|
|
||||||
/// Row indices that are invalid drop targets for the current drag
|
/// The day number the user is targeting during custom item drag (for stable positioning)
|
||||||
|
private var dragTargetDay: Int?
|
||||||
|
|
||||||
|
/// Row indices that are invalid drop targets for the current drag (for visual dimming)
|
||||||
private var invalidRowIndices: Set<Int> = []
|
private var invalidRowIndices: Set<Int> = []
|
||||||
|
|
||||||
|
/// Row indices that ARE valid drop targets - pre-calculated at drag start for stability
|
||||||
|
/// Using a sorted array enables O(log n) nearest-neighbor lookup
|
||||||
|
private var validDropRows: [Int] = []
|
||||||
|
|
||||||
/// IDs of games that act as barriers for the current travel drag (for gold highlighting)
|
/// IDs of games that act as barriers for the current travel drag (for gold highlighting)
|
||||||
private var barrierGameIds: Set<String> = []
|
private var barrierGameIds: Set<String> = []
|
||||||
|
|
||||||
@@ -605,7 +612,9 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
/// 4. Removes visual dimming and highlighting
|
/// 4. Removes visual dimming and highlighting
|
||||||
private func endDrag() {
|
private func endDrag() {
|
||||||
draggingItem = nil
|
draggingItem = nil
|
||||||
|
dragTargetDay = nil
|
||||||
invalidRowIndices = []
|
invalidRowIndices = []
|
||||||
|
validDropRows = []
|
||||||
barrierGameIds = []
|
barrierGameIds = []
|
||||||
isInValidZone = true
|
isInValidZone = true
|
||||||
|
|
||||||
@@ -629,41 +638,40 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
// Get valid day range from pre-calculated ranges
|
// Get valid day range from pre-calculated ranges
|
||||||
guard let validRange = travelValidRanges[travelId] else {
|
guard let validRange = travelValidRanges[travelId] else {
|
||||||
invalidRowIndices = []
|
invalidRowIndices = []
|
||||||
|
validDropRows = []
|
||||||
barrierGameIds = []
|
barrierGameIds = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate invalid row indices (rows outside valid day range)
|
// Calculate invalid and valid row indices based on day range
|
||||||
|
// Pre-calculate ALL valid positions for stable drag behavior
|
||||||
var invalidRows = Set<Int>()
|
var invalidRows = Set<Int>()
|
||||||
|
var validRows: [Int] = []
|
||||||
|
|
||||||
for (index, rowItem) in flatItems.enumerated() {
|
for (index, rowItem) in flatItems.enumerated() {
|
||||||
|
let dayNum: Int
|
||||||
switch rowItem {
|
switch rowItem {
|
||||||
case .dayHeader(let dayNum, _):
|
case .dayHeader(let d, _):
|
||||||
// Day header rows for days outside valid range are invalid drop targets
|
dayNum = d
|
||||||
// (travel would appear BEFORE this header, making it belong to this day)
|
case .games(_, let d):
|
||||||
if !validRange.contains(dayNum) {
|
dayNum = d
|
||||||
invalidRows.insert(index)
|
case .travel(_, let d):
|
||||||
}
|
dayNum = d
|
||||||
case .games(_, let dayNum):
|
|
||||||
// Games on days outside valid range are invalid
|
|
||||||
if !validRange.contains(dayNum) {
|
|
||||||
invalidRows.insert(index)
|
|
||||||
}
|
|
||||||
case .travel(_, let dayNum):
|
|
||||||
// Other travel rows on days outside valid range
|
|
||||||
if !validRange.contains(dayNum) {
|
|
||||||
invalidRows.insert(index)
|
|
||||||
}
|
|
||||||
case .customItem(let item):
|
case .customItem(let item):
|
||||||
// Custom items on days outside valid range
|
dayNum = item.day
|
||||||
if !validRange.contains(item.day) {
|
}
|
||||||
invalidRows.insert(index)
|
|
||||||
}
|
if validRange.contains(dayNum) {
|
||||||
|
validRows.append(index)
|
||||||
|
} else {
|
||||||
|
invalidRows.insert(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidRowIndices = invalidRows
|
invalidRowIndices = invalidRows
|
||||||
|
validDropRows = validRows // Already sorted since we iterate in order
|
||||||
|
|
||||||
// Find barrier games using ItineraryConstraints
|
// Find barrier games using ItineraryConstraints
|
||||||
// First, find or create the ItineraryItem for this travel
|
|
||||||
if let travelItem = findItineraryItem(for: segment),
|
if let travelItem = findItineraryItem(for: segment),
|
||||||
let constraints = constraints {
|
let constraints = constraints {
|
||||||
let barriers = constraints.barrierGames(for: travelItem)
|
let barriers = constraints.barrierGames(for: travelItem)
|
||||||
@@ -678,15 +686,22 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
/// Custom items can go on any day, but we mark certain positions as
|
/// Custom items can go on any day, but we mark certain positions as
|
||||||
/// less ideal (e.g., directly on day headers or before travel).
|
/// less ideal (e.g., directly on day headers or before travel).
|
||||||
private func calculateCustomItemDragZones(item: ItineraryItem) {
|
private func calculateCustomItemDragZones(item: ItineraryItem) {
|
||||||
// Custom items are flexible - no day restrictions
|
// Custom items are flexible - can go anywhere except ON day headers
|
||||||
// But we can mark day headers as invalid since items shouldn't drop ON them
|
// Pre-calculate ALL valid row indices for stable drag behavior
|
||||||
var invalidRows = Set<Int>()
|
var invalidRows = Set<Int>()
|
||||||
|
var validRows: [Int] = []
|
||||||
|
|
||||||
for (index, rowItem) in flatItems.enumerated() {
|
for (index, rowItem) in flatItems.enumerated() {
|
||||||
if case .dayHeader = rowItem {
|
if case .dayHeader = rowItem {
|
||||||
invalidRows.insert(index)
|
invalidRows.insert(index)
|
||||||
|
} else {
|
||||||
|
// All non-header rows are valid drop targets
|
||||||
|
validRows.append(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidRowIndices = invalidRows
|
invalidRowIndices = invalidRows
|
||||||
|
validDropRows = validRows // Already sorted since we iterate in order
|
||||||
barrierGameIds = [] // No barrier highlighting for custom items
|
barrierGameIds = [] // No barrier highlighting for custom items
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,6 +874,16 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
// Calculate the new day and sortOrder for the dropped position
|
// Calculate the new day and sortOrder for the dropped position
|
||||||
let destinationDay = dayNumber(forRow: destinationIndexPath.row)
|
let destinationDay = dayNumber(forRow: destinationIndexPath.row)
|
||||||
let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
|
let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
|
||||||
|
|
||||||
|
// DEBUG: Log the final state after insertion
|
||||||
|
print("🎯 [Drop] source=\(sourceIndexPath.row) → dest=\(destinationIndexPath.row)")
|
||||||
|
print("🎯 [Drop] flatItems around dest:")
|
||||||
|
for i in max(0, destinationIndexPath.row - 2)...min(flatItems.count - 1, destinationIndexPath.row + 2) {
|
||||||
|
let marker = i == destinationIndexPath.row ? "→" : " "
|
||||||
|
print("🎯 [Drop] \(marker) [\(i)] \(flatItems[i])")
|
||||||
|
}
|
||||||
|
print("🎯 [Drop] Calculated day=\(destinationDay), sortOrder=\(sortOrder)")
|
||||||
|
|
||||||
onCustomItemMoved?(customItem.id, destinationDay, sortOrder)
|
onCustomItemMoved?(customItem.id, destinationDay, sortOrder)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -924,7 +949,7 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
|
|
||||||
// DRAG START DETECTION
|
// DRAG START DETECTION
|
||||||
// The first call to this method indicates drag has started.
|
// The first call to this method indicates drag has started.
|
||||||
// Initialize drag state, calculate invalid zones, and trigger pickup haptic.
|
// Initialize drag state, calculate valid/invalid zones, and trigger pickup haptic.
|
||||||
if draggingItem == nil {
|
if draggingItem == nil {
|
||||||
beginDrag(at: sourceIndexPath)
|
beginDrag(at: sourceIndexPath)
|
||||||
}
|
}
|
||||||
@@ -941,75 +966,10 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
checkZoneTransition(at: proposedRow)
|
checkZoneTransition(at: proposedRow)
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .travel(let segment, _):
|
case .travel, .customItem:
|
||||||
// TRAVEL CONSTRAINT LOGIC
|
// UNIFIED CONSTRAINT LOGIC using pre-calculated validDropRows
|
||||||
// Travel can only be on certain days (must finish games in departure city,
|
// This eliminates bouncing by using a simple lookup instead of recalculating
|
||||||
// must arrive by game time in destination city)
|
return snapToValidRow(proposedRow)
|
||||||
|
|
||||||
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
|
||||||
|
|
||||||
guard let validRange = travelValidRanges[travelId] else {
|
|
||||||
print("No valid range for travel: \(travelId)")
|
|
||||||
return proposedDestinationIndexPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out which day the user is trying to drop onto
|
|
||||||
var proposedDay = dayForTravelAtProposed(row: proposedRow, excluding: sourceIndexPath.row)
|
|
||||||
|
|
||||||
// Clamp to valid range - this is what creates the "snap" effect
|
|
||||||
if proposedDay < validRange.lowerBound {
|
|
||||||
proposedDay = validRange.lowerBound
|
|
||||||
} else if proposedDay > validRange.upperBound {
|
|
||||||
proposedDay = validRange.upperBound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert day number back to row position (right before that day's header)
|
|
||||||
if let headerRow = dayHeaderRow(forDay: proposedDay) {
|
|
||||||
var targetRow = headerRow
|
|
||||||
// Account for the fact that the source row will be removed first
|
|
||||||
// This is a UITableView quirk - the destination index is calculated
|
|
||||||
// as if the source has already been removed
|
|
||||||
if sourceIndexPath.row < headerRow {
|
|
||||||
targetRow -= 1
|
|
||||||
}
|
|
||||||
return IndexPath(row: max(0, targetRow), section: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proposedDestinationIndexPath
|
|
||||||
|
|
||||||
case .customItem(let customItem):
|
|
||||||
// CUSTOM ITEM CONSTRAINT LOGIC
|
|
||||||
// Custom items are flexible - they can go anywhere within the itinerary,
|
|
||||||
// but we prevent dropping in places that would be confusing
|
|
||||||
|
|
||||||
// Use ItineraryConstraints to validate position
|
|
||||||
let proposedDay = dayNumber(forRow: proposedRow)
|
|
||||||
let proposedSortOrder = calculateSortOrder(at: proposedRow)
|
|
||||||
|
|
||||||
if let constraints = constraints {
|
|
||||||
if !constraints.isValidPosition(for: customItem, day: proposedDay, sortOrder: proposedSortOrder) {
|
|
||||||
// If position is invalid, try to find a valid nearby position
|
|
||||||
// For now, just prevent dropping on day headers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't drop ON a day header - go after it instead
|
|
||||||
if proposedRow < flatItems.count, case .dayHeader = flatItems[proposedRow] {
|
|
||||||
return IndexPath(row: proposedRow + 1, section: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't drop before travel at the start of a day
|
|
||||||
// (would visually appear before the travel card, which is confusing)
|
|
||||||
if proposedRow < flatItems.count, case .travel = flatItems[proposedRow] {
|
|
||||||
// Find the day header after this travel and drop after the header
|
|
||||||
for i in proposedRow..<flatItems.count {
|
|
||||||
if case .dayHeader = flatItems[i] {
|
|
||||||
return IndexPath(row: i + 1, section: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return IndexPath(row: proposedRow, section: 0)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Fixed items (shouldn't reach here since canMoveRowAt returns false)
|
// Fixed items (shouldn't reach here since canMoveRowAt returns false)
|
||||||
@@ -1017,6 +977,55 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Snaps a proposed row to the nearest valid drop position.
|
||||||
|
///
|
||||||
|
/// Uses pre-calculated `validDropRows` for O(log n) lookup.
|
||||||
|
/// If the proposed row is already valid, returns it immediately (prevents bouncing).
|
||||||
|
/// Otherwise, finds the nearest valid row using binary search.
|
||||||
|
private func snapToValidRow(_ proposedRow: Int) -> IndexPath {
|
||||||
|
// Fast path: if proposed row is valid, return it immediately
|
||||||
|
// This is the key to preventing bouncing - no recalculation needed
|
||||||
|
if validDropRows.contains(proposedRow) {
|
||||||
|
return IndexPath(row: proposedRow, section: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proposed row is invalid - find the nearest valid row
|
||||||
|
guard !validDropRows.isEmpty else {
|
||||||
|
return IndexPath(row: proposedRow, section: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search for insertion point
|
||||||
|
var low = 0
|
||||||
|
var high = validDropRows.count
|
||||||
|
|
||||||
|
while low < high {
|
||||||
|
let mid = (low + high) / 2
|
||||||
|
if validDropRows[mid] < proposedRow {
|
||||||
|
low = mid + 1
|
||||||
|
} else {
|
||||||
|
high = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// low is now the insertion point - check neighbors to find nearest
|
||||||
|
let before = low > 0 ? validDropRows[low - 1] : nil
|
||||||
|
let after = low < validDropRows.count ? validDropRows[low] : nil
|
||||||
|
|
||||||
|
let nearest: Int
|
||||||
|
if let b = before, let a = after {
|
||||||
|
// Both neighbors exist - pick the closer one
|
||||||
|
nearest = (proposedRow - b) <= (a - proposedRow) ? b : a
|
||||||
|
} else if let b = before {
|
||||||
|
nearest = b
|
||||||
|
} else if let a = after {
|
||||||
|
nearest = a
|
||||||
|
} else {
|
||||||
|
nearest = proposedRow // Fallback (shouldn't happen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return IndexPath(row: nearest, section: 0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculates which day a travel segment would belong to if dropped at a proposed position.
|
/// Calculates which day a travel segment would belong to if dropped at a proposed position.
|
||||||
///
|
///
|
||||||
/// Similar to `dayForTravelAt`, but used during the drag (before the move completes).
|
/// Similar to `dayForTravelAt`, but used during the drag (before the move completes).
|
||||||
|
|||||||
Reference in New Issue
Block a user