Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions App/Composition/AttachmentCardView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// AttachmentCardView.swift
//
// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import UIKit

/// Shared layout constants for attachment card views
enum AttachmentCardLayout {
/// Size of the thumbnail image (width and height)
static let imageSize: CGFloat = 60
/// Corner radius for the thumbnail image
static let imageCornerRadius: CGFloat = 4
/// Corner radius for the card container
static let cardCornerRadius: CGFloat = 8
/// Padding around the card edges
static let cardPadding: CGFloat = 12
/// Spacing between image and text labels
static let imageSpacing: CGFloat = 12
/// Top padding for labels (larger to optically center with image)
static let labelTopPadding: CGFloat = 16
/// Spacing between title and detail labels
static let titleDetailSpacing: CGFloat = 4
/// Size of action buttons (remove, etc.)
static let actionButtonSize: CGFloat = 30
}

/// Base class for attachment card views with common UI elements and styling
class AttachmentCardView: UIView {

let imageView: UIImageView = {
let iv = UIImageView()
iv.clipsToBounds = true
iv.layer.cornerRadius = AttachmentCardLayout.imageCornerRadius
iv.backgroundColor = .secondarySystemFill
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()

let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

let detailLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .caption1)
label.textColor = .secondaryLabel
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

override init(frame: CGRect) {
super.init(frame: frame)
configureCardAppearance()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
configureCardAppearance()
}

/// Updates the text color for title and detail labels
func updateTextColor(_ color: UIColor?) {
titleLabel.textColor = color
detailLabel.textColor = color?.withAlphaComponent(0.7)
}

/// Configures the card's appearance with standard styling
private func configureCardAppearance() {
backgroundColor = .clear
layer.cornerRadius = AttachmentCardLayout.cardCornerRadius
}
}
103 changes: 103 additions & 0 deletions App/Composition/AttachmentEditView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// AttachmentEditView.swift
//
// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import UIKit

/// A card-style view that shows existing attachment info with options to keep or delete it.
final class AttachmentEditView: AttachmentCardView {

private let actionSegmentedControl: UISegmentedControl = {
let items = [
LocalizedString("compose.attachment.action-keep"),
LocalizedString("compose.attachment.action-delete")
]
let sc = UISegmentedControl(items: items)
sc.selectedSegmentIndex = 0
sc.translatesAutoresizingMaskIntoConstraints = false
return sc
}()

var onActionChanged: ((AttachmentAction) -> Void)?

func updateSegmentedControlColors(selectedColor: UIColor?) {
actionSegmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .normal)

if let selectedColor = selectedColor {
actionSegmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
actionSegmentedControl.selectedSegmentTintColor = selectedColor
}
}

enum AttachmentAction {
case keep
case delete
}

override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
}

private func setupViews() {
imageView.contentMode = .scaleAspectFit
titleLabel.text = LocalizedString("compose.attachment.edit-title")

addSubview(imageView)
addSubview(titleLabel)
addSubview(detailLabel)
addSubview(actionSegmentedControl)

actionSegmentedControl.addTarget(self, action: #selector(actionChanged), for: .valueChanged)

NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AttachmentCardLayout.cardPadding),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: AttachmentCardLayout.cardPadding),
imageView.widthAnchor.constraint(equalToConstant: AttachmentCardLayout.imageSize),
imageView.heightAnchor.constraint(equalToConstant: AttachmentCardLayout.imageSize),

titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: AttachmentCardLayout.imageSpacing),
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: AttachmentCardLayout.labelTopPadding),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AttachmentCardLayout.cardPadding),

detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: AttachmentCardLayout.titleDetailSpacing),
detailLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),

actionSegmentedControl.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AttachmentCardLayout.cardPadding),
actionSegmentedControl.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AttachmentCardLayout.cardPadding),
actionSegmentedControl.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: AttachmentCardLayout.imageSpacing),
actionSegmentedControl.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -AttachmentCardLayout.cardPadding),
])
}

