Skip to content

Commit 6ffd5e7

Browse files
committed
Improved touch bar scrubber
1 parent dcbc011 commit 6ffd5e7

10 files changed

+231
-42
lines changed

PodcastMenu.xcodeproj/project.pbxproj

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
DD1AE2FC1E9A72180003FFF0 /* PlaybackInfoParser.js in Resources */ = {isa = PBXBuildFile; fileRef = DD1AE2FB1E9A72180003FFF0 /* PlaybackInfoParser.js */; };
3636
DD1AE2FE1E9A72350003FFF0 /* PlaybackInfoAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1AE2FD1E9A72350003FFF0 /* PlaybackInfoAdapter.swift */; };
3737
DD1AE3001E9A725D0003FFF0 /* PlaybackInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1AE2FF1E9A725D0003FFF0 /* PlaybackInfo.swift */; };
38+
DD1AE3021E9A8C010003FFF0 /* OvercastModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1AE3011E9A8C010003FFF0 /* OvercastModel.swift */; };
39+
DD1AE30C1E9A8E630003FFF0 /* IGListDiff.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1AE3091E9A8E470003FFF0 /* IGListDiff.framework */; };
40+
DD1AE30D1E9A8E630003FFF0 /* IGListDiff.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD1AE3091E9A8E470003FFF0 /* IGListDiff.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3841
DD1EFA781CE671A800E0C623 /* look.js in Resources */ = {isa = PBXBuildFile; fileRef = DD1EFA771CE671A800E0C623 /* look.js */; };
3942
DD1EFA7A1CE671B200E0C623 /* PMWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1EFA791CE671B200E0C623 /* PMWebView.swift */; };
4043
DD6DFF471DD69700004954DE /* Podcast.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6DFF461DD69700004954DE /* Podcast.swift */; };
@@ -56,6 +59,20 @@
5659
/* End PBXBuildFile section */
5760

