Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit c767399

Browse files
authored
view images in code browser (#2449)
1 parent 7828693 commit c767399

File tree

5 files changed

+213
-6
lines changed

5 files changed

+213
-6
lines changed

Classes/Models/FilePath.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ struct FilePath {
4545
return FilePath(components: components + [component])
4646
}
4747

48+
var isImage: Bool {
49+
guard let last = components.last else { return false }
50+
for format in ["png", "jpg", "jpeg", "gif"] {
51+
if last.hasSuffix(format) {
52+
return true
53+
}
54+
}
55+
return false
56+
}
57+
4858
}
4959

5060
// MARK: - FilePath (BinaryFile) -
@@ -74,7 +84,8 @@ extension FilePath {
7484
// MARK: Private API
7585

7686
private var binarySuffix: String? {
77-
return FilePath.supportedBinaries.keys.first(where: { path.hasSuffix($0) })
87+
guard let last = components.last else { return nil }
88+
return FilePath.supportedBinaries.keys.first(where: { last.hasSuffix($0) })
7889
}
7990

8091
}

Classes/Repository/RepositoryCodeDirectoryViewController.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ IndicatorInfoProvider {
3636
target: self,
3737
action: #selector(RepositoryCodeDirectoryViewController.onShare(sender:)))
3838
barButtonItem.isEnabled = false
39-
4039
return barButtonItem
4140
}()
4241

