Add tabs to the page view for comparing pages
This commit is contained in:
@@ -5,7 +5,7 @@ import Observation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
enum Page: Equatable {
|
enum Page: Hashable {
|
||||||
case file(taskId: String, path: String)
|
case file(taskId: String, path: String)
|
||||||
case preview(taskId: String, port: Int)
|
case preview(taskId: String, port: Int)
|
||||||
|
|
||||||
@@ -15,6 +15,13 @@ enum Page: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFile: Bool {
|
||||||
|
if case .file = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func title(_ store: Store) -> String {
|
func title(_ store: Store) -> String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -38,11 +45,43 @@ func isPage(_ path: String) -> Bool {
|
|||||||
pageExts.contains((path as NSString).pathExtension.lowercased())
|
pageExts.contains((path as NSString).pathExtension.lowercased())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ShownPages {
|
||||||
|
var pages: [Page]
|
||||||
|
var current: Page
|
||||||
|
}
|
||||||
|
|
||||||
extension Store {
|
extension Store {
|
||||||
func openPage(_ page: Page) {
|
func openPage(_ page: Page) {
|
||||||
shownPages[page.taskId] = page
|
var shown = shownPages[page.taskId] ?? ShownPages(pages: [], current: page)
|
||||||
|
if !shown.pages.contains(page) {
|
||||||
|
shown.pages.append(page)
|
||||||
|
}
|
||||||
|
shown.current = page
|
||||||
|
shownPages[page.taskId] = shown
|
||||||
sheet = Sheet(owner: .task(page.taskId), view: .tab(.page))
|
sheet = Sheet(owner: .task(page.taskId), view: .tab(.page))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closePage(_ page: Page) {
|
||||||
|
guard var shown = shownPages[page.taskId], let i = shown.pages.firstIndex(of: page) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shown.pages.remove(at: i)
|
||||||
|
if shown.pages.isEmpty {
|
||||||
|
shownPages[page.taskId] = nil
|
||||||
|
if sheet?.tab == .page {
|
||||||
|
sheet = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if shown.current == page {
|
||||||
|
shown.current = shown.pages[min(i, shown.pages.count - 1)]
|
||||||
|
}
|
||||||
|
shownPages[page.taskId] = shown
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentPage() -> Page? {
|
||||||
|
selectedTaskId.flatMap { shownPages[$0]?.current }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -239,23 +278,37 @@ private final class PageWatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PageView: View {
|
struct PageTabs: View {
|
||||||
let page: Page
|
let shown: ShownPages
|
||||||
@Environment(Store.self) private var store
|
@Environment(Store.self) private var store
|
||||||
@ViewState private var watch = PageWatch()
|
@ViewState private var watch = PageWatch()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
VStack(spacing: 0) {
|
||||||
if let url = page.url(store) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
WebPage(url: url, reloads: watch.reloads)
|
HStack(spacing: 4) {
|
||||||
} else {
|
ForEach(shown.pages, id: \.self) { page in
|
||||||
EmptyText(text: "Loading…")
|
PageTabButton(page: page, active: page == shown.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.overlay(alignment: .bottom) {
|
||||||
|
Rectangle().fill(Palette.border).frame(height: 1)
|
||||||
|
}
|
||||||
|
ZStack {
|
||||||
|
ForEach(shown.pages, id: \.self) { page in
|
||||||
|
if let url = page.url(store) {
|
||||||
|
WebPage(url: url, reloads: page.isFile ? watch.reloads : 0, shown: page == shown.current)
|
||||||
|
} else if page == shown.current {
|
||||||
|
EmptyText(text: "Loading…")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task(id: page) {
|
.task(id: shown.current.taskId) {
|
||||||
guard case .file(let taskId, _) = page else {
|
let taskId = shown.current.taskId
|
||||||
return
|
|
||||||
}
|
|
||||||
let watch = watch
|
let watch = watch
|
||||||
let api = store.api
|
let api = store.api
|
||||||
let pages = store.pages
|
let pages = store.pages
|
||||||
@@ -271,9 +324,38 @@ struct PageView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct PageTabButton: View {
|
||||||
|
let page: Page
|
||||||
|
let active: Bool
|
||||||
|
@Environment(Store.self) private var store
|
||||||
|
@ViewState private var hover = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
PaneTabButton(label: page.title(store), active: active) {
|
||||||
|
store.openPage(page)
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
store.closePage(page)
|
||||||
|
} label: {
|
||||||
|
Text("✕")
|
||||||
|
.font(Fonts.font(11))
|
||||||
|
.foregroundStyle(hover ? Palette.text : Palette.muted)
|
||||||
|
.padding(.trailing, 6)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Close this page")
|
||||||
|
.onHover { hover = $0 }
|
||||||
|
}
|
||||||
|
.help(page.url(store)?.absoluteString ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct WebPage: NSViewRepresentable {
|
private struct WebPage: NSViewRepresentable {
|
||||||
let url: URL
|
let url: URL
|
||||||
let reloads: Int
|
let reloads: Int
|
||||||
|
let shown: Bool
|
||||||
|
|
||||||
final class Coordinator: NSObject, WKUIDelegate {
|
final class Coordinator: NSObject, WKUIDelegate {
|
||||||
var url: URL?
|
var url: URL?
|
||||||
@@ -300,6 +382,7 @@ private struct WebPage: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ view: WKWebView, context: Context) {
|
func updateNSView(_ view: WKWebView, context: Context) {
|
||||||
|
view.isHidden = !shown
|
||||||
let c = context.coordinator
|
let c = context.coordinator
|
||||||
if c.url != url {
|
if c.url != url {
|
||||||
c.url = url
|
c.url = url
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ struct TaskTabBody: View {
|
|||||||
case .files:
|
case .files:
|
||||||
FilesView()
|
FilesView()
|
||||||
case .page:
|
case .page:
|
||||||
if let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }) {
|
if let shown = store.selectedTaskId.flatMap({ store.shownPages[$0] }) {
|
||||||
PageView(page: page)
|
PageTabs(shown: shown)
|
||||||
}
|
}
|
||||||
case .terminal:
|
case .terminal:
|
||||||
TerminalTab()
|
TerminalTab()
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ private struct SheetPanel: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
if sheet.tab == .page, let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }), let url = page.url(store) {
|
if sheet.tab == .page, let url = store.currentPage()?.url(store) {
|
||||||
Button {
|
Button {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ final class Store {
|
|||||||
var connected = false
|
var connected = false
|
||||||
private(set) var focus = Focus()
|
private(set) var focus = Focus()
|
||||||
var sheet: Sheet?
|
var sheet: Sheet?
|
||||||
var shownPages: [String: Page] = [:]
|
var shownPages: [String: ShownPages] = [:]
|
||||||
var leadSessions: [AgentSession] = []
|
var leadSessions: [AgentSession] = []
|
||||||
let leadFeed: Feed
|
let leadFeed: Feed
|
||||||
var jump: Jump?
|
var jump: Jump?
|
||||||
|
|||||||
Reference in New Issue
Block a user