5861
/* Begin PBXContainerItemProxy section */
62+
DD1AE3081E9A8E470003FFF0 /* PBXContainerItemProxy */ = {
63+
isa = PBXContainerItemProxy;
64+
containerPortal = DD1AE3031E9A8E470003FFF0 /* IGListDiff.xcodeproj */;
65+
proxyType = 2;
66+
remoteGlobalIDString = DD7CA1BE1DAECCC10020B93F;
67+
remoteInfo = IGListDiff;
68+
};
69+
DD1AE30E1E9A8E630003FFF0 /* PBXContainerItemProxy */ = {
70+
isa = PBXContainerItemProxy;
71+
containerPortal = DD1AE3031E9A8E470003FFF0 /* IGListDiff.xcodeproj */;
72+
proxyType = 1;
73+
remoteGlobalIDString = DD7CA1BD1DAECCC10020B93F;
74+
remoteInfo = IGListDiff;
75+
};
5976
DD6DFF5E1DD6A122004954DE /* PBXContainerItemProxy */ = {
6077
isa = PBXContainerItemProxy;
6178
containerPortal = DD0BFE311CE25D1400446474 /* Project object */;
@@ -73,6 +90,7 @@
7390
dstSubfolderSpec = 10;
7491
files = (
7592
DD0BFE6B1CE2817100446474 /* Sparkle.framework in Embed Frameworks */,
93+
DD1AE30D1E9A8E630003FFF0 /* IGListDiff.framework in Embed Frameworks */,
7694
);
7795
name = "Embed Frameworks";
7896
runOnlyForDeploymentPostprocessing = 0;
@@ -110,6 +128,8 @@
110128
DD1AE2FB1E9A72180003FFF0 /* PlaybackInfoParser.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = PlaybackInfoParser.js; sourceTree = "<group>"; };
111129
DD1AE2FD1E9A72350003FFF0 /* PlaybackInfoAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackInfoAdapter.swift; sourceTree = "<group>"; };
112130
DD1AE2FF1E9A725D0003FFF0 /* PlaybackInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackInfo.swift; sourceTree = "<group>"; };
131+
DD1AE3011E9A8C010003FFF0 /* OvercastModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OvercastModel.swift; sourceTree = "<group>"; };
132+
DD1AE3031E9A8E470003FFF0 /* IGListDiff.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = IGListDiff.xcodeproj; path = lib/IGListDiff/IGListDiff.xcodeproj; sourceTree = "<group>"; };
113133
DD1EFA771CE671A800E0C623 /* look.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = look.js; sourceTree = "<group>"; };
114134
DD1EFA791CE671B200E0C623 /* PMWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMWebView.swift; sourceTree = "<group>"; };
115135
DD6DFF461DD69700004954DE /* Podcast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Podcast.swift; sourceTree = "<group>"; };
@@ -140,6 +160,7 @@
140160
buildActionMask = 2147483647;
141161
files = (
142162
DD0BFE6A1CE2817100446474 /* Sparkle.framework in Frameworks */,
163+
DD1AE30C1E9A8E630003FFF0 /* IGListDiff.framework in Frameworks */,
143164
DD0BFE741CE2890000446474 /* Crashlytics.framework in Frameworks */,
144165
DD0BFE731CE2890000446474 /* Fabric.framework in Frameworks */,
145166
);
@@ -265,6 +286,7 @@
265286
DD0BFE691CE2816C00446474 /* Vendor */ = {
266287
isa = PBXGroup;
267288
children = (
289+
DD1AE3031E9A8E470003FFF0 /* IGListDiff.xcodeproj */,
268290
DD6DFF661DD6A1DF004954DE /* SwiftyJSON.swift */,
269291
DD0BFE711CE2890000446474 /* Fabric.framework */,
270292
DD0BFE721CE2890000446474 /* Crashlytics.framework */,
@@ -294,11 +316,20 @@
294316
name = "Touch Bar";
295317
sourceTree = "<group>";
296318
};
319+
DD1AE3041E9A8E470003FFF0 /* Products */ = {
320+
isa = PBXGroup;
321+
children = (
322+
DD1AE3091E9A8E470003FFF0 /* IGListDiff.framework */,
323+
);
324+
name = Products;
325+
sourceTree = "<group>";
326+
};
297327
DD6DFF451DD696F6004954DE /* Models */ = {
298328
isa = PBXGroup;
299329
children = (
300330
DD6DFF4A1DD697BD004954DE /* Adapters */,
301331
DD6DFF4D1DD697E5004954DE /* Result.swift */,
332+
DD1AE3011E9A8C010003FFF0 /* OvercastModel.swift */,
302333
DD6DFF461DD69700004954DE /* Podcast.swift */,
303334
DD6DFF481DD69734004954DE /* Episode.swift */,
304335
DD1AE2FF1E9A725D0003FFF0 /* PlaybackInfo.swift */,
@@ -373,6 +404,7 @@
373404
buildRules = (
374405
);
375406
dependencies = (
407+
DD1AE30F1E9A8E630003FFF0 /* PBXTargetDependency */,
376408
);
377409
name = PodcastMenu;
378410
productName = PodcastMenu;
@@ -431,6 +463,12 @@
431463
mainGroup = DD0BFE301CE25D1400446474;
432464
productRefGroup = DD0BFE3A1CE25D1400446474 /* Products */;
433465
projectDirPath = "";
466+
projectReferences = (
467+
{
468+
ProductGroup = DD1AE3041E9A8E470003FFF0 /* Products */;
469+
ProjectRef = DD1AE3031E9A8E470003FFF0 /* IGListDiff.xcodeproj */;
470+
},
471+
);
434472
projectRoot = "";
435473
targets = (
436474
DD0BFE381CE25D1400446474 /* PodcastMenu */,
@@ -439,6 +477,16 @@
439477
};
440478
/* End PBXProject section */
441479

480+
/* Begin PBXReferenceProxy section */
481+
DD1AE3091E9A8E470003FFF0 /* IGListDiff.framework */ = {
482+
isa = PBXReferenceProxy;
483+
fileType = wrapper.framework;
484+
path = IGListDiff.framework;
485+
remoteRef = DD1AE3081E9A8E470003FFF0 /* PBXContainerItemProxy */;
486+
sourceTree = BUILT_PRODUCTS_DIR;
487+
};
488+
/* End PBXReferenceProxy section */
489+
442490
/* Begin PBXResourcesBuildPhase section */
443491
DD0BFE371CE25D1400446474 /* Resources */ = {
444492
isa = PBXResourcesBuildPhase;
@@ -503,6 +551,7 @@
503551
DD0BFE4A1CE2602B00446474 /* StatusPopoverController.swift in Sources */,
504552
DD1EFA7A1CE671B200E0C623 /* PMWebView.swift in Sources */,
505553
DD14EB8D1DD6C4A600906DAD /* ScrubberRemoteImageItemView.swift in Sources */,
554+
DD1AE3021E9A8C010003FFF0 /* OvercastModel.swift in Sources */,
506555
DD0BFE4E1CE260FC00446474 /* Metrics.swift in Sources */,
507556
DD14EB8B1DD6C2A000906DAD /* ImageCache.swift in Sources */,
508557
DDC61FED1CE2F20E00C0FADA /* NSImage+CGImage.m in Sources */,
@@ -533,6 +582,11 @@
533582
/* End PBXSourcesBuildPhase section */
534583

535584
/* Begin PBXTargetDependency section */
585+
DD1AE30F1E9A8E630003FFF0 /* PBXTargetDependency */ = {
586+
isa = PBXTargetDependency;
587+
name = IGListDiff;
588+
targetProxy = DD1AE30E1E9A8E630003FFF0 /* PBXContainerItemProxy */;
589+
};
536590
DD6DFF5F1DD6A122004954DE /* PBXTargetDependency */ = {
537591
isa = PBXTargetDependency;
538592
target = DD0BFE381CE25D1400446474 /* PodcastMenu */;

PodcastMenu/Episode.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ struct Episode {
2020
let poster: URL
2121
let date: Date
2222
let time: Time
23-
let link: URL
23+
let link: URL?
2424

2525
}

PodcastMenu/ImageCache.swift

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,25 @@ private extension String {
1818

1919
final class ImageCache {
2020

21+
static let shared: ImageCache = ImageCache()
22+
23+
typealias CancellationHandler = () -> ()
24+
2125
class func cacheUrl(for imageUrl: URL) -> URL {
22-
let filename = imageUrl.absoluteString.base64encoded ?? imageUrl.lastPathComponent
26+
let filebase = imageUrl.path.replacingOccurrences(of: "/", with: "_")
27+
let filename = filebase + "-" + imageUrl.lastPathComponent
2328

2429
let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/" + filename + "-" + imageUrl.lastPathComponent
2530

26-
NSLog("\(path)")
27-
2831
return URL(fileURLWithPath: path)
2932
}
3033

31-
func fetchImage(at imageUrl: URL, completion: @escaping (URL, NSImage?) -> ()) {
34+
func fetchImage(at imageUrl: URL, completion: @escaping (URL, NSImage?) -> ()) -> CancellationHandler {
3235
let cacheUrl = ImageCache.cacheUrl(for: imageUrl)
3336

3437
guard !FileManager.default.fileExists(atPath: cacheUrl.path) else {
3538
completion(imageUrl, NSImage(contentsOfFile: cacheUrl.path))
36-
return
39+
return { }
3740
}
3841

3942
let task = URLSession.shared.dataTask(with: imageUrl) { data, _, error in
@@ -44,17 +47,40 @@ final class ImageCache {
4447
return
4548
}
4649

47-
do {
48-
try data.write(to: cacheUrl)
49-
} catch {
50-
NSLog("Error saving image to cache: \(error)")
50+
guard let cachedImage = self.cache(data: data, cacheURL: cacheUrl) else {
51+
DispatchQueue.main.async {
52+
completion(imageUrl, nil)
53+
}
54+
return
5155
}
5256

5357
DispatchQueue.main.async {
54-
completion(imageUrl, NSImage(data: data))
58+
completion(imageUrl, cachedImage)
5559
}
5660
}
5761
task.resume()
62+
63+
return { task.cancel() }
64+
}
65+
66+
private func cache(data: Data, cacheURL: URL) -> NSImage? {
67+
guard let inputImage = NSImage(data: data) else {
68+
return nil
69+
}
70+
71+
let outputImage = NSImage(size: Metrics.thumbnailSize)
72+
73+
outputImage.lockFocus()
74+
inputImage.draw(in: NSRect(origin: .zero, size: Metrics.thumbnailSize))
75+
outputImage.unlockFocus()
76+
77+
do {
78+
try outputImage.tiffRepresentation?.write(to: cacheURL)
79+
} catch {
80+
NSLog("Error saving image to cache: \(error)")
81+
}
82+
83+
return outputImage
5884
}
5985

6086
}

PodcastMenu/Metrics.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ struct Metrics {
1616
static let errorBarHeight = CGFloat(48.0)
1717
static let errorBarMargin = CGFloat(8.0)
1818
static let controlToWindowMargin = CGFloat(8.0)
19+
static let thumbnailSize = NSSize(width: 100, height: 100)
1920
}

PodcastMenu/OvercastModel.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// OvercastModel.swift
3+
// PodcastMenu
4+
//
5+
// Created by Guilherme Rambo on 09/04/17.
6+
// Copyright © 2017 Guilherme Rambo. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
protocol OvercastModel {
12+
13+
var title: String { get }
14+
var link: URL? { get }
15+
var poster: URL { get }
16+
17+
}
18+
19+
extension Podcast: OvercastModel {
20+
21+
var title: String {
22+
return name
23+
}
24+
25+
}
26+
27+
extension Episode: OvercastModel { }
28+
29+
extension OvercastModel {
30+
31+
func compare(to other: OvercastModel) -> Bool {
32+
return self.title == other.title && self.link == other.link && self.poster == other.poster
33+
}
34+
35+
}

PodcastMenu/PodcastWebAppViewController.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,13 @@ class PodcastWebAppViewController: NSViewController {
146146
// MARK: - Touch Bar
147147

148148
fileprivate lazy var touchBarController: TouchBarController = {
149-
return TouchBarController(webView: self.webView)
149+
let c = TouchBarController(webView: self.webView)
150+
151+
if #available(macOS 10.12.2, *) {
152+
c.scrubberController.delegate = self
153+
}
154+
155+
return c
150156
}()
151157

152158
private lazy var episodesParserScript: String? = {
@@ -397,6 +403,15 @@ extension PodcastWebAppViewController {
397403

398404
}
399405

406+
@available(OSX 10.12.2, *)
407+
extension PodcastWebAppViewController: TouchBarScrubberViewControllerDelegate {
408+
409+
func didSelectLink(_ linkURL: URL) {
410+
webView.load(URLRequest(url: linkURL))
411+
}
412+
413+
}
414+
400415
// MARK: Menu Validation
401416

402417
private enum ConfigMenuItem: Int {

PodcastMenu/ScrubberRemoteImageItemView.swift

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,48 @@ import Cocoa
1010

1111
@available(OSX 10.12.2, *)
1212
class ScrubberRemoteImageItemView: NSScrubberImageItemView {
13-
14-
fileprivate lazy var imageCache = ImageCache()
13+
14+
var indexInScrubber: Int = -1
1515

1616
var imageUrl: URL? {
1717
didSet {
18-
guard let imageUrl = imageUrl else { return }
18+
guard imageUrl != nil, superview != nil else { return }
19+
20+
displayImage()
21+
}
22+
}
23+
24+
override func viewDidMoveToSuperview() {
25+
super.viewDidMoveToSuperview()
26+
27+
displayImage()
28+
}
29+
30+
private var cancelDownload: ImageCache.CancellationHandler?
31+
32+
override func prepareForReuse() {
33+
super.prepareForReuse()
34+
35+
cancelDownload?()
36+
}
37+
38+
private func displayImage() {
39+
guard let imageUrl = imageUrl else { return }
40+
41+
let imageUrlWhenDownloadStarted = imageUrl
42+
43+
cancelDownload = ImageCache.shared.fetchImage(at: imageUrl) { [weak self] _, image in
44+
guard let welf = self else { return }
1945

20-
imageCache.fetchImage(at: imageUrl) { [weak self] url, image in
21-
guard url == self?.imageUrl else { return }
22-
guard let image = image else { return }
23-
24-
self?.image = image
46+
guard imageUrlWhenDownloadStarted == welf.imageUrl else {
47+
#if DEBUG
48+
NSLog("Skipped setting scrubber item image because the URL changed \(imageUrlWhenDownloadStarted)")
49+
#endif
50+
return
2551
}
52+
guard let image = image else { return }
53+
54+
welf.image = image
2655
}
2756
}
2857

PodcastMenu/TouchBarController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class TouchBarController: NSObject {
2323
}
2424

2525
@available(OSX 10.12.2, *)
26-
fileprivate lazy var scrubberController = TouchBarScrubberViewController()
26+
lazy var scrubberController = TouchBarScrubberViewController()
2727

2828
var currentEpisodeTitle: String? = nil {
2929
didSet {

0 commit comments

Comments
 (0)