@@ -141,6 +140,13 @@ IndicatorInfoProvider {
141140
branch: branch,
142141
path: path
143142
)
143+
} else if path.isImage {
144+
controller = RepositoryImageViewController(
145+
repo: repo,
146+
branch: branch,
147+
path: path,
148+
client: client
149+
)
144150
} else {
145151
controller = RepositoryCodeBlobViewController(
146152
client: client,
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//
2+
// RepositoryImageViewController.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 11/17/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import SDWebImage
11+
import SnapKit
12+
import TUSafariActivity
13+
14+
final class RepositoryImageViewController: UIViewController,
15+
EmptyViewDelegate,
16+
UIScrollViewDelegate {
17+
18+
private let branch: String
19+
private let path: FilePath
20+
private let repo: RepositoryDetails
21+
private let client: GithubClient
22+
private let emptyView = EmptyView()
23+
private let scrollView = UIScrollView()
24+
private let imageView = FLAnimatedImageView()
25+
private let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
26+
private var imageUrl: URL? {
27+
let builder = URLBuilder.github()
28+
.add(path: repo.owner)
29+
.add(path: repo.name)
30+
.add(path: "raw")
31+
.add(path: branch)
32+
path.components.forEach { builder.add(path: $0) }
33+
return builder.url
34+
}
35+
36+
private lazy var moreOptionsItem: UIBarButtonItem = {
37+
let barButtonItem = UIBarButtonItem(
38+
image: UIImage(named: "bullets-hollow"),
39+
target: self,
40+
action: #selector(RepositoryImageViewController.onShare(sender:)))
41+
barButtonItem.isEnabled = false
42+
return barButtonItem
43+
}()
44+
45+
init(repo: RepositoryDetails, branch: String, path: FilePath, client: GithubClient) {
46+
self.branch = branch
47+
self.path = path
48+
self.repo = repo
49+
self.client = client
50+
super.init(nibName: nil, bundle: nil)
51+
}
52+
53+
required init?(coder aDecoder: NSCoder) {
54+
fatalError("init(coder:) has not been implemented")
55+
}
56+
57+
override func viewDidLoad() {
58+
super.viewDidLoad()
59+
60+
view.backgroundColor = .white
61+
62+
configureTitle(filePath: path, target: self, action: #selector(onFileNavigationTitle(sender:)))
63+
makeBackBarItemEmpty()
64+
navigationItem.rightBarButtonItem = moreOptionsItem
65+
66+
emptyView.isHidden = true
67+
emptyView.label.text = NSLocalizedString("Error loading image", comment: "")
68+
view.addSubview(emptyView)
69+
emptyView.snp.makeConstraints { make in
70+
make.edges.equalToSuperview()
71+
}
72+
73+
scrollView.contentInsetAdjustmentBehavior = .never
74+
scrollView.alwaysBounceVertical = true
75+
scrollView.alwaysBounceHorizontal = true
76+
scrollView.showsHorizontalScrollIndicator = false
77+
scrollView.showsVerticalScrollIndicator = false
78+
scrollView.maximumZoomScale = 4
79+
scrollView.delegate = self
80+
view.addSubview(scrollView)
81+
scrollView.snp.makeConstraints { make in
82+
make.edges.equalToSuperview()
83+
}
84+
85+
imageView.contentMode = .scaleAspectFit
86+
scrollView.addSubview(imageView)
87+
88+
view.addSubview(activityIndicator)
89+
activityIndicator.snp.makeConstraints { make in
90+
make.center.equalToSuperview()
91+
}
92+
93+
fetch()
94+
}
95+
96+
// MARK: Private API
97+
98+
@objc func onFileNavigationTitle(sender: UIView) {
99+
showAlert(filePath: path, sender: sender)
100+
}
101+
102+
private func fetch() {
103+
emptyView.isHidden = true
104+
activityIndicator.startAnimating()
105+
imageView.sd_setImage(with: imageUrl) { [weak self] (_, error, _, _) in
106+
self?.handle(success: error == nil)
107+
}
108+
}
109+
110+
private func handle(success: Bool) {
111+
moreOptionsItem.isEnabled = success
112+
emptyView.isHidden = success
113+
114+
activityIndicator.stopAnimating()
115+
116+
if let size = imageView.image?.size {
117+
// centered later in UIScrollViewDelegate
118+
imageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
119+
scrollView.contentSize = size
120+
let bounds = view.bounds
121+
let scale = min(bounds.width / size.width, bounds.height / size.height)
122+
123+
// min must be set before zoom
124+
scrollView.minimumZoomScale = scale
125+
scrollView.zoomScale = scale
126+
}
127+
}
128+
129+
@objc func onShare(sender: UIButton) {
130+
let alertTitle = "\(repo.owner)/\(repo.name):\(branch)"
131+
let alert = UIAlertController.configured(title: alertTitle, preferredStyle: .actionSheet)
132+
133+
let alertBuilder = AlertActionBuilder { [weak self] in $0.rootViewController = self }
134+
var actions = [
135+
viewHistoryAction(owner: repo.owner, repo: repo.name, branch: branch, client: client, path: path),
136+
AlertAction(alertBuilder).share([path.path], activities: nil, type: .shareFilePath) {
137+
$0.popoverPresentationController?.setSourceView(sender)
138+
}
139+
]
140+
141+
if let image = imageView.image {
142+
actions.append(AlertAction(alertBuilder).share([image], activities: nil, type: .shareContent) {
143+
$0.popoverPresentationController?.setSourceView(sender)
144+
})
145+
}
146+
147+
if let url = imageUrl {
148+
actions.append(AlertAction(alertBuilder).share([url], activities: [TUSafariActivity()], type: .shareUrl) {
149+
$0.popoverPresentationController?.setSourceView(sender)
150+
})
151+
}
152+
actions.append(AlertAction.cancel())
153+
154+
if let name = self.path.components.last {
155+
actions.append(AlertAction(alertBuilder).share([name], activities: nil, type: .shareFileName) {
156+
$0.popoverPresentationController?.setSourceView(sender)
157+
})
158+
}
159+
160+
alert.addActions(actions)
161+
alert.popoverPresentationController?.setSourceView(sender)
162+
present(alert, animated: trueUnlessReduceMotionEnabled)
163+
}
164+
165+
// MARK: EmptyViewDelegate
166+
167+
func didTapRetry(view: EmptyView) {
168+
fetch()
169+
}
170+
171+
// MARK: UIScrollViewDelegate
172+
173+
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
174+
return imageView
175+
}
176+
177+
func scrollViewDidZoom(_ scrollView: UIScrollView) {
178+
let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) / 2, 0)
179+
let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) / 2, 0)
180+
imageView.center = CGPoint(
181+
x: scrollView.contentSize.width / 2 + offsetX,
182+
y: scrollView.contentSize.height / 2 + offsetY
183+
)
184+
}
185+
186+
}

