ios: adds file uploading
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import SwiftUI
|
||||
|
||||
enum SybilAttachmentTone {
|
||||
case composer
|
||||
case user
|
||||
case assistant
|
||||
}
|
||||
|
||||
struct SybilAttachmentListView: View {
|
||||
var attachments: [ChatAttachment]
|
||||
var tone: SybilAttachmentTone
|
||||
var onRemove: ((String) -> Void)? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(attachments) { attachment in
|
||||
Group {
|
||||
if attachment.kind == .image {
|
||||
imageCard(attachment)
|
||||
} else {
|
||||
textCard(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func imageCard(_ attachment: ChatAttachment) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let image = SybilChatAttachmentSupport.image(for: attachment) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 180)
|
||||
.clipped()
|
||||
} else {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(Color.black.opacity(0.18))
|
||||
Image(systemName: "photo")
|
||||
.font(.system(size: 22, weight: .medium))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
}
|
||||
.frame(height: 140)
|
||||
}
|
||||
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "photo")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(titleColor.opacity(0.92))
|
||||
.frame(width: 26, height: 26)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.white.opacity(0.06))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(attachment.filename)
|
||||
.font(.sybil(.footnote, weight: .medium))
|
||||
.foregroundStyle(titleColor)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(attachment.mimeType)
|
||||
.font(.sybil(.caption2))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
if let onRemove {
|
||||
removeButton(for: attachment.id, onRemove: onRemove)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
.background(cardBackground)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(cardBorder, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func textCard(_ attachment: ChatAttachment) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(titleColor.opacity(0.92))
|
||||
.frame(width: 26, height: 26)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.white.opacity(0.06))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(attachment.filename)
|
||||
.font(.sybil(.footnote, weight: .medium))
|
||||
.foregroundStyle(titleColor)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(
|
||||
attachment.truncated == true
|
||||
? "\(attachment.mimeType) • truncated"
|
||||
: attachment.mimeType
|
||||
)
|
||||
.font(.sybil(.caption2))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
if let onRemove {
|
||||
removeButton(for: attachment.id, onRemove: onRemove)
|
||||
}
|
||||
}
|
||||
|
||||
Text(SybilChatAttachmentSupport.previewText(for: attachment))
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(bodyColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.black.opacity(0.16))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.white.opacity(0.05), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding(12)
|
||||
.background(cardBackground)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(cardBorder, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
private func removeButton(for attachmentID: String, onRemove: @escaping (String) -> Void) -> some View {
|
||||
Button {
|
||||
onRemove(attachmentID)
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
.frame(width: 24, height: 24)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.06))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Remove attachment")
|
||||
}
|
||||
|
||||
private var cardBackground: some ShapeStyle {
|
||||
switch tone {
|
||||
case .composer:
|
||||
return AnyShapeStyle(
|
||||
LinearGradient(
|
||||
colors: [SybilTheme.surface.opacity(0.86), SybilTheme.surfaceStrong.opacity(0.78)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
case .user:
|
||||
return AnyShapeStyle(Color.black.opacity(0.14))
|
||||
case .assistant:
|
||||
return AnyShapeStyle(
|
||||
LinearGradient(
|
||||
colors: [SybilTheme.surface.opacity(0.58), SybilTheme.surfaceStrong.opacity(0.42)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var cardBorder: Color {
|
||||
switch tone {
|
||||
case .composer:
|
||||
return SybilTheme.border.opacity(0.82)
|
||||
case .user:
|
||||
return Color.white.opacity(0.12)
|
||||
case .assistant:
|
||||
return SybilTheme.border.opacity(0.58)
|
||||
}
|
||||
}
|
||||
|
||||
private var titleColor: Color {
|
||||
switch tone {
|
||||
case .composer, .assistant:
|
||||
return SybilTheme.text
|
||||
case .user:
|
||||
return SybilTheme.text
|
||||
}
|
||||
}
|
||||
|
||||
private var bodyColor: Color {
|
||||
switch tone {
|
||||
case .composer, .assistant:
|
||||
return SybilTheme.text.opacity(0.94)
|
||||
case .user:
|
||||
return SybilTheme.text.opacity(0.96)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user