8e4a3e5f62
Setting a new base called setSide twice, so the first call kicked off a comparison against the *old* other side. Scans run concurrently, so that stale result could land last and win: the left had zoomed into the archive while the right still showed the parent, which is exactly what it looked like on screen. Both sides are now set in one go before comparing, and every scan is stamped so a slower, older run can no longer overwrite a newer result. Also renamed the pairing entries to mention 'set as base', since marking one side and picking the other achieves the same thing for items whose names differ and that wasn't obvious from the wording. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
65 lines
2.0 KiB
Swift
65 lines
2.0 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
/// Holds the open comparisons. Each tab is an independent `ComparisonModel`, so
|
|
/// switching tabs keeps every scan, selection and set of options intact.
|
|
@MainActor
|
|
@Observable
|
|
final class TabsModel {
|
|
private(set) var tabs: [ComparisonModel]
|
|
var selectedID: ComparisonModel.ID
|
|
|
|
init() {
|
|
let first = ComparisonModel()
|
|
tabs = [first]
|
|
selectedID = first.id
|
|
}
|
|
|
|
var selected: ComparisonModel {
|
|
tabs.first { $0.id == selectedID } ?? tabs[0]
|
|
}
|
|
|
|
@discardableResult
|
|
func newTab() -> ComparisonModel {
|
|
let tab = ComparisonModel()
|
|
tabs.append(tab)
|
|
selectedID = tab.id
|
|
return tab
|
|
}
|
|
|
|
func close(_ tab: ComparisonModel) {
|
|
guard let index = tabs.firstIndex(where: { $0.id == tab.id }) else { return }
|
|
tabs.remove(at: index)
|
|
// Never leave the window without a tab; reuse an empty one instead.
|
|
if tabs.isEmpty {
|
|
let fresh = ComparisonModel()
|
|
tabs = [fresh]
|
|
selectedID = fresh.id
|
|
return
|
|
}
|
|
if selectedID == tab.id {
|
|
selectedID = tabs[min(index, tabs.count - 1)].id
|
|
}
|
|
}
|
|
|
|
func closeSelected() { close(selected) }
|
|
|
|
/// Opens an arbitrary pair, either replacing the current comparison or in a
|
|
/// new tab, which is what makes a hand-picked pair the new base.
|
|
func openPair(left: URL, right: URL, inNewTab: Bool) {
|
|
let target = inNewTab ? newTab() : selected
|
|
target.setBoth(left: left, right: right)
|
|
selectedID = target.id
|
|
}
|
|
|
|
/// Opens a comparison in a tab: reuses the current one while it's still empty
|
|
/// (the common case right after launch) and otherwise starts a new one, so an
|
|
/// in-progress comparison is never overwritten.
|
|
func open(left: URL?, right: URL?) {
|
|
let target = selected.isPristine ? selected : newTab()
|
|
if let left { target.setSide(.left, to: left) }
|
|
if let right { target.setSide(.right, to: right) }
|
|
selectedID = target.id
|
|
}
|
|
}
|