import UIKit func errorAlert(_ message: String) -> UIAlertController { let alert = UIAlertController(title: "Something went wrong", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) return alert } extension UIViewController { func show(error: Error) { present(errorAlert(error.localizedDescription), animated: true) } func beginNavigationLoading(_ spinner: UIActivityIndicatorView) { guard navigationItem.rightBarButtonItem == nil else { return } spinner.startAnimating() navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner) } func endNavigationLoading(_ spinner: UIActivityIndicatorView) { spinner.stopAnimating() if navigationItem.rightBarButtonItem?.customView === spinner { navigationItem.rightBarButtonItem = nil } } } @MainActor class RefreshingTableViewController: UITableViewController { private let spinner = UIActivityIndicatorView(style: .medium) var loadingTask: Task? init() { super.init(style: .plain) tableView.backgroundColor = .systemGroupedBackground tableView.separatorInset = .zero tableView.refreshControl = UIRefreshControl() tableView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged) navigationItem.largeTitleDisplayMode = .never } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } deinit { loadingTask?.cancel() } func loadContent(refreshing: Bool) {} func beginLoading(refreshing: Bool) { if !refreshing { beginNavigationLoading(spinner) } } func endLoading() { endNavigationLoading(spinner) refreshControl?.endRefreshing() } @objc private func refreshRequested() { loadContent(refreshing: true) } } final class EmptyBackgroundView: UIView { init(title: String, detail: String) { super.init(frame: .zero) let titleLabel = UILabel() titleLabel.text = title titleLabel.font = .preferredFont(forTextStyle: .title2) titleLabel.textAlignment = .center let detailLabel = UILabel() detailLabel.text = detail detailLabel.font = .preferredFont(forTextStyle: .body) detailLabel.textColor = .secondaryLabel detailLabel.textAlignment = .center detailLabel.numberOfLines = 0 let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel]) stack.axis = .vertical stack.spacing = 8 stack.translatesAutoresizingMaskIntoConstraints = false addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28), stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28), stack.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } } extension UIColor { convenience init?(hex: String) { let value = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) guard value.count == 6, let rgb = Int(value, radix: 16) else { return nil } self.init( red: CGFloat((rgb >> 16) & 0xff) / 255, green: CGFloat((rgb >> 8) & 0xff) / 255, blue: CGFloat(rgb & 0xff) / 255, alpha: 1 ) } } func markdown(_ source: String, textStyle: UIFont.TextStyle = .body) -> NSAttributedString { let attributed = (try? AttributedString( markdown: source, options: .init(interpretedSyntax: .full) )) ?? AttributedString(source) var mutable = attributed mutable.font = .preferredFont(forTextStyle: textStyle) mutable.foregroundColor = UIColor.label return NSAttributedString(mutable) } func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, image: UIImage? = nil) { var content = cell.defaultContentConfiguration() content.text = title content.secondaryText = detail content.secondaryTextProperties.numberOfLines = 2 content.image = image content.imageProperties.tintColor = .tintColor cell.contentConfiguration = content cell.backgroundColor = .systemBackground } func configureRepositoryContentCell(_ cell: UITableViewCell, row: RepositoryContentRow) { let directory = row.kind == "dir" let detail = directory ? "Folder" : ByteCountFormatter.string(fromByteCount: row.size, countStyle: .file) configureTextCell( cell, title: row.name, detail: detail, image: UIImage(systemName: directory ? "folder.fill" : "doc") ) cell.accessoryType = .disclosureIndicator } @MainActor func showRepositoryContent( _ row: RepositoryContentRow, context: AppContext, owner: String, repository: String, navigationController: UINavigationController? ) { let destination: UIViewController = row.kind == "dir" ? RepositoryDirectoryViewController( context: context, owner: owner, repository: repository, path: row.path ) : RepositoryFileViewController( context: context, owner: owner, repository: repository, path: row.path ) navigationController?.pushViewController(destination, animated: true) } func sourceLanguage(_ path: String) -> String? { let filename = (path as NSString).lastPathComponent.lowercased() let filenames = [ "cmakelists.txt": "cmake", "dockerfile": "dockerfile", "gemfile": "ruby", "makefile": "makefile", "podfile": "ruby", ] if let language = filenames[filename] { return language } let languages = [ "asm": "x86asm", "c": "c", "cc": "cpp", "clj": "clojure", "cpp": "cpp", "cs": "csharp", "css": "css", "dart": "dart", "ex": "elixir", "exs": "elixir", "fs": "fsharp", "go": "go", "groovy": "groovy", "h": "cpp", "hpp": "cpp", "hs": "haskell", "html": "xml", "java": "java", "jl": "julia", "js": "javascript", "json": "json", "jsx": "javascript", "kt": "kotlin", "kts": "kotlin", "lua": "lua", "m": "objectivec", "md": "markdown", "mm": "objectivec", "php": "php", "pl": "perl", "plist": "xml", "ps1": "powershell", "py": "python", "r": "r", "rb": "ruby", "rs": "rust", "scala": "scala", "scss": "scss", "sh": "bash", "sql": "sql", "swift": "swift", "toml": "ini", "ts": "typescript", "tsx": "typescript", "txt": "plaintext", "xml": "xml", "yaml": "yaml", "yml": "yaml", ] return languages[(path as NSString).pathExtension.lowercased()] } func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString { let output = NSMutableAttributedString() if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) { let attachment = NSTextAttachment(image: image) attachment.bounds = CGRect(x: 0, y: -2, width: font.pointSize, height: font.pointSize) output.append(NSAttributedString(attachment: attachment)) output.append(NSAttributedString(string: " ")) } output.append(NSAttributedString(string: text, attributes: [ .font: font, .foregroundColor: color, ])) return output }