201 lines
7.4 KiB
Swift
201 lines
7.4 KiB
Swift
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<Void, Never>?
|
|
|
|
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
|
|
}
|