Classes/Repository/RepositoryOverviewViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ IndicatorInfoProvider {
4343
feed.collectionView.contentInset = UIEdgeInsets(
4444
top: Styles.Sizes.columnSpacing,
4545
left: 0,
46-
bottom: Styles.Sizes.columnSpacing,
46+
bottom: Styles.Sizes.columnSpacing,
4747
right: 0
4848
)
4949
feed.collectionView.backgroundColor = .white

Freetime.xcodeproj/project.pbxproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@
341341
29B205FE217B971300E4DD9F /* String+FirstLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205FD217B971300E4DD9F /* String+FirstLine.swift */; };
342342
29B20600217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205FF217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift */; };
343343
29B20602217BC37B00E4DD9F /* UIViewController+HistoryAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B20601217BC37B00E4DD9F /* UIViewController+HistoryAction.swift */; };
344+
29B3B91721A0B38C006BED05 /* RepositoryImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B3B91621A0B38C006BED05 /* RepositoryImageViewController.swift */; };
344345
29B5D08B20D578DB003DFBE2 /* InboxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5D08A20D578DB003DFBE2 /* InboxType.swift */; };
345346
29B75AD9210A9A0300C28131 /* IssueLabelStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B75AD8210A9A0300C28131 /* IssueLabelStatusCell.swift */; };
346347
29B75ADB210A9B3100C28131 /* IssueLabelStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B75ADA210A9B3100C28131 /* IssueLabelStatusModel.swift */; };
@@ -901,6 +902,7 @@
901902
29B205FD217B971300E4DD9F /* String+FirstLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FirstLine.swift"; sourceTree = "<group>"; };
902903
29B205FF217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoFileHistoryQueryDataToPathHistoryViewModel.swift; sourceTree = "<group>"; };
903904
29B20601217BC37B00E4DD9F /* UIViewController+HistoryAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+HistoryAction.swift"; sourceTree = "<group>"; };
905+
29B3B91621A0B38C006BED05 /* RepositoryImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryImageViewController.swift; sourceTree = "<group>"; };
904906
29B5D08A20D578DB003DFBE2 /* InboxType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxType.swift; sourceTree = "<group>"; };
905907
29B75AD8210A9A0300C28131 /* IssueLabelStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabelStatusCell.swift; sourceTree = "<group>"; };
906908
29B75ADA210A9B3100C28131 /* IssueLabelStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabelStatusModel.swift; sourceTree = "<group>"; };
@@ -2134,14 +2136,15 @@
21342136
292ACE191F5CAF9F00C9A02C /* Empty */,
21352137
29A5AF441F9298360065D529 /* GitHubClient+Repository.swift */,
21362138
986B873B1F2CEB1500AAB55C /* GQL+RepositoryIssueSummaryType.swift */,
2139+
BDB6AA5D215FBC35009BB73C /* RepositoryBranches */,
21372140
986B873D1F2E1CE400AAB55C /* RepositoryClient.swift */,
21382141
29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */,
2139-
29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */,
21402142
29C62F9F218F47A9001724B2 /* RepositoryCodeDirectorySectionController.swift */,
2141-
750986592048959D00D1E37A /* RepositoryWebViewController.swift */,
2143+
29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */,
21422144
295A77BD1F75C1CC007BC403 /* RepositoryDetails.swift */,
21432145
29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */,
21442146
29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */,
2147+
29B3B91621A0B38C006BED05 /* RepositoryImageViewController.swift */,
21452148
292ACE171F5C945B00C9A02C /* RepositoryIssueSummaryModel.swift */,
21462149
29A5AF401F92677D0065D529 /* RepositoryIssueSummaryModel+Filterable.swift */,
21472150
986B87331F2CAE9800AAB55C /* RepositoryIssueSummaryType.swift */,
@@ -2152,7 +2155,7 @@
21522155
986B87371F2CB29700AAB55C /* RepositorySummaryCell.swift */,
21532156
986B87351F2CB28C00AAB55C /* RepositorySummarySectionController.swift */,
21542157
2905AFAE1F7357FA0015AE32 /* RepositoryViewController.swift */,
2155-
BDB6AA5D215FBC35009BB73C /* RepositoryBranches */,
2158+
750986592048959D00D1E37A /* RepositoryWebViewController.swift */,
21562159
);
21572160
path = Repository;
21582161
sourceTree = "<group>";
@@ -2915,6 +2918,7 @@
29152918
2958406D1EE8EBF3007723C6 /* IssueCommentPhoto.swift in Sources */,
29162919
295840DA1EEA07E4007723C6 /* IssueCommentQuoteCell.swift in Sources */,
29172920
295840D81EEA0686007723C6 /* IssueCommentQuoteModel.swift in Sources */,
2921+
29B3B91721A0B38C006BED05 /* RepositoryImageViewController.swift in Sources */,
29182922
292FCB071EDFCC510026635E /* IssueCommentReactionCell.swift in Sources */,
29192923
291E987B21973B8F00E5EED9 /* URLBuilder.swift in Sources */,
29202924
292FA33D1FBCE0F000BBB0BB /* Milestone.swift in Sources */,

0 commit comments

Comments
 (0)