@objc private func actionChanged() {
let action: AttachmentAction = actionSegmentedControl.selectedSegmentIndex == 0 ? .keep : .delete
onActionChanged?(action)
}

func configure(filename: String, filesize: String?, image: UIImage? = nil) {
titleLabel.text = LocalizedString("compose.attachment.edit-title")
if let filesize = filesize {
detailLabel.text = "\(filename) • \(filesize)"
} else {
detailLabel.text = filename
}

if let image = image {
imageView.image = image
imageView.tintColor = nil
imageView.contentMode = .scaleAspectFit
} else {
let config = UIImage.SymbolConfiguration(pointSize: 40, weight: .light)
imageView.image = UIImage(systemName: "doc.fill", withConfiguration: config)
imageView.tintColor = .tertiaryLabel
imageView.contentMode = .center
}
}
}
100 changes: 100 additions & 0 deletions App/Composition/AttachmentPreviewView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// AttachmentPreviewView.swift
//
// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import AwfulCore
import os
import UIKit

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AttachmentPreviewView")

/// A card-style view that shows a preview of an attached image with options to remove it.
final class AttachmentPreviewView: AttachmentCardView {

private let removeButton: UIButton = {
let button = UIButton(type: .system)
let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)
button.setImage(UIImage(systemName: "xmark.circle.fill", withConfiguration: config), for: .normal)
button.tintColor = .secondaryLabel
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()

var onRemove: (() -> Void)?

func showResizingPlaceholder() {
titleLabel.text = LocalizedString("compose.attachment.resizing-title")
detailLabel.text = LocalizedString("compose.attachment.resizing-message")
imageView.image = nil
imageView.backgroundColor = .secondarySystemFill
}

override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
}

private func setupViews() {
imageView.contentMode = .scaleAspectFill
titleLabel.text = LocalizedString("compose.attachment.preview-title")

addSubview(imageView)
addSubview(titleLabel)
addSubview(detailLabel)
addSubview(removeButton)

removeButton.addTarget(self, action: #selector(didTapRemove), for: .touchUpInside)

NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AttachmentCardLayout.cardPadding),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: AttachmentCardLayout.cardPadding),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -AttachmentCardLayout.cardPadding),
imageView.widthAnchor.constraint(equalToConstant: AttachmentCardLayout.imageSize),
imageView.heightAnchor.constraint(equalToConstant: AttachmentCardLayout.imageSize),

titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: AttachmentCardLayout.imageSpacing),
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: AttachmentCardLayout.labelTopPadding),
titleLabel.trailingAnchor.constraint(equalTo: removeButton.leadingAnchor, constant: -8),

detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: AttachmentCardLayout.titleDetailSpacing),
detailLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),

removeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AttachmentCardLayout.cardPadding),
removeButton.centerYAnchor.constraint(equalTo: centerYAnchor),
removeButton.widthAnchor.constraint(equalToConstant: AttachmentCardLayout.actionButtonSize),
removeButton.heightAnchor.constraint(equalToConstant: AttachmentCardLayout.actionButtonSize),
])
}

@objc private func didTapRemove() {
onRemove?()
}

func configure(with attachment: ForumAttachment) {
titleLabel.text = LocalizedString("compose.attachment.preview-title")
imageView.backgroundColor = .clear
imageView.image = attachment.image

if let image = attachment.image {
let width = Int(image.size.width * image.scale)
let height = Int(image.size.height * image.scale)

do {
let (data, _, _) = try attachment.imageData()
let formatter = ByteCountFormatter()
formatter.countStyle = .file
let sizeString = formatter.string(fromByteCount: Int64(data.count))
detailLabel.text = "\(width) × \(height) • \(sizeString)"
} catch {
logger.error("Failed to get image data for attachment preview: \(error.localizedDescription)")
detailLabel.text = "\(width) × \(height)"
}
}
}
}
Loading
Loading