├── .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 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 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 | 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 | ![Screenshot displaying the Netflix interface](https://github.com/jellybeansoup/macos-netflix/blob/main/Resources/screenshot.png) 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 | ![Replacing the app icon](https://github.com/jellybeansoup/macos-netflix/blob/main/Resources/alternative-icons.gif) 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 --------------------------------------------------------------------------------