├── .gitignore
├── .swiftlint.yml
├── Icons
├── Big Sur.icns
└── Catalina.icns
├── LICENSE
├── Netflix.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── Netflix
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon_1024x1024.png
│ │ ├── icon_128x128.png
│ │ ├── icon_16x16.png
│ │ ├── icon_256x256.png
│ │ ├── icon_32x32.png
│ │ ├── icon_512x512.png
│ │ └── icon_64x64.png
│ ├── Contents.json
│ ├── playing-in-pip.imageset
│ │ ├── Contents.json
│ │ └── playing_in_pip.pdf
│ └── site-spinner.imageset
│ │ ├── Contents.json
│ │ └── site-spinner.png
├── Base.lproj
│ ├── Credits.rtf
│ └── Main.storyboard
├── Classes
│ ├── ActivityIndicator.swift
│ ├── AppDelegate.swift
│ ├── NSRect.swift
│ ├── TitleView.swift
│ ├── ViewController.swift
│ └── WindowController.swift
├── Customization.js
├── Info.plist
├── Netflix.entitlements
├── Netflix.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ └── contents.xcworkspacedata
├── PIP_BridgingHeader.h
├── PictureInPictureDelegate.swift
├── WebView.swift
└── update-version.sh
├── README.md
└── Resources
├── alternative-icons.gif
├── icon.sketch
└── screenshot.png
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Build generated
2 | build/
3 | DerivedData
4 |
5 | ## Various settings
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata
15 |
16 | ## Fastlane
17 | fastlane/report.xml
18 | fastlane/Preview.html
19 | fastlane/screenshots
20 | fastlane/test_output
21 |
22 | ## Other
23 | *.xccheckout
24 | *.moved-aside
25 | *.xcuserstate
26 | *.xcscmblueprint
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 | *.ipa
31 | *.dSYM.zip
32 | .DS_Store
33 | /changelog.txt
34 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | #opt_in_rules:
2 | # - force_unwrapping
3 | # - empty_count
4 |
5 | disabled_rules:
6 | - function_parameter_count
7 | - line_length
8 | - variable_name
9 | - cyclomatic_complexity
10 | - nesting
11 | - operator_whitespace
12 | - redundant_discardable_let
13 | - multiple_closures_with_trailing_closure
14 | - empty_enum_arguments
15 |
16 | excluded:
17 | - Resources
18 | - Vendor
19 |
20 | type_body_length:
21 | - 700 #warning
22 | - 1000 #error
23 |
24 | file_length:
25 | - 1000 #warning
26 | - 1200 #error
27 |
28 | function_body_length:
29 | - 125 #warning
30 | - 200 #error
31 |
32 | type_name:
33 | min_length: 3 # only warning
34 | max_length: # warning and error
35 | warning: 50
36 | error: 50
37 |
38 | statement_position:
39 | statement_mode: uncuddled_else
40 |
41 | trailing_comma:
42 | mandatory_comma: true
43 |
44 | large_tuple:
45 | - 4
46 | - 6
47 |
48 | custom_rules:
49 | indentation:
50 | name: "Indentation"
51 | message: "Tabs should be used for indentation of lines."
52 | regex: "(^\t* +\t*)"
53 | severity: warning
54 |
--------------------------------------------------------------------------------
/Icons/Big Sur.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Icons/Big Sur.icns
--------------------------------------------------------------------------------
/Icons/Catalina.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Icons/Catalina.icns
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Daniel Farrelly. Released under the BSD license. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 |
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 |
--------------------------------------------------------------------------------
/Netflix.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Netflix.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Netflix.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Sparkle",
6 | "repositoryURL": "https://github.com/sparkle-project/Sparkle.git",
7 | "state": {
8 | "branch": "master",
9 | "revision": "02d0d749a006752403a2ab0120033af7912d063f",
10 | "version": null
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon_16x16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon_32x32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon_32x32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon_64x64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon_128x128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon_256x256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon_256x256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon_512x512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon_512x512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon_1024x1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/AppIcon.appiconset/icon_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/AppIcon.appiconset/icon_64x64.png
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/playing-in-pip.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "playing_in_pip.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/playing-in-pip.imageset/playing_in_pip.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/playing-in-pip.imageset/playing_in_pip.pdf
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/site-spinner.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "site-spinner.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Netflix/Assets.xcassets/site-spinner.imageset/site-spinner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Netflix/Assets.xcassets/site-spinner.imageset/site-spinner.png
--------------------------------------------------------------------------------
/Netflix/Base.lproj/Credits.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200
2 | {\fonttbl\f0\fnil\fcharset0 .SFNSText;}
3 | {\colortbl;\red255\green255\blue255;}
4 | {\*\expandedcolortbl;;}
5 | \paperw11900\paperh16840\margl1440\margr1440\vieww25620\viewh17440\viewkind0
6 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0
7 |
8 | \f0\fs20 \cf0 Copyright \'a9 2018 Daniel Farrelly. All rights reserved.\
9 | \
10 | This project is not associated with, affiliated with, or endorsed by Netflix, Inc.\
11 | \
12 | {\field{\*\fldinst{HYPERLINK "https://github.com/jellybeansoup/macos-netflix"}}{\fldrslt github.com/jellybeansoup/macos-netflix}}}
--------------------------------------------------------------------------------
/Netflix/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
--------------------------------------------------------------------------------
/Netflix/Classes/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | @IBDesignable
4 | class ActivityIndicator: NSView {
5 |
6 | override init(frame frameRect: NSRect) {
7 | super.init(frame: frameRect)
8 | initialize()
9 | }
10 |
11 | required init?(coder decoder: NSCoder) {
12 | super.init(coder: decoder)
13 | initialize()
14 | }
15 |
16 | private var imageLayer: CALayer?
17 |
18 | private func initialize() {
19 | wantsLayer = true
20 | layerContentsRedrawPolicy = .onSetNeedsDisplay
21 |
22 | guard let layer = layer else {
23 | return
24 | }
25 |
26 | let sublayer = CALayer()
27 | sublayer.contents = NSImage(named: "site-spinner")
28 | sublayer.frame = bounds
29 | layer.addSublayer(sublayer)
30 | imageLayer = sublayer
31 |
32 | let angle: CGFloat = 0 - 360 * .pi / 180
33 |
34 | let transform = CATransform3DRotate(sublayer.transform, angle, 0, 0, 1)
35 | sublayer.transform = transform
36 |
37 | let animation = CABasicAnimation(keyPath: "transform.rotation")
38 | animation.duration = 0.9
39 | animation.fromValue = 0
40 | animation.toValue = angle
41 | animation.repeatCount = .greatestFiniteMagnitude
42 | sublayer.add(animation, forKey: "transform.rotation")
43 | }
44 |
45 | override var frame: NSRect {
46 | didSet { setNeedsDisplay(bounds) }
47 | }
48 |
49 | override func updateLayer() {
50 | super.updateLayer()
51 |
52 | guard let layer = layer else {
53 | return
54 | }
55 |
56 | let diameter = min(layer.bounds.size.width, layer.bounds.size.height)
57 | let x = (layer.bounds.size.width - diameter) / 2
58 | let y = (layer.bounds.size.height - diameter) / 2
59 | imageLayer?.frame = CGRect(x: x, y: y, width: diameter, height: diameter)
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Netflix/Classes/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import WebKit
3 |
4 | @NSApplicationMain
5 | class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation, PictureInPictureDelegate {
6 |
7 | static weak var shared: AppDelegate? = {
8 | return NSApplication.shared.delegate as? AppDelegate
9 | }()
10 |
11 | func applicationDidFinishLaunching(_ notification: Notification) {
12 | self.defaultViewController?.pipDelegate = self
13 | }
14 |
15 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
16 | return true
17 | }
18 |
19 | // MARK: Accessing various classes
20 |
21 | private weak var defaultWindowController: WindowController? {
22 | // When window is minituarized, NSApplication.shared.mainWindow is nil
23 | guard let window = NSApplication.shared.mainWindow ?? NSApplication.shared.windows.first else {
24 | return nil
25 | }
26 |
27 | guard let windowController = window.windowController as? WindowController else {
28 | return nil
29 | }
30 |
31 | return windowController
32 | }
33 |
34 | private weak var defaultViewController: ViewController? {
35 | guard let viewController = defaultWindowController?.contentViewController as? ViewController else {
36 | return nil
37 | }
38 |
39 | return viewController
40 | }
41 |
42 | // MARK: Menu actions
43 |
44 | @IBOutlet weak var searchMenuItem: NSMenuItem?
45 |
46 | @IBOutlet weak var keepInFrontMenuItem: NSMenuItem?
47 |
48 | @IBOutlet weak var snapToCornersMenuItem: NSMenuItem?
49 |
50 | @IBOutlet weak var pictureInPictureMenuItem: NSMenuItem?
51 |
52 | func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
53 | if let searchMenuItem = searchMenuItem, menuItem === searchMenuItem {
54 | return defaultViewController?.canSearch ?? false
55 | }
56 | else if let keepInFrontMenuItem = keepInFrontMenuItem, menuItem === keepInFrontMenuItem {
57 | menuItem.state = (defaultWindowController?.keepInFront ?? true) ? .on : .off
58 | }
59 | else if let snapToCornersMenuItem = snapToCornersMenuItem, menuItem === snapToCornersMenuItem {
60 | menuItem.state = (defaultWindowController?.snapToCorners ?? true) ? .on : .off
61 | }
62 |
63 | return true
64 | }
65 |
66 | @IBAction func about(_ sender: Any?) {
67 | NSApplication.shared.orderFrontStandardAboutPanel(options: [
68 | NSApplication.AboutPanelOptionKey(rawValue: "ApplicationName"): "Netflix wrapper for macOS",
69 | NSApplication.AboutPanelOptionKey(rawValue: "Copyright"): "",
70 | ])
71 | }
72 |
73 | @IBAction func search(_ sender: Any?) {
74 | defaultViewController?.search(sender)
75 | }
76 |
77 | @IBAction func reload(_ sender: Any?) {
78 | defaultViewController?.webView?.reload(sender)
79 | }
80 |
81 | @IBAction func actualSize(_ sender: Any?) {
82 | defaultViewController?.webView?.magnification = 1.0
83 | }
84 |
85 | @IBAction func zoomIn(_ sender: Any?) {
86 | defaultViewController?.webView?.magnification += 0.1
87 | }
88 |
89 | @IBAction func zoomOut(_ sender: Any?) {
90 | defaultViewController?.webView?.magnification -= 0.1
91 | }
92 |
93 | @IBAction func keepInFront(_ sender: Any?) {
94 | defaultWindowController?.toggleKeepInFront(sender)
95 | }
96 |
97 | @IBAction func snapToCorners(_ sender: Any?) {
98 | defaultWindowController?.toggleSnapToCorners(sender)
99 | }
100 |
101 | @IBAction func pictureInPicture(_ sender: Any?) {
102 | defaultWindowController?.togglePictureInPicture(sender)
103 | }
104 |
105 | @IBAction func github(_ sender: Any?) {
106 | guard let url = URL(string: "https://github.com/jellybeansoup/macos-netflix") else {
107 | fatalError("Could not prepare URL for GitHub Repository")
108 | }
109 |
110 | NSWorkspace.shared.open(url)
111 | }
112 |
113 | @IBAction func help(_ sender: Any?) {
114 | guard let url = URL(string: "https://help.netflix.com/en") else {
115 | fatalError("Could not prepare URL for Netflix Help")
116 | }
117 |
118 | NSWorkspace.shared.open(url)
119 | }
120 |
121 | // MARK: Picture in picture delegate
122 |
123 | func willEnterPictureInPicture(_ sender: Any?) {
124 | //
125 | }
126 |
127 | func didEnterPictureInPicture(_ sender: Any?) {
128 | pictureInPictureMenuItem?.title = NSLocalizedString("Exit Picture In Picture", comment: "Exit Picture In Picture")
129 | }
130 |
131 | func willExitPictureInPicture(_ sender: Any?) {
132 | //
133 | }
134 |
135 | func didExitPictureInPicture(_ sender: Any?) {
136 | pictureInPictureMenuItem?.title = NSLocalizedString("Enter Picture In Picture", comment: "Enter Picture In Picture")
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/Netflix/Classes/NSRect.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension NSRect {
4 |
5 | struct Corner: CustomStringConvertible {
6 | var x: Alignment = .leading
7 | var y: Alignment = .leading
8 |
9 | enum Alignment: CustomStringConvertible {
10 | case leading
11 | case trailing
12 |
13 | static var left = Alignment.leading
14 | static var right = Alignment.trailing
15 | static var bottom = Alignment.leading
16 | static var top = Alignment.trailing
17 |
18 | var description: String {
19 | switch self {
20 | case .leading: return "leading"
21 | case .trailing: return "trailing"
22 | }
23 | }
24 |
25 | }
26 |
27 | struct Insets: CustomStringConvertible {
28 | var x: CGFloat = 0
29 | var y: CGFloat = 0
30 |
31 | static let zero = Insets(x: 0, y: 0)
32 |
33 | var description: String {
34 | return "(x = \(x), y = \(y))"
35 | }
36 |
37 | }
38 |
39 | var description: String {
40 | return "(x = \(x), y = \(y))"
41 | }
42 |
43 | }
44 |
45 | func snappedCorner(of screen: NSScreen, with insets: NSRect.Corner.Insets = .zero) -> NSRect.Corner? {
46 | var corner = NSRect.Corner()
47 |
48 | if origin.x == screen.visibleFrame.minX + insets.x {
49 | corner.x = .leading
50 | }
51 | else if origin.x == screen.visibleFrame.maxX - (width + insets.x) {
52 | corner.x = .trailing
53 | }
54 | else {
55 | return nil
56 | }
57 |
58 | if origin.y == screen.visibleFrame.minY + insets.y {
59 | corner.y = .leading
60 | }
61 | else if origin.y == screen.visibleFrame.maxY - (height + insets.y) {
62 | corner.y = .trailing
63 | }
64 | else {
65 | return nil
66 | }
67 |
68 | return corner
69 | }
70 |
71 | func preferredCorner(for screen: NSScreen) -> NSRect.Corner {
72 | var corner = NSRect.Corner()
73 |
74 | let left = minX - screen.visibleFrame.minX
75 | let right = screen.visibleFrame.maxX - maxX
76 | corner.x = left < right ? .leading : .trailing
77 |
78 | let bottom = minY - screen.visibleFrame.minY
79 | let top = screen.visibleFrame.maxY - maxY
80 | corner.y = bottom > top ? .trailing : .leading
81 |
82 | return corner
83 | }
84 |
85 | func originForSnappingToPreferredCorner(of screen: NSScreen, with insets: NSRect.Corner.Insets = .zero) -> NSPoint {
86 | return size.originForSnapping(to: preferredCorner(for: screen), of: screen, with: insets)
87 | }
88 |
89 | func originForSnapping(to corner: NSRect.Corner, of screen: NSScreen, with insets: NSRect.Corner.Insets = .zero) -> NSPoint {
90 | return size.originForSnapping(to: corner, of: screen, with: insets)
91 | }
92 |
93 | }
94 |
95 | extension NSSize {
96 |
97 | func originForSnapping(to corner: NSRect.Corner, of screen: NSScreen, with insets: NSRect.Corner.Insets = .zero) -> NSPoint {
98 | var origin: NSPoint = .zero
99 |
100 | switch corner.x {
101 | case .leading: origin.x = screen.visibleFrame.minX + insets.x
102 | case .trailing: origin.x = screen.visibleFrame.maxX - (width + insets.x)
103 | }
104 |
105 | switch corner.y {
106 | case .leading: origin.y = screen.visibleFrame.minY + insets.y
107 | case .trailing: origin.y = screen.visibleFrame.maxY - (height + insets.y)
108 | }
109 |
110 | return origin
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/Netflix/Classes/TitleView.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | protocol TitleViewDelegate: class {
4 |
5 | func titleViewDidChangeVisibility(_ titleView: TitleView)
6 |
7 | }
8 |
9 | class TitleView: NSVisualEffectView {
10 |
11 | weak var delegate: TitleViewDelegate?
12 |
13 | override init(frame frameRect: NSRect) {
14 | super.init(frame: frameRect)
15 | initialize()
16 | }
17 |
18 | required init?(coder: NSCoder) {
19 | super.init(coder: coder)
20 | initialize()
21 | }
22 |
23 | private func initialize() {
24 | wantsLayer = true
25 | blendingMode = .withinWindow
26 | material = .titlebar
27 |
28 | let overlay = CALayer()
29 | overlay.frame = layer?.bounds ?? .zero
30 | overlay.backgroundColor = NSColor(deviceWhite: 0, alpha: 0.4).cgColor
31 | overlay.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
32 | layer?.addSublayer(overlay)
33 |
34 | let shadow = NSShadow()
35 | shadow.shadowBlurRadius = 1
36 | shadow.shadowColor = NSColor(deviceWhite: 0, alpha: 0.5)
37 | self.shadow = shadow
38 | }
39 |
40 | var shouldHideWhenInactive: Bool = false {
41 | didSet { updateVisibility() }
42 | }
43 |
44 | override var mouseDownCanMoveWindow: Bool {
45 | return true
46 | }
47 |
48 | override func hitTest(_ point: NSPoint) -> NSView? {
49 | guard self.frame.contains(point) else {
50 | return nil
51 | }
52 |
53 | return self.superview
54 | }
55 |
56 | private var keyWindowObservers: [Any]?
57 |
58 | override func viewWillMove(toWindow newWindow: NSWindow?) {
59 | keyWindowObservers?.forEach { NotificationCenter.default.removeObserver($0) }
60 | keyWindowObservers = nil
61 | }
62 |
63 | override func viewDidMoveToWindow() {
64 | let handler: (_ notification: Notification) -> Void = { [weak self] _ in
65 | self?.updateVisibility()
66 | }
67 |
68 | let becomeKey = NotificationCenter.default.addObserver(forName: NSWindow.didBecomeKeyNotification, object: nil, queue: .main, using: handler)
69 | let resignKey = NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: nil, queue: .main, using: handler)
70 | let enterFullScreen = NotificationCenter.default.addObserver(forName: NSWindow.didEnterFullScreenNotification, object: nil, queue: .main, using: handler)
71 | let exitFullScreen = NotificationCenter.default.addObserver(forName: NSWindow.didExitFullScreenNotification, object: nil, queue: .main, using: handler)
72 |
73 | keyWindowObservers = [becomeKey, resignKey, enterFullScreen, exitFullScreen]
74 | updateVisibility()
75 | }
76 |
77 | @IBOutlet private var heightConstraint: NSLayoutConstraint?
78 |
79 | override func layout() {
80 | super.layout()
81 |
82 | if let heightConstraint = heightConstraint,
83 | let themeFrame = window?.contentView?.superview,
84 | let unmanagedTitlebarViewController = themeFrame.perform(NSSelectorFromString("titlebarViewController")),
85 | let titlebarViewController = unmanagedTitlebarViewController.takeUnretainedValue() as? NSViewController {
86 | heightConstraint.constant = titlebarViewController.view.bounds.size.height
87 | }
88 | }
89 |
90 | override var isHidden: Bool {
91 | didSet { delegate?.titleViewDidChangeVisibility(self) }
92 | }
93 |
94 | private func updateVisibility() {
95 | guard let window = window else {
96 | return
97 | }
98 |
99 | let shouldHide = window.styleMask.contains(.fullScreen) || (shouldHideWhenInactive && !window.isKeyWindow)
100 |
101 | guard isHidden != shouldHide else {
102 | return
103 | }
104 |
105 | isHidden = shouldHide
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/Netflix/Classes/ViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import WebKit
3 |
4 | class ViewController: NSViewController {
5 |
6 | enum PlaybackStatus: String {
7 | case none = "none"
8 | case paused = "paused"
9 | case playing = "playing"
10 | }
11 |
12 | enum PIPStatus {
13 | case notInPIP
14 | case inPIP
15 | case intermediate
16 | }
17 |
18 | private weak var windowController: WindowController? {
19 | return view.window?.windowController as? WindowController
20 | }
21 |
22 | var webView: WebView!
23 |
24 | @IBOutlet weak var titleView: TitleView?
25 |
26 | @IBOutlet weak var activityIndicator: ActivityIndicator?
27 |
28 | @IBOutlet weak var pipOverlayView: NSVisualEffectView!
29 |
30 | private var currentNavigation: WKNavigation?
31 |
32 | private var pipStatus = PIPStatus.notInPIP
33 | private var playbackStatus: PlaybackStatus = .none
34 |
35 | lazy var pip: PIPViewController = {
36 | let pip = PIPViewController()
37 | pip.delegate = self
38 | return pip
39 | }()
40 |
41 | var pipViewVC: NSViewController!
42 |
43 | weak var pipDelegate: PictureInPictureDelegate?
44 |
45 | override func viewDidLoad() {
46 | super.viewDidLoad()
47 |
48 | view.layer?.backgroundColor = NSColor(deviceRed: 0.078, green: 0.078, blue: 0.078, alpha: 1).cgColor
49 | pipOverlayView.isHidden = true
50 |
51 | titleView?.delegate = self
52 |
53 | let configuration = WKWebViewConfiguration()
54 | configuration.userContentController.add(self, name: "jellystyle")
55 | configuration.userContentController.add(self, name: "requestFullscreen")
56 | configuration.userContentController.add(self, name: "playback")
57 | configuration.preferences.setValue(true, forKey: "developerExtrasEnabled")
58 |
59 | webView = WebView(frame: view.frame, configuration: configuration)
60 | webView.isHidden = true
61 | webView.navigationDelegate = self
62 | webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6"
63 |
64 | addWebViewToView()
65 |
66 | // let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
67 | // webView.configuration.websiteDataStore.removeData(ofTypes: dataTypes, modifiedSince: .distantPast, completionHandler: {})
68 |
69 | guard let url = URL(string: "https://www.netflix.com/browse") else {
70 | return
71 | }
72 |
73 | let request = URLRequest(url: url)
74 |
75 | guard let navigation = webView.load(request) else {
76 | return
77 | }
78 |
79 | currentNavigation = navigation
80 | }
81 |
82 | override func viewWillAppear() {
83 | super.viewWillAppear()
84 |
85 | windowController?.fullscreenDelegate = self
86 | }
87 |
88 | internal fileprivate(set) var displayingHeader = true
89 |
90 | internal fileprivate(set) var displayingControls = false
91 |
92 | internal fileprivate(set) var displayingOverlay = false
93 |
94 | internal fileprivate(set) var canSearch = false
95 |
96 | private func addWebViewToView() {
97 | webView.translatesAutoresizingMaskIntoConstraints = false
98 | view.addSubview(webView, positioned: .below, relativeTo: titleView)
99 | view.addConstraints([
100 | webView.topAnchor.constraint(equalTo: view.topAnchor),
101 | webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
102 | webView.leftAnchor.constraint(equalTo: view.leftAnchor),
103 | webView.rightAnchor.constraint(equalTo: view.rightAnchor),
104 | ])
105 | }
106 |
107 | func resume() {
108 | webView.evaluateJavaScript("window.jellystyle.resume();", completionHandler: didEvaluateJavascript)
109 | }
110 |
111 | func pause() {
112 | webView.evaluateJavaScript("window.jellystyle.pause();", completionHandler: didEvaluateJavascript)
113 | }
114 |
115 | @IBAction func search(_ sender: Any?) {
116 | guard let webView = webView, !webView.isHidden else {
117 | return
118 | }
119 |
120 | webView.evaluateJavaScript("window.jellystyle.focusSearch();", completionHandler: didEvaluateJavascript)
121 | }
122 |
123 | func enterPictureInPicture() {
124 | guard playbackStatus != .none, pipStatus == .notInPIP, let webView = webView, !webView.isHidden else {
125 | return
126 | }
127 |
128 | pipDelegate?.willEnterPictureInPicture(self)
129 | pipOverlayView.isHidden = false
130 | pipStatus = .intermediate
131 |
132 | webView.evaluateJavaScript("window.jellystyle.setControlsVisibility(false);", completionHandler: didEvaluateJavascript)
133 |
134 | // wait for transition animation end
135 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
136 | let oldMinSize = self.pip.minSize
137 | let oldMaxSize = self.pip.maxSize
138 |
139 | self.pipViewVC = NSViewController()
140 | self.pipViewVC.view = webView
141 | self.pip.title = webView.title
142 |
143 | self.pip.minSize = webView.frame.size
144 | self.pip.maxSize = webView.frame.size
145 |
146 | webView.isInPipMode = true
147 |
148 | self.pip.presentAsPicture(inPicture: self.pipViewVC)
149 |
150 | webView.autoresizingMask = [.width, .height]
151 | webView.translatesAutoresizingMaskIntoConstraints = true
152 | webView.updateConstraints()
153 |
154 | if let view = webView.superview {
155 | view.addConstraints([
156 | webView.topAnchor.constraint(equalTo: view.topAnchor),
157 | webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
158 | webView.leftAnchor.constraint(equalTo: view.leftAnchor),
159 | webView.rightAnchor.constraint(equalTo: view.rightAnchor),
160 | ])
161 | }
162 |
163 | self.pipStatus = .inPIP
164 | self.pipDelegate?.didEnterPictureInPicture(self)
165 |
166 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
167 | self.pip.minSize = oldMinSize
168 | self.pip.maxSize = oldMaxSize
169 | self.windowController?.window?.miniaturize(self)
170 | }
171 | }
172 | }
173 |
174 | func exitPictureInPicture() {
175 | guard pipStatus == .inPIP else {
176 | return
177 | }
178 |
179 | if pipShouldClose(pip) {
180 | pip.dismiss(self.pipViewVC!)
181 | }
182 | }
183 |
184 | func togglePictureInPicture() {
185 | switch pipStatus {
186 | case .notInPIP:
187 | enterPictureInPicture()
188 |
189 | case .inPIP:
190 | exitPictureInPicture()
191 |
192 | case .intermediate:
193 | break
194 | }
195 | }
196 |
197 | }
198 |
199 | class ViewControllerView: NSView {
200 |
201 | override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
202 | return true
203 | }
204 |
205 | }
206 |
207 | extension ViewController: TitleViewDelegate {
208 |
209 | func titleViewDidChangeVisibility(_ titleView: TitleView) {
210 | guard let webView = webView, !webView.isHidden else {
211 | return
212 | }
213 |
214 | let value: String = titleView.isHidden ? "null" : String(format: "\"%1.1fpx\"", titleView.bounds.size.height)
215 | webView.evaluateJavaScript("window.jellystyle.setTitleViewInset(\(value));", completionHandler: didEvaluateJavascript)
216 |
217 | if let window = view.window, !window.styleMask.contains(.fullScreen) {
218 | window.standardWindowButton(.closeButton)?.superview?.isHidden = titleView.isHidden
219 | }
220 | }
221 |
222 | }
223 |
224 | extension ViewController: WindowControllerFullscreenDelegate {
225 |
226 | func windowDidEnterFullScreen(_ window: NSWindow) {
227 | guard let webView = webView, !webView.isHidden else {
228 | return
229 | }
230 |
231 | webView.evaluateJavaScript("window.jellystyle.windowDidEnterFullScreen(true);", completionHandler: didEvaluateJavascript)
232 | }
233 |
234 | func windowDidFailToEnterFullScreen(_ window: NSWindow) {
235 | guard let webView = webView, !webView.isHidden else {
236 | return
237 | }
238 |
239 | webView.evaluateJavaScript("window.jellystyle.windowDidEnterFullScreen(false);", completionHandler: didEvaluateJavascript)
240 | }
241 |
242 | func windowDidExitFullScreen(_ window: NSWindow) {
243 | guard let webView = webView, !webView.isHidden else {
244 | return
245 | }
246 |
247 | webView.evaluateJavaScript("window.jellystyle.windowDidExitFullScreen(true);", completionHandler: didEvaluateJavascript)
248 | }
249 |
250 | func windowDidFailToExitFullScreen(_ window: NSWindow) {
251 | guard let webView = webView, !webView.isHidden else {
252 | return
253 | }
254 |
255 | webView.evaluateJavaScript("window.jellystyle.windowDidExitFullScreen(false);", completionHandler: didEvaluateJavascript)
256 | }
257 |
258 | }
259 |
260 | extension ViewController: WKScriptMessageHandler {
261 |
262 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
263 | switch message.name {
264 | case "jellystyle":
265 | guard let dictionary = message.body as? [String: Any] else {
266 | print(message.name, message.body)
267 | return
268 | }
269 |
270 | displayingControls = (dictionary["controlsVisible"] as? NSNumber)?.boolValue ?? false
271 | displayingOverlay = (dictionary["overlayVisible"] as? NSNumber)?.boolValue ?? false
272 | displayingHeader = (dictionary["hasHeader"] as? NSNumber)?.boolValue ?? false
273 | canSearch = (dictionary["hasSearch"] as? NSNumber)?.boolValue ?? false
274 |
275 | if let size = dictionary["videoSize"] as? [NSNumber] {
276 | let aspectRatio = NSSize(width: size[0].doubleValue, height: size[1].doubleValue)
277 | windowController?.update(aspectRatio: aspectRatio)
278 | pip.aspectRatio = aspectRatio
279 | }
280 | else {
281 | windowController?.update(aspectRatio: nil)
282 | }
283 |
284 | titleView?.shouldHideWhenInactive = !(displayingControls || displayingHeader)
285 |
286 | case "requestFullscreen":
287 | guard
288 | let boolValue = (message.body as? NSNumber)?.boolValue,
289 | let window = view.window,
290 | window.styleMask.contains(.fullScreen) != boolValue
291 | else {
292 | print(message.name, message.body)
293 | return
294 | }
295 |
296 | window.toggleFullScreen(self)
297 |
298 | case "playback":
299 | guard
300 | let dictionary = message.body as? [String: Any],
301 | let status = PlaybackStatus(rawValue: String((dictionary["status"] as? NSString) ?? "none"))
302 | else {
303 | print(message.name, message.body)
304 | return
305 | }
306 |
307 | guard status != playbackStatus else {
308 | return
309 | }
310 |
311 | switch (playbackStatus, status) {
312 | case (_, .none):
313 | pip.playing = false
314 | exitPictureInPicture()
315 |
316 | case (_, .paused):
317 | pip.playing = false
318 |
319 | case (_, .playing):
320 | pip.playing = true
321 | }
322 |
323 | playbackStatus = status
324 |
325 | default:
326 | print(message.name, message.body)
327 | }
328 | }
329 |
330 | }
331 |
332 | extension ViewController: WKNavigationDelegate {
333 |
334 | func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
335 | activityIndicator?.isHidden = !webView.isHidden
336 | }
337 |
338 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
339 | activityIndicator?.isHidden = true
340 | webView.isHidden = false
341 |
342 | guard let scriptURL = Bundle.main.url(forResource: "Customization", withExtension: "js"), let script = try? String(contentsOf: scriptURL) else {
343 | return
344 | }
345 |
346 | webView.evaluateJavaScript(script, completionHandler: {
347 | self.didLoadCustomizationJavascript($0, $1)
348 | self.didEvaluateJavascript($0, $1)
349 | })
350 | }
351 |
352 | private func didLoadCustomizationJavascript(_ response: Any?, _ error: Error?) {
353 | if let titleView = self.titleView {
354 | self.titleViewDidChangeVisibility(titleView)
355 | }
356 |
357 | if let window = self.view.window, window.styleMask.contains(.fullScreen) {
358 | self.windowDidEnterFullScreen(window)
359 | }
360 | }
361 |
362 | private func didEvaluateJavascript(_ response: Any?, _ error: Error?) {
363 | if let error = error {
364 | print(error)
365 | }
366 | else if let response = response {
367 | print(response)
368 | }
369 | }
370 |
371 | }
372 |
373 | extension ViewController: PIPViewControllerDelegate {
374 |
375 | private func preparePipClose() {
376 | pipDelegate?.willExitPictureInPicture(self)
377 | pipStatus = .intermediate
378 | pip.replacementView = view
379 |
380 | NSApp.activate(ignoringOtherApps: true)
381 |
382 | if let window = windowController?.window {
383 | window.deminiaturize(pip)
384 |
385 | var frame = window.frame
386 | frame.size = webView.frame.size
387 | window.setFrame(frame, display: true, animate: true)
388 | }
389 | }
390 |
391 | private func pipClosed() {
392 | addWebViewToView()
393 | pipOverlayView.isHidden = true
394 | pipStatus = .notInPIP
395 | webView.isInPipMode = false
396 | pipDelegate?.didExitPictureInPicture(self)
397 | }
398 |
399 | public func pipShouldClose(_ pip: PIPViewController) -> Bool {
400 | preparePipClose()
401 | return true
402 | }
403 |
404 | public func pipWillClose(_ pip: PIPViewController) {
405 | preparePipClose()
406 | }
407 |
408 | public func pipDidClose(_ pip: PIPViewController) {
409 | pipClosed()
410 | }
411 |
412 | public func pipActionPlay(_ pip: PIPViewController) {
413 | resume()
414 | }
415 |
416 | public func pipActionPause(_ pip: PIPViewController) {
417 | pause()
418 | }
419 |
420 | public func pipActionStop(_ pip: PIPViewController) {
421 | pause()
422 | }
423 |
424 | }
425 |
--------------------------------------------------------------------------------
/Netflix/Classes/WindowController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | protocol WindowControllerFullscreenDelegate: class {
4 |
5 | func windowDidEnterFullScreen(_ window: NSWindow)
6 |
7 | func windowDidFailToEnterFullScreen(_ window: NSWindow)
8 |
9 | func windowDidExitFullScreen(_ window: NSWindow)
10 |
11 | func windowDidFailToExitFullScreen(_ window: NSWindow)
12 |
13 | }
14 |
15 | class WindowController: NSWindowController, NSWindowDelegate {
16 |
17 | weak var fullscreenDelegate: WindowControllerFullscreenDelegate?
18 |
19 | override func windowDidLoad() {
20 | super.windowDidLoad()
21 |
22 | guard let window = window else {
23 | return
24 | }
25 |
26 | window.delegate = self
27 | window.title = ""
28 | window.styleMask.insert(.fullSizeContentView)
29 | window.collectionBehavior.insert(.fullScreenPrimary)
30 | window.titlebarAppearsTransparent = true
31 | window.isMovableByWindowBackground = true
32 |
33 | window.setContentSize(NSSize(width: 1280, height: 904))
34 | window.contentMinSize = NSSize(width: 180, height: 102)
35 |
36 | didSetKeepInFront()
37 | didSetSnapToCorners()
38 | }
39 |
40 | // MARK: Lock aspect ratio in videos
41 |
42 | func update(aspectRatio: NSSize?) {
43 | guard let window = window, let screen = window.screen else {
44 | return
45 | }
46 |
47 | if let aspectRatio = aspectRatio, !window.styleMask.contains(.fullScreen) {
48 | guard window.contentAspectRatio != aspectRatio else {
49 | return
50 | }
51 |
52 | window.contentAspectRatio = aspectRatio
53 |
54 | // We want to snap to the corner we're closest to pre-resize
55 | let preferredCorner = window.frame.preferredCorner(for: screen)
56 |
57 | var frame = window.frame
58 | frame.size.height = aspectRatio.height / aspectRatio.width * frame.size.width
59 |
60 | if snapToCorners {
61 | frame.origin = frame.originForSnapping(to: preferredCorner, of: screen, with: snapInset)
62 | }
63 | else {
64 | // When not snapping, scale towards the center of the window.
65 | frame.origin.y += (window.frame.size.height - frame.size.height) / 2
66 | }
67 |
68 | window.setFrame(frame, display: true, animate: true)
69 | }
70 | else {
71 | window.contentResizeIncrements = NSSize(width: 1, height: 1)
72 | }
73 | }
74 |
75 | // MARK: Keep in front
76 |
77 | static private let keepInFrontKey = "com.jellystyle.Netflix.WindowController.KeepInFront"
78 |
79 | private func didSetKeepInFront() {
80 | guard let window = window else {
81 | return
82 | }
83 |
84 | window.level = self.keepInFront ? .modalPanel : .normal
85 | }
86 |
87 | var keepInFront: Bool {
88 | get { return (UserDefaults.standard.object(forKey: WindowController.keepInFrontKey) as? NSNumber)?.boolValue ?? true }
89 | set {
90 | UserDefaults.standard.set(NSNumber(value: newValue), forKey: WindowController.keepInFrontKey)
91 | didSetKeepInFront()
92 | }
93 | }
94 |
95 | @IBAction func toggleKeepInFront(_ sender: Any?) {
96 | self.keepInFront = !self.keepInFront
97 | }
98 |
99 | // MARK: Snap to corners
100 |
101 | static private let snapToCornersKey = "com.jellystyle.Netflix.WindowController.SnapToCorners"
102 |
103 | private let snapInset = NSRect.Corner.Insets(x: 20, y: 20)
104 |
105 | private func didSetSnapToCorners() {
106 | guard snapToCorners, let window = window, !window.styleMask.contains(.fullScreen), let screen = window.screen else {
107 | self.window?.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
108 |
109 | return
110 | }
111 |
112 | window.maxSize = NSSize(width: screen.visibleFrame.width * 0.75, height: screen.visibleFrame.height * 0.75)
113 |
114 | guard !isMoving, window.frame.snappedCorner(of: screen, with: snapInset) == nil else {
115 | return
116 | }
117 |
118 | var frame = window.frame
119 | frame.origin = frame.originForSnappingToPreferredCorner(of: screen, with: snapInset)
120 |
121 | if frame.size.width > window.maxSize.width {
122 | frame.size.width = window.maxSize.width
123 | }
124 |
125 | if frame.size.height > window.maxSize.height {
126 | frame.size.height = window.maxSize.height
127 | }
128 |
129 | window.setFrame(frame, display: false, animate: true)
130 | }
131 |
132 | var snapToCorners: Bool {
133 | get { return (UserDefaults.standard.object(forKey: WindowController.snapToCornersKey) as? NSNumber)?.boolValue ?? false }
134 | set {
135 | UserDefaults.standard.set(NSNumber(value: newValue), forKey: WindowController.snapToCornersKey)
136 | didSetSnapToCorners()
137 | }
138 | }
139 |
140 | @IBAction func toggleSnapToCorners(_ sender: Any?) {
141 | self.snapToCorners = !self.snapToCorners
142 | }
143 |
144 | // MARK: Picture In Picture
145 |
146 | @IBAction func togglePictureInPicture(_ sender: Any?) {
147 | if let vc = self.contentViewController as? ViewController {
148 | vc.togglePictureInPicture()
149 | }
150 | }
151 |
152 | // MARK: NSResponder
153 |
154 | private var isMoving = false
155 |
156 | override func mouseUp(with event: NSEvent) {
157 | super.mouseUp(with: event)
158 |
159 | guard isMoving else {
160 | return
161 | }
162 |
163 | isMoving = false
164 |
165 | guard !event.modifierFlags.contains(.command) else {
166 | return
167 | }
168 |
169 | didSetSnapToCorners()
170 | }
171 |
172 | // MARK: Window delegate
173 |
174 | func windowWillMove(_ notification: Notification) {
175 | isMoving = true
176 | }
177 |
178 | func windowDidMove(_ notification: Notification) {
179 | isMoving = true // Sometimes windowWillMove isn't called
180 | }
181 |
182 | func windowDidChangeScreen(_ notification: Notification) {
183 | didSetSnapToCorners()
184 | }
185 |
186 | func windowDidChangeBackingProperties(_ notification: Notification) {
187 | didSetSnapToCorners()
188 | }
189 |
190 | func windowDidEndLiveResize(_ notification: Notification) {
191 | didSetSnapToCorners()
192 | }
193 |
194 | func windowDidEnterFullScreen(_ notification: Notification) {
195 | guard let window = notification.object as? NSWindow else {
196 | return
197 | }
198 |
199 | fullscreenDelegate?.windowDidEnterFullScreen(window)
200 | }
201 |
202 | func windowDidFailToEnterFullScreen(_ window: NSWindow) {
203 | fullscreenDelegate?.windowDidFailToEnterFullScreen(window)
204 | }
205 |
206 | func windowDidExitFullScreen(_ notification: Notification) {
207 | didSetSnapToCorners()
208 |
209 | guard let window = notification.object as? NSWindow else {
210 | return
211 | }
212 |
213 | fullscreenDelegate?.windowDidExitFullScreen(window)
214 | }
215 |
216 | func windowDidFailToExitFullScreen(_ window: NSWindow) {
217 | fullscreenDelegate?.windowDidFailToExitFullScreen(window)
218 | }
219 |
220 | }
221 |
--------------------------------------------------------------------------------
/Netflix/Customization.js:
--------------------------------------------------------------------------------
1 | ;(function(undefined){
2 |
3 | 'use strict';
4 |
5 | var jellystyle = window.jellystyle = {
6 | mutationObservers: {}
7 | };
8 |
9 | jellystyle.mutationCallback = function(mutation) {
10 | var message = {
11 | hasHeader: false,
12 | hasSearch: window.jellystyle.hasSearch(),
13 | controlsVisible: false,
14 | overlayVisible: false,
15 | playerClass: null,
16 | videoSize: null
17 | };
18 |
19 | var pinningHeader = document.getElementsByClassName("pinning-header-container").item(0);
20 | var mainHeader = document.getElementsByClassName("main-header").item(0);
21 | var memberHeader = document.getElementsByClassName("member-header").item(0);
22 | var ourStoryHeader = document.getElementsByClassName("our-story-header").item(0);
23 | var loginHeader = document.getElementsByClassName("login-header").item(0);
24 | var globalHeader = document.getElementsByClassName("global-header").item(0);
25 | message.hasHeader = pinningHeader !== null || mainHeader !== null || memberHeader !== null || ourStoryHeader !== null || loginHeader !== null || globalHeader !== null;
26 |
27 | var player = window.jellystyle.playerContainer();
28 | if (player !== null) {
29 | message.controlsVisible = player.className.match(/\bactive\b/) !== null;
30 | message.overlayVisible = !message.controlsVisible && player.className.match(/\binactive\b/) === null;
31 | message.playerClass = player.className;
32 |
33 | var video = player.getElementsByTagName("video").item(0);
34 | if (video !== null) {
35 | message.videoSize = [video.videoWidth, video.videoHeight];
36 |
37 | jellystyle.updateVideoElement(video);
38 | } else {
39 | jellystyle.updateVideoElement(null);
40 | }
41 |
42 | if (!jellystyle.isObserving("player", player)) {
43 | // Dispatch an event to get the full screen icon to reflect current status
44 | document.dispatchEvent(new Event("fullscreenchange"))
45 | }
46 |
47 | jellystyle.startObserving("player", player, { attributes: true, attributeFilter: ["class"] });
48 | }
49 |
50 | // Refresh any insets, just in case we've modified the document
51 | jellystyle.setTitleViewInset(jellystyle.currentTitleViewInset);
52 |
53 | window.webkit.messageHandlers.jellystyle.postMessage(message);
54 | };
55 |
56 | jellystyle.playerContainer = function() {
57 | var player = document.getElementsByClassName("nf-player-container").item(0);
58 |
59 | if (player === null ) {
60 | return null;
61 | }
62 |
63 | var potentialControlElements = [
64 | player.getElementsByClassName("controls"),
65 | player.getElementsByClassName("PlayerControlsNeo__all-controls"),
66 | ]
67 |
68 | for(var i in potentialControlElements) {
69 | var potentialControlElement = potentialControlElements[i];
70 |
71 | if (potentialControlElement.length >= 1) {
72 | return player
73 | }
74 | }
75 |
76 | return null;
77 | };
78 |
79 | jellystyle.currentTitleViewInset = null;
80 |
81 | jellystyle.setTitleViewInset = function(value) {
82 | var classNames = {
83 | "main-header": "marginTop",
84 | "member-header": "marginTop",
85 | "our-story-header": "marginTop",
86 | "login-header": "marginTop",
87 | "login-body": "paddingTop",
88 | "global-header": "paddingTop",
89 | "top-left-controls": "paddingTop",
90 | };
91 |
92 | for(const [className, propertyName] of Object.entries(classNames)) {
93 | //var propertyName = classNames[className];
94 | var element = document.getElementsByClassName(className).item(0)
95 |
96 | if (element === null) {
97 | continue;
98 | }
99 |
100 | element.style[propertyName] = value;
101 | }
102 |
103 | jellystyle.currentTitleViewInset = value;
104 | };
105 |
106 | jellystyle.hasSearch = function() {
107 | var searchButton = document.getElementsByClassName("searchTab").item(0);
108 | if (searchButton !== null) {
109 | return true;
110 | }
111 |
112 | var searchInput = document.getElementsByTagName("input").item(0);
113 | if (searchInput !== null && searchInput.hasAttribute("data-search-input") && searchInput.attributes["data-search-input"].value === "true") {
114 | return true;
115 | }
116 |
117 | return false;
118 | };
119 |
120 | jellystyle.focusSearch = function() {
121 | var searchButton = document.getElementsByClassName("searchTab").item(0);
122 | if (searchButton !== null) {
123 | searchButton.click();
124 | }
125 |
126 | var searchInput = document.getElementsByTagName("input").item(0);
127 | if (searchInput !== null && searchInput.hasAttribute("data-search-input") && searchInput.attributes["data-search-input"].value === "true") {
128 | searchInput.focus();
129 | }
130 | };
131 |
132 | jellystyle.startObserving = function(key, element, options) {
133 | if (jellystyle.isObserving(key, element)) {
134 | return;
135 | }
136 |
137 | var observer = new MutationObserver(jellystyle.mutationCallback);
138 | observer.observe(element, options);
139 | jellystyle.mutationObservers[key] = [observer, element];
140 | };
141 |
142 | jellystyle.isObserving = function(key, element) {
143 | if (jellystyle.mutationObservers[key] === undefined) {
144 | return false;
145 | }
146 |
147 | var observer = jellystyle.mutationObservers[key];
148 |
149 | if (observer[1] !== element) {
150 | return false;
151 | }
152 |
153 | return true;
154 | };
155 |
156 | jellystyle.stopObserving = function(key, element, options) {
157 | if (!jellystyle.isObserving(key, element)) {
158 | return;
159 | }
160 |
161 | jellystyle.mutationObservers[key][0].disconnect();
162 | delete jellystyle.mutationObservers[key];
163 | };
164 |
165 | // MARK: Console override
166 |
167 | console.log = function() {
168 | var message = Array.from(arguments).join(" ");
169 | window.webkit.messageHandlers.jellystyle.postMessage(message);
170 | }
171 |
172 | // MARK: Full screen mode
173 |
174 | HTMLDocument.prototype.fullscreenEnabled = true;
175 |
176 | HTMLElement.prototype.requestFullscreen = function() {
177 | window.jellystyle.pendingFullScreenElement = this;
178 | window.webkit.messageHandlers.requestFullscreen.postMessage(true);
179 | return new Promise(function(resolve) { resolve(); });
180 | };
181 |
182 | HTMLDocument.prototype.exitFullscreen = function() {
183 | window.webkit.messageHandlers.requestFullscreen.postMessage(false);
184 | return new Promise(function(resolve) { resolve(); });
185 | };
186 |
187 | jellystyle.pendingFullScreenElement = null;
188 |
189 | jellystyle.windowDidEnterFullScreen = function(success) {
190 | if (success) {
191 | document.fullscreen = true;
192 | document.fullscreenElement = window.jellystyle.pendingFullScreenElement || document.body;
193 | document.dispatchEvent(new Event("fullscreenchange"))
194 | }
195 | else {
196 | document.dispatchEvent(new Event("fullscreenerror"))
197 | }
198 |
199 | window.jellystyle.pendingFullScreenElement = null;
200 | };
201 |
202 | jellystyle.windowDidExitFullScreen = function(success) {
203 | if (success) {
204 | document.fullscreen = false;
205 | document.fullscreenElement = null;
206 | document.dispatchEvent(new Event("fullscreenchange"))
207 | }
208 | else {
209 | document.dispatchEvent(new Event("fullscreenerror"))
210 | }
211 | };
212 |
213 | // MARK: Playback control
214 |
215 | var setVideoStatus = function(status) {
216 | window.webkit.messageHandlers.playback.postMessage({
217 | status: status
218 | });
219 | };
220 |
221 | var onVideoPlay = setVideoStatus.bind(null, "playing");
222 | var onVideoPause = setVideoStatus.bind(null, "paused");
223 |
224 | jellystyle.updateVideoElement = function(video) {
225 | if ((jellystyle.videoElement || null) === (video || null)) {
226 | return;
227 | }
228 |
229 | if (!video) {
230 | setVideoStatus("none");
231 | return;
232 | }
233 |
234 | if (jellystyle.videoElement) {
235 | jellystyle.videoElement.removeEventListener("play", onVideoPlay);
236 | jellystyle.videoElement.removeEventListener("pause", onVideoPause);
237 | }
238 |
239 | setVideoStatus(video.paused ? "paused" : "playing");
240 |
241 | video.addEventListener("play", onVideoPlay);
242 | video.addEventListener("pause", onVideoPause);
243 |
244 | jellystyle.videoElement = video;
245 | };
246 |
247 | jellystyle.resume = function() {
248 | var video = window.jellystyle.videoElement;
249 | if (video) {
250 | video.play();
251 | }
252 | };
253 |
254 | jellystyle.pause = function() {
255 | var video = window.jellystyle.videoElement;
256 | if (video) {
257 | video.pause();
258 | }
259 | };
260 |
261 | // MARK: Other utilities
262 |
263 | jellystyle.setControlsVisibility = function(flag) {
264 | var controlsElement = document.getElementsByClassName("PlayerControlsNeo__layout").item(0);
265 | if (!controlsElement) {
266 | return;
267 | }
268 |
269 | if (flag) {
270 | controlsElement.classList.add('PlayerControlsNeo__layout--active');
271 | controlsElement.classList.remove('PlayerControlsNeo__layout--inactive')
272 | } else {
273 | controlsElement.classList.add('PlayerControlsNeo__layout--inactive');
274 | controlsElement.classList.remove('PlayerControlsNeo__layout--active')
275 | }
276 | };
277 |
278 |
279 | jellystyle.startObserving("body", document.body, { attributes: true, subtree: true });
280 |
281 | // Always run the mutation callback at least once
282 | jellystyle.mutationCallback(null);
283 |
284 |
285 | })();
286 |
--------------------------------------------------------------------------------
/Netflix/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0.5
21 | CFBundleVersion
22 | 1
23 | LSApplicationCategoryType
24 | public.app-category.entertainment
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | NSHumanReadableCopyright
28 | Copyright © 2018 Daniel Farrelly. All rights reserved.
29 | NSMainStoryboardFile
30 | Main
31 | NSPrincipalClass
32 | NSApplication
33 | SUFeedURL
34 | https://jellystyle.com/resources/macOS/Netflix.xml
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Netflix/Netflix.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.get-task-allow
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Netflix/Netflix.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A10B13232069DC6F00A5CB98 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10B13222069DC6F00A5CB98 /* ActivityIndicator.swift */; };
11 | A11E73F3206B586200FEE183 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = A11E73F2206B586200FEE183 /* Credits.rtf */; };
12 | A11E73F5206BA12900FEE183 /* NSRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E73F4206BA12900FEE183 /* NSRect.swift */; };
13 | A1310602259060E000548F12 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A1310601259060E000548F12 /* Sparkle */; };
14 | A139AF97205F775A008680F4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A139AF96205F775A008680F4 /* AppDelegate.swift */; };
15 | A139AF99205F775A008680F4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A139AF98205F775A008680F4 /* ViewController.swift */; };
16 | A139AF9B205F775A008680F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A139AF9A205F775A008680F4 /* Assets.xcassets */; };
17 | A139AF9E205F775A008680F4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A139AF9C205F775A008680F4 /* Main.storyboard */; };
18 | A139AFA7205F7949008680F4 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A139AFA6205F7949008680F4 /* WindowController.swift */; };
19 | A139AFA9205FCBFA008680F4 /* TitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A139AFA8205FCBFA008680F4 /* TitleView.swift */; };
20 | A139AFAB205FD565008680F4 /* Customization.js in Resources */ = {isa = PBXBuildFile; fileRef = A139AFAA205FD565008680F4 /* Customization.js */; };
21 | C083408E264EF45D00938BC4 /* PictureInPictureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C083408D264EF45D00938BC4 /* PictureInPictureDelegate.swift */; };
22 | C09C23722649D0F700416DBD /* PIP.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C09C23712649D0F700416DBD /* PIP.framework */; };
23 | C09C23732649D0F700416DBD /* PIP.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C09C23712649D0F700416DBD /* PIP.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
24 | C09C23782649E95C00416DBD /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09C23772649E95C00416DBD /* WebView.swift */; };
25 | /* End PBXBuildFile section */
26 |
27 | /* Begin PBXCopyFilesBuildPhase section */
28 | C09C23742649D0F700416DBD /* Embed Frameworks */ = {
29 | isa = PBXCopyFilesBuildPhase;
30 | buildActionMask = 2147483647;
31 | dstPath = "";
32 | dstSubfolderSpec = 10;
33 | files = (
34 | C09C23732649D0F700416DBD /* PIP.framework in Embed Frameworks */,
35 | );
36 | name = "Embed Frameworks";
37 | runOnlyForDeploymentPostprocessing = 0;
38 | };
39 | /* End PBXCopyFilesBuildPhase section */
40 |
41 | /* Begin PBXFileReference section */
42 | A10B13222069DC6F00A5CB98 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActivityIndicator.swift; path = Classes/ActivityIndicator.swift; sourceTree = ""; };
43 | A11E73F2206B586200FEE183 /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; name = Credits.rtf; path = Base.lproj/Credits.rtf; sourceTree = ""; };
44 | A11E73F4206BA12900FEE183 /* NSRect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NSRect.swift; path = Classes/NSRect.swift; sourceTree = ""; };
45 | A139AF93205F775A008680F4 /* Netflix.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Netflix.app; sourceTree = BUILT_PRODUCTS_DIR; };
46 | A139AF96205F775A008680F4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Classes/AppDelegate.swift; sourceTree = ""; };
47 | A139AF98205F775A008680F4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = Classes/ViewController.swift; sourceTree = ""; };
48 | A139AF9A205F775A008680F4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
49 | A139AF9D205F775A008680F4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
50 | A139AF9F205F775A008680F4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
51 | A139AFA0205F775A008680F4 /* Netflix.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Netflix.entitlements; sourceTree = ""; };
52 | A139AFA6205F7949008680F4 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WindowController.swift; path = Classes/WindowController.swift; sourceTree = ""; };
53 | A139AFA8205FCBFA008680F4 /* TitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TitleView.swift; path = Classes/TitleView.swift; sourceTree = ""; };
54 | A139AFAA205FD565008680F4 /* Customization.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Customization.js; sourceTree = ""; };
55 | C083408D264EF45D00938BC4 /* PictureInPictureDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureDelegate.swift; sourceTree = ""; };
56 | C09C236F2649C2C500416DBD /* PIP_BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PIP_BridgingHeader.h; sourceTree = ""; };
57 | C09C23712649D0F700416DBD /* PIP.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PIP.framework; path = /System/Library/PrivateFrameworks/PIP.framework; sourceTree = ""; };
58 | C09C23772649E95C00416DBD /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; };
59 | /* End PBXFileReference section */
60 |
61 | /* Begin PBXFrameworksBuildPhase section */
62 | A139AF90205F775A008680F4 /* Frameworks */ = {
63 | isa = PBXFrameworksBuildPhase;
64 | buildActionMask = 2147483647;
65 | files = (
66 | C09C23722649D0F700416DBD /* PIP.framework in Frameworks */,
67 | A1310602259060E000548F12 /* Sparkle in Frameworks */,
68 | );
69 | runOnlyForDeploymentPostprocessing = 0;
70 | };
71 | /* End PBXFrameworksBuildPhase section */
72 |
73 | /* Begin PBXGroup section */
74 | A11E73EE206B1CCB00FEE183 /* Supporting Files */ = {
75 | isa = PBXGroup;
76 | children = (
77 | A11E73F2206B586200FEE183 /* Credits.rtf */,
78 | A139AFAA205FD565008680F4 /* Customization.js */,
79 | A139AF9F205F775A008680F4 /* Info.plist */,
80 | C09C236F2649C2C500416DBD /* PIP_BridgingHeader.h */,
81 | A139AFA0205F775A008680F4 /* Netflix.entitlements */,
82 | );
83 | name = "Supporting Files";
84 | sourceTree = "";
85 | };
86 | A11E73F6206BA14F00FEE183 /* Class Extensions */ = {
87 | isa = PBXGroup;
88 | children = (
89 | A11E73F4206BA12900FEE183 /* NSRect.swift */,
90 | );
91 | name = "Class Extensions";
92 | sourceTree = "";
93 | };
94 | A11E73F7206BA1B900FEE183 /* Custom Views */ = {
95 | isa = PBXGroup;
96 | children = (
97 | A139AFA8205FCBFA008680F4 /* TitleView.swift */,
98 | A10B13222069DC6F00A5CB98 /* ActivityIndicator.swift */,
99 | C09C23772649E95C00416DBD /* WebView.swift */,
100 | );
101 | name = "Custom Views";
102 | sourceTree = "";
103 | };
104 | A139AF8A205F775A008680F4 = {
105 | isa = PBXGroup;
106 | children = (
107 | A139AF95205F775A008680F4 /* Netflix */,
108 | A139AF94205F775A008680F4 /* Products */,
109 | C09C23702649D0F700416DBD /* Frameworks */,
110 | );
111 | sourceTree = "";
112 | usesTabs = 1;
113 | };
114 | A139AF94205F775A008680F4 /* Products */ = {
115 | isa = PBXGroup;
116 | children = (
117 | A139AF93205F775A008680F4 /* Netflix.app */,
118 | );
119 | name = Products;
120 | sourceTree = "";
121 | };
122 | A139AF95205F775A008680F4 /* Netflix */ = {
123 | isa = PBXGroup;
124 | children = (
125 | A139AF9C205F775A008680F4 /* Main.storyboard */,
126 | A139AF96205F775A008680F4 /* AppDelegate.swift */,
127 | A139AF98205F775A008680F4 /* ViewController.swift */,
128 | A139AFA6205F7949008680F4 /* WindowController.swift */,
129 | C083408D264EF45D00938BC4 /* PictureInPictureDelegate.swift */,
130 | A11E73F7206BA1B900FEE183 /* Custom Views */,
131 | A11E73F6206BA14F00FEE183 /* Class Extensions */,
132 | A139AF9A205F775A008680F4 /* Assets.xcassets */,
133 | A11E73EE206B1CCB00FEE183 /* Supporting Files */,
134 | );
135 | name = Netflix;
136 | sourceTree = "";
137 | };
138 | C09C23702649D0F700416DBD /* Frameworks */ = {
139 | isa = PBXGroup;
140 | children = (
141 | C09C23712649D0F700416DBD /* PIP.framework */,
142 | );
143 | name = Frameworks;
144 | sourceTree = "";
145 | };
146 | /* End PBXGroup section */
147 |
148 | /* Begin PBXNativeTarget section */
149 | A139AF92205F775A008680F4 /* Netflix */ = {
150 | isa = PBXNativeTarget;
151 | buildConfigurationList = A139AFA3205F775A008680F4 /* Build configuration list for PBXNativeTarget "Netflix" */;
152 | buildPhases = (
153 | A11E73F1206B228600FEE183 /* SwiftLint */,
154 | A11E73EF206B1F0700FEE183 /* Determine Build Number */,
155 | A139AF8F205F775A008680F4 /* Sources */,
156 | A139AF90205F775A008680F4 /* Frameworks */,
157 | A139AF91205F775A008680F4 /* Resources */,
158 | A11E73F0206B203C00FEE183 /* Reset Build Number */,
159 | C09C23742649D0F700416DBD /* Embed Frameworks */,
160 | );
161 | buildRules = (
162 | );
163 | dependencies = (
164 | );
165 | name = Netflix;
166 | packageProductDependencies = (
167 | A1310601259060E000548F12 /* Sparkle */,
168 | );
169 | productName = Netflix;
170 | productReference = A139AF93205F775A008680F4 /* Netflix.app */;
171 | productType = "com.apple.product-type.application";
172 | };
173 | /* End PBXNativeTarget section */
174 |
175 | /* Begin PBXProject section */
176 | A139AF8B205F775A008680F4 /* Project object */ = {
177 | isa = PBXProject;
178 | attributes = {
179 | LastSwiftUpdateCheck = 0920;
180 | LastUpgradeCheck = 1230;
181 | ORGANIZATIONNAME = "Daniel Farrelly";
182 | TargetAttributes = {
183 | A139AF92205F775A008680F4 = {
184 | CreatedOnToolsVersion = 9.2;
185 | LastSwiftMigration = 1160;
186 | ProvisioningStyle = Automatic;
187 | SystemCapabilities = {
188 | com.apple.HardenedRuntime = {
189 | enabled = 1;
190 | };
191 | com.apple.Sandbox = {
192 | enabled = 0;
193 | };
194 | };
195 | };
196 | };
197 | };
198 | buildConfigurationList = A139AF8E205F775A008680F4 /* Build configuration list for PBXProject "Netflix" */;
199 | compatibilityVersion = "Xcode 8.0";
200 | developmentRegion = en;
201 | hasScannedForEncodings = 0;
202 | knownRegions = (
203 | en,
204 | Base,
205 | );
206 | mainGroup = A139AF8A205F775A008680F4;
207 | packageReferences = (
208 | A1310600259060E000548F12 /* XCRemoteSwiftPackageReference "Sparkle" */,
209 | );
210 | productRefGroup = A139AF94205F775A008680F4 /* Products */;
211 | projectDirPath = "";
212 | projectRoot = "";
213 | targets = (
214 | A139AF92205F775A008680F4 /* Netflix */,
215 | );
216 | };
217 | /* End PBXProject section */
218 |
219 | /* Begin PBXResourcesBuildPhase section */
220 | A139AF91205F775A008680F4 /* Resources */ = {
221 | isa = PBXResourcesBuildPhase;
222 | buildActionMask = 2147483647;
223 | files = (
224 | A139AFAB205FD565008680F4 /* Customization.js in Resources */,
225 | A139AF9B205F775A008680F4 /* Assets.xcassets in Resources */,
226 | A139AF9E205F775A008680F4 /* Main.storyboard in Resources */,
227 | A11E73F3206B586200FEE183 /* Credits.rtf in Resources */,
228 | );
229 | runOnlyForDeploymentPostprocessing = 0;
230 | };
231 | /* End PBXResourcesBuildPhase section */
232 |
233 | /* Begin PBXShellScriptBuildPhase section */
234 | A11E73EF206B1F0700FEE183 /* Determine Build Number */ = {
235 | isa = PBXShellScriptBuildPhase;
236 | buildActionMask = 2147483647;
237 | files = (
238 | );
239 | inputPaths = (
240 | );
241 | name = "Determine Build Number";
242 | outputPaths = (
243 | );
244 | runOnlyForDeploymentPostprocessing = 0;
245 | shellPath = /bin/sh;
246 | shellScript = "# The repo is a good way to track builds, since we'll reset this later\nsh \"update-version.sh\" --reflect-commits\n";
247 | };
248 | A11E73F0206B203C00FEE183 /* Reset Build Number */ = {
249 | isa = PBXShellScriptBuildPhase;
250 | buildActionMask = 2147483647;
251 | files = (
252 | );
253 | inputPaths = (
254 | );
255 | name = "Reset Build Number";
256 | outputPaths = (
257 | );
258 | runOnlyForDeploymentPostprocessing = 0;
259 | shellPath = /bin/sh;
260 | shellScript = "# We don't want to gum up the repo with build number updates\nsh \"update-version.sh\" --build=1";
261 | };
262 | A11E73F1206B228600FEE183 /* SwiftLint */ = {
263 | isa = PBXShellScriptBuildPhase;
264 | buildActionMask = 2147483647;
265 | files = (
266 | );
267 | inputPaths = (
268 | );
269 | name = SwiftLint;
270 | outputPaths = (
271 | );
272 | runOnlyForDeploymentPostprocessing = 0;
273 | shellPath = /bin/sh;
274 | shellScript = "if which swiftlint >/dev/null; then\n cd .. && swiftlint # Run SwiftLint from the root directory, not just the one for this xcodeproj\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
275 | };
276 | /* End PBXShellScriptBuildPhase section */
277 |
278 | /* Begin PBXSourcesBuildPhase section */
279 | A139AF8F205F775A008680F4 /* Sources */ = {
280 | isa = PBXSourcesBuildPhase;
281 | buildActionMask = 2147483647;
282 | files = (
283 | A139AF99205F775A008680F4 /* ViewController.swift in Sources */,
284 | A10B13232069DC6F00A5CB98 /* ActivityIndicator.swift in Sources */,
285 | A11E73F5206BA12900FEE183 /* NSRect.swift in Sources */,
286 | A139AFA7205F7949008680F4 /* WindowController.swift in Sources */,
287 | A139AF97205F775A008680F4 /* AppDelegate.swift in Sources */,
288 | A139AFA9205FCBFA008680F4 /* TitleView.swift in Sources */,
289 | C083408E264EF45D00938BC4 /* PictureInPictureDelegate.swift in Sources */,
290 | C09C23782649E95C00416DBD /* WebView.swift in Sources */,
291 | );
292 | runOnlyForDeploymentPostprocessing = 0;
293 | };
294 | /* End PBXSourcesBuildPhase section */
295 |
296 | /* Begin PBXVariantGroup section */
297 | A139AF9C205F775A008680F4 /* Main.storyboard */ = {
298 | isa = PBXVariantGroup;
299 | children = (
300 | A139AF9D205F775A008680F4 /* Base */,
301 | );
302 | name = Main.storyboard;
303 | sourceTree = "";
304 | };
305 | /* End PBXVariantGroup section */
306 |
307 | /* Begin XCBuildConfiguration section */
308 | A139AFA1205F775A008680F4 /* Debug */ = {
309 | isa = XCBuildConfiguration;
310 | buildSettings = {
311 | ALWAYS_SEARCH_USER_PATHS = NO;
312 | CLANG_ANALYZER_NONNULL = YES;
313 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
314 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
315 | CLANG_CXX_LIBRARY = "libc++";
316 | CLANG_ENABLE_MODULES = YES;
317 | CLANG_ENABLE_OBJC_ARC = YES;
318 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
319 | CLANG_WARN_BOOL_CONVERSION = YES;
320 | CLANG_WARN_COMMA = YES;
321 | CLANG_WARN_CONSTANT_CONVERSION = YES;
322 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
324 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
325 | CLANG_WARN_EMPTY_BODY = YES;
326 | CLANG_WARN_ENUM_CONVERSION = YES;
327 | CLANG_WARN_INFINITE_RECURSION = YES;
328 | CLANG_WARN_INT_CONVERSION = YES;
329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
333 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
334 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
335 | CLANG_WARN_STRICT_PROTOTYPES = YES;
336 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
337 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
338 | CLANG_WARN_UNREACHABLE_CODE = YES;
339 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
340 | CODE_SIGN_IDENTITY = "Mac Developer";
341 | COPY_PHASE_STRIP = NO;
342 | DEBUG_INFORMATION_FORMAT = dwarf;
343 | ENABLE_STRICT_OBJC_MSGSEND = YES;
344 | ENABLE_TESTABILITY = YES;
345 | GCC_C_LANGUAGE_STANDARD = gnu11;
346 | GCC_DYNAMIC_NO_PIC = NO;
347 | GCC_NO_COMMON_BLOCKS = YES;
348 | GCC_OPTIMIZATION_LEVEL = 0;
349 | GCC_PREPROCESSOR_DEFINITIONS = (
350 | "DEBUG=1",
351 | "$(inherited)",
352 | );
353 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
354 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
355 | GCC_WARN_UNDECLARED_SELECTOR = YES;
356 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
357 | GCC_WARN_UNUSED_FUNCTION = YES;
358 | GCC_WARN_UNUSED_VARIABLE = YES;
359 | MACOSX_DEPLOYMENT_TARGET = 10.11;
360 | MTL_ENABLE_DEBUG_INFO = YES;
361 | ONLY_ACTIVE_ARCH = YES;
362 | SDKROOT = macosx;
363 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
364 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
365 | };
366 | name = Debug;
367 | };
368 | A139AFA2205F775A008680F4 /* Release */ = {
369 | isa = XCBuildConfiguration;
370 | buildSettings = {
371 | ALWAYS_SEARCH_USER_PATHS = NO;
372 | CLANG_ANALYZER_NONNULL = YES;
373 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
374 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
375 | CLANG_CXX_LIBRARY = "libc++";
376 | CLANG_ENABLE_MODULES = YES;
377 | CLANG_ENABLE_OBJC_ARC = YES;
378 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
379 | CLANG_WARN_BOOL_CONVERSION = YES;
380 | CLANG_WARN_COMMA = YES;
381 | CLANG_WARN_CONSTANT_CONVERSION = YES;
382 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
383 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
384 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
385 | CLANG_WARN_EMPTY_BODY = YES;
386 | CLANG_WARN_ENUM_CONVERSION = YES;
387 | CLANG_WARN_INFINITE_RECURSION = YES;
388 | CLANG_WARN_INT_CONVERSION = YES;
389 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
390 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
391 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
392 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
393 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
394 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
395 | CLANG_WARN_STRICT_PROTOTYPES = YES;
396 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
397 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
398 | CLANG_WARN_UNREACHABLE_CODE = YES;
399 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
400 | CODE_SIGN_IDENTITY = "Mac Developer";
401 | COPY_PHASE_STRIP = NO;
402 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
403 | ENABLE_NS_ASSERTIONS = NO;
404 | ENABLE_STRICT_OBJC_MSGSEND = YES;
405 | GCC_C_LANGUAGE_STANDARD = gnu11;
406 | GCC_NO_COMMON_BLOCKS = YES;
407 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
408 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
409 | GCC_WARN_UNDECLARED_SELECTOR = YES;
410 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
411 | GCC_WARN_UNUSED_FUNCTION = YES;
412 | GCC_WARN_UNUSED_VARIABLE = YES;
413 | MACOSX_DEPLOYMENT_TARGET = 10.11;
414 | MTL_ENABLE_DEBUG_INFO = NO;
415 | SDKROOT = macosx;
416 | SWIFT_COMPILATION_MODE = wholemodule;
417 | SWIFT_OPTIMIZATION_LEVEL = "-O";
418 | };
419 | name = Release;
420 | };
421 | A139AFA4205F775A008680F4 /* Debug */ = {
422 | isa = XCBuildConfiguration;
423 | buildSettings = {
424 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
425 | CODE_SIGN_ENTITLEMENTS = Netflix.entitlements;
426 | CODE_SIGN_IDENTITY = "Apple Development";
427 | CODE_SIGN_STYLE = Automatic;
428 | COMBINE_HIDPI_IMAGES = YES;
429 | DEVELOPMENT_TEAM = SGWXH8G2WP;
430 | ENABLE_HARDENED_RUNTIME = YES;
431 | INFOPLIST_FILE = Info.plist;
432 | LD_RUNPATH_SEARCH_PATHS = (
433 | "$(inherited)",
434 | "@executable_path/../Frameworks",
435 | "@loader_path/../Frameworks",
436 | );
437 | PRODUCT_BUNDLE_IDENTIFIER = "com.jellystyle.Netflix-wrapper";
438 | PRODUCT_NAME = "$(TARGET_NAME)";
439 | SWIFT_OBJC_BRIDGING_HEADER = PIP_BridgingHeader.h;
440 | SWIFT_VERSION = 5.0;
441 | SYSTEM_FRAMEWORK_SEARCH_PATHS = (
442 | "$(inherited)",
443 | "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
444 | );
445 | };
446 | name = Debug;
447 | };
448 | A139AFA5205F775A008680F4 /* Release */ = {
449 | isa = XCBuildConfiguration;
450 | buildSettings = {
451 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
452 | CODE_SIGN_ENTITLEMENTS = Netflix.entitlements;
453 | CODE_SIGN_IDENTITY = "Apple Development";
454 | CODE_SIGN_STYLE = Automatic;
455 | COMBINE_HIDPI_IMAGES = YES;
456 | DEVELOPMENT_TEAM = SGWXH8G2WP;
457 | ENABLE_HARDENED_RUNTIME = YES;
458 | INFOPLIST_FILE = Info.plist;
459 | LD_RUNPATH_SEARCH_PATHS = (
460 | "$(inherited)",
461 | "@executable_path/../Frameworks",
462 | "@loader_path/../Frameworks",
463 | );
464 | PRODUCT_BUNDLE_IDENTIFIER = "com.jellystyle.Netflix-wrapper";
465 | PRODUCT_NAME = "$(TARGET_NAME)";
466 | SWIFT_OBJC_BRIDGING_HEADER = PIP_BridgingHeader.h;
467 | SWIFT_VERSION = 5.0;
468 | SYSTEM_FRAMEWORK_SEARCH_PATHS = (
469 | "$(inherited)",
470 | "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
471 | );
472 | };
473 | name = Release;
474 | };
475 | /* End XCBuildConfiguration section */
476 |
477 | /* Begin XCConfigurationList section */
478 | A139AF8E205F775A008680F4 /* Build configuration list for PBXProject "Netflix" */ = {
479 | isa = XCConfigurationList;
480 | buildConfigurations = (
481 | A139AFA1205F775A008680F4 /* Debug */,
482 | A139AFA2205F775A008680F4 /* Release */,
483 | );
484 | defaultConfigurationIsVisible = 0;
485 | defaultConfigurationName = Release;
486 | };
487 | A139AFA3205F775A008680F4 /* Build configuration list for PBXNativeTarget "Netflix" */ = {
488 | isa = XCConfigurationList;
489 | buildConfigurations = (
490 | A139AFA4205F775A008680F4 /* Debug */,
491 | A139AFA5205F775A008680F4 /* Release */,
492 | );
493 | defaultConfigurationIsVisible = 0;
494 | defaultConfigurationName = Release;
495 | };
496 | /* End XCConfigurationList section */
497 |
498 | /* Begin XCRemoteSwiftPackageReference section */
499 | A1310600259060E000548F12 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
500 | isa = XCRemoteSwiftPackageReference;
501 | repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
502 | requirement = {
503 | branch = master;
504 | kind = branch;
505 | };
506 | };
507 | /* End XCRemoteSwiftPackageReference section */
508 |
509 | /* Begin XCSwiftPackageProductDependency section */
510 | A1310601259060E000548F12 /* Sparkle */ = {
511 | isa = XCSwiftPackageProductDependency;
512 | package = A1310600259060E000548F12 /* XCRemoteSwiftPackageReference "Sparkle" */;
513 | productName = Sparkle;
514 | };
515 | /* End XCSwiftPackageProductDependency section */
516 | };
517 | rootObject = A139AF8B205F775A008680F4 /* Project object */;
518 | }
519 |
--------------------------------------------------------------------------------
/Netflix/Netflix.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Netflix/PIP_BridgingHeader.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @protocol PIPViewControllerDelegate;
4 |
5 | @interface PIPViewController : NSViewController
6 |
7 | @property (nonatomic, copy, nullable) NSString *name;
8 | @property (nonatomic, weak, nullable) id delegate;
9 | @property (nonatomic, weak, nullable) NSWindow *replacementWindow;
10 | @property (nonatomic, weak, nullable) NSView *replacementView;
11 | @property (nonatomic) NSRect replacementRect;
12 | @property (nonatomic) bool playing;
13 | @property (nonatomic) bool userCanResize;
14 | @property (nonatomic) NSSize aspectRatio;
15 | @property (nonatomic) NSSize minSize;
16 | @property (nonatomic) NSSize maxSize;
17 |
18 | - (void)presentViewControllerAsPictureInPicture:(NSViewController * _Nonnull)viewController;
19 |
20 | @end
21 |
22 | @protocol PIPViewControllerDelegate
23 |
24 | @optional
25 | - (BOOL)pipShouldClose:(PIPViewController * _Nonnull)pip;
26 | - (void)pipWillClose:(PIPViewController * _Nonnull)pip;
27 | - (void)pipDidClose:(PIPViewController * _Nonnull)pip;
28 | - (void)pipActionPlay:(PIPViewController * _Nonnull)pip;
29 | - (void)pipActionPause:(PIPViewController * _Nonnull)pip;
30 | - (void)pipActionStop:(PIPViewController * _Nonnull)pip;
31 |
32 | @end
33 |
--------------------------------------------------------------------------------
/Netflix/PictureInPictureDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | protocol PictureInPictureDelegate: AnyObject {
4 | func willEnterPictureInPicture(_ sender: Any?)
5 |
6 | func didEnterPictureInPicture(_ sender: Any?)
7 |
8 | func willExitPictureInPicture(_ sender: Any?)
9 |
10 | func didExitPictureInPicture(_ sender: Any?)
11 | }
12 |
--------------------------------------------------------------------------------
/Netflix/WebView.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import WebKit
3 |
4 | class WebView: WKWebView {
5 | var isInPipMode = false
6 |
7 | override public func mouseDown(with event: NSEvent) {
8 | if isInPipMode {
9 | window?.performDrag(with: event)
10 | } else {
11 | super.mouseDown(with: event)
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Netflix/update-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Link:
3 | #
4 | # A command-line script for incrementing build numbers for all known targets in an Xcode project.
5 | #
6 | # This script has two main goals: firstly, to ensure that all the targets in a project have the
7 | # same CFBundleVersion and CFBundleShortVersionString values. This is because mismatched values
8 | # can cause a warning when submitting to the App Store. Secondly, to ensure that the build number
9 | # is incremented appropriately when git has changes.
10 | #
11 | # If not using git, you are a braver soul than I.
12 |
13 | ##
14 | # The xcodeproj. This is usually found by the script, but you may need to specify its location
15 | # if it's not in the same folder as the script is called from (the project root if called as a
16 | # build phase run script).
17 | #
18 | # This value can also be provided (or overridden) using "--xcodeproj="
19 | #
20 | #xcodeproj="Project.xcodeproj"
21 |
22 | ##
23 | # We have to define an Info.plist as the source of truth. This is typically the one for the main
24 | # target. If not set, the script will try to guess the correct file from the list it gathers from
25 | # the xcodeproj file, but this can be overriden by setting the path here.
26 | #
27 | # This value can also be provided (or overridden) using "--plist="
28 | #
29 | #plist="Project/Info.plist"
30 |
31 | ##
32 | # By default, the script ensures that the build number is incremented when changes are declared
33 | # based on git's records. Alternatively the number of commits on the current branch can be used
34 | # by toggling the "reflect_commits" variable to true. If not on "master", the current branch name
35 | # will be used to ensure no version collisions across branches, i.e. "497-develop".
36 | #
37 | # This setting can also be enabled using "--reflect-commits"
38 | #
39 | #reflect_commits=true
40 |
41 | ##
42 | # If you would like to iterate the build number only when a specific branch is checked out
43 | # (i.e. "master"), you can specify the branch name. The current version will still be replicated
44 | # across all Info.plist files (to ensure consistency) if they don't match the source of truth.
45 | #
46 | # This setting can be enabled for multiple branches can be enabled by using comma separated names
47 | # (i.e. "master,develop"). No spacing is permitted.
48 | #
49 | # This setting can also be enabled using "--branch"
50 | #
51 | #enable_for_branch="master"
52 |
53 | ##
54 | # Released under the BSD License
55 | #
56 | # Copyright © 2017 Daniel Farrelly
57 | #
58 | # Redistribution and use in source and binary forms, with or without modification,
59 | # are permitted provided that the following conditions are met:
60 | #
61 | # * Redistributions of source code must retain the above copyright notice, this list
62 | # of conditions and the following disclaimer.
63 | # * Redistributions in binary form must reproduce the above copyright notice, this
64 | # list of conditions and the following disclaimer in the documentation and/or
65 | # other materials provided with the distribution.
66 | #
67 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
68 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
69 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
70 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
71 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
72 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
73 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
74 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
75 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
76 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
77 |
78 | # We use PlistBuddy to handle the Info.plist values. Here we define where it lives.
79 | plistBuddy="/usr/libexec/PlistBuddy"
80 |
81 | # Parse input variables and update settings.
82 | for i in "$@"; do
83 | case $i in
84 | -h|--help)
85 | echo "usage: sh version-update.sh [options...]\n"
86 | echo "Options: (when provided via the CLI, these will override options set within the script itself)"
87 | echo "-b, --branch= Only allow the script to run on the branch with the given name(s)."
88 | echo " --build= Apply the given value to the build number (CFBundleVersion) for the project."
89 | echo "-i, --ignore-changes Ignore git status when iterating build number (doesn't apply to manual values or --reflect-commits)."
90 | echo "-p, --plist= Use the specified plist file as the source of truth for version details."
91 | echo " --reflect-commits Reflect the number of commits in the current branch when preparing build numbers."
92 | echo " --version= Apply the given value to the marketing version (CFBundleShortVersionString) for the project."
93 | echo "-x, --xcodeproj= Use the specified Xcode project file to gather plist names."
94 | echo "\nFor more detailed information on the use of these variables, see the script source."
95 | exit 1
96 | ;;
97 | --reflect-commits)
98 | reflect_commits=true
99 | shift
100 | ;;
101 | -x=*|--xcodeproj=*)
102 | xcodeproj="${i#*=}"
103 | shift
104 | ;;
105 | -p=*|--plist=*)
106 | plist="${i#*=}"
107 | shift
108 | ;;
109 | -b=*|--branch=*)
110 | enable_for_branch="${i#*=}"
111 | shift
112 | ;;
113 | --build=*)
114 | specified_build="${i#*=}"
115 | shift
116 | ;;
117 | --version=*)
118 | specified_version="${i#*=}"
119 | shift
120 | ;;
121 | -i|--ignore-changes)
122 | ignore_git_status=true
123 | shift
124 | ;;
125 | *)
126 | ;;
127 | esac
128 | done
129 |
130 | # Locate the xcodeproj.
131 | # If we've specified a xcodeproj above, we'll simply use that instead.
132 | if [[ -z ${xcodeproj} ]]; then
133 | xcodeproj=$(find . -depth 1 -name "*.xcodeproj" | sed -e 's/^\.\///g')
134 | fi
135 |
136 | # Check that the xcodeproj file we've located is valid, and warn if it isn't.
137 | # This could also indicate an issue with the code used to automatically locate the xcodeproj file.
138 | # If you're encountering this and the file exists, ensure that ${xcodeproj} contains the correct
139 | # path, or use the "--xcodeproj" variable to provide an accurate location.
140 | if [[ ! -f "${xcodeproj}/project.pbxproj" ]]; then
141 | echo "${BASH_SOURCE}:${LINENO}: error: Could not locate the xcodeproj file \"${xcodeproj}\"."
142 | exit 1
143 | else
144 | echo "Xcode Project: \"${xcodeproj}\""
145 | fi
146 |
147 | # Find unique references to Info.plist files in the project
148 | projectFile="${xcodeproj}/project.pbxproj"
149 | plists=$(grep "^\s*INFOPLIST_FILE.*$" "${projectFile}" | sed -Ee 's/^[[:space:]]+INFOPLIST_FILE[[:space:]*=[[:space:]]*["]?([^"]+)["]?;$/\1/g' | sort | uniq)
150 |
151 | # Attempt to guess the plist based on the list we have.
152 | # If we've specified a plist above, we'll simply use that instead.
153 | if [[ -z ${plist} ]]; then
154 | read -r plist <<< "${plists}"
155 | fi
156 |
157 | # Check that the plist file we've located is valid, and warn if it isn't.
158 | # This could also indicate an issue with the code used to match plist files in the xcodeproj file.
159 | # If you're encountering this and the file exists, ensure that ${plists} contains _ONLY_ filenames.
160 | if [[ ! -f ${plist} ]]; then
161 | echo "${BASH_SOURCE}:${LINENO}: error: Could not locate the plist file \"${plist}\"."
162 | exit 1
163 | else
164 | echo "Source Info.plist: \"${plist}\""
165 | fi
166 |
167 | # Find the current build number in the main Info.plist
168 | mainBundleVersion=$("${plistBuddy}" -c "Print CFBundleVersion" "${plist}")
169 | mainBundleShortVersionString=$("${plistBuddy}" -c "Print CFBundleShortVersionString" "${plist}")
170 | echo "Current project version is ${mainBundleShortVersionString} (${mainBundleVersion})."
171 |
172 | # If the user specified a marketing version (via "--version"), we overwrite the version from the source of truth.
173 | if [[ ! -z ${specified_version} ]]; then
174 | mainBundleShortVersionString=${specified_version}
175 | echo "Applying specified marketing version (${specified_version})..."
176 | fi
177 |
178 | # Increment the build number if git says things have changed. Note that we also check the main
179 | # Info.plist file, and if it has already been modified, we don't increment the build number.
180 | # Alternatively, if the script has been called using "--reflect-commits", we just update to the
181 | # current number of commits. We can also specify a build number to use with "--build".
182 | git=$(sh /etc/profile; which git)
183 | branchName=$("${git}" rev-parse --abbrev-ref HEAD)
184 | if [[ -z ${enable_for_branch} ]] || [[ ",${enable_for_branch}," == *",${branchName},"* ]]; then
185 | if [[ ! -z ${specified_build} ]]; then
186 | mainBundleVersion=${specified_build}
187 | echo "Applying specified build number (${specified_build})..."
188 | elif [[ ! -z ${reflect_commits} ]] && [[ ${reflect_commits} ]]; then
189 | currentBundleVersion=${mainBundleVersion}
190 | mainBundleVersion=$("${git}" rev-list --count HEAD)
191 | if [[ ${branchName} != "master" ]]; then
192 | mainBundleVersion="${mainBundleVersion}-${branchName}"
193 | fi
194 | if [[ ${currentBundleVersion} != ${mainBundleVersion} ]]; then
195 | echo "Branch \"${branchName}\" has ${mainBundleVersion} commit(s). Updating build number..."
196 | else
197 | echo "Branch \"${branchName}\" has ${mainBundleVersion} commit(s). Version is stable."
198 | fi
199 | elif [[ ! -z ${ignore_git_status} ]] && [[ ${ignore_git_status} ]]; then
200 | echo "Iterating build number (forced)..."
201 | mainBundleVersion=$((${mainBundleVersion} + 1))
202 | else
203 | status=$("${git}" status --porcelain)
204 | if [[ ${#status} == 0 ]]; then
205 | echo "Repository does not have any changes. Version is stable."
206 | elif [[ ${status} == *"M ${plist}"* ]] || [[ ${status} == *"M \"${plist}\""* ]]; then
207 | echo "The source Info.plist has been modified. Version is assumed to be stable. Use --ignore-changes to override."
208 | else
209 | echo "Repository is dirty. Iterating build number..."
210 | mainBundleVersion=$((${mainBundleVersion} + 1))
211 | fi
212 | fi
213 | else
214 | echo "${xcodeproj}:0: warning: Version number updates are disabled for the current git branch (${branchName})."
215 | fi
216 |
217 | # Update all of the Info.plist files we discovered
218 | while read -r thisPlist; do
219 | # Find out the current version
220 | thisBundleVersion=$("${plistBuddy}" -c "Print CFBundleVersion" "${thisPlist}")
221 | thisBundleShortVersionString=$("${plistBuddy}" -c "Print CFBundleShortVersionString" "${thisPlist}")
222 | # Update the CFBundleVersion if needed
223 | if [[ ${thisBundleVersion} != ${mainBundleVersion} ]]; then
224 | echo "Updating \"${thisPlist}\" with build ${mainBundleVersion}..."
225 | "${plistBuddy}" -c "Set :CFBundleVersion ${mainBundleVersion}" "${thisPlist}"
226 | fi
227 | # Update the CFBundleShortVersionString if needed
228 | if [[ ${thisBundleShortVersionString} != ${mainBundleShortVersionString} ]]; then
229 | echo "Updating \"${thisPlist}\" with marketing version ${mainBundleShortVersionString}..."
230 | "${plistBuddy}" -c "Set :CFBundleShortVersionString ${mainBundleShortVersionString}" "${thisPlist}"
231 | fi
232 | done <<< "${plists}"
233 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Netflix wrapper for macOS
2 |
3 | A very simple proof-of-concept app for a `WKWebView` based app to allow viewing Netflix outside the browser on macOS. The goal is to somewhat allow a picture-in-picture style browsing and watching experience. There's a bunch of similar apps on the Mac App Store, but this one is mine, and the idea is to keep it pretty lightweight (no majorly additive features).
4 |
5 | 
6 |
7 | ## Installation Instructions
8 |
9 | ### Direct Download
10 |
11 | Go to [the page for the latest release](https://github.com/jellybeansoup/macos-netflix/releases/latest) and download the zipped app. You want the file labelled something akin to "Netflix.zip", and not the ones labelled "source code".
12 |
13 | Unzip the app, and drag it into your Applications folder. If you accidentally downloaded the source code, you'll be quite confused, so go back a step and download the _other_ zip file.
14 |
15 | That's all there is. Run and enjoy.
16 |
17 | ### Homebrew
18 |
19 | If you have [Homebrew](https://brew.sh/) installed, try running the following in your terminal:
20 | ```shell
21 | brew install --cask jellybeansoup-netflix
22 | ```
23 |
24 | ## Alternative Icons
25 |
26 | The [Icons](https://github.com/jellybeansoup/macos-netflix/tree/main/Icons) folder contains alternative icons that may be used with the app. After downloading, you can apply the icon of your choice by right clicking on the installed app in Finder, clicking Get Info, and dragging it into the icon well at the top of the window.
27 |
28 | 
29 |
30 | ## Legal
31 |
32 | Copyright © 2020 Daniel Farrelly. Released under the [BSD license](https://github.com/jellybeansoup/macos-netflix/blob/main/LICENSE).
33 |
34 | App icon template is from the [Bjango Templates repository](https://github.com/bjango/Bjango-Templates), which are released under the BSD License, and are copyright © Bjango and Marc Edwards.
35 |
36 | This project is not associated with, affiliated with, or endorsed by Netflix, Inc. A Netflix subscription is required to use this app. Please visit Netflix.com to verify that service is available in your country.
37 |
--------------------------------------------------------------------------------
/Resources/alternative-icons.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Resources/alternative-icons.gif
--------------------------------------------------------------------------------
/Resources/icon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Resources/icon.sketch
--------------------------------------------------------------------------------
/Resources/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellybeansoup/macos-netflix/88d5f88d010ec96cdfe7f5e98934ea4dab382157/Resources/screenshot.png
--------------------------------------------------------------------------------