Add a Files tab to the project page

This commit is contained in:
Ian Gulliver
2026-10-22 19:17:08 -07:00
parent 2aba745531
commit d8ce1cee41
12 changed files with 769 additions and 437 deletions
+31 -13
View File
@@ -18,11 +18,14 @@ enum AppLink {
return c.url!
}
static func file(_ taskId: String, _ path: String) -> URL {
static func file(_ root: FileRoot, _ path: String) -> URL {
var c = URLComponents()
c.scheme = "byre-app"
c.host = "file"
c.queryItems = [URLQueryItem(name: "task", value: taskId), URLQueryItem(name: "path", value: path)]
c.queryItems = switch root {
case .task(let id): [URLQueryItem(name: "task", value: id), URLQueryItem(name: "path", value: path)]
case .project(let id): [URLQueryItem(name: "project", value: id), URLQueryItem(name: "path", value: path)]
}
return c.url!
}
@@ -61,7 +64,11 @@ func openAppLink(_ url: URL, _ store: Store, _ overlays: Overlays) -> OpenURLAct
case "route":
store.route(q["hash"] ?? "")
case "file":
store.openFile(q["task"] ?? "", q["path"] ?? "")
if let project = q["project"] {
store.showInProjectFiles(project, q["path"] ?? "", overlays)
} else {
store.openFile(q["task"] ?? "", q["path"] ?? "")
}
case "preview":
let taskId = q["task"] ?? ""
store.route("#/task/\(taskId)")
@@ -169,17 +176,23 @@ struct InlineRenderer {
let store: Store
let size: CGFloat
let taskId: String?
let project: String?
let weight: CGFloat
let dir: String
private let root: String
init(store: Store, size: CGFloat, taskId: String?, weight: CGFloat = 400, dir: String = "") {
init(store: Store, size: CGFloat, taskId: String?, project: String? = nil, weight: CGFloat = 400, dir: String = "") {
self.store = store
self.size = size
self.taskId = taskId
self.project = project
self.weight = weight
self.dir = dir
root = taskId.flatMap { store.findTask($0)?.worktreePath } ?? ""
if let taskId {
root = store.findTask(taskId)?.worktreePath ?? ""
} else {
root = project.flatMap { id in store.projects.first { $0.id == id }?.repoPath } ?? ""
}
}
private struct Style {
@@ -275,7 +288,7 @@ struct InlineRenderer {
s.italic = true
render(inner, s, &out)
case .image(let src, let alt):
if let source = worktreeImageSource(src, taskId, dir: dir) {
if let source = worktreeImageSource(src, files, dir: dir) {
out.append(.image(source, src: src))
} else {
link(src, [.text(alt.isEmpty ? src : alt)], style, &out)
@@ -327,11 +340,15 @@ struct InlineRenderer {
render(inner, style, &out)
}
private var files: FileRoot? {
taskId.map(FileRoot.task) ?? project.map(FileRoot.project)
}
private func fileLink(_ path: String) -> URL? {
guard let taskId, !root.isEmpty, let p = normalPath(path) else {
guard let files, !root.isEmpty, let p = normalPath(path) else {
return nil
}
return AppLink.file(taskId, p)
return AppLink.file(files, p)
}
private func codeFile(_ code: String) -> URL? {
@@ -451,6 +468,7 @@ struct MDContext {
var size: CGFloat = 13
var color: Color = Palette.text
var taskId: String?
var project: String?
var choice: ((MDItem, Bool) -> AnyView)?
var discuss: ((MDItem) -> AnyView)?
var gridColumns = 4
@@ -590,7 +608,7 @@ private struct FlowBuilder {
}
private func inline(_ inlines: [MDInline], _ size: CGFloat) -> NSAttributedString {
let a = InlineRenderer(store: store, size: size, taskId: ctx.taskId, weight: ctx.weight, dir: ctx.dir).text(inlines)
let a = InlineRenderer(store: store, size: size, taskId: ctx.taskId, project: ctx.project, weight: ctx.weight, dir: ctx.dir).text(inlines)
return nativeString(a)
}
@@ -902,7 +920,7 @@ struct InlineView: View {
@Environment(\.centerImages) private var centerImages
var body: some View {
let segs = InlineRenderer(store: store, size: ctx.size, taskId: ctx.taskId, weight: ctx.weight, dir: ctx.dir).segments(inlines)
let segs = InlineRenderer(store: store, size: ctx.size, taskId: ctx.taskId, project: ctx.project, weight: ctx.weight, dir: ctx.dir).segments(inlines)
VStack(alignment: .leading, spacing: 4) {
ForEach(Array(segs.enumerated()), id: \.offset) { _, seg in
switch seg {
@@ -948,7 +966,7 @@ private struct BlockView: View {
case .paragraph(let inlines):
InlineView(inlines: inlines, ctx: ctx)
case .heading(let level, let inlines):
InlineView(inlines: [.strong(inlines)], ctx: MDContext(size: level == 3 ? 14 : 13, color: ctx.color, taskId: ctx.taskId, dir: ctx.dir))
InlineView(inlines: [.strong(inlines)], ctx: MDContext(size: level == 3 ? 14 : 13, color: ctx.color, taskId: ctx.taskId, project: ctx.project, dir: ctx.dir))
.padding(.top, 4)
case .rule:
Rectangle()
@@ -956,7 +974,7 @@ private struct BlockView: View {
.frame(height: 1)
.padding(.vertical, 2)
case .quote(let blocks):
MarkdownView(blocks: blocks, ctx: MDContext(size: ctx.size, color: Palette.muted, taskId: ctx.taskId, dir: ctx.dir))
MarkdownView(blocks: blocks, ctx: MDContext(size: ctx.size, color: Palette.muted, taskId: ctx.taskId, project: ctx.project, dir: ctx.dir))
.padding(.leading, 13)
.overlay(alignment: .leading) {
Rectangle().fill(Palette.border).frame(width: 3)
@@ -1107,7 +1125,7 @@ private struct DetailsBlock: View {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(open ? "▾" : "▸")
.foregroundStyle(Palette.accent)
InlineView(inlines: summary, ctx: MDContext(size: ctx.size, color: Palette.accent, taskId: ctx.taskId, dir: ctx.dir))
InlineView(inlines: summary, ctx: MDContext(size: ctx.size, color: Palette.accent, taskId: ctx.taskId, project: ctx.project, dir: ctx.dir))
}
.contentShape(Rectangle())
}