mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-01-12 22:47:34 +08:00
New ContextMenu UX for managing Issues & PRs (#1912)
* wip * New managing UX using ContextMenu
This commit is contained in:
32
Classes/Issues/IssueManageButton.swift
Normal file
32
Classes/Issues/IssueManageButton.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// IssueManageButton.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ryan Nystrom on 7/15/18.
|
||||
// Copyright © 2018 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
func IssueManageButton() -> UIButton {
|
||||
let button = UIButton()
|
||||
if let image = UIImage(named: "three-bars")?.withRenderingMode(.alwaysTemplate) {
|
||||
button.setImage(image, for: .normal)
|
||||
}
|
||||
button.tintColor = .white
|
||||
button.backgroundColor = Styles.Colors.Blue.medium.color
|
||||
let width = Styles.Sizes.tableCellHeight
|
||||
button.frame = CGRect(
|
||||
origin: .zero,
|
||||
size: CGSize(width: width, height: width)
|
||||
)
|
||||
button.layer.cornerRadius = width / 2
|
||||
button.layer.shadowOpacity = 0.12
|
||||
button.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
button.layer.shadowColor = UIColor.black.cgColor
|
||||
button.layer.shadowRadius = 4
|
||||
button.layer.borderColor = UIColor(white: 1, alpha: 0.2).cgColor
|
||||
button.layer.borderWidth = 1
|
||||
button.accessibilityLabel = NSLocalizedString("Show options", comment: "")
|
||||
return button
|
||||
}
|
||||
331
Classes/Issues/IssueManagingContextController.swift
Normal file
331
Classes/Issues/IssueManagingContextController.swift
Normal file
@@ -0,0 +1,331 @@
|
||||
//
|
||||
// IssueManagingContextController.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ryan Nystrom on 7/15/18.
|
||||
// Copyright © 2018 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ContextMenu
|
||||
import GitHubAPI
|
||||
|
||||
final class IssueManagingContextController: NSObject, ContextMenuDelegate {
|
||||
|
||||
// Int with lowers-highest permissions to do rank comparisons
|
||||
enum Permissions: Int {
|
||||
case none
|
||||
case author
|
||||
case collaborator
|
||||
}
|
||||
|
||||
let manageButton: UIView
|
||||
|
||||
private var _permissions: Permissions = .none {
|
||||
didSet {
|
||||
updateButtonVisibility()
|
||||
}
|
||||
}
|
||||
var permissions: Permissions {
|
||||
get { return _permissions }
|
||||
set (newValue) {
|
||||
// only allow setting to the higher permissions
|
||||
if newValue.rawValue > _permissions.rawValue {
|
||||
_permissions = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
let model: IssueDetailsModel
|
||||
var resultID: String? {
|
||||
didSet {
|
||||
guard let result = self.result else { return }
|
||||
if result.viewerCanUpdate {
|
||||
permissions = .author
|
||||
}
|
||||
updateButtonVisibility()
|
||||
}
|
||||
}
|
||||
let client: GithubClient
|
||||
weak var viewController: UIViewController?
|
||||
|
||||
init(model: IssueDetailsModel, client: GithubClient) {
|
||||
let button = IssueManageButton()
|
||||
manageButton = button
|
||||
self.client = client
|
||||
self.model = model
|
||||
|
||||
super.init()
|
||||
|
||||
button.isHidden = true
|
||||
button.addTarget(self, action: #selector(onButton(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
var result: IssueResult? {
|
||||
guard let id = resultID else { return nil }
|
||||
return client.cache.get(id: id) as IssueResult?
|
||||
}
|
||||
|
||||
func updateButtonVisibility() {
|
||||
let hidden: Bool
|
||||
if resultID == nil {
|
||||
hidden = true
|
||||
} else {
|
||||
switch permissions {
|
||||
case .none: hidden = true
|
||||
case .author, .collaborator: hidden = false
|
||||
}
|
||||
}
|
||||
manageButton.isHidden = hidden
|
||||
}
|
||||
|
||||
enum Action {
|
||||
case labels
|
||||
case milestone
|
||||
case assignees
|
||||
case reviewers
|
||||
case unlock
|
||||
case lock
|
||||
case reopen
|
||||
case close
|
||||
}
|
||||
|
||||
func item(_ action: Action) -> ContrastContextMenuItem {
|
||||
let title: String
|
||||
let icon: String
|
||||
|
||||
switch action {
|
||||
case .labels:
|
||||
title = Constants.Strings.labels
|
||||
icon = "tag"
|
||||
case .milestone:
|
||||
title = Constants.Strings.milestone
|
||||
icon = "milestone"
|
||||
case .assignees:
|
||||
title = Constants.Strings.assignees
|
||||
icon = "person"
|
||||
case .reviewers:
|
||||
title = Constants.Strings.reviewers
|
||||
icon = "reviewer"
|
||||
case .unlock:
|
||||
title = NSLocalizedString("Unlock", comment: "")
|
||||
icon = "key"
|
||||
case .lock:
|
||||
title = NSLocalizedString("Lock", comment: "")
|
||||
icon = "lock"
|
||||
case .reopen:
|
||||
title = Constants.Strings.reopen
|
||||
icon = "sync"
|
||||
case .close:
|
||||
title = Constants.Strings.close
|
||||
icon = "x"
|
||||
}
|
||||
|
||||
let separator: Bool
|
||||
switch action {
|
||||
case .reopen, .close: separator = true
|
||||
default: separator = false
|
||||
}
|
||||
|
||||
return ContrastContextMenuItem(title: title, iconName: icon, separator: separator, action: actionBlock(action))
|
||||
}
|
||||
|
||||
func actionBlock(_ action: Action) -> (ContrastContextMenu) -> Void {
|
||||
return { [weak self] menu in
|
||||
menu.dismiss(animated: true)
|
||||
guard let strongSelf = self else { return }
|
||||
switch action {
|
||||
case .labels: strongSelf.presentContextMenu(with: strongSelf.newLabelsController())
|
||||
case .milestone: strongSelf.presentContextMenu(with: strongSelf.newMilestonesController())
|
||||
case .assignees: strongSelf.presentContextMenu(with: strongSelf.newPeopleController(type: .assignee))
|
||||
case .reviewers: strongSelf.presentContextMenu(with: strongSelf.newPeopleController(type: .reviewer))
|
||||
case .unlock: strongSelf.lock(false)
|
||||
case .lock: strongSelf.lock(true)
|
||||
case .reopen: strongSelf.close(false)
|
||||
case .close: strongSelf.close(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func onButton(sender: UIButton) {
|
||||
guard let result = self.result,
|
||||
let viewController = self.viewController
|
||||
else { return }
|
||||
|
||||
var items = [ContrastContextMenuItem]()
|
||||
|
||||
if case .collaborator = permissions {
|
||||
items += [ item(.labels), item(.milestone), item(.assignees) ]
|
||||
if result.pullRequest {
|
||||
items.append(item(.reviewers))
|
||||
}
|
||||
if result.status.locked {
|
||||
items.append(item(.unlock))
|
||||
} else {
|
||||
items.append(item(.lock))
|
||||
}
|
||||
}
|
||||
|
||||
switch result.status.status {
|
||||
case .closed:
|
||||
items.append(item(.reopen))
|
||||
case .open:
|
||||
items.append(item(.close))
|
||||
case .merged: break
|
||||
}
|
||||
|
||||
ContextMenu.shared.show(
|
||||
sourceViewController: viewController,
|
||||
viewController: ContrastContextMenu(items: items),
|
||||
options: ContextMenu.Options(
|
||||
containerStyle: ContextMenu.ContainerStyle(
|
||||
xPadding: 0,
|
||||
yPadding: 4,
|
||||
backgroundColor: Styles.Colors.menuBackgroundColor.color
|
||||
),
|
||||
menuStyle: .minimal
|
||||
),
|
||||
sourceView: sender
|
||||
)
|
||||
}
|
||||
|
||||
func newLabelsController() -> UIViewController {
|
||||
return LabelsViewController(
|
||||
selected: result?.labels.labels ?? [],
|
||||
client: client,
|
||||
owner: model.owner,
|
||||
repo: model.repo
|
||||
)
|
||||
}
|
||||
|
||||
func newMilestonesController() -> UIViewController {
|
||||
return MilestonesViewController(
|
||||
client: client,
|
||||
owner: model.owner,
|
||||
repo: model.repo,
|
||||
selected: result?.milestone
|
||||
)
|
||||
}
|
||||
|
||||
func newPeopleController(type: PeopleViewController.PeopleType) -> UIViewController {
|
||||
let result = self.result
|
||||
let selections: [String]
|
||||
let exclusions: [String]
|
||||
switch type {
|
||||
case .assignee:
|
||||
selections = result?.assignee.users.map { $0.login } ?? []
|
||||
exclusions = []
|
||||
case .reviewer:
|
||||
selections = result?.reviewers?.users.map { $0.login } ?? []
|
||||
if let isPullRequest = result?.pullRequest,
|
||||
let pullRequestAuthor = result?.rootComment?.details.login,
|
||||
isPullRequest {
|
||||
exclusions = [pullRequestAuthor]
|
||||
} else {
|
||||
exclusions = []
|
||||
}
|
||||
}
|
||||
return PeopleViewController(
|
||||
selections: selections,
|
||||
exclusions: exclusions,
|
||||
type: type,
|
||||
client: client,
|
||||
owner: model.owner,
|
||||
repo: model.repo
|
||||
)
|
||||
}
|
||||
|
||||
func presentContextMenu(with controller: UIViewController) {
|
||||
guard let viewController = self.viewController else { return }
|
||||
ContextMenu.shared.show(
|
||||
sourceViewController: viewController,
|
||||
viewController: controller,
|
||||
options: ContextMenu.Options(
|
||||
containerStyle: ContextMenu.ContainerStyle(
|
||||
backgroundColor: Styles.Colors.menuBackgroundColor.color
|
||||
)
|
||||
),
|
||||
delegate: self
|
||||
)
|
||||
}
|
||||
|
||||
func close(_ doClose: Bool) {
|
||||
guard let previous = result else { return }
|
||||
client.setStatus(
|
||||
previous: previous,
|
||||
owner: model.owner,
|
||||
repo: model.repo,
|
||||
number: model.number,
|
||||
close: doClose
|
||||
)
|
||||
Haptic.triggerNotification(.success)
|
||||
}
|
||||
|
||||
func lock(_ doLock: Bool) {
|
||||
guard let previous = result else { return }
|
||||
client.setLocked(
|
||||
previous: previous,
|
||||
owner: model.owner,
|
||||
repo: model.repo,
|
||||
number: model.number,
|
||||
locked: doLock
|
||||
)
|
||||
Haptic.triggerNotification(.success)
|
||||
}
|
||||
|
||||
func didDismiss(selected labels: [RepositoryLabel]) {
|
||||
guard let previous = result else { return }
|
||||
client.mutateLabels(
|
||||
previous: previous,
|
||||
owner: model.owner,
|
||||
repo: model.repo,
|
||||
number: model.number,
|
||||
labels: labels
|
||||
)
|
||||
}
|
||||
|
||||
func didDismiss(controller: PeopleViewController) {
|
||||
guard let previous = result else { return }
|
||||
|
||||
let mutationType: V3AddPeopleRequest.PeopleType
|
||||
switch controller.type {
|
||||
case .assignee: mutationType = .assignees
|
||||
case .reviewer: mutationType = .reviewers
|
||||
}
|
||||
|
||||
client.addPeople(
|
||||
type: mutationType,
|
||||
previous: previous,
|
||||
owner: model.owner,
|
||||
repo: model.repo,
|
||||
number: model.number,
|
||||
people: controller.selected
|
||||
)
|
||||
}
|
||||
|
||||
func didDismiss(controller: MilestonesViewController) {
|
||||
guard let previous = result else { return }
|
||||
client.setMilestone(
|
||||
previous: previous,
|
||||
owner: model.owner,
|
||||
repo: model.repo,
|
||||
number: model.number,
|
||||
milestone: controller.selected
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: ContextMenuDelegate
|
||||
|
||||
func contextMenuWillDismiss(viewController: UIViewController, animated: Bool) {
|
||||
if let milestones = viewController as? MilestonesViewController {
|
||||
didDismiss(controller: milestones)
|
||||
} else if let people = viewController as? PeopleViewController {
|
||||
didDismiss(controller: people)
|
||||
} else if let labels = viewController as? LabelsViewController {
|
||||
didDismiss(selected: labels.selected)
|
||||
}
|
||||
}
|
||||
|
||||
func contextMenuDidDismiss(viewController: UIViewController, animated: Bool) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import SnapKit
|
||||
import FlatCache
|
||||
import MessageViewController
|
||||
import Squawk
|
||||
import ContextMenu
|
||||
import GitHubAPI
|
||||
|
||||
final class IssuesViewController:
|
||||
MessageViewController,
|
||||
@@ -23,7 +25,6 @@ final class IssuesViewController:
|
||||
FeedSelectionProviding,
|
||||
IssueNeckLoadSectionControllerDelegate,
|
||||
FlatCacheListener,
|
||||
IssueManagingNavSectionControllerDelegate,
|
||||
IssueCommentSectionControllerDelegate {
|
||||
|
||||
private let client: GithubClient
|
||||
@@ -32,6 +33,7 @@ final class IssuesViewController:
|
||||
private let textActionsController = TextActionsController()
|
||||
private var bookmarkNavController: BookmarkNavigationController? = nil
|
||||
private var autocompleteController: AutocompleteController!
|
||||
private let manageController: IssueManagingContextController
|
||||
|
||||
private var needsScrollToBottom = false
|
||||
private var lastTimelineElement: ListDiffable?
|
||||
@@ -67,6 +69,8 @@ final class IssuesViewController:
|
||||
hidden = true
|
||||
}
|
||||
self.setMessageView(hidden: hidden, animated: trueUnlessReduceMotionEnabled)
|
||||
|
||||
self.manageController.resultID = resultID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +98,7 @@ final class IssuesViewController:
|
||||
self.model = model
|
||||
self.addCommentClient = AddCommentClient(client: client)
|
||||
self.needsScrollToBottom = scrollToBottom
|
||||
self.manageController = IssueManagingContextController(model: model, client: client)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
@@ -109,6 +114,8 @@ final class IssuesViewController:
|
||||
self.addCommentClient.addListener(listener: self)
|
||||
|
||||
cacheKey = "issue.\(model.owner).\(model.repo).\(model.number)"
|
||||
|
||||
manageController.viewController = self
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -167,10 +174,11 @@ final class IssuesViewController:
|
||||
|
||||
actions.frame = CGRect(x: 0, y: 0, width: 0, height: 32)
|
||||
messageView.add(contentView: actions)
|
||||
|
||||
|
||||
//show disabled bookmark button until issue has finished loading
|
||||
navigationItem.rightBarButtonItems = [ moreOptionsItem, BookmarkNavigationController.disabledNavigationItem ]
|
||||
|
||||
view.addSubview(manageController.manageButton)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
@@ -198,6 +206,16 @@ final class IssuesViewController:
|
||||
feed.collectionView.updateSafeInset(container: view, base: Styles.Sizes.threadInset)
|
||||
}
|
||||
|
||||
override func didLayout() {
|
||||
let manageButtonSize = manageController.manageButton.bounds.size
|
||||
manageController.manageButton.frame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: view.bounds.width - manageButtonSize.width - Styles.Sizes.gutter - view.safeAreaInsets.right,
|
||||
y: messageView.frame.minY - manageButtonSize.height - Styles.Sizes.gutter),
|
||||
size: manageButtonSize
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
|
||||
@objc func didPressButton(_ sender: Any?) {
|
||||
@@ -269,6 +287,9 @@ final class IssuesViewController:
|
||||
case .read, .none: collab = false
|
||||
}
|
||||
self?.viewerIsCollaborator = collab
|
||||
if collab {
|
||||
self?.manageController.permissions = .collaborator
|
||||
}
|
||||
// avoid finishLoading() so empty view doesn't appear
|
||||
self?.feed.adapter.performUpdates(animated: trueUnlessReduceMotionEnabled)
|
||||
case .error:
|
||||
@@ -397,14 +418,6 @@ final class IssuesViewController:
|
||||
// side effect so to jump to the last element when auto scrolling
|
||||
lastTimelineElement = objects.last
|
||||
|
||||
if viewerIsCollaborator || current.viewerCanUpdate {
|
||||
objects.append(IssueManagingModel(
|
||||
objectId: current.id,
|
||||
pullRequest: current.pullRequest,
|
||||
role: viewerIsCollaborator ? .collaborator : .author
|
||||
))
|
||||
}
|
||||
|
||||
if viewerIsCollaborator,
|
||||
current.status.status == .open,
|
||||
let merge = current.mergeModel {
|
||||
@@ -415,10 +428,6 @@ final class IssuesViewController:
|
||||
}
|
||||
|
||||
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
|
||||
if let object = object as? ListDiffable, object === manageKey {
|
||||
return IssueManagingNavSectionController(delegate: self)
|
||||
}
|
||||
|
||||
switch object {
|
||||
case is IssueTitleModel: return IssueTitleSectionController()
|
||||
case is IssueCommentModel:
|
||||
@@ -448,7 +457,6 @@ final class IssuesViewController:
|
||||
case is IssueNeckLoadModel: return IssueNeckLoadSectionController(delegate: self)
|
||||
case is Milestone: return IssueMilestoneSectionController(issueModel: model)
|
||||
case is IssueFileChangesModel: return IssueViewFilesSectionController(issueModel: model, client: client)
|
||||
case is IssueManagingModel: return IssueManagingSectionController(model: model, client: client)
|
||||
case is IssueMergeModel: return IssueMergeSectionController(model: model, client: client, resultID: resultID)
|
||||
case is IssueTargetBranchModel: return IssueTargetBranchSectionController()
|
||||
default: fatalError("Unhandled object: \(object)")
|
||||
@@ -537,12 +545,6 @@ final class IssuesViewController:
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: IssueManagingNavSectionControllerDelegate
|
||||
|
||||
func didSelect(managingNavController: IssueManagingNavSectionController) {
|
||||
feed.collectionView.scrollToBottom(animated: true)
|
||||
}
|
||||
|
||||
// MARK: IssueCommentSectionControllerDelegate
|
||||
|
||||
func didSelectReply(to sectionController: IssueCommentSectionController, commentModel: IssueCommentModel) {
|
||||
|
||||
@@ -74,7 +74,9 @@ ContextMenuDelegate {
|
||||
init(model: IssueDetailsModel, client: GithubClient) {
|
||||
self.model = model
|
||||
self.client = client
|
||||
|
||||
super.init()
|
||||
|
||||
inset = UIEdgeInsets(top: Styles.Sizes.gutter, left: 0, bottom: Styles.Sizes.gutter, right: 0)
|
||||
minimumInteritemSpacing = Styles.Sizes.rowSpacing
|
||||
minimumLineSpacing = Styles.Sizes.rowSpacing
|
||||
|
||||
@@ -17,7 +17,8 @@ final class LabelMenuCell: SelectableCell {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = .white
|
||||
backgroundColor = nil
|
||||
contentView.backgroundColor = nil
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits |= UIAccessibilityTraitButton
|
||||
@@ -30,10 +31,8 @@ final class LabelMenuCell: SelectableCell {
|
||||
}
|
||||
|
||||
contentView.addSubview(button)
|
||||
button.titleLabel?.font = Styles.Text.secondaryBold.preferredFont
|
||||
button.titleLabel?.font = Styles.Text.bodyBold.preferredFont
|
||||
button.layer.cornerRadius = Styles.Sizes.avatarCornerRadius
|
||||
button.layer.borderColor = Styles.Colors.Gray.border.color.cgColor
|
||||
button.layer.borderWidth = 1 / UIScreen.main.scale
|
||||
button.clipsToBounds = true
|
||||
button.isUserInteractionEnabled = false
|
||||
button.contentEdgeInsets = UIEdgeInsets(
|
||||
@@ -48,7 +47,8 @@ final class LabelMenuCell: SelectableCell {
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
|
||||
let border = contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
|
||||
border.backgroundColor = Styles.Colors.Gray.medium.color
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
||||
@@ -25,7 +25,7 @@ final class LabelSectionController: ListSwiftSectionController<RepositoryLabel>
|
||||
size: {
|
||||
return CGSize(
|
||||
width: $0.collection.containerSize.width,
|
||||
height: Styles.Sizes.tableCellHeight
|
||||
height: Styles.Sizes.tableCellHeightLarge
|
||||
)
|
||||
},
|
||||
configure: { [selected] in
|
||||
|
||||
@@ -27,8 +27,9 @@ final class LabelsViewController: BaseListViewController2<String>, BaseListViewC
|
||||
self.client = client
|
||||
self.request = RepositoryLabelsQuery(owner: owner, repo: repo)
|
||||
super.init(emptyErrorMessage: NSLocalizedString("No labels found", comment: ""))
|
||||
title = NSLocalizedString("Labels", comment: "")
|
||||
preferredContentSize = CGSize(width: 200, height: 240)
|
||||
preferredContentSize = Styles.Sizes.contextMenuSize
|
||||
title = Constants.Strings.labels
|
||||
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
|
||||
dataSource = self
|
||||
}
|
||||
|
||||
@@ -36,6 +37,11 @@ final class LabelsViewController: BaseListViewController2<String>, BaseListViewC
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
var selected: [RepositoryLabel] {
|
||||
|
||||
@@ -18,7 +18,8 @@ final class MilestoneCell2: SelectableCell {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = .white
|
||||
backgroundColor = nil
|
||||
contentView.backgroundColor = nil
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits |= UIAccessibilityTraitButton
|
||||
@@ -31,8 +32,8 @@ final class MilestoneCell2: SelectableCell {
|
||||
}
|
||||
|
||||
contentView.addSubview(label)
|
||||
label.font = Styles.Text.secondaryBold.preferredFont
|
||||
label.textColor = Styles.Colors.Gray.dark.color
|
||||
label.font = Styles.Text.bodyBold.preferredFont
|
||||
label.textColor = .white
|
||||
label.snp.makeConstraints { make in
|
||||
make.left.equalTo(Styles.Sizes.gutter)
|
||||
make.right.lessThanOrEqualTo(checkedImageView.snp.left)
|
||||
@@ -41,14 +42,15 @@ final class MilestoneCell2: SelectableCell {
|
||||
|
||||
contentView.addSubview(detailLabel)
|
||||
detailLabel.font = Styles.Text.secondary.preferredFont
|
||||
detailLabel.textColor = Styles.Colors.Gray.medium.color
|
||||
detailLabel.textColor = Styles.Colors.Gray.light.color
|
||||
detailLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(label)
|
||||
make.right.lessThanOrEqualTo(checkedImageView.snp.left)
|
||||
make.top.equalTo(label.snp.bottom)
|
||||
}
|
||||
|
||||
contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
|
||||
let border = contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
|
||||
border.backgroundColor = Styles.Colors.Gray.medium.color
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
||||
@@ -24,7 +24,7 @@ final class MilestoneSectionController: ListSwiftSectionController<MilestoneView
|
||||
size: {
|
||||
return CGSize(
|
||||
width: $0.collection.containerSize.width,
|
||||
height: Styles.Sizes.tableCellHeight
|
||||
height: Styles.Sizes.tableCellHeightLarge
|
||||
)
|
||||
},
|
||||
configure: {
|
||||
@@ -34,6 +34,7 @@ final class MilestoneSectionController: ListSwiftSectionController<MilestoneView
|
||||
},
|
||||
didSelect: { [weak self] context in
|
||||
guard let strongSelf = self else { return }
|
||||
context.deselect(animated: true)
|
||||
strongSelf.delegate?.didSelect(value: context.value, controller: strongSelf)
|
||||
})
|
||||
]
|
||||
|
||||
@@ -40,8 +40,9 @@ MilestoneSectionControllerDelegate {
|
||||
self.repo = repo
|
||||
self.selected = selected
|
||||
super.init(emptyErrorMessage: NSLocalizedString("No milestones found.", comment: ""))
|
||||
title = NSLocalizedString("Milestones", comment: "")
|
||||
preferredContentSize = CGSize(width: 200, height: 240)
|
||||
title = Constants.Strings.milestone
|
||||
preferredContentSize = Styles.Sizes.contextMenuSize
|
||||
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
|
||||
dataSource = self
|
||||
}
|
||||
|
||||
@@ -49,6 +50,11 @@ MilestoneSectionControllerDelegate {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
|
||||
}
|
||||
|
||||
// MARK: Overrides
|
||||
|
||||
override func fetch(page: String?) {
|
||||
|
||||
@@ -19,7 +19,8 @@ final class PeopleCell: SelectableCell {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = .white
|
||||
backgroundColor = nil
|
||||
contentView.backgroundColor = nil
|
||||
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(usernameLabel)
|
||||
@@ -31,7 +32,8 @@ final class PeopleCell: SelectableCell {
|
||||
make.size.equalTo(Styles.Sizes.avatar)
|
||||
}
|
||||
|
||||
usernameLabel.font = Styles.Text.secondary.preferredFont
|
||||
usernameLabel.font = Styles.Text.bodyBold.preferredFont
|
||||
usernameLabel.textColor = .white
|
||||
usernameLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(self.contentView)
|
||||
make.left.equalTo(avatarImageView.snp.right).offset(Styles.Sizes.gutter)
|
||||
@@ -47,9 +49,10 @@ final class PeopleCell: SelectableCell {
|
||||
make.size.equalTo(Styles.Sizes.icon)
|
||||
}
|
||||
|
||||
avatarImageView.configureForAvatar()
|
||||
avatarImageView.configureForAvatar(border: false)
|
||||
|
||||
contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
|
||||
let border = contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
|
||||
border.backgroundColor = Styles.Colors.Gray.medium.color
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
||||
@@ -31,7 +31,7 @@ final class PeopleSectionController: ListSwiftSectionController<IssueAssigneeVie
|
||||
size: {
|
||||
return CGSize(
|
||||
width: $0.collection.containerSize.width,
|
||||
height: Styles.Sizes.tableCellHeight
|
||||
height: Styles.Sizes.tableCellHeightLarge
|
||||
)
|
||||
},
|
||||
configure: { [selected] in
|
||||
|
||||
@@ -54,7 +54,8 @@ PeopleSectionControllerDelegate {
|
||||
case .reviewer: title = NSLocalizedString("Reviewers", comment: "")
|
||||
}
|
||||
|
||||
preferredContentSize = CGSize(width: 280, height: 240)
|
||||
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
|
||||
preferredContentSize = Styles.Sizes.contextMenuSize
|
||||
updateSelectionCount()
|
||||
}
|
||||
|
||||
@@ -62,6 +63,11 @@ PeopleSectionControllerDelegate {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
var selected: [IssueAssigneeViewModel] {
|
||||
|
||||
108
Classes/View Controllers/ContrastContextMenu.swift
Normal file
108
Classes/View Controllers/ContrastContextMenu.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// ContrastContextMenu.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ryan Nystrom on 7/15/18.
|
||||
// Copyright © 2018 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct ContrastContextMenuItem {
|
||||
|
||||
let title: String
|
||||
let iconName: String?
|
||||
let separator: Bool
|
||||
let action: ((ContrastContextMenu) -> Void)?
|
||||
|
||||
init(
|
||||
title: String,
|
||||
iconName: String? = nil,
|
||||
separator: Bool = false,
|
||||
action: ((ContrastContextMenu) -> Void)? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.iconName = iconName
|
||||
self.separator = separator
|
||||
self.action = action
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class ContrastContextMenu: UITableViewController {
|
||||
|
||||
private class Cell: UITableViewCell {
|
||||
static let reuseIdentifier = "cell"
|
||||
var border: UIView? = nil
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
selectedBackgroundView = UIView()
|
||||
selectedBackgroundView?.backgroundColor = Styles.Colors.Gray.medium.color
|
||||
contentView.backgroundColor = nil
|
||||
backgroundColor = nil
|
||||
|
||||
textLabel?.font = Styles.Text.bodyBold.preferredFont
|
||||
textLabel?.textColor = .white
|
||||
|
||||
imageView?.tintColor = Styles.Colors.Blue.medium.color
|
||||
|
||||
border = contentView.addBorder(.top)
|
||||
border?.backgroundColor = Styles.Colors.Gray.medium.color
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private let items: [ContrastContextMenuItem]
|
||||
|
||||
init(items: [ContrastContextMenuItem]) {
|
||||
self.items = items
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(Cell.self, forCellReuseIdentifier: Cell.reuseIdentifier)
|
||||
tableView.rowHeight = Styles.Sizes.tableCellHeight
|
||||
tableView.separatorStyle = .none
|
||||
tableView.reloadData()
|
||||
tableView.layoutIfNeeded()
|
||||
preferredContentSize = CGSize(width: 180, height: tableView.contentSize.height)
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return items.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let item = items[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath)
|
||||
cell.textLabel?.text = item.title
|
||||
if let iconName = item.iconName {
|
||||
cell.imageView?.image = UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
if let cell = cell as? Cell {
|
||||
cell.border?.isHidden = !item.separator
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: UITableViewDelegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
items[indexPath.row].action?(self)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,4 +69,20 @@ extension UINavigationItem {
|
||||
}
|
||||
}
|
||||
|
||||
func configure(title: NSAttributedString, accessibilityLabel: String? = nil) {
|
||||
let label = UILabel()
|
||||
label.backgroundColor = .clear
|
||||
label.numberOfLines = 0
|
||||
label.textAlignment = .center
|
||||
label.attributedText = title
|
||||
label.lineBreakMode = .byTruncatingHead
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.sizeToFit()
|
||||
|
||||
titleView = label
|
||||
if let accessibilityLabel = accessibilityLabel { // prevent from setting to nil if we don't provide anything, use default instead
|
||||
titleView?.accessibilityLabel = accessibilityLabel
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,5 +49,9 @@ enum Constants {
|
||||
static let bookmark = NSLocalizedString("Bookmark", comment: "")
|
||||
static let removeBookmark = NSLocalizedString("Remove Bookmark", comment: "")
|
||||
static let bookmarks = NSLocalizedString("Bookmarks", comment: "")
|
||||
static let labels = NSLocalizedString("Labels", comment: "")
|
||||
static let milestone = NSLocalizedString("Milestone", comment: "")
|
||||
static let assignees = NSLocalizedString("Assignees", comment: "")
|
||||
static let reviewers = NSLocalizedString("Reviewers", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ enum Styles {
|
||||
right: Styles.Sizes.commentGutter
|
||||
)
|
||||
static let maxImageHeight: CGFloat = 300
|
||||
static let contextMenuSize: CGSize = CGSize(width: 280, height: 240)
|
||||
|
||||
enum HTML {
|
||||
static let boldWeight = 600
|
||||
@@ -87,6 +88,7 @@ enum Styles {
|
||||
static let background = Styles.Colors.Gray.lighter.color
|
||||
static let purple = "6f42c1"
|
||||
static let blueGray = "8697af"
|
||||
static let menuBackgroundColor = "292D35"
|
||||
|
||||
enum Red {
|
||||
static let medium = "cb2431"
|
||||
|
||||
@@ -10,12 +10,14 @@ import UIKit
|
||||
|
||||
extension UIImageView {
|
||||
|
||||
func configureForAvatar() {
|
||||
func configureForAvatar(border: Bool = true) {
|
||||
contentMode = .scaleAspectFill
|
||||
backgroundColor = Styles.Colors.Gray.lighter.color
|
||||
layer.cornerRadius = Styles.Sizes.avatarCornerRadius
|
||||
layer.borderColor = Styles.Colors.Gray.light.color.cgColor
|
||||
layer.borderWidth = 1.0 / UIScreen.main.scale
|
||||
if border {
|
||||
layer.borderColor = Styles.Colors.Gray.light.color.cgColor
|
||||
layer.borderWidth = 1.0 / UIScreen.main.scale
|
||||
}
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,9 @@
|
||||
296B4E341F7C80B800C16887 /* GraphQLIDDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296B4E331F7C80B800C16887 /* GraphQLIDDecodeTests.swift */; };
|
||||
2971722B1F069E6B005E43AC /* SpinnerSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2971722A1F069E6B005E43AC /* SpinnerSectionController.swift */; };
|
||||
2971722D1F069E96005E43AC /* SpinnerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2971722C1F069E96005E43AC /* SpinnerCell.swift */; };
|
||||
2973E80D20FBD7BF0050233F /* ContrastContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2973E80C20FBD7BF0050233F /* ContrastContextMenu.swift */; };
|
||||
2973E80F20FBDB620050233F /* IssueManageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2973E80E20FBDB620050233F /* IssueManageButton.swift */; };
|
||||
2973E81120FBF73D0050233F /* IssueManagingContextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2973E81020FBF73D0050233F /* IssueManagingContextController.swift */; };
|
||||
297403D11F184F8D00ABA95A /* IssueAssigneesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D01F184F8D00ABA95A /* IssueAssigneesModel.swift */; };
|
||||
297403D31F1850DC00ABA95A /* IssueAssigneeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D21F1850DC00ABA95A /* IssueAssigneeViewModel.swift */; };
|
||||
297403D51F18515A00ABA95A /* IssueAssigneeSummaryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D41F18515A00ABA95A /* IssueAssigneeSummaryCell.swift */; };
|
||||
@@ -702,6 +705,9 @@
|
||||
2971722A1F069E6B005E43AC /* SpinnerSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerSectionController.swift; sourceTree = "<group>"; };
|
||||
2971722C1F069E96005E43AC /* SpinnerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerCell.swift; sourceTree = "<group>"; };
|
||||
2971CED22055C8F600342296 /* Freetime.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Freetime.entitlements; sourceTree = "<group>"; };
|
||||
2973E80C20FBD7BF0050233F /* ContrastContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContrastContextMenu.swift; sourceTree = "<group>"; };
|
||||
2973E80E20FBDB620050233F /* IssueManageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueManageButton.swift; sourceTree = "<group>"; };
|
||||
2973E81020FBF73D0050233F /* IssueManagingContextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueManagingContextController.swift; sourceTree = "<group>"; };
|
||||
297403D01F184F8D00ABA95A /* IssueAssigneesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneesModel.swift; sourceTree = "<group>"; };
|
||||
297403D21F1850DC00ABA95A /* IssueAssigneeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneeViewModel.swift; sourceTree = "<group>"; };
|
||||
297403D41F18515A00ABA95A /* IssueAssigneeSummaryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneeSummaryCell.swift; sourceTree = "<group>"; };
|
||||
@@ -1116,9 +1122,9 @@
|
||||
292FCAC71EDFCC510026635E /* Issues */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
15245483205DC004005810A6 /* Branches */,
|
||||
290744A81F24D2DA00FD9E48 /* AddCommentClient.swift */,
|
||||
297403CF1F184F8400ABA95A /* Assignees */,
|
||||
15245483205DC004005810A6 /* Branches */,
|
||||
292FCAC81EDFCC510026635E /* Comments */,
|
||||
293A45A01F29953800DD1006 /* Commit */,
|
||||
292CD3B91F0AF26900D3D57B /* DiffHunk */,
|
||||
@@ -1128,10 +1134,12 @@
|
||||
294563EB1EE5012100DBCD35 /* Issue+IssueType.swift */,
|
||||
292CD3C41F0C9EB200D3D57B /* IssueCommentModelHandling.swift */,
|
||||
291929461F3EAB250012067B /* IssueDetailsModel.swift */,
|
||||
2973E80E20FBDB620050233F /* IssueManageButton.swift */,
|
||||
29F7F05B1F2A751B00F6075D /* IssueResult.swift */,
|
||||
290D2A411F04D3470082E6CC /* IssueStatus.swift */,
|
||||
295C31CE1F0AA67600521CED /* IssueStatus+ButtonState.swift */,
|
||||
292FCAE91EDFCC510026635E /* IssuesViewController.swift */,
|
||||
2973E81020FBF73D0050233F /* IssueManagingContextController.swift */,
|
||||
292FF8AF1F2FDC33009E63F7 /* IssueTextActionsView.swift */,
|
||||
294563EF1EE5036A00DBCD35 /* IssueType.swift */,
|
||||
292FCAEA1EDFCC510026635E /* IssueViewModels.swift */,
|
||||
@@ -1616,6 +1624,7 @@
|
||||
2950AB1A2082E47200C6F19A /* AppSplitViewController.swift */,
|
||||
29CEA5CC1F84DB1B009827DB /* BaseListViewController.swift */,
|
||||
29C8F9AA208BF64D0075931C /* BaseListViewController2.swift */,
|
||||
2973E80C20FBD7BF0050233F /* ContrastContextMenu.swift */,
|
||||
297DD5E01F061BBE006E7E63 /* CreateProfileViewController.swift */,
|
||||
29316DBE1ECC95DB007CAE3F /* RootViewControllers.swift */,
|
||||
29C33FDA1F127DBB00EC8D40 /* SplitPlaceholderViewController.swift */,
|
||||
@@ -2692,6 +2701,7 @@
|
||||
2949674C1EF9716400B1CF1A /* IssueCommentHrModel.swift in Sources */,
|
||||
294967531EFC1EDB00B1CF1A /* IssueCommentHtmlCell.swift in Sources */,
|
||||
294967511EFC1E9E00B1CF1A /* IssueCommentHtmlModel.swift in Sources */,
|
||||
2973E80D20FBD7BF0050233F /* ContrastContextMenu.swift in Sources */,
|
||||
292FCAFB1EDFCC510026635E /* IssueCommentImageCell.swift in Sources */,
|
||||
299997362031DAC000995FFD /* MergeButton.swift in Sources */,
|
||||
29AF1E8A1F8AB1C30008A0EF /* TextActionsController.swift in Sources */,
|
||||
@@ -2735,6 +2745,7 @@
|
||||
292CD3C31F0AF3D400D3D57B /* IssueDiffHunkPreviewCell.swift in Sources */,
|
||||
29CC294F1FF93AD7006B6DE7 /* IssueCommentEmptyCell.swift in Sources */,
|
||||
292CD3BD1F0AF3B800D3D57B /* IssueDiffHunkSectionController.swift in Sources */,
|
||||
2973E80F20FBDB620050233F /* IssueManageButton.swift in Sources */,
|
||||
291929531F3F9E490012067B /* IssueFileCell.swift in Sources */,
|
||||
29C8F9AF208C02BE0075931C /* String+ListSwift.swift in Sources */,
|
||||
98F9F4011F9CCFFE005A0266 /* ImgurClient.swift in Sources */,
|
||||
@@ -2769,6 +2780,7 @@
|
||||
29F7F05F1F2A839100F6075D /* IssueNeckLoadSectionController.swift in Sources */,
|
||||
2919295F1F3FD1F40012067B /* IssuePatchContentViewController.swift in Sources */,
|
||||
292FF8B91F303DB0009E63F7 /* IssuePreviewModel.swift in Sources */,
|
||||
2973E81120FBF73D0050233F /* IssueManagingContextController.swift in Sources */,
|
||||
292FF8B51F303BD0009E63F7 /* IssuePreviewSectionController.swift in Sources */,
|
||||
292FF8B71F303BD9009E63F7 /* IssuePreviewViewController.swift in Sources */,
|
||||
292FCB211EDFCF870026635E /* IssueReactionCell.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user