├── .gitignore ├── .gitmodules ├── Bookmarklet ├── README.md └── nightlight_bookmarklet.js ├── Common.swift ├── LICENSE ├── Nightlight.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Nightlight.xcscheme │ └── SafariExtension.xcscheme ├── Nightlight ├── AppDefaults.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── AppIcon128.png │ │ ├── AppIcon128@2x.png │ │ ├── AppIcon16.png │ │ ├── AppIcon16@2x.png │ │ ├── AppIcon256.png │ │ ├── AppIcon256@2x.png │ │ ├── AppIcon32.png │ │ ├── AppIcon32@2x.png │ │ ├── AppIcon512.png │ │ ├── AppIcon512@2x.png │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── Nightlight.entitlements └── ViewController.swift ├── README.md └── SafariExtension ├── .jshintrc ├── AhoCorasick.swift ├── AppDefaults.swift ├── AutoOnTimer.swift ├── Base.lproj └── SafariExtensionViewController.xib ├── CacheProxy.swift ├── Calendar+.swift ├── Info.plist ├── MessageHandler.swift ├── SafariExtension.entitlements ├── SafariExtensionHandler.swift ├── SafariExtensionViewController.swift ├── SafariServices+.swift ├── ToolbarItemIcon.pdf └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | xcuserdata/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Submodules/Solar"] 2 | path = Submodules/Solar 3 | url = https://github.com/ceeK/Solar 4 | -------------------------------------------------------------------------------- /Bookmarklet/README.md: -------------------------------------------------------------------------------- 1 | ### Build 2 | The bookmarklet is generated using Peter Coles' online [generator](http://mrcoles.com/bookmarklet/). -------------------------------------------------------------------------------- /Bookmarklet/nightlight_bookmarklet.js: -------------------------------------------------------------------------------- 1 | var IGNORED_ELEMENTS = ['VIDEO', 'SCRIPT']; 2 | var WEBPAGE_COLORS = { 3 | bgColors: new Map(), 4 | textColors: new Map() 5 | }; 6 | 7 | function Color(r, g, b, a) { 8 | this.r = r; 9 | this.g = g; 10 | this.b = b; 11 | this.a = a; 12 | this.rgbString = function() { 13 | return ( 14 | this.a == 1 ? 'rgb('+this.r+','+this.g+','+this.b+')' : 15 | 'rgba('+this.r+','+this.g+','+this.b+','+this.a+')' 16 | ); 17 | }; 18 | this.hexString = function() { 19 | return '#' + ( 20 | ((0|(1<<8) + this.r).toString(16)).substr(1) + 21 | ((0|(1<<8) + this.g).toString(16)).substr(1) + 22 | ((0|(1<<8) + this.b).toString(16)).substr(1) 23 | ); 24 | }; 25 | this.isTransparent = function() { 26 | return this.a === 0; 27 | }; 28 | /// Returns saturation from HSV color model 29 | this.saturation = function() { 30 | var max = Math.max(this.r, Math.max(this.g, this.b)); 31 | var min = Math.min(this.r, Math.min(this.g, this.b)); 32 | return (max - min) / max; 33 | }; 34 | /// Returns whether the color should be treated as color or as grayscale 35 | this.isColorful = function() { 36 | return this.saturation() > 0.15; 37 | }; 38 | /// Returns approximate ITU-R BT.601 luminance 39 | this.luminance = function() { 40 | return ((this.r*3)+this.b+(this.g*4)) >> 3; 41 | }; 42 | this.isLight = function() { 43 | return this.luminance() > 100; 44 | }; 45 | /// Returns a lighter or darker color 46 | /// percent: positive integer (brighter), negative (darker), 47 | /// must be between [-100, 100] 48 | this.shade = function(percent) { 49 | var r = this.r + (256 - this.r) * percent/100; 50 | var g = this.g + (256 - this.g) * percent/100; 51 | var b = this.b + (256 - this.b) * percent/100; 52 | r = (r < 0 ? 0 : r); 53 | g = (g < 0 ? 0 : g); 54 | b = (b < 0 ? 0 : b); 55 | r = (r > 255 ? 255 : r); 56 | g = (g > 255 ? 255 : g); 57 | b = (b > 255 ? 255 : b); 58 | return new Color(Math.round(r), Math.round(g), Math.round(b), this.a); 59 | }; 60 | /// Returns the inverted color 61 | this.invert = function() { 62 | return new Color(255-this.r, 255-this.g, 255-this.b, this.a); 63 | }; 64 | } 65 | 66 | function ColorFromStr(rgbStr) { 67 | if (rgbStr === undefined) { 68 | return; 69 | } 70 | var rgb = rgbStr.replace(/rgba?\(/, '').replace(/\)/, '').split(','); 71 | var r = parseInt(rgb[0]); 72 | var g = parseInt(rgb[1]); 73 | var b = parseInt(rgb[2]); 74 | var a = (rgb[3] === undefined ? 1 : parseFloat(rgb[3])); 75 | return new Color(r, g, b, a); 76 | } 77 | 78 | function BlackColor() { 79 | return new Color(0, 0, 0, 1); 80 | } 81 | 82 | function GrayColor() { 83 | return new Color(128, 128, 128, 1); 84 | } 85 | 86 | function TransparentColor() { 87 | return new Color(0, 0, 0, 0); 88 | } 89 | 90 | function setNewColor(type, oldColor, newColor) { 91 | switch (type) { 92 | case 'bg': 93 | WEBPAGE_COLORS.bgColors.set(oldColor.rgbString(), newColor.rgbString()); 94 | break; 95 | case 'text': 96 | WEBPAGE_COLORS.textColors.set(oldColor.rgbString(), newColor.rgbString()); 97 | break; 98 | } 99 | } 100 | 101 | function getNewColor(type, oldColorStr) { 102 | switch (type) { 103 | case 'bg': 104 | return WEBPAGE_COLORS.bgColors.get(oldColorStr); 105 | case 'text': 106 | return WEBPAGE_COLORS.textColors.get(oldColorStr); 107 | } 108 | } 109 | 110 | function nightlight(element) { 111 | if (element === undefined || 112 | [3, 8].includes(element.nodeType) || 113 | IGNORED_ELEMENTS.includes(element.tagName)) { 114 | return; 115 | } 116 | 117 | var style = getComputedStyle(element); 118 | var oldBgColor = ColorFromStr(style.backgroundColor); 119 | var oldTextColor = ColorFromStr(style.color); 120 | var newBgColor, newTextColor; 121 | 122 | if (!WEBPAGE_COLORS.bgColors.has(oldBgColor.rgbString())) { 123 | if (oldBgColor.isTransparent()) { 124 | setNewColor('bg', oldBgColor, TransparentColor()); 125 | } else if (oldBgColor.isColorful()) { 126 | setNewColor('bg', oldBgColor, oldBgColor.shade(-20)); 127 | } else if (oldBgColor.isLight()) { 128 | setNewColor('bg', oldBgColor, oldBgColor.invert()); 129 | } else { 130 | setNewColor('bg', oldBgColor, oldBgColor.shade(-50)); 131 | } 132 | } 133 | newBgColor = ColorFromStr(getNewColor('bg', oldBgColor.rgbString())); 134 | 135 | if (!WEBPAGE_COLORS.textColors.has(oldTextColor.rgbString())) { 136 | if (oldTextColor.isTransparent()) { 137 | setNewColor('text', oldTextColor, TransparentColor()); 138 | } else if (oldTextColor.isColorful()) { 139 | if (!oldTextColor.isLight()) { 140 | setNewColor('text', oldTextColor, oldTextColor.shade(50)); 141 | } else { 142 | setNewColor('text', oldTextColor, oldTextColor); 143 | } 144 | } else { 145 | if (!oldTextColor.isLight()) { 146 | setNewColor('text', oldTextColor, oldTextColor.invert()); 147 | } else { 148 | setNewColor('text', oldTextColor, oldTextColor); 149 | } 150 | } 151 | } 152 | newTextColor = ColorFromStr(getNewColor('text', oldTextColor.rgbString())); 153 | 154 | if (newBgColor === undefined) { 155 | newBgColor = BlackColor(); 156 | } 157 | if (newTextColor === undefined) { 158 | newTextColor = GrayColor(); 159 | } 160 | 161 | switch (element.tagName) { 162 | case 'BODY': 163 | if (newBgColor.isTransparent()) { 164 | newBgColor = BlackColor(); 165 | } 166 | element.style.setProperty('background-image', 'none', 'important'); 167 | element.style.setProperty('background-color', newBgColor.rgbString(), 'important'); 168 | element.style.setProperty('color', newTextColor.rgbString(), 'important'); 169 | break; 170 | case 'CANVAS': 171 | element.parentNode.removeChild(element); 172 | break; 173 | case 'IMG': 174 | element.style.filter = 'brightness(70%)'; 175 | break; 176 | case 'DIV': 177 | case 'SPAN': 178 | if (style.backgroundImage != 'none') { 179 | return; 180 | } 181 | element.style.setProperty('background-color', newBgColor.rgbString(), 'important'); 182 | element.style.setProperty('border-color', newTextColor.rgbString(), 'important'); 183 | if (!newBgColor.isLight()) { 184 | element.style.setProperty('color', newTextColor.rgbString(), 'important'); 185 | } 186 | break; 187 | default: 188 | element.style.setProperty('background-color', newBgColor.rgbString(), 'important'); 189 | element.style.setProperty('color', newTextColor.rgbString(), 'important'); 190 | break; 191 | } 192 | 193 | element = element.firstChild; 194 | while (element) { 195 | nightlight(element); 196 | element = element.nextSibling; 197 | } 198 | } 199 | 200 | nightlight(document.body); -------------------------------------------------------------------------------- /Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Basic.swift 3 | // Nightlight 4 | // 5 | // Created by David Wu on 7/30/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import struct CoreLocation.CLLocationCoordinate2D 10 | import Foundation 11 | 12 | let _defaults: [String: Any] = [ 13 | AppDefaultKind.autoOnMode.rawValue: AutoOnMode.manual.rawValue, 14 | AppDefaultKind.autoOnFromTime.rawValue: 72000, 15 | AppDefaultKind.autoOnToTime.rawValue: 28800 16 | ] 17 | let _notificationEmptyCache = NSNotification.Name("emptyCache") 18 | let _nightlight = "net.gofake1.Nightlight" 19 | 20 | enum AutoOnMode: String { 21 | case manual 22 | case custom 23 | case sunset 24 | case system 25 | } 26 | 27 | enum AppDefaultKind: String { 28 | case autoOnMode 29 | case autoOnFromTime 30 | case autoOnToTime 31 | case autoOnLatitude 32 | case autoOnLongitude 33 | case isOn 34 | } 35 | 36 | extension CLLocationCoordinate2D { 37 | func makeDatesForLabel() -> (from: Date, to: Date)? { 38 | let now = Date() 39 | guard let sol = Solar(for: now, coordinate: self), let sunrise = sol.sunrise, let sunset = sol.sunset 40 | else { return nil } 41 | let yesterday = Calendar.autoupdatingCurrent.date(byAdding: .day, value: -1, to: now)! 42 | let tomorrow = Calendar.autoupdatingCurrent.date(byAdding: .day, value: 1, to: now)! 43 | if now < sunrise { 44 | guard let sol = Solar(for: yesterday, coordinate: self), let prevSunset = sol.sunset else { return nil } 45 | return (prevSunset, sunrise) 46 | } else { 47 | guard let sol = Solar(for: tomorrow, coordinate: self), let nextSunrise = sol.sunrise else { return nil } 48 | return (sunset, nextSunrise) 49 | } 50 | } 51 | } 52 | 53 | extension UserDefaults { 54 | func doubleIfExists(forDefault default: AppDefaultKind) -> Double? { 55 | if let _ = object(forKey: `default`.rawValue) { 56 | return double(forKey: `default`.rawValue) 57 | } else { 58 | return nil 59 | } 60 | } 61 | 62 | func integer(forDefault default: AppDefaultKind) -> Int { 63 | return integer(forKey: `default`.rawValue) 64 | } 65 | 66 | func string(forDefault default: AppDefaultKind) -> String? { 67 | return string(forKey: `default`.rawValue) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016-2018 Gofake1 (david@gofake1.net) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Nightlight.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 630563BC2114E47B0028BF36 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63AAA2A62110397A001C2801 /* Common.swift */; }; 11 | 63093F00211E702300BBAA22 /* AutoOnTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63093EFF211E702300BBAA22 /* AutoOnTimer.swift */; }; 12 | 631B2034218131C6004429CE /* CacheProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631B2033218131C6004429CE /* CacheProxy.swift */; }; 13 | 631C62202124A30100DF9B4C /* Calendar+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631C621F2124A30100DF9B4C /* Calendar+.swift */; }; 14 | 631C622521263A8400DF9B4C /* MessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631C622421263A8400DF9B4C /* MessageHandler.swift */; }; 15 | 6325219C217C2E20005B6BC5 /* AhoCorasick.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6325219B217C2E20005B6BC5 /* AhoCorasick.swift */; }; 16 | 632935D62112DC3A00F3AA97 /* Solar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632935BC2112DC3A00F3AA97 /* Solar.swift */; }; 17 | 632935E32112DC6300F3AA97 /* Solar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632935BC2112DC3A00F3AA97 /* Solar.swift */; }; 18 | 6355EDC12151DA0B006092C2 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6355EDC02151DA0B006092C2 /* AppKit.framework */; }; 19 | 63A8277E20FC922500671659 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A8277D20FC922500671659 /* AppDelegate.swift */; }; 20 | 63A8278020FC922500671659 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A8277F20FC922500671659 /* ViewController.swift */; }; 21 | 63A8278220FC922900671659 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63A8278120FC922900671659 /* Assets.xcassets */; }; 22 | 63A8278520FC922900671659 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63A8278320FC922900671659 /* Main.storyboard */; }; 23 | 63A8279720FC92F100671659 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A8279620FC92F100671659 /* SafariExtensionHandler.swift */; }; 24 | 63A8279920FC92F100671659 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A8279820FC92F100671659 /* SafariExtensionViewController.swift */; }; 25 | 63A8279C20FC92F100671659 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63A8279A20FC92F100671659 /* SafariExtensionViewController.xib */; }; 26 | 63A8279F20FC92F100671659 /* script.js in Resources */ = {isa = PBXBuildFile; fileRef = 63A8279E20FC92F100671659 /* script.js */; }; 27 | 63A827A120FC92F100671659 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 63A827A020FC92F100671659 /* ToolbarItemIcon.pdf */; }; 28 | 63A827A520FC92F100671659 /* SafariExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 63A8279120FC92F100671659 /* SafariExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 29 | 63AAA2A5210FF89C001C2801 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63AAA2A4210FF89C001C2801 /* AppDefaults.swift */; }; 30 | 63AAA2A72110397A001C2801 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63AAA2A62110397A001C2801 /* Common.swift */; }; 31 | 63BA14A7213E18DE005BE57B /* SafariServices+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BA14A6213E18DE005BE57B /* SafariServices+.swift */; }; 32 | 63E03E8A2116BD5D00DC7474 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E03E892116BD5D00DC7474 /* AppDefaults.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | 632935D02112DC3A00F3AA97 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 632935C12112DC3A00F3AA97 /* Solar.xcodeproj */; 39 | proxyType = 2; 40 | remoteGlobalIDString = F7FC5B5A1D469B2700C4D3EB; 41 | remoteInfo = Solar_iOS; 42 | }; 43 | 632935D22112DC3A00F3AA97 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 632935C12112DC3A00F3AA97 /* Solar.xcodeproj */; 46 | proxyType = 2; 47 | remoteGlobalIDString = 1410E9C01EF12B5A001829A5; 48 | remoteInfo = Solar_watchOS; 49 | }; 50 | 632935D42112DC3A00F3AA97 /* PBXContainerItemProxy */ = { 51 | isa = PBXContainerItemProxy; 52 | containerPortal = 632935C12112DC3A00F3AA97 /* Solar.xcodeproj */; 53 | proxyType = 2; 54 | remoteGlobalIDString = 97837EAB1E4BB1DE000FEF64; 55 | remoteInfo = Solar_iOSTests; 56 | }; 57 | 63A827A320FC92F100671659 /* PBXContainerItemProxy */ = { 58 | isa = PBXContainerItemProxy; 59 | containerPortal = 63A8277220FC922500671659 /* Project object */; 60 | proxyType = 1; 61 | remoteGlobalIDString = 63A8279020FC92F100671659; 62 | remoteInfo = SafariExtension; 63 | }; 64 | /* End PBXContainerItemProxy section */ 65 | 66 | /* Begin PBXCopyFilesBuildPhase section */ 67 | 63A827A920FC92F100671659 /* Embed App Extensions */ = { 68 | isa = PBXCopyFilesBuildPhase; 69 | buildActionMask = 2147483647; 70 | dstPath = ""; 71 | dstSubfolderSpec = 13; 72 | files = ( 73 | 63A827A520FC92F100671659 /* SafariExtension.appex in Embed App Extensions */, 74 | ); 75 | name = "Embed App Extensions"; 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | /* End PBXCopyFilesBuildPhase section */ 79 | 80 | /* Begin PBXFileReference section */ 81 | 63093EFF211E702300BBAA22 /* AutoOnTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoOnTimer.swift; sourceTree = ""; }; 82 | 631B2033218131C6004429CE /* CacheProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheProxy.swift; sourceTree = ""; }; 83 | 631C621F2124A30100DF9B4C /* Calendar+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+.swift"; sourceTree = ""; }; 84 | 631C622421263A8400DF9B4C /* MessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageHandler.swift; sourceTree = ""; }; 85 | 6325219B217C2E20005B6BC5 /* AhoCorasick.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AhoCorasick.swift; sourceTree = ""; }; 86 | 632935BC2112DC3A00F3AA97 /* Solar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Solar.swift; sourceTree = ""; }; 87 | 632935BD2112DC3A00F3AA97 /* Info-iOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; 88 | 632935BE2112DC3A00F3AA97 /* Info-watchOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-watchOS.plist"; sourceTree = ""; }; 89 | 632935BF2112DC3A00F3AA97 /* solar-logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "solar-logo.png"; sourceTree = ""; }; 90 | 632935C02112DC3A00F3AA97 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 91 | 632935C12112DC3A00F3AA97 /* Solar.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = Solar.xcodeproj; sourceTree = ""; }; 92 | 632935C52112DC3A00F3AA97 /* Solar_iOSTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Solar_iOSTests.swift; sourceTree = ""; }; 93 | 632935C62112DC3A00F3AA97 /* City.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = City.swift; sourceTree = ""; }; 94 | 632935C72112DC3A00F3AA97 /* CorrectResults.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = CorrectResults.json; sourceTree = ""; }; 95 | 632935C82112DC3A00F3AA97 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 96 | 632935C92112DC3A00F3AA97 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 97 | 632935CA2112DC3A00F3AA97 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 98 | 632935CB2112DC3A00F3AA97 /* Solar.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Solar.podspec; sourceTree = ""; }; 99 | 632935CC2112DC3A00F3AA97 /* .travis.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .travis.yml; sourceTree = ""; }; 100 | 6355EDC02151DA0B006092C2 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 101 | 63A8277A20FC922500671659 /* Nightlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nightlight.app; sourceTree = BUILT_PRODUCTS_DIR; }; 102 | 63A8277D20FC922500671659 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 103 | 63A8277F20FC922500671659 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 104 | 63A8278120FC922900671659 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 105 | 63A8278420FC922900671659 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 106 | 63A8278620FC922900671659 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 107 | 63A8278720FC922900671659 /* Nightlight.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nightlight.entitlements; sourceTree = ""; }; 108 | 63A8279120FC92F100671659 /* SafariExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SafariExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 109 | 63A8279620FC92F100671659 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; 110 | 63A8279820FC92F100671659 /* SafariExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionViewController.swift; sourceTree = ""; }; 111 | 63A8279B20FC92F100671659 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SafariExtensionViewController.xib; sourceTree = ""; }; 112 | 63A8279D20FC92F100671659 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 113 | 63A8279E20FC92F100671659 /* script.js */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = script.js; sourceTree = ""; tabWidth = 2; }; 114 | 63A827A020FC92F100671659 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; }; 115 | 63A827A220FC92F100671659 /* SafariExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SafariExtension.entitlements; sourceTree = ""; }; 116 | 63AAA2A4210FF89C001C2801 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; 117 | 63AAA2A62110397A001C2801 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; 118 | 63BA14A6213E18DE005BE57B /* SafariServices+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SafariServices+.swift"; sourceTree = ""; }; 119 | 63DB0F462101ADC8001E4051 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 120 | 63DB0F472101ADC8001E4051 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; 121 | 63E03E892116BD5D00DC7474 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; 122 | /* End PBXFileReference section */ 123 | 124 | /* Begin PBXFrameworksBuildPhase section */ 125 | 63A8277720FC922500671659 /* Frameworks */ = { 126 | isa = PBXFrameworksBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | ); 130 | runOnlyForDeploymentPostprocessing = 0; 131 | }; 132 | 63A8278E20FC92F100671659 /* Frameworks */ = { 133 | isa = PBXFrameworksBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 6355EDC12151DA0B006092C2 /* AppKit.framework in Frameworks */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXFrameworksBuildPhase section */ 141 | 142 | /* Begin PBXGroup section */ 143 | 632935BA2112DC3A00F3AA97 /* Solar */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 632935C12112DC3A00F3AA97 /* Solar.xcodeproj */, 147 | 632935BB2112DC3A00F3AA97 /* Solar */, 148 | 632935C42112DC3A00F3AA97 /* Solar iOSTests */, 149 | 632935CA2112DC3A00F3AA97 /* .gitignore */, 150 | 632935CC2112DC3A00F3AA97 /* .travis.yml */, 151 | 632935BF2112DC3A00F3AA97 /* solar-logo.png */, 152 | 632935C02112DC3A00F3AA97 /* LICENSE */, 153 | 632935C92112DC3A00F3AA97 /* README.md */, 154 | 632935CB2112DC3A00F3AA97 /* Solar.podspec */, 155 | ); 156 | path = Solar; 157 | sourceTree = ""; 158 | }; 159 | 632935BB2112DC3A00F3AA97 /* Solar */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 632935BC2112DC3A00F3AA97 /* Solar.swift */, 163 | 632935BD2112DC3A00F3AA97 /* Info-iOS.plist */, 164 | 632935BE2112DC3A00F3AA97 /* Info-watchOS.plist */, 165 | ); 166 | path = Solar; 167 | sourceTree = ""; 168 | }; 169 | 632935C22112DC3A00F3AA97 /* Products */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 632935D12112DC3A00F3AA97 /* Solar.framework */, 173 | 632935D32112DC3A00F3AA97 /* Solar.framework */, 174 | 632935D52112DC3A00F3AA97 /* Solar_iOSTests.xctest */, 175 | ); 176 | name = Products; 177 | sourceTree = ""; 178 | }; 179 | 632935C42112DC3A00F3AA97 /* Solar iOSTests */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 632935C52112DC3A00F3AA97 /* Solar_iOSTests.swift */, 183 | 632935C62112DC3A00F3AA97 /* City.swift */, 184 | 632935C72112DC3A00F3AA97 /* CorrectResults.json */, 185 | 632935C82112DC3A00F3AA97 /* Info.plist */, 186 | ); 187 | path = "Solar iOSTests"; 188 | sourceTree = ""; 189 | }; 190 | 63A8277120FC922500671659 = { 191 | isa = PBXGroup; 192 | children = ( 193 | 63DB0F462101ADC8001E4051 /* README.md */, 194 | 63DB0F472101ADC8001E4051 /* LICENSE */, 195 | 63AAA2A62110397A001C2801 /* Common.swift */, 196 | 63A8277C20FC922500671659 /* Nightlight */, 197 | 63A8279520FC92F100671659 /* SafariExtension */, 198 | 63DB0F292101AD60001E4051 /* Submodules */, 199 | 63A8279220FC92F100671659 /* Frameworks */, 200 | 63A8277B20FC922500671659 /* Products */, 201 | ); 202 | sourceTree = ""; 203 | }; 204 | 63A8277B20FC922500671659 /* Products */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 63A8277A20FC922500671659 /* Nightlight.app */, 208 | 63A8279120FC92F100671659 /* SafariExtension.appex */, 209 | ); 210 | name = Products; 211 | sourceTree = ""; 212 | }; 213 | 63A8277C20FC922500671659 /* Nightlight */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 63AAA2A4210FF89C001C2801 /* AppDefaults.swift */, 217 | 63A8277D20FC922500671659 /* AppDelegate.swift */, 218 | 63A8277F20FC922500671659 /* ViewController.swift */, 219 | 63A8278320FC922900671659 /* Main.storyboard */, 220 | 63A8278120FC922900671659 /* Assets.xcassets */, 221 | 63A8278620FC922900671659 /* Info.plist */, 222 | 63A8278720FC922900671659 /* Nightlight.entitlements */, 223 | ); 224 | path = Nightlight; 225 | sourceTree = ""; 226 | }; 227 | 63A8279220FC92F100671659 /* Frameworks */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 6355EDC02151DA0B006092C2 /* AppKit.framework */, 231 | ); 232 | name = Frameworks; 233 | sourceTree = ""; 234 | }; 235 | 63A8279520FC92F100671659 /* SafariExtension */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | 6325219B217C2E20005B6BC5 /* AhoCorasick.swift */, 239 | 63E03E892116BD5D00DC7474 /* AppDefaults.swift */, 240 | 63093EFF211E702300BBAA22 /* AutoOnTimer.swift */, 241 | 631B2033218131C6004429CE /* CacheProxy.swift */, 242 | 631C621F2124A30100DF9B4C /* Calendar+.swift */, 243 | 631C622421263A8400DF9B4C /* MessageHandler.swift */, 244 | 63A8279620FC92F100671659 /* SafariExtensionHandler.swift */, 245 | 63A8279820FC92F100671659 /* SafariExtensionViewController.swift */, 246 | 63BA14A6213E18DE005BE57B /* SafariServices+.swift */, 247 | 63A8279A20FC92F100671659 /* SafariExtensionViewController.xib */, 248 | 63A8279E20FC92F100671659 /* script.js */, 249 | 63A827A020FC92F100671659 /* ToolbarItemIcon.pdf */, 250 | 63A8279D20FC92F100671659 /* Info.plist */, 251 | 63A827A220FC92F100671659 /* SafariExtension.entitlements */, 252 | ); 253 | path = SafariExtension; 254 | sourceTree = ""; 255 | }; 256 | 63DB0F292101AD60001E4051 /* Submodules */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | 632935BA2112DC3A00F3AA97 /* Solar */, 260 | ); 261 | path = Submodules; 262 | sourceTree = SOURCE_ROOT; 263 | }; 264 | /* End PBXGroup section */ 265 | 266 | /* Begin PBXNativeTarget section */ 267 | 63A8277920FC922500671659 /* Nightlight */ = { 268 | isa = PBXNativeTarget; 269 | buildConfigurationList = 63A8278A20FC922900671659 /* Build configuration list for PBXNativeTarget "Nightlight" */; 270 | buildPhases = ( 271 | 63A8277620FC922500671659 /* Sources */, 272 | 63A8277720FC922500671659 /* Frameworks */, 273 | 63A8277820FC922500671659 /* Resources */, 274 | 63A827A920FC92F100671659 /* Embed App Extensions */, 275 | ); 276 | buildRules = ( 277 | ); 278 | dependencies = ( 279 | 63A827A420FC92F100671659 /* PBXTargetDependency */, 280 | ); 281 | name = Nightlight; 282 | productName = Nightlight; 283 | productReference = 63A8277A20FC922500671659 /* Nightlight.app */; 284 | productType = "com.apple.product-type.application"; 285 | }; 286 | 63A8279020FC92F100671659 /* SafariExtension */ = { 287 | isa = PBXNativeTarget; 288 | buildConfigurationList = 63A827A620FC92F100671659 /* Build configuration list for PBXNativeTarget "SafariExtension" */; 289 | buildPhases = ( 290 | 63A8278D20FC92F100671659 /* Sources */, 291 | 63A8278E20FC92F100671659 /* Frameworks */, 292 | 63A8278F20FC92F100671659 /* Resources */, 293 | ); 294 | buildRules = ( 295 | ); 296 | dependencies = ( 297 | ); 298 | name = SafariExtension; 299 | productName = SafariExtension; 300 | productReference = 63A8279120FC92F100671659 /* SafariExtension.appex */; 301 | productType = "com.apple.product-type.app-extension"; 302 | }; 303 | /* End PBXNativeTarget section */ 304 | 305 | /* Begin PBXProject section */ 306 | 63A8277220FC922500671659 /* Project object */ = { 307 | isa = PBXProject; 308 | attributes = { 309 | LastSwiftUpdateCheck = 1000; 310 | LastUpgradeCheck = 1000; 311 | ORGANIZATIONNAME = Gofake1; 312 | TargetAttributes = { 313 | 63A8277920FC922500671659 = { 314 | CreatedOnToolsVersion = 10.0; 315 | LastSwiftMigration = 1020; 316 | SystemCapabilities = { 317 | com.apple.ApplicationGroups.Mac = { 318 | enabled = 1; 319 | }; 320 | com.apple.HardenedRuntime = { 321 | enabled = 0; 322 | }; 323 | com.apple.Sandbox = { 324 | enabled = 1; 325 | }; 326 | }; 327 | }; 328 | 63A8279020FC92F100671659 = { 329 | CreatedOnToolsVersion = 10.0; 330 | LastSwiftMigration = 1020; 331 | SystemCapabilities = { 332 | com.apple.ApplicationGroups.Mac = { 333 | enabled = 1; 334 | }; 335 | com.apple.HardenedRuntime = { 336 | enabled = 0; 337 | }; 338 | com.apple.Sandbox = { 339 | enabled = 1; 340 | }; 341 | }; 342 | }; 343 | }; 344 | }; 345 | buildConfigurationList = 63A8277520FC922500671659 /* Build configuration list for PBXProject "Nightlight" */; 346 | compatibilityVersion = "Xcode 9.3"; 347 | developmentRegion = en; 348 | hasScannedForEncodings = 0; 349 | knownRegions = ( 350 | en, 351 | Base, 352 | ); 353 | mainGroup = 63A8277120FC922500671659; 354 | productRefGroup = 63A8277B20FC922500671659 /* Products */; 355 | projectDirPath = ""; 356 | projectReferences = ( 357 | { 358 | ProductGroup = 632935C22112DC3A00F3AA97 /* Products */; 359 | ProjectRef = 632935C12112DC3A00F3AA97 /* Solar.xcodeproj */; 360 | }, 361 | ); 362 | projectRoot = ""; 363 | targets = ( 364 | 63A8277920FC922500671659 /* Nightlight */, 365 | 63A8279020FC92F100671659 /* SafariExtension */, 366 | ); 367 | }; 368 | /* End PBXProject section */ 369 | 370 | /* Begin PBXReferenceProxy section */ 371 | 632935D12112DC3A00F3AA97 /* Solar.framework */ = { 372 | isa = PBXReferenceProxy; 373 | fileType = wrapper.framework; 374 | path = Solar.framework; 375 | remoteRef = 632935D02112DC3A00F3AA97 /* PBXContainerItemProxy */; 376 | sourceTree = BUILT_PRODUCTS_DIR; 377 | }; 378 | 632935D32112DC3A00F3AA97 /* Solar.framework */ = { 379 | isa = PBXReferenceProxy; 380 | fileType = wrapper.framework; 381 | path = Solar.framework; 382 | remoteRef = 632935D22112DC3A00F3AA97 /* PBXContainerItemProxy */; 383 | sourceTree = BUILT_PRODUCTS_DIR; 384 | }; 385 | 632935D52112DC3A00F3AA97 /* Solar_iOSTests.xctest */ = { 386 | isa = PBXReferenceProxy; 387 | fileType = wrapper.cfbundle; 388 | path = Solar_iOSTests.xctest; 389 | remoteRef = 632935D42112DC3A00F3AA97 /* PBXContainerItemProxy */; 390 | sourceTree = BUILT_PRODUCTS_DIR; 391 | }; 392 | /* End PBXReferenceProxy section */ 393 | 394 | /* Begin PBXResourcesBuildPhase section */ 395 | 63A8277820FC922500671659 /* Resources */ = { 396 | isa = PBXResourcesBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | 63A8278220FC922900671659 /* Assets.xcassets in Resources */, 400 | 63A8278520FC922900671659 /* Main.storyboard in Resources */, 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | }; 404 | 63A8278F20FC92F100671659 /* Resources */ = { 405 | isa = PBXResourcesBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | 63A827A120FC92F100671659 /* ToolbarItemIcon.pdf in Resources */, 409 | 63A8279C20FC92F100671659 /* SafariExtensionViewController.xib in Resources */, 410 | 63A8279F20FC92F100671659 /* script.js in Resources */, 411 | ); 412 | runOnlyForDeploymentPostprocessing = 0; 413 | }; 414 | /* End PBXResourcesBuildPhase section */ 415 | 416 | /* Begin PBXSourcesBuildPhase section */ 417 | 63A8277620FC922500671659 /* Sources */ = { 418 | isa = PBXSourcesBuildPhase; 419 | buildActionMask = 2147483647; 420 | files = ( 421 | 63AAA2A72110397A001C2801 /* Common.swift in Sources */, 422 | 63AAA2A5210FF89C001C2801 /* AppDefaults.swift in Sources */, 423 | 63A8277E20FC922500671659 /* AppDelegate.swift in Sources */, 424 | 63A8278020FC922500671659 /* ViewController.swift in Sources */, 425 | 632935D62112DC3A00F3AA97 /* Solar.swift in Sources */, 426 | ); 427 | runOnlyForDeploymentPostprocessing = 0; 428 | }; 429 | 63A8278D20FC92F100671659 /* Sources */ = { 430 | isa = PBXSourcesBuildPhase; 431 | buildActionMask = 2147483647; 432 | files = ( 433 | 630563BC2114E47B0028BF36 /* Common.swift in Sources */, 434 | 6325219C217C2E20005B6BC5 /* AhoCorasick.swift in Sources */, 435 | 63E03E8A2116BD5D00DC7474 /* AppDefaults.swift in Sources */, 436 | 63093F00211E702300BBAA22 /* AutoOnTimer.swift in Sources */, 437 | 631B2034218131C6004429CE /* CacheProxy.swift in Sources */, 438 | 631C62202124A30100DF9B4C /* Calendar+.swift in Sources */, 439 | 631C622521263A8400DF9B4C /* MessageHandler.swift in Sources */, 440 | 63A8279720FC92F100671659 /* SafariExtensionHandler.swift in Sources */, 441 | 63A8279920FC92F100671659 /* SafariExtensionViewController.swift in Sources */, 442 | 63BA14A7213E18DE005BE57B /* SafariServices+.swift in Sources */, 443 | 632935E32112DC6300F3AA97 /* Solar.swift in Sources */, 444 | ); 445 | runOnlyForDeploymentPostprocessing = 0; 446 | }; 447 | /* End PBXSourcesBuildPhase section */ 448 | 449 | /* Begin PBXTargetDependency section */ 450 | 63A827A420FC92F100671659 /* PBXTargetDependency */ = { 451 | isa = PBXTargetDependency; 452 | target = 63A8279020FC92F100671659 /* SafariExtension */; 453 | targetProxy = 63A827A320FC92F100671659 /* PBXContainerItemProxy */; 454 | }; 455 | /* End PBXTargetDependency section */ 456 | 457 | /* Begin PBXVariantGroup section */ 458 | 63A8278320FC922900671659 /* Main.storyboard */ = { 459 | isa = PBXVariantGroup; 460 | children = ( 461 | 63A8278420FC922900671659 /* Base */, 462 | ); 463 | name = Main.storyboard; 464 | sourceTree = ""; 465 | }; 466 | 63A8279A20FC92F100671659 /* SafariExtensionViewController.xib */ = { 467 | isa = PBXVariantGroup; 468 | children = ( 469 | 63A8279B20FC92F100671659 /* Base */, 470 | ); 471 | name = SafariExtensionViewController.xib; 472 | sourceTree = ""; 473 | }; 474 | /* End PBXVariantGroup section */ 475 | 476 | /* Begin XCBuildConfiguration section */ 477 | 63A8278820FC922900671659 /* Debug */ = { 478 | isa = XCBuildConfiguration; 479 | buildSettings = { 480 | ALWAYS_SEARCH_USER_PATHS = NO; 481 | CLANG_ANALYZER_NONNULL = YES; 482 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 483 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 484 | CLANG_CXX_LIBRARY = "libc++"; 485 | CLANG_ENABLE_MODULES = YES; 486 | CLANG_ENABLE_OBJC_ARC = YES; 487 | CLANG_ENABLE_OBJC_WEAK = YES; 488 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 489 | CLANG_WARN_BOOL_CONVERSION = YES; 490 | CLANG_WARN_COMMA = YES; 491 | CLANG_WARN_CONSTANT_CONVERSION = YES; 492 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 493 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 494 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 495 | CLANG_WARN_EMPTY_BODY = YES; 496 | CLANG_WARN_ENUM_CONVERSION = YES; 497 | CLANG_WARN_INFINITE_RECURSION = YES; 498 | CLANG_WARN_INT_CONVERSION = YES; 499 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 500 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 501 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 502 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 503 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 504 | CLANG_WARN_STRICT_PROTOTYPES = YES; 505 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 506 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 507 | CLANG_WARN_UNREACHABLE_CODE = YES; 508 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 509 | CODE_SIGN_IDENTITY = "Mac Developer"; 510 | COPY_PHASE_STRIP = NO; 511 | DEBUG_INFORMATION_FORMAT = dwarf; 512 | ENABLE_STRICT_OBJC_MSGSEND = YES; 513 | ENABLE_TESTABILITY = YES; 514 | GCC_C_LANGUAGE_STANDARD = gnu11; 515 | GCC_DYNAMIC_NO_PIC = NO; 516 | GCC_NO_COMMON_BLOCKS = YES; 517 | GCC_OPTIMIZATION_LEVEL = 0; 518 | GCC_PREPROCESSOR_DEFINITIONS = ( 519 | "DEBUG=1", 520 | "$(inherited)", 521 | ); 522 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 523 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 524 | GCC_WARN_UNDECLARED_SELECTOR = YES; 525 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 526 | GCC_WARN_UNUSED_FUNCTION = YES; 527 | GCC_WARN_UNUSED_VARIABLE = YES; 528 | MACOSX_DEPLOYMENT_TARGET = 10.13; 529 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 530 | MTL_FAST_MATH = YES; 531 | ONLY_ACTIVE_ARCH = YES; 532 | SDKROOT = macosx; 533 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 534 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 535 | }; 536 | name = Debug; 537 | }; 538 | 63A8278920FC922900671659 /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ALWAYS_SEARCH_USER_PATHS = NO; 542 | CLANG_ANALYZER_NONNULL = YES; 543 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 544 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 545 | CLANG_CXX_LIBRARY = "libc++"; 546 | CLANG_ENABLE_MODULES = YES; 547 | CLANG_ENABLE_OBJC_ARC = YES; 548 | CLANG_ENABLE_OBJC_WEAK = YES; 549 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 550 | CLANG_WARN_BOOL_CONVERSION = YES; 551 | CLANG_WARN_COMMA = YES; 552 | CLANG_WARN_CONSTANT_CONVERSION = YES; 553 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 554 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 555 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 556 | CLANG_WARN_EMPTY_BODY = YES; 557 | CLANG_WARN_ENUM_CONVERSION = YES; 558 | CLANG_WARN_INFINITE_RECURSION = YES; 559 | CLANG_WARN_INT_CONVERSION = YES; 560 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 561 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 562 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 563 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 564 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 565 | CLANG_WARN_STRICT_PROTOTYPES = YES; 566 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 567 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 568 | CLANG_WARN_UNREACHABLE_CODE = YES; 569 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 570 | CODE_SIGN_IDENTITY = "Mac Developer"; 571 | COPY_PHASE_STRIP = NO; 572 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 573 | ENABLE_NS_ASSERTIONS = NO; 574 | ENABLE_STRICT_OBJC_MSGSEND = YES; 575 | GCC_C_LANGUAGE_STANDARD = gnu11; 576 | GCC_NO_COMMON_BLOCKS = YES; 577 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 578 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 579 | GCC_WARN_UNDECLARED_SELECTOR = YES; 580 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 581 | GCC_WARN_UNUSED_FUNCTION = YES; 582 | GCC_WARN_UNUSED_VARIABLE = YES; 583 | MACOSX_DEPLOYMENT_TARGET = 10.13; 584 | MTL_ENABLE_DEBUG_INFO = NO; 585 | MTL_FAST_MATH = YES; 586 | SDKROOT = macosx; 587 | SWIFT_COMPILATION_MODE = wholemodule; 588 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 589 | }; 590 | name = Release; 591 | }; 592 | 63A8278B20FC922900671659 /* Debug */ = { 593 | isa = XCBuildConfiguration; 594 | buildSettings = { 595 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 596 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 597 | CODE_SIGN_ENTITLEMENTS = Nightlight/Nightlight.entitlements; 598 | CODE_SIGN_STYLE = Automatic; 599 | COMBINE_HIDPI_IMAGES = YES; 600 | DEVELOPMENT_TEAM = W6KLMFETUQ; 601 | INFOPLIST_FILE = Nightlight/Info.plist; 602 | LD_RUNPATH_SEARCH_PATHS = ( 603 | "$(inherited)", 604 | "@executable_path/../Frameworks", 605 | ); 606 | PRODUCT_BUNDLE_IDENTIFIER = net.gofake1.Nightlight; 607 | PRODUCT_NAME = "$(TARGET_NAME)"; 608 | SWIFT_VERSION = 5.0; 609 | }; 610 | name = Debug; 611 | }; 612 | 63A8278C20FC922900671659 /* Release */ = { 613 | isa = XCBuildConfiguration; 614 | buildSettings = { 615 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 616 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 617 | CODE_SIGN_ENTITLEMENTS = Nightlight/Nightlight.entitlements; 618 | CODE_SIGN_STYLE = Automatic; 619 | COMBINE_HIDPI_IMAGES = YES; 620 | DEVELOPMENT_TEAM = W6KLMFETUQ; 621 | INFOPLIST_FILE = Nightlight/Info.plist; 622 | LD_RUNPATH_SEARCH_PATHS = ( 623 | "$(inherited)", 624 | "@executable_path/../Frameworks", 625 | ); 626 | PRODUCT_BUNDLE_IDENTIFIER = net.gofake1.Nightlight; 627 | PRODUCT_NAME = "$(TARGET_NAME)"; 628 | SWIFT_VERSION = 5.0; 629 | }; 630 | name = Release; 631 | }; 632 | 63A827A720FC92F100671659 /* Debug */ = { 633 | isa = XCBuildConfiguration; 634 | buildSettings = { 635 | CODE_SIGN_ENTITLEMENTS = SafariExtension/SafariExtension.entitlements; 636 | CODE_SIGN_STYLE = Automatic; 637 | DEVELOPMENT_TEAM = W6KLMFETUQ; 638 | INFOPLIST_FILE = SafariExtension/Info.plist; 639 | LD_RUNPATH_SEARCH_PATHS = ( 640 | "$(inherited)", 641 | "@executable_path/../Frameworks", 642 | "@executable_path/../../../../Frameworks", 643 | ); 644 | PRODUCT_BUNDLE_IDENTIFIER = net.gofake1.Nightlight.SafariExtension; 645 | PRODUCT_NAME = "$(TARGET_NAME)"; 646 | SKIP_INSTALL = YES; 647 | SWIFT_VERSION = 5.0; 648 | }; 649 | name = Debug; 650 | }; 651 | 63A827A820FC92F100671659 /* Release */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | CODE_SIGN_ENTITLEMENTS = SafariExtension/SafariExtension.entitlements; 655 | CODE_SIGN_STYLE = Automatic; 656 | DEVELOPMENT_TEAM = W6KLMFETUQ; 657 | INFOPLIST_FILE = SafariExtension/Info.plist; 658 | LD_RUNPATH_SEARCH_PATHS = ( 659 | "$(inherited)", 660 | "@executable_path/../Frameworks", 661 | "@executable_path/../../../../Frameworks", 662 | ); 663 | PRODUCT_BUNDLE_IDENTIFIER = net.gofake1.Nightlight.SafariExtension; 664 | PRODUCT_NAME = "$(TARGET_NAME)"; 665 | SKIP_INSTALL = YES; 666 | SWIFT_VERSION = 5.0; 667 | }; 668 | name = Release; 669 | }; 670 | /* End XCBuildConfiguration section */ 671 | 672 | /* Begin XCConfigurationList section */ 673 | 63A8277520FC922500671659 /* Build configuration list for PBXProject "Nightlight" */ = { 674 | isa = XCConfigurationList; 675 | buildConfigurations = ( 676 | 63A8278820FC922900671659 /* Debug */, 677 | 63A8278920FC922900671659 /* Release */, 678 | ); 679 | defaultConfigurationIsVisible = 0; 680 | defaultConfigurationName = Release; 681 | }; 682 | 63A8278A20FC922900671659 /* Build configuration list for PBXNativeTarget "Nightlight" */ = { 683 | isa = XCConfigurationList; 684 | buildConfigurations = ( 685 | 63A8278B20FC922900671659 /* Debug */, 686 | 63A8278C20FC922900671659 /* Release */, 687 | ); 688 | defaultConfigurationIsVisible = 0; 689 | defaultConfigurationName = Release; 690 | }; 691 | 63A827A620FC92F100671659 /* Build configuration list for PBXNativeTarget "SafariExtension" */ = { 692 | isa = XCConfigurationList; 693 | buildConfigurations = ( 694 | 63A827A720FC92F100671659 /* Debug */, 695 | 63A827A820FC92F100671659 /* Release */, 696 | ); 697 | defaultConfigurationIsVisible = 0; 698 | defaultConfigurationName = Release; 699 | }; 700 | /* End XCConfigurationList section */ 701 | }; 702 | rootObject = 63A8277220FC922500671659 /* Project object */; 703 | } 704 | -------------------------------------------------------------------------------- /Nightlight.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Nightlight.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Nightlight.xcodeproj/xcshareddata/xcschemes/Nightlight.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Nightlight.xcodeproj/xcshareddata/xcschemes/SafariExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 70 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Nightlight/AppDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDefaults.swift 3 | // Nightlight 4 | // 5 | // Created by David Wu on 7/30/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import typealias CoreLocation.CLLocationDegrees 10 | import class Foundation.UserDefaults 11 | 12 | final class AppDefaults { 13 | static var autoOnMode: AutoOnMode { 14 | get { return AutoOnMode(rawValue: groupDefaults.string(forDefault: .autoOnMode)!)! } 15 | set { groupDefaults.set(newValue.rawValue, forDefault: .autoOnMode) } 16 | } 17 | static var autoOnFromTime: Int { 18 | get { return groupDefaults.integer(forDefault: .autoOnFromTime) } 19 | set { groupDefaults.set(newValue, forDefault: .autoOnFromTime) } 20 | } 21 | static var autoOnToTime: Int { 22 | get { return groupDefaults.integer(forDefault: .autoOnToTime) } 23 | set { groupDefaults.set(newValue, forDefault: .autoOnToTime) } 24 | } 25 | static var autoOnLatitude: CLLocationDegrees? { 26 | get { return groupDefaults.doubleIfExists(forDefault: .autoOnLatitude) } 27 | set { groupDefaults.setOrRemove(newValue, forDefault: .autoOnLatitude) } 28 | } 29 | static var autoOnLongitude: CLLocationDegrees? { 30 | get { return groupDefaults.doubleIfExists(forDefault: .autoOnLongitude) } 31 | set { groupDefaults.setOrRemove(newValue, forDefault: .autoOnLongitude) } 32 | } 33 | private static let groupDefaults = UserDefaults(suiteName: "W6KLMFETUQ.net.gofake1.Nightlight")! 34 | 35 | static func registerDefaults() { 36 | groupDefaults.register(defaults: _defaults) 37 | } 38 | } 39 | 40 | extension UserDefaults { 41 | fileprivate func set(_ value: Any?, forDefault default: AppDefaultKind) { 42 | set(value, forKey: `default`.rawValue) 43 | } 44 | 45 | fileprivate func set(_ value: Int, forDefault default: AppDefaultKind) { 46 | set(value, forKey: `default`.rawValue) 47 | } 48 | 49 | fileprivate func setOrRemove(_ value: Double?, forDefault default: AppDefaultKind) { 50 | if let value = value { 51 | set(value, forKey: `default`.rawValue) 52 | } else { 53 | removeObject(forKey: `default`.rawValue) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Nightlight/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Nightlight 4 | // 5 | // Created by David Wu on 7/16/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @NSApplicationMain 12 | final class AppDelegate: NSObject { 13 | @IBAction func emptyCache(_ sender: NSMenuItem) { 14 | DistributedNotificationCenter.default().post(name: _notificationEmptyCache, object: _nightlight) 15 | } 16 | 17 | @IBAction func showHelp(_ sender: NSMenuItem) { 18 | NSWorkspace.shared.open(URL(string: "https://gofake1.net/projects/nightlight.html")!) 19 | } 20 | } 21 | 22 | extension AppDelegate: NSApplicationDelegate { 23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon128.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon128@2x.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon16.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon16@2x.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon256.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon256@2x.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon32.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon32@2x.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon512.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/Nightlight/Assets.xcassets/AppIcon.appiconset/AppIcon512@2x.png -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "AppIcon16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "AppIcon16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "AppIcon32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "AppIcon32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "AppIcon128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "AppIcon128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "AppIcon256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "AppIcon256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "AppIcon512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "AppIcon512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Nightlight/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Nightlight/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.3 21 | CFBundleVersion 22 | 10 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2018 Gofake1. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Nightlight/Nightlight.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)net.gofake1.Nightlight 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Nightlight/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Nightlight 4 | // 5 | // Created by David Wu on 7/16/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import struct CoreLocation.CLLocationCoordinate2D 11 | import class SafariServices.SFSafariExtensionManager 12 | 13 | final class ViewController: NSViewController { 14 | @IBOutlet weak var extensionStatusLabel: NSTextField! 15 | @IBOutlet weak var manualRadio: NSButton! 16 | @IBOutlet weak var customRadio: NSButton! 17 | @IBOutlet weak var customFromDatePicker: NSDatePicker! 18 | @IBOutlet weak var customToDatePicker: NSDatePicker! 19 | @IBOutlet weak var sunsetRadio: NSButton! 20 | @IBOutlet weak var sunsetLabel: NSTextField! 21 | @IBOutlet weak var latitudeField: NSTextField! 22 | @IBOutlet weak var longitudeField: NSTextField! 23 | @IBOutlet weak var systemRadio: NSButton! 24 | 25 | private let df: DateFormatter = { 26 | let df = DateFormatter() 27 | df.dateStyle = .none 28 | df.timeStyle = .short 29 | return df 30 | }() 31 | 32 | override func viewDidLoad() { 33 | AppDefaults.registerDefaults() 34 | 35 | setExtensionStatusText { [extensionStatusLabel] in extensionStatusLabel!.stringValue = $0 } 36 | if #available(macOS 10.14, *) { 37 | systemRadio.isEnabled = true 38 | } 39 | switch AppDefaults.autoOnMode { 40 | case .manual: manualRadio.state = .on 41 | case .custom: customRadio.state = .on 42 | case .sunset: sunsetRadio.state = .on 43 | case .system: systemRadio.state = .on 44 | } 45 | customFromDatePicker.dateValue = AppDefaults.autoOnFromTime.date 46 | customToDatePicker.dateValue = AppDefaults.autoOnToTime.date 47 | sunsetLabel.stringValue = makeSunsetLabelText() 48 | latitudeField.stringValue = AppDefaults.autoOnLatitude?.description ?? "" 49 | longitudeField.stringValue = AppDefaults.autoOnLongitude?.description ?? "" 50 | } 51 | 52 | @IBAction func refreshExtensionStatus(_ sender: NSButton) { 53 | setExtensionStatusText { [extensionStatusLabel] in extensionStatusLabel!.stringValue = $0 } 54 | } 55 | 56 | @IBAction func radioChanged(_ sender: NSButton) { 57 | switch sender { 58 | case manualRadio: 59 | AppDefaults.autoOnMode = .manual 60 | case customRadio: 61 | AppDefaults.autoOnMode = .custom 62 | case sunsetRadio: 63 | AppDefaults.autoOnMode = .sunset 64 | sunsetLabel.stringValue = makeSunsetLabelText() 65 | case systemRadio: 66 | AppDefaults.autoOnMode = .system 67 | default: 68 | fatalError() 69 | } 70 | } 71 | 72 | @IBAction func datePickerValueChanged(_ sender: NSDatePicker) { 73 | switch sender { 74 | case customFromDatePicker: 75 | AppDefaults.autoOnFromTime = customFromDatePicker.dateValue.secondsPastMidnight 76 | case customToDatePicker: 77 | AppDefaults.autoOnToTime = customToDatePicker.dateValue.secondsPastMidnight 78 | default: 79 | fatalError() 80 | } 81 | } 82 | 83 | @IBAction func coordinateFieldValueChanged(_ sender: NSTextField) { 84 | switch sender { 85 | case latitudeField: 86 | AppDefaults.autoOnLatitude = latitudeField.stringValue == "" ? nil : latitudeField.doubleValue 87 | case longitudeField: 88 | AppDefaults.autoOnLongitude = longitudeField.stringValue == "" ? nil : longitudeField.doubleValue 89 | default: 90 | fatalError() 91 | } 92 | sunsetLabel.stringValue = makeSunsetLabelText() 93 | } 94 | 95 | private func setExtensionStatusText(completion completionHandler: @escaping (String) -> ()) { 96 | let identifier = "net.gofake1.Nightlight.SafariExtension" 97 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: identifier) { 98 | if let error = $1 { 99 | DispatchQueue.main.async { completionHandler(error.localizedDescription) } 100 | } else if let state = $0 { 101 | DispatchQueue.main.async { completionHandler(state.isEnabled ? "Nightlight is Enabled" : 102 | "Nightlight is Disabled") } 103 | } 104 | } 105 | } 106 | 107 | private func makeSunsetLabelText() -> String { 108 | if let latitude = AppDefaults.autoOnLatitude, let longitude = AppDefaults.autoOnLongitude { 109 | if let (fromDate, toDate) = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 110 | .makeDatesForLabel() 111 | { 112 | return "From \(df.string(from: fromDate)) to \(df.string(from: toDate))" 113 | } else { 114 | return "Error: Invalid coordinate" 115 | } 116 | } else { 117 | return "From --:-- to --:--" 118 | } 119 | } 120 | } 121 | 122 | extension Date { 123 | fileprivate var secondsPastMidnight: Int { 124 | let dc = Calendar.autoupdatingCurrent.dateComponents([.hour, .minute], from: self) 125 | return (dc.hour! * 3600) + (dc.minute! * 60) 126 | } 127 | } 128 | 129 | extension Int { 130 | fileprivate var date: Date { 131 | assert(self >= 0 && self < 86400) 132 | return Calendar.autoupdatingCurrent.date(from: DateComponents(second: self))! 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Nightlight](https://gofake1.net/projects/nightlight.html) 2 | Browse the internet more comfortably at night 3 | 4 | ![Nightlight screenshot](https://gofake1.net/images/nightlight_hero.jpg) 5 | 6 | Nightlight is a Safari app extension that darkens websites while attempting to preserve their original designs. 7 | 8 | To Little Snitch users: you'll notice that SafariExtension sends HTTP(S) requests to work around Safari limitations. 9 | 10 | ### Acknowledgements 11 | 12 | * [Solar](https://github.com/ceeK/Solar) (MIT) 13 | 14 | *This project is available under the MIT License.* 15 | -------------------------------------------------------------------------------- /SafariExtension/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "globals": { 4 | "console": false, 5 | "document": false, 6 | "Node": false, 7 | "MutationObserver": false, 8 | "safari": false, 9 | "window": false 10 | }, 11 | "undef": true 12 | } -------------------------------------------------------------------------------- /SafariExtension/AhoCorasick.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AhoCorasick.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 10/20/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | typealias ACMatch = (Int, Int, String) 10 | 11 | class ACNode { 12 | weak var fail: ACNode? 13 | var success = [Character: ACNode]() 14 | var outputs = Set() 15 | private let depth: Int 16 | private weak var root: ACNode? 17 | 18 | init(depth: Int) { 19 | self.depth = depth 20 | root = (depth == 0) ? self : nil 21 | } 22 | 23 | func nextNode(character ch: Character) -> ACNode? { 24 | if let node = success[ch] { 25 | return node 26 | } else if let root = root { 27 | return root 28 | } else { 29 | return nil 30 | } 31 | } 32 | 33 | func addNode(character ch: Character) -> ACNode { 34 | if let node = success[ch] { 35 | return node 36 | } else { 37 | let node = ACNode(depth: depth+1) 38 | success[ch] = node 39 | return node 40 | } 41 | } 42 | 43 | func addOutputs(_ outputs: Set) { 44 | for output in outputs { 45 | self.outputs.insert(output) 46 | } 47 | } 48 | } 49 | 50 | class ACTrie { 51 | let root = ACNode(depth: 0) 52 | 53 | init(matching keywords: T) where T.Element == String { 54 | // Build trie 55 | for keyword in keywords { 56 | var current = root 57 | for ch in keyword { 58 | current = current.addNode(character: ch) 59 | } 60 | current.outputs.insert(keyword) 61 | } 62 | // Build failure transitions 63 | var queue = [ACNode]() 64 | for (_, node) in root.success { 65 | node.fail = root 66 | queue.append(node) 67 | } 68 | while !queue.isEmpty { 69 | let current = queue.removeFirst() 70 | for (ch, target) in current.success { 71 | queue.append(target) 72 | var fail = current.fail 73 | while fail?.nextNode(character: ch) == nil { 74 | fail = fail?.fail 75 | } 76 | target.fail = fail?.nextNode(character: ch) 77 | target.addOutputs(target.fail?.outputs ?? []) 78 | } 79 | } 80 | } 81 | 82 | static func nextNode(current: ACNode, character ch: Character) -> ACNode { 83 | var current = current 84 | var next = current.nextNode(character: ch) 85 | while next == nil { 86 | current = current.fail! 87 | next = current.nextNode(character: ch) 88 | } 89 | return next! 90 | } 91 | 92 | func match(string: String) -> [ACMatch] { 93 | var current = root 94 | var out = [(Int, Int, String)]() 95 | for (idx, ch) in string.enumerated() { 96 | current = ACTrie.nextNode(current: current, character: ch) 97 | out += current.outputs.map { (idx-$0.count+1, idx, $0) } 98 | } 99 | return out 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /SafariExtension/AppDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDefaults.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 8/5/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import typealias CoreLocation.CLLocationDegrees 10 | import class Foundation.NSObject 11 | import class Foundation.UserDefaults 12 | 13 | final class AppDefaults { 14 | static var autoOnMode: AutoOnMode { 15 | return AutoOnMode(rawValue: groupDefaults.string(forDefault: .autoOnMode)!)! 16 | } 17 | static var autoOnFromTime: Int { 18 | return groupDefaults.integer(forDefault: .autoOnFromTime) 19 | } 20 | static var autoOnToTime: Int { 21 | return groupDefaults.integer(forDefault: .autoOnToTime) 22 | } 23 | static var autoOnLatitude: CLLocationDegrees? { 24 | return groupDefaults.doubleIfExists(forDefault: .autoOnLatitude) 25 | } 26 | static var autoOnLongitude: CLLocationDegrees? { 27 | return groupDefaults.doubleIfExists(forDefault: .autoOnLongitude) 28 | } 29 | static var isOn: Bool { 30 | get { return groupDefaults.bool(forDefault: .isOn) } 31 | set { groupDefaults.set(newValue, forDefault: .isOn) } 32 | } 33 | private static let groupDefaults = UserDefaults(suiteName: "W6KLMFETUQ.net.gofake1.Nightlight")! 34 | 35 | static func addObserver(_ object: NSObject, forDefaults defaults: Set) { 36 | for `default` in defaults { 37 | groupDefaults.addObserver(object, forKeyPath: `default`.rawValue, options: .new, context: nil) 38 | } 39 | } 40 | 41 | static func removeObserver(_ object: NSObject, forDefaults defaults: Set) { 42 | for `default` in defaults { 43 | groupDefaults.removeObserver(object, forKeyPath: `default`.rawValue) 44 | } 45 | } 46 | 47 | static func registerDefaults() { 48 | groupDefaults.register(defaults: _defaults) 49 | } 50 | } 51 | 52 | extension UserDefaults { 53 | fileprivate func bool(forDefault default: AppDefaultKind) -> Bool { 54 | return bool(forKey: `default`.rawValue) 55 | } 56 | 57 | fileprivate func set(_ value: Bool, forDefault default: AppDefaultKind) { 58 | set(value, forKey: `default`.rawValue) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SafariExtension/AutoOnTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoOnTimer.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 8/10/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import var AppKit.NSApp 10 | import class AppKit.NSWorkspace 11 | import struct CoreLocation.CLLocationCoordinate2D 12 | import typealias CoreLocation.CLLocationDegrees 13 | import Foundation 14 | 15 | private let _df: DateFormatter = { 16 | let df = DateFormatter() 17 | df.dateStyle = .short 18 | df.timeStyle = .short 19 | return df 20 | }() 21 | 22 | final class AutoOn: NSObject { 23 | static let shared = AutoOn() 24 | private var impl: NSObject? 25 | 26 | private override init() { 27 | super.init() 28 | AppDefaults.registerDefaults() 29 | impl = AppDefaults.autoOnMode.makeImpl() 30 | AppDefaults.addObserver(self, forDefaults: [.autoOnMode]) 31 | } 32 | 33 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, 34 | context: UnsafeMutableRawPointer?) 35 | { 36 | switch AppDefaultKind(rawValue: keyPath!)! { 37 | case .autoOnMode: 38 | impl = AutoOnMode(rawValue: change![.newKey]! as! String)!.makeImpl() 39 | default: 40 | fatalError() 41 | } 42 | } 43 | 44 | deinit { 45 | AppDefaults.removeObserver(self, forDefaults: [.autoOnMode]) 46 | } 47 | } 48 | 49 | private final class CustomTimeImpl: NSObject { 50 | private var timers = Set() 51 | 52 | override init() { 53 | super.init() 54 | timers = makeTimers() 55 | RunLoop.main.add(timers) 56 | AppDefaults.addObserver(self, forDefaults: [.autoOnFromTime, .autoOnToTime]) 57 | NotificationCenter.default.addObserver(self, selector: #selector(systemClockDidChange(_:)), 58 | name: .NSSystemClockDidChange, object: nil) 59 | NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(workspaceDidWake(_:)), 60 | name: NSWorkspace.didWakeNotification, object: nil) 61 | } 62 | 63 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, 64 | context: UnsafeMutableRawPointer?) 65 | { 66 | let kind = AppDefaultKind(rawValue: keyPath!)! 67 | assert(kind == .autoOnFromTime || kind == .autoOnToTime) 68 | timers.forEach { $0.invalidate() } 69 | timers = makeTimers() 70 | RunLoop.main.add(timers) 71 | } 72 | 73 | private func makeDates(using date: Date = Date()) -> (Date, Date) { 74 | let cal = Calendar.autoupdatingCurrent 75 | let next = cal.date(byAdding: .day, value: 1, to: date)! 76 | let onDate = cal.date(timeInSeconds: AppDefaults.autoOnFromTime, using: date)! 77 | let offDate = cal.date(timeInSeconds: AppDefaults.autoOnToTime, using: date)! 78 | let onDateNext = cal.date(timeInSeconds: AppDefaults.autoOnFromTime, using: next)! 79 | let offDateNext = cal.date(timeInSeconds: AppDefaults.autoOnToTime, using: next)! 80 | return (date < onDate ? onDate : onDateNext, date < offDate ? offDate : offDateNext) 81 | } 82 | 83 | private func makeTimers(using date: Date = Date()) -> Set { 84 | let (onDate, offDate) = makeDates(using: date) 85 | NSLog("onTimer scheduled for \(_df.string(from: onDate))") //* 86 | NSLog("offTimer scheduled for \(_df.string(from: offDate))") //* 87 | return [ 88 | Timer(fireAt: onDate, interval: 0, target: self, selector: #selector(onTimerFired(_:)), userInfo: nil, 89 | repeats: false), 90 | Timer(fireAt: offDate, interval: 0, target: self, selector: #selector(offTimerFired(_:)), userInfo: nil, 91 | repeats: false) 92 | ] 93 | } 94 | 95 | private func isOn(_ date: Date) -> Bool { 96 | let cal = Calendar.autoupdatingCurrent 97 | let onDate = cal.date(timeInSeconds: AppDefaults.autoOnFromTime, using: date)! 98 | let offDate = cal.date(timeInSeconds: AppDefaults.autoOnToTime, using: date)! 99 | return date < offDate || date > onDate 100 | } 101 | 102 | private func reset() { 103 | let now = Date() 104 | timers.forEach { $0.invalidate() } 105 | timers = makeTimers(using: now) 106 | RunLoop.main.add(timers) 107 | AppDefaults.isOn = isOn(now) 108 | } 109 | 110 | @objc private func onTimerFired(_ timer: Timer) { 111 | NSLog("onTimerFired") //* 112 | AppDefaults.isOn = true 113 | timers.remove(timer) 114 | let (onDate, _) = makeDates() 115 | let onTimer = Timer(fireAt: onDate, interval: 0, target: self, selector: #selector(onTimerFired(_:)), 116 | userInfo: nil, repeats: false) 117 | timers.insert(onTimer) 118 | RunLoop.main.add(onTimer, forMode: .common) 119 | } 120 | 121 | @objc private func offTimerFired(_ timer: Timer) { 122 | NSLog("offTimerFired") //* 123 | AppDefaults.isOn = false 124 | timers.remove(timer) 125 | let (_, offDate) = makeDates() 126 | let offTimer = Timer(fireAt: offDate, interval: 0, target: self, selector: #selector(offTimerFired(_:)), 127 | userInfo: nil, repeats: false) 128 | timers.insert(offTimer) 129 | RunLoop.main.add(offTimer, forMode: .common) 130 | } 131 | 132 | @objc private func systemClockDidChange(_ notification: Notification) { 133 | NSLog("systemClockDidChange") //* 134 | DispatchQueue.main.async { [weak self] in self?.reset() } 135 | } 136 | 137 | @objc private func workspaceDidWake(_ notification: Notification) { 138 | NSLog("workspaceDidWake") //* 139 | DispatchQueue.main.async { [weak self] in self?.reset() } 140 | } 141 | 142 | deinit { 143 | timers.forEach { $0.invalidate() } 144 | AppDefaults.removeObserver(self, forDefaults: [.autoOnFromTime, .autoOnToTime]) 145 | NotificationCenter.default.removeObserver(self) 146 | NSWorkspace.shared.notificationCenter.removeObserver(self) 147 | } 148 | } 149 | 150 | private final class SunsetTimeImpl: NSObject { 151 | private var coordinate: CLLocationCoordinate2D 152 | private var timers = Set() 153 | 154 | override init() { 155 | coordinate = CLLocationCoordinate2D(latitude: AppDefaults.autoOnLatitude!, 156 | longitude: AppDefaults.autoOnLongitude!) 157 | super.init() 158 | timers = makeTimers() 159 | RunLoop.main.add(timers) 160 | AppDefaults.addObserver(self, forDefaults: [.autoOnLatitude, .autoOnLongitude]) 161 | NotificationCenter.default.addObserver(self, selector: #selector(systemClockDidChange(_:)), 162 | name: .NSSystemClockDidChange, object: nil) 163 | NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(workspaceDidWake(_:)), 164 | name: NSWorkspace.didWakeNotification, object: nil) 165 | } 166 | 167 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, 168 | context: UnsafeMutableRawPointer?) 169 | { 170 | let kind = AppDefaultKind(rawValue: keyPath!)! 171 | assert(kind == .autoOnLatitude || kind == .autoOnLongitude) 172 | timers.forEach { $0.invalidate() } 173 | guard let latitude = AppDefaults.autoOnLatitude, let longitude = AppDefaults.autoOnLongitude else { return } 174 | coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 175 | timers = makeTimers() 176 | RunLoop.main.add(timers) 177 | } 178 | 179 | private func makeDates(using date: Date = Date()) -> (Date, Date)? { 180 | guard let sol = Solar(for: date, coordinate: coordinate), let sunrise = sol.sunrise, 181 | let sunset = sol.sunset else { return nil } 182 | if date < sunrise { 183 | return (sunset, sunrise) 184 | } 185 | let next = Calendar.autoupdatingCurrent.date(byAdding: .day, value: 1, to: date)! 186 | guard let nextSol = Solar(for: next, coordinate: coordinate), let nextSunrise = nextSol.sunrise, 187 | let nextSunset = nextSol.sunset else { return nil } 188 | if date >= sunrise && date < sunset { 189 | return (sunset, nextSunrise) 190 | } else { 191 | return (nextSunset, nextSunrise) 192 | } 193 | } 194 | 195 | private func makeTimers(using date: Date = Date()) -> Set { 196 | guard let (onDate, offDate) = makeDates(using: date) else { return [] } 197 | NSLog("onTimer scheduled for \(_df.string(from: onDate))") //* 198 | NSLog("offTimer scheduled for \(_df.string(from: offDate))") //* 199 | return [ 200 | Timer(fireAt: onDate, interval: 0, target: self, selector: #selector(onTimerFired(_:)), 201 | userInfo: nil, repeats: false), 202 | Timer(fireAt: offDate, interval: 0, target: self, selector: #selector(offTimerFired(_:)), 203 | userInfo: nil, repeats: false) 204 | ] 205 | } 206 | 207 | private func isOn(_ date: Date) -> Bool? { 208 | guard let sol = Solar(for: date, coordinate: coordinate), let sunrise = sol.sunrise, 209 | let sunset = sol.sunset else { return nil } 210 | return date < sunrise || date > sunset 211 | } 212 | 213 | private func reset() { 214 | let now = Date() 215 | timers.forEach { $0.invalidate() } 216 | timers = makeTimers(using: now) 217 | RunLoop.main.add(timers) 218 | if let isOn = isOn(now) { 219 | AppDefaults.isOn = isOn 220 | } 221 | } 222 | 223 | @objc private func onTimerFired(_ timer: Timer) { 224 | NSLog("onTimerFired") //* 225 | AppDefaults.isOn = true 226 | timers.remove(timer) 227 | guard let (onDate, _) = makeDates() else { return } 228 | NSLog("onTimer scheduled for \(_df.string(from: onDate))") //* 229 | let onTimer = Timer(fireAt: onDate, interval: 0, target: self, selector: #selector(onTimerFired(_:)), 230 | userInfo: nil, repeats: false) 231 | timers.insert(onTimer) 232 | RunLoop.main.add(onTimer, forMode: .common) 233 | } 234 | 235 | @objc private func offTimerFired(_ timer: Timer) { 236 | NSLog("offTimerFired") //* 237 | AppDefaults.isOn = false 238 | timers.remove(timer) 239 | guard let (_, offDate) = makeDates() else { return } 240 | NSLog("offTimer scheduled for \(_df.string(from: offDate))") //* 241 | let offTimer = Timer(fireAt: offDate, interval: 0, target: self, selector: #selector(offTimerFired(_:)), 242 | userInfo: nil, repeats: false) 243 | timers.insert(offTimer) 244 | RunLoop.main.add(offTimer, forMode: .common) 245 | } 246 | 247 | @objc private func systemClockDidChange(_ notification: Notification) { 248 | NSLog("systemClockDidChange") //* 249 | DispatchQueue.main.async { [weak self] in self?.reset() } 250 | } 251 | 252 | @objc private func workspaceDidWake(_ notification: Notification) { 253 | NSLog("workspaceDidWake") //* 254 | DispatchQueue.main.async { [weak self] in self?.reset() } 255 | } 256 | 257 | deinit { 258 | timers.forEach { $0.invalidate() } 259 | AppDefaults.removeObserver(self, forDefaults: [.autoOnLatitude, .autoOnLongitude]) 260 | NotificationCenter.default.removeObserver(self) 261 | NSWorkspace.shared.notificationCenter.removeObserver(self) 262 | } 263 | } 264 | 265 | private final class SystemAppearanceImpl: NSObject { 266 | private let effectiveAppearanceObv: NSKeyValueObservation 267 | 268 | override init() { 269 | if #available(OSXApplicationExtension 10.14, *) { 270 | effectiveAppearanceObv = NSApp.observe(\.effectiveAppearance) { _, _ in 271 | guard let bestMatch = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) else { return } 272 | AppDefaults.isOn = bestMatch == .darkAqua 273 | } 274 | super.init() 275 | guard let bestMatch = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) else { return } 276 | AppDefaults.isOn = bestMatch == .darkAqua 277 | } else { 278 | fatalError() 279 | } 280 | } 281 | } 282 | 283 | extension AutoOnMode { 284 | fileprivate func makeImpl() -> NSObject? { 285 | switch self { 286 | case .manual: return nil 287 | case .custom: return CustomTimeImpl() 288 | case .sunset: return SunsetTimeImpl() 289 | case .system: return SystemAppearanceImpl() 290 | } 291 | } 292 | } 293 | 294 | extension RunLoop { 295 | fileprivate func add(_ timers: S, forMode mode: Mode = .common) where S.Element == Timer { 296 | for timer in timers { 297 | add(timer, forMode: mode) 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /SafariExtension/Base.lproj/SafariExtensionViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /SafariExtension/CacheProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheProxy.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 10/24/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class CacheProxy: NSObject { 12 | static let shared = CacheProxy() 13 | 14 | private override init() { 15 | super.init() 16 | DistributedNotificationCenter.default().addObserver(self, selector: #selector(emptyCache), 17 | name: _notificationEmptyCache, 18 | object: _nightlight, suspensionBehavior: .drop) 19 | } 20 | 21 | @objc private func emptyCache() { 22 | URLCache.shared.removeAllCachedResponses() 23 | } 24 | 25 | deinit { 26 | DistributedNotificationCenter.default().removeObserver(self) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SafariExtension/Calendar+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calendar+.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 8/15/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import struct Foundation.Calendar 10 | import struct Foundation.Date 11 | 12 | extension Calendar { 13 | func date(timeInSeconds seconds: Int, using date: Date = Date()) -> Date? { 14 | let hour = seconds / 3600 15 | let minute = (seconds - (hour * 3600)) / 60 16 | let second = seconds - (hour * 3600) - (minute * 60) 17 | assert(second == 0) 18 | return self.date(bySettingHour: hour, minute: minute, second: 0, of: date) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SafariExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Nightlight 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.3 21 | CFBundleVersion 22 | 10 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSExtension 31 | 32 | NSExtensionPointIdentifier 33 | com.apple.Safari.extension 34 | NSExtensionPrincipalClass 35 | $(PRODUCT_MODULE_NAME).SafariExtensionHandler 36 | SFSafariContentScript 37 | 38 | 39 | Script 40 | script.js 41 | 42 | 43 | SFSafariToolbarItem 44 | 45 | Action 46 | Popover 47 | Identifier 48 | Button 49 | Image 50 | ToolbarItemIcon.pdf 51 | Label 52 | Nightlight 53 | 54 | SFSafariWebsiteAccess 55 | 56 | Allowed Domains 57 | 58 | Level 59 | All 60 | 61 | 62 | NSHumanReadableCopyright 63 | Copyright © 2018 Gofake1. All rights reserved. 64 | NSHumanReadableDescription 65 | Darkens webpages for more pleasant surfing at night. 66 | 67 | 68 | -------------------------------------------------------------------------------- /SafariExtension/MessageHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageHandler.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 8/16/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import class SafariServices.SFSafariApplication 11 | import class SafariServices.SFSafariPage 12 | 13 | final class MessageHandler { 14 | fileprivate static var impl = AppDefaults.isOn.makeMessageHandlerImpl() 15 | 16 | static func stateReady(page: SFSafariPage) { 17 | impl.stateReady(page: page) 18 | } 19 | 20 | static func wantsResource(page: SFSafariPage, href: String) { 21 | impl.wantsResource(page: page, href: href) 22 | } 23 | } 24 | 25 | final class IsOnObserver: NSObject { 26 | static let shared = IsOnObserver() 27 | 28 | private override init() { 29 | super.init() 30 | AppDefaults.addObserver(self, forDefaults: [.isOn]) 31 | } 32 | 33 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, 34 | context: UnsafeMutableRawPointer?) 35 | { 36 | switch AppDefaultKind(rawValue: keyPath!)! { 37 | case .isOn: 38 | MessageHandler.impl = (change![.newKey]! as! Bool).makeMessageHandlerImpl() 39 | default: 40 | fatalError() 41 | } 42 | } 43 | 44 | deinit { 45 | AppDefaults.removeObserver(self, forDefaults: [.isOn]) 46 | } 47 | } 48 | 49 | protocol MessageHandlerImplType { 50 | func stateReady(page: SFSafariPage) 51 | func wantsResource(page: SFSafariPage, href: String) 52 | } 53 | 54 | typealias StyleSheetResource = String 55 | 56 | extension MessageHandlerImplType { 57 | func wantsResource(page: SFSafariPage, href: String) { 58 | guard let url = URL(string: href) else { return } 59 | let request = URLRequest(url: url) 60 | if let data = URLCache.shared.cachedResponse(for: request)?.data { 61 | let resource = String(data: data, encoding: .utf8)! 62 | page.dispatchMessageToScript(withName: "resource", userInfo: ["resource": resource]) 63 | } else { 64 | URLSession.shared.dataTask(with: request) { (data, res, error) in 65 | if let error = error { 66 | NSLog("cache error: \(href) -- \(error)") //* 67 | } else if let data = data, let res = res { 68 | let resource = StyleSheetResource(data: data, encoding: .utf8)!.fixed(url: url) 69 | page.dispatchMessageToScript(withName: "resource", userInfo: ["resource": resource]) 70 | let cachedRes = CachedURLResponse(response: res, data: resource.data(using: .utf8)!) 71 | URLCache.shared.storeCachedResponse(cachedRes, for: request) 72 | } 73 | } .resume() 74 | } 75 | } 76 | } 77 | 78 | final class DisabledMessageHandlerImpl { 79 | init() { 80 | SFSafariApplication.dispatchMessageToActivePage(withName: "STOP") 81 | } 82 | } 83 | 84 | final class EnabledMessageHandlerImpl { 85 | init() { 86 | SFSafariApplication.dispatchMessageToActivePage(withName: "START") 87 | } 88 | } 89 | 90 | extension DisabledMessageHandlerImpl: MessageHandlerImplType { 91 | func stateReady(page: SFSafariPage) { 92 | // Do nothing 93 | } 94 | } 95 | 96 | extension EnabledMessageHandlerImpl: MessageHandlerImplType { 97 | func stateReady(page: SFSafariPage) { 98 | page.dispatchMessageToScript(withName: "START") 99 | } 100 | } 101 | 102 | extension Bool { 103 | fileprivate func makeMessageHandlerImpl() -> MessageHandlerImplType { 104 | return self ? EnabledMessageHandlerImpl() : DisabledMessageHandlerImpl() 105 | } 106 | } 107 | 108 | extension Array where Element == ACMatch { 109 | /// - precondition: Array is sorted 110 | fileprivate func removingOverlaps() -> [Element] { 111 | guard count > 1 else { return self } 112 | var withoutOverlaps = [self[0]] 113 | for element in self[1...] { 114 | let last = withoutOverlaps.last! 115 | if (last.0 == element.0 && last.1 <= element.1) { 116 | withoutOverlaps.removeLast() 117 | } 118 | withoutOverlaps.append(element) 119 | } 120 | return withoutOverlaps 121 | } 122 | } 123 | 124 | extension String { 125 | fileprivate func replacingOccurrences(mapping: [String: String], trie: ACTrie) -> String { 126 | let matches = trie.match(string: self).removingOverlaps() 127 | var newStr = self 128 | for match in matches.reversed() { 129 | let startIdx = index(startIndex, offsetBy: match.0) 130 | let endIdx = index(startIndex, offsetBy: match.1) 131 | let replacement = mapping[match.2]! 132 | newStr.replaceSubrange(startIdx...endIdx, with: replacement) 133 | } 134 | return newStr 135 | } 136 | } 137 | 138 | private let _trie = ACTrie(matching: [ 139 | "url(//", "url('//", "url(\"//", 140 | "url(http:", "url('http:", "url(\"http:", 141 | "url(https:", "url('https:", "url(\"https:", 142 | "url(data:", "url('data:", "url(\"data:", 143 | "url(", "url('", "url(\"", 144 | "url(/", "url('/", "url(\"" 145 | ]) 146 | 147 | extension StyleSheetResource { 148 | fileprivate func fixed(url: URL) -> StyleSheetResource { 149 | // Replace relative URLs with absolute 150 | var uc = URLComponents(url: url, resolvingAgainstBaseURL: true)! 151 | let parent: String = { 152 | let parentPath = url.pathComponents.dropFirst().dropLast() 153 | $0.path = parentPath.isEmpty ? "" : "/"+parentPath.joined(separator: "/") 154 | $0.query = nil 155 | return $0.string! 156 | }(&uc) 157 | let root: String = { 158 | $0.path = "" 159 | return $0.string! 160 | }(&uc) 161 | let mapping = [ 162 | "url(//": "url(//", 163 | "url('//": "url('//", 164 | "url(\"//": "url(\"//", 165 | "url(http:": "url(http:", 166 | "url('http:": "url('http:", 167 | "url(\"http:": "url(\"http:", 168 | "url(https:": "url(https:", 169 | "url('https:": "url('https:", 170 | "url(\"https:": "url(\"https:", 171 | "url(data:": "url(data:", 172 | "url('data:": "url('data:", 173 | "url(\"data:": "url(\"data:", 174 | "url(": "url(\(parent)/", 175 | "url('": "url('\(parent)/", 176 | "url(\"": "url(\"\(parent)/", 177 | "url(/": "url(\(root)/", 178 | "url('/": "url('\(root)/", 179 | "url(\"/": "url(\"\(root)/" 180 | ] 181 | return replacingOccurrences(mapping: mapping, trie: _trie) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /SafariExtension/SafariExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)net.gofake1.Nightlight 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SafariExtension/SafariExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariExtensionHandler.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 7/16/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import class SafariServices.SFSafariExtensionHandler 10 | import class SafariServices.SFSafariExtensionViewController 11 | import class SafariServices.SFSafariPage 12 | 13 | final class SafariExtensionHandler: SFSafariExtensionHandler { 14 | override init() { 15 | super.init() 16 | // The system instantiates this class often, so KVO is handled by helper singletons 17 | _ = AutoOn.shared 18 | _ = CacheProxy.shared 19 | _ = IsOnObserver.shared 20 | } 21 | 22 | override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) { 23 | switch messageName { 24 | case "READY": 25 | MessageHandler.stateReady(page: page) 26 | case "wantsResource": 27 | MessageHandler.wantsResource(page: page, href: userInfo!["href"]! as! String) 28 | default: 29 | fatalError() 30 | } 31 | } 32 | 33 | override func popoverViewController() -> SFSafariExtensionViewController { 34 | return SafariExtensionViewController.shared 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SafariExtension/SafariExtensionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariExtensionViewController.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 7/16/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import struct CoreLocation.CLLocationCoordinate2D 11 | import typealias CoreLocation.CLLocationDegrees 12 | import class SafariServices.SFSafariApplication 13 | import class SafariServices.SFSafariExtensionViewController 14 | 15 | final class SafariExtensionViewController: SFSafariExtensionViewController { 16 | @IBOutlet weak var isOnCheckbox: NSButton! 17 | @IBOutlet weak var autoOnLabel: NSTextField! 18 | 19 | static let shared: SafariExtensionViewController = { 20 | let shared = SafariExtensionViewController() 21 | shared.preferredContentSize = NSSize(width: 320, height: 80) 22 | return shared 23 | }() 24 | private lazy var df: DateFormatter = { 25 | let df = DateFormatter() 26 | df.dateStyle = .none 27 | df.timeStyle = .short 28 | return df 29 | }() 30 | 31 | override func viewDidAppear() { 32 | isOnCheckbox.state = AppDefaults.isOn ? .on : .off 33 | autoOnLabel.stringValue = makeAutoOnLabelText() 34 | } 35 | 36 | @IBAction func isOnCheckboxChanged(_ sender: NSButton) { 37 | AppDefaults.isOn = sender.state == .on 38 | } 39 | 40 | @IBAction func toggleForThisPage(_ sender: NSButton) { 41 | SFSafariApplication.dispatchMessageToActivePage(withName: "TOGGLE") 42 | } 43 | 44 | private func makeAutoOnLabelText() -> String { 45 | switch AppDefaults.autoOnMode { 46 | case .manual: 47 | return "Nightlight" 48 | case .custom: 49 | return makeCustomAutoOnLabelText(from: AppDefaults.autoOnFromTime, to: AppDefaults.autoOnToTime) 50 | case .sunset: 51 | return makeSunsetAutoOnLabelText(latitude: AppDefaults.autoOnLatitude, 52 | longitude: AppDefaults.autoOnLongitude) 53 | case .system: 54 | return "Nightlight: Match System Appearance" 55 | } 56 | } 57 | 58 | private func makeCustomAutoOnLabelText(from: Int, to: Int) -> String { 59 | let builder = Date() 60 | let fromDate = Calendar.autoupdatingCurrent.date(timeInSeconds: from, using: builder)! 61 | let toDate = Calendar.autoupdatingCurrent.date(timeInSeconds: to, using: builder)! 62 | return "Nightlight: \(df.string(from: fromDate)) - \(df.string(from: toDate))" 63 | } 64 | 65 | private func makeSunsetAutoOnLabelText(latitude: CLLocationDegrees?, longitude: CLLocationDegrees?) -> String { 66 | guard let latitude = latitude, let longitude = longitude else { return "Nightlight: No coordinate set" } 67 | guard let (fromDate, toDate) = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 68 | .makeDatesForLabel() else { return "Nightlight: Invalid coordinate" } 69 | return "Nightlight: \(df.string(from: fromDate)) - \(df.string(from: toDate))" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /SafariExtension/SafariServices+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariServices+.swift 3 | // SafariExtension 4 | // 5 | // Created by David Wu on 9/3/18. 6 | // Copyright © 2018 Gofake1. All rights reserved. 7 | // 8 | 9 | import class SafariServices.SFSafariApplication 10 | 11 | extension SFSafariApplication { 12 | static func dispatchMessageToActivePage(withName name: String) { 13 | getActiveWindow { 14 | $0?.getActiveTab { 15 | $0?.getActivePage { 16 | $0?.dispatchMessageToScript(withName: name, userInfo: nil) 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SafariExtension/ToolbarItemIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gofake1/Nightlight/67fd7e0f92d13d6d298ea3106892b44bcdaeccbf/SafariExtension/ToolbarItemIcon.pdf -------------------------------------------------------------------------------- /SafariExtension/script.js: -------------------------------------------------------------------------------- 1 | class BundleList { 2 | constructor(bundles, enabled) { 3 | this.bundles = bundles; 4 | this.enabled = enabled; 5 | } 6 | setEnabled(enabled) { 7 | this.enabled = enabled; 8 | if(enabled) { 9 | this.bundles.forEach(b => b.enable()); 10 | } else { 11 | this.bundles.forEach(b => b.disable()); 12 | } 13 | } 14 | hotload(newBundles) { 15 | this.bundles = this.bundles.concat(newBundles); 16 | if(this.enabled) { 17 | newBundles.forEach(b => b.enable()); 18 | } 19 | } 20 | } 21 | 22 | class StyleSheetBuiltinBundle { 23 | constructor(str) { 24 | this.node = document.createElement('style'); 25 | this.node.appendChild(document.createTextNode(str)); 26 | this.node.disabled = true; 27 | this.node.id = '_nightlight_builtin_'+StyleSheetBuiltinBundle.nextId(); 28 | document.head.appendChild(this.node); 29 | } 30 | enable() { 31 | this.node.disabled = false; 32 | } 33 | disable() { 34 | this.node.disabled = true; 35 | } 36 | static nextId() { 37 | const id = StyleSheetBuiltinBundle.id; 38 | StyleSheetBuiltinBundle.id++; 39 | return id; 40 | } 41 | } 42 | StyleSheetBuiltinBundle.id = 0; 43 | 44 | class StyleSheetCustomBundle { 45 | constructor(str) { 46 | this.node = document.createElement('style'); 47 | this.node.appendChild(document.createTextNode(str)); 48 | this.node.disabled = true; 49 | this.node.id = '_nightlight_custom_'+StyleSheetCustomBundle.nextId(); 50 | document.head.appendChild(this.node); 51 | } 52 | enable() { 53 | this.node.disabled = false; 54 | } 55 | disable() { 56 | this.node.disabled = true; 57 | } 58 | static nextId() { 59 | const id = StyleSheetCustomBundle.id; 60 | StyleSheetCustomBundle.id++; 61 | return id; 62 | } 63 | } 64 | StyleSheetCustomBundle.id = 0; 65 | 66 | class StyleSheetOverrideBundle { 67 | constructor(str, originalStyle) { 68 | this.originalStyle = originalStyle; 69 | this.node = document.createElement('style'); 70 | this.node.appendChild(document.createTextNode(str)); 71 | this.node.disabled = true; 72 | this.node.id = '_nightlight_override_'+StyleSheetOverrideBundle.nextId(); 73 | document.head.appendChild(this.node); 74 | } 75 | enable() { 76 | this.node.disabled = false; 77 | this.originalStyle.disabled = true; 78 | } 79 | disable() { 80 | this.originalStyle.disabled = false; 81 | this.node.disabled = true; 82 | } 83 | static nextId() { 84 | const id = StyleSheetOverrideBundle.id; 85 | StyleSheetOverrideBundle.id++; 86 | return id; 87 | } 88 | } 89 | StyleSheetOverrideBundle.id = 0; 90 | 91 | class AttributeBundle { 92 | constructor(node, attr, originalValue, newValue) { 93 | this.node = node; 94 | this.attr = attr; 95 | this.originalValue = originalValue; 96 | this.newValue = newValue; 97 | } 98 | enable() { 99 | this.node.setAttribute(this.attr, this.newValue); 100 | } 101 | disable() { 102 | this.node.setAttribute(this.attr, this.originalValue); 103 | } 104 | } 105 | 106 | class ImageBundle { 107 | constructor(node, originalSrc, newSrc) { 108 | this.node = node; 109 | this.originalSrc = originalSrc; 110 | this.newSrc = newSrc; 111 | } 112 | enable() { 113 | this.node.src = this.newSrc; 114 | } 115 | disable() { 116 | this.node.src = this.originalSrc; 117 | } 118 | } 119 | 120 | let MUTATION_OBSERVER; 121 | let BUNDLE_LIST; 122 | let WARMED_UP = false; 123 | 124 | if(window == window.top) { 125 | MUTATION_OBSERVER = new MutationObserver(onMutation); 126 | BUNDLE_LIST = new BundleList([], false); 127 | 128 | safari.self.addEventListener('message', event => { 129 | switch(event.name) { 130 | case 'START': 131 | warmup(); 132 | BUNDLE_LIST.setEnabled(true); 133 | break; 134 | case 'STOP': 135 | BUNDLE_LIST.setEnabled(false); 136 | break; 137 | case 'TOGGLE': 138 | warmup(); 139 | BUNDLE_LIST.setEnabled(!BUNDLE_LIST.enabled); 140 | break; 141 | case 'resource': 142 | addStyleSheetCustomBundle(event.message.resource); 143 | break; 144 | } 145 | }); 146 | 147 | safari.extension.dispatchMessage('READY'); 148 | } 149 | 150 | function warmup() { 151 | if(!WARMED_UP) { 152 | WARMED_UP = true; 153 | if(document.readyState == 'loading') { 154 | document.addEventListener('DOMContentLoaded', onDomContentLoaded); 155 | } else if(document.readyState == 'interactive') { 156 | onDomContentLoaded(); 157 | } else if(document.readyState == 'complete') { 158 | onDomContentLoaded(); 159 | } 160 | } 161 | } 162 | 163 | function onDomContentLoaded() { 164 | makeImageShader(); 165 | BUNDLE_LIST.hotload(start()); 166 | MUTATION_OBSERVER.observe(document, { childList: true, subtree: true }); 167 | } 168 | 169 | // TODO: Handle removed nodes and changes to attributes 170 | function onMutation(mutations) { 171 | function makeBundlesForMutation(arr, m) { 172 | function makeBundle(arr, node) { 173 | if(node.nodeType != Node.ELEMENT_NODE) { 174 | return arr; 175 | } 176 | if(node.sheet) { 177 | arr = makeStyleSheetOverrideBundle(arr, node.sheet); 178 | } 179 | if(node.style) { 180 | arr = makeStyleBundle(arr, node); 181 | } 182 | if(node.bgColor) { 183 | arr = makeBgColorBundle(arr, node); 184 | } 185 | if(node.getAttribute('fill')) { 186 | arr = makeSvgFillBundle(arr, node); 187 | } 188 | if(node.getAttribute('flood-color')) { 189 | arr = makeSvgFloodColorBundle(arr, node); 190 | } 191 | if(node.getAttribute('lighting-color')) { 192 | arr = makeSvgLightingColorBundle(arr, node); 193 | } 194 | if(node.getAttribute('stroke')) { 195 | arr = makeSvgStrokeBundle(arr, node); 196 | } 197 | if(node.getAttribute('stop-color')) { 198 | arr = makeSvgStopColorBundle(arr, node); 199 | } 200 | if(node.tagName == 'IMG') { 201 | arr = makeImageBundle(arr, node); 202 | } 203 | return arr; 204 | } 205 | 206 | arr.concat([].slice.call(m.addedNodes).reduce(makeBundle, [])); 207 | return arr; 208 | } 209 | 210 | const bundles = mutations.reduce(makeBundlesForMutation, []); 211 | if(bundles.length == 0) { 212 | return; 213 | } 214 | console.log('bundles from mutations', bundles); //* 215 | BUNDLE_LIST.hotload(bundles); 216 | } 217 | 218 | function start() { 219 | const BASIC = 'html,input,textarea{background-color:rgb(8,8,8);color:#d2d2d2;}a{color:#56a9ff;}input[type="search"]{-webkit-appearance:none;}'; 220 | const builtins = [BASIC].reduce(makeStyleSheetBuiltinBundle, []); 221 | const overrides = [].slice.call(document.styleSheets) 222 | .reduce(makeStyleSheetOverrideBundle, []); 223 | const styles = [].slice.call(document.querySelectorAll('[style]')) 224 | .reduce(makeStyleBundle, []); 225 | const bgColors = [].slice.call(document.querySelectorAll('[bgcolor]')) 226 | .reduce(makeBgColorBundle, []); 227 | const bodyColors = ['text', 'link', 'alink', 'vlink'].reduce(makeBodyColorBundle, []); 228 | const fontColors = [].slice.call(document.querySelectorAll('font[color]')) 229 | .reduce(makeFontColorBundle, []); 230 | const svgFills = [].slice.call(document.querySelectorAll('[fill]')) 231 | .reduce(makeSvgFillBundle, []); 232 | const svgFloods = [].slice.call(document.querySelectorAll('[flood-color]')) 233 | .reduce(makeSvgFloodColorBundle, []); 234 | const svgLights = [].slice 235 | .call(document.querySelectorAll('[lighting-color]')) 236 | .reduce(makeSvgLightingColorBundle, []); 237 | const svgStrokes = [].slice.call(document.querySelectorAll('[stroke]')) 238 | .reduce(makeSvgStrokeBundle, []); 239 | const svgStops = [].slice.call(document.querySelectorAll('[stop-color]')) 240 | .reduce(makeSvgStopColorBundle, []); 241 | const images = [].slice.call(document.getElementsByTagName('img')) 242 | .reduce(makeImageBundle, []); 243 | return builtins.concat(overrides).concat(styles).concat(bgColors) 244 | .concat(bodyColors).concat(fontColors).concat(svgFills).concat(images); 245 | } 246 | 247 | // --- Bundle helpers --- 248 | 249 | function makeStyleSheetBuiltinBundle(arr, str) { 250 | arr.push(new StyleSheetBuiltinBundle(str)); 251 | return arr; 252 | } 253 | 254 | // sheet - `CSSStyleSheet` 255 | function makeStyleSheetOverrideBundle(arr, sheet) { 256 | if(sheet.ownerNode.id.substring(0, 11) == '_nightlight') { 257 | return arr; 258 | } 259 | if(sheet.cssRules) { 260 | return makeStyleSheetOverrideBundleFromSheet(arr, sheet); 261 | } else { 262 | // FIXME: Redundant network requests due to iframes 263 | safari.extension.dispatchMessage('wantsResource', { href: sheet.href }); 264 | return arr; 265 | } 266 | } 267 | 268 | function makeStyleSheetOverrideBundleFromSheet(arr, sheet) { 269 | // Style sheets in elements may have a media attribute 270 | if(!isValidMediaType(sheet)) { 271 | return arr; 272 | } 273 | const str = makeStyle(sheet); 274 | if(str != '') { 275 | arr.push(new StyleSheetOverrideBundle(str, sheet)); 276 | } 277 | return arr; 278 | } 279 | 280 | function addStyleSheetCustomBundle(str) { 281 | const style = document.createElement('style'); 282 | style.appendChild(document.createTextNode(str)); 283 | style.disabled = true; 284 | document.head.appendChild(style); 285 | const newStr = makeStyle(style.sheet); 286 | document.head.removeChild(style); 287 | if(newStr != '') { 288 | BUNDLE_LIST.hotload([new StyleSheetCustomBundle(newStr)]); 289 | } 290 | } 291 | 292 | function makeStyleBundle(arr, node) { 293 | const str = makeAttributeDeclStr(node.style); 294 | if(str) { 295 | arr.push(new AttributeBundle(node, 'style', node.style.cssText, str)); 296 | } 297 | return arr; 298 | } 299 | 300 | function makeBgColorBundle(arr, node) { 301 | const str = makeDarkStyleColor(node.bgColor); 302 | if(str) { 303 | // Workaround: Safari doesn't display correct color when formatted as RGB 304 | arr.push(new AttributeBundle(node, 'bgcolor', node.bgColor, 305 | rgbToHex(str))); 306 | } 307 | return arr; 308 | } 309 | 310 | function makeBodyColorBundle(arr, bodyAttr) { 311 | const value = document.body.getAttribute(bodyAttr); 312 | if(value) { 313 | const newValue = makeLightStyleColor(value); 314 | if(newValue) { 315 | arr.push(new AttributeBundle(document.body, bodyAttr, value, newValue)); 316 | } 317 | } 318 | return arr; 319 | } 320 | 321 | function makeFontColorBundle(arr, node) { 322 | const str = makeLightStyleColor(node.color); 323 | if(str) { 324 | // Workaround: Safari doesn't display correct color when formatted as RGB 325 | arr.push(new AttributeBundle(node, 'color', node.color, rgbToHex(str))); 326 | } 327 | return arr; 328 | } 329 | 330 | function makeSvgFillBundle(arr, node) { 331 | const fill = node.getAttribute('fill'); 332 | const newFill = makeSvgFillColor(fill); 333 | if(newFill) { 334 | arr.push(new AttributeBundle(node, 'fill', fill, newFill)); 335 | } 336 | return arr; 337 | } 338 | 339 | function makeSvgFloodColorBundle(arr, node) { 340 | const flood = node.getAttribute('flood-color'); 341 | const newFlood = makeSvgFloodColor(flood); 342 | if(newFlood) { 343 | 344 | } 345 | return arr; 346 | } 347 | 348 | function makeSvgLightingColorBundle(arr, node) { 349 | const lighting = node.getAttribute('lighting-color'); 350 | const newLighting = makeSvgLightingColor(lighting); 351 | if(newLighting) { 352 | 353 | } 354 | return arr; 355 | } 356 | 357 | function makeSvgStrokeBundle(arr, node) { 358 | const stroke = node.getAttribute('stroke'); 359 | const newStroke = makeSvgStrokeColor(stroke); 360 | if(newStroke) { 361 | 362 | } 363 | return arr; 364 | } 365 | 366 | function makeSvgStopColorBundle(arr, node) { 367 | const stop = node.getAttribute('stop-color'); 368 | const newStop = makeSvgStopColor(stop); 369 | if(newStop) { 370 | 371 | } 372 | return arr; 373 | } 374 | 375 | function makeImageBundle(arr, node) { 376 | // FIXME 377 | if(shouldProcessImage(node)) { 378 | 379 | } 380 | return arr; 381 | } 382 | 383 | // --- 384 | 385 | // --- SVG helpers --- 386 | 387 | const SVG_FILL_CACHE = {}; 388 | 389 | function makeSvgFillColor(str) { 390 | if(str.substring(0, 4) == 'url(') { 391 | return null; 392 | } 393 | return makeColor(str, SVG_FILL_CACHE, function(r, g, b, a) { 394 | if(saturation(r, g, b) < 0.15 && luminance(r, g, b) <= 100) { 395 | // Invert dark grays 396 | return inverted(r, g, b, a); 397 | } 398 | return same(r, g, b, a); 399 | }); 400 | } 401 | 402 | function makeSvgStrokeColor(str) { 403 | if(str != '') { 404 | console.log('FIXME strokeColor', str); //* 405 | } 406 | return null; 407 | } 408 | 409 | function makeSvgFloodColor(str) { 410 | if(str != '') { 411 | console.log('FIXME floodColor', str); //* 412 | } 413 | return null; 414 | } 415 | 416 | function makeSvgLightingColor(str) { 417 | if(str != '') { 418 | console.log('FIXME lightingColor', str); //* 419 | } 420 | return null; 421 | } 422 | 423 | function makeSvgStopColor(str) { 424 | if(str != '') { 425 | console.log('FIXME stopColor', str); //* 426 | } 427 | return null; 428 | } 429 | 430 | // --- 431 | 432 | // --- Style sheet/attribute and background image helpers --- 433 | 434 | const DARK_STYLE_CACHE = {}; 435 | 436 | function makeDarkStyleColor(str) { 437 | return makeColor(str, DARK_STYLE_CACHE, function(r, g, b, a) { 438 | if(saturation(r, g, b) > 0.15) { 439 | // Darken colors 440 | return shaded(-50, r, g, b, a); 441 | } else if(luminance(r, g, b) > 100) { 442 | // Invert bright grays 443 | return inverted(r, g, b, a); 444 | } else { 445 | // Darken dark grays 446 | return shaded(-50, r, g, b, a); 447 | } 448 | }); 449 | } 450 | 451 | // --- 452 | 453 | // --- Style sheet/attribute helpers --- 454 | 455 | const CSS_NAME_FOR_PROP = { 456 | 'backgroundColor': 'background-color', 457 | 'backgroundImage': 'background-image', 458 | 'borderBottomColor': 'border-bottom-color', 459 | 'borderColor': 'border-color', 460 | 'borderLeftColor': 'border-left-color', 461 | 'borderRightColor': 'border-right-color', 462 | 'borderTopColor': 'border-top-color', 463 | 'caretColor': 'caret-color', 464 | 'color': 'color', 465 | 'columnRuleColor': 'column-rule-color', 466 | 'fill': 'fill', 467 | 'floodColor': 'flood-color', 468 | 'lightingColor': 'lighting-color', 469 | 'outlineColor': 'outline-color', 470 | 'stopColor': 'stop-color', 471 | 'strokeColor': 'stroke-color', 472 | 'webkitBorderAfterColor': '-webkit-border-after-color', 473 | 'webkitBorderBeforeColor': '-webkit-border-before-color', 474 | 'webkitBorderEndColor': '-webkit-border-end-color', 475 | 'webkitBorderStartColor': '-webkit-border-start-color', 476 | 'webkitTextDecorationColor': '-webkit-text-decoration-color', 477 | 'webkitTextEmphasisColor': '-webkit-text-emphasis-color', 478 | 'webkitTextFillColor': '-webkit-text-fill-color', 479 | 'webkitTextStrokeColor': '-webkit-text-stroke-color' 480 | }; 481 | const DARK_PROPS = ['backgroundColor', 'webkitTextFillColor']; 482 | const LIGHT_PROPS = [ 483 | 'borderBottomColor', 484 | //'borderColor', // Workaround: `CSSStyleDeclaration` returns weird value (e.g. "blue blue rgb(...)") 485 | 'borderLeftColor', 486 | 'borderRightColor', 487 | 'borderTopColor', 488 | 'caretColor', 489 | 'color', 490 | 'columnRuleColor', 491 | 'outlineColor', 492 | 'webkitBorderAfterColor', 493 | 'webkitBorderBeforeColor', 494 | 'webkitBorderEndColor', 495 | 'webkitBorderStartColor', 496 | 'webkitTextDecorationColor', 497 | 'webkitTextEmphasisColor', 498 | 'webkitTextStrokeColor' 499 | ]; 500 | const LIGHT_STYLE_CACHE = {}; 501 | 502 | function makeLightStyleColor(str) { 503 | return makeColor(str, LIGHT_STYLE_CACHE, function(r, g, b, a) { 504 | if(luminance(r, g, b) <= 100) { 505 | if(saturation(r, g, b) > 0.15) { 506 | // Lighten dark colors 507 | return shaded(50, r, g, b, a); 508 | } else { 509 | // Invert dark grays 510 | return inverted(r, g, b, a); 511 | } 512 | } 513 | return same(r, g, b, a); 514 | }); 515 | } 516 | 517 | function makeCtxs(makeCtx, decl) { 518 | return DARK_PROPS.map(prop => makeCtx(prop, decl, makeDarkStyleColor)) 519 | .concat(LIGHT_PROPS.map(prop => makeCtx(prop, decl, makeLightStyleColor))) 520 | .concat([makeCtx('backgroundImage', decl, makeBackgroundImage), 521 | makeCtx('fill', decl, makeSvgFillColor), 522 | makeCtx('strokeColor', decl, makeSvgStrokeColor), 523 | makeCtx('lightingColor', decl, makeSvgLightingColor), 524 | makeCtx('floodColor', decl, makeSvgFloodColor), 525 | makeCtx('stopColor', decl, makeSvgStopColor)]); 526 | } 527 | 528 | function makeCtxs_Selection(makeCtx, decl) { 529 | return [makeCtx('backgroundColor', decl, makeLightStyleColor), 530 | makeCtx('color', decl, makeDarkStyleColor)] 531 | } 532 | 533 | // --- 534 | 535 | // --- Style sheet helpers --- 536 | 537 | const VALID_MEDIA_TYPES = new Set(['all', 'screen', 'only screen']); 538 | 539 | function isValidMediaType(sheet) { 540 | // Imported style sheets have null media 541 | if(!sheet.media || sheet.media.mediaText == '') { 542 | return true; 543 | } 544 | for(const styleMedium of sheet.media) { 545 | if(!VALID_MEDIA_TYPES.has(styleMedium)) { 546 | return false; 547 | } 548 | } 549 | return true; 550 | } 551 | 552 | // ruler - `CSSStyleSheet` or `CSSMediaRule` 553 | // Returns string 554 | function makeStyle(ruler) { 555 | if(ruler.constructor.name == 'CSSStyleSheet' && !isValidMediaType(ruler)) { 556 | return ''; 557 | } 558 | if(!ruler.cssRules) { 559 | // Workaround: Importing cross-origin style sheet 560 | safari.extension.dispatchMessage('wantsResource', { href: ruler.href }); 561 | return ''; 562 | } else { 563 | return [].slice.call(ruler.cssRules).reduce(makeRuleStr, ''); 564 | } 565 | } 566 | 567 | // rule - `CSSRule` 568 | // Returns string 569 | function makeRuleStr(str, rule) { 570 | if(rule.type == 1) { 571 | // TODO: Make "::selection" check more correct 572 | if(rule.selectorText == '::selection') { 573 | str += rule.selectorText+' { '+makeAttributeDeclStr_Selection(rule.style)+' } '; 574 | } else { 575 | str += rule.selectorText+' { '+makeAttributeDeclStr(rule.style)+' } '; 576 | } 577 | } else if(rule.type == 3) { 578 | str += makeStyle(rule.styleSheet); 579 | } else if(rule.type == 4) { 580 | str += '@media '+rule.media.mediaText+' { '+makeStyle(rule)+' } '; 581 | } else { 582 | str += rule.cssText; 583 | } 584 | return str; 585 | } 586 | 587 | // --- 588 | 589 | // --- Style attribute helpers --- 590 | 591 | function makeCtx(prop, decl, f) { 592 | return { 593 | prop: prop, 594 | priority: decl.getPropertyPriority(CSS_NAME_FOR_PROP[prop]), 595 | f: f 596 | }; 597 | } 598 | 599 | function modifyDecl(ctx, decl) { 600 | const value = decl[ctx.prop]; 601 | if(value == '') { 602 | return; 603 | } 604 | decl.setProperty(CSS_NAME_FOR_PROP[ctx.prop], ctx.f(value), ctx.priority); 605 | } 606 | 607 | // decl - `CSSStyleDeclaration` 608 | // Returns string 609 | function makeAttributeDeclStr(decl) { 610 | const div = document.createElement('div'); 611 | div.style = decl.cssText; 612 | const newDecl = div.style; 613 | makeCtxs(makeCtx, decl).forEach(ctx => modifyDecl(ctx, newDecl)); 614 | return newDecl.cssText; 615 | } 616 | 617 | function makeAttributeDeclStr_Selection(decl) { 618 | const div = document.createElement('div'); 619 | div.style = decl.cssText; 620 | const newDecl = div.style; 621 | makeCtxs_Selection(makeCtx, decl).forEach(ctx => modifyDecl(ctx, newDecl)); 622 | return newDecl.cssText; 623 | } 624 | 625 | // --- 626 | 627 | // --- Image helpers --- 628 | 629 | function makeImageShader() { 630 | 631 | } 632 | 633 | function shouldProcessImage(image) { 634 | 635 | } 636 | 637 | // Returns string or null 638 | function makeImage(image) { 639 | if(shouldProcessImage(image)) { 640 | 641 | } else { 642 | return null; 643 | } 644 | } 645 | 646 | // --- 647 | 648 | // --- Color and background image helpers --- 649 | 650 | const HEX_FOR_NAMED_COLOR = { 651 | 'aliceblue':'#f0f8ff','antiquewhite':'#faebd7','aqua':'#00ffff','aquamarine':'#7fffd4','azure':'#f0ffff', 652 | 'beige':'#f5f5dc','bisque':'#ffe4c4','black':'#000000','blanchedalmond':'#ffebcd','blue':'#0000ff','blueviolet':'#8a2be2','brown':'#a52a2a','burlywood':'#deb887', 653 | 'cadetblue':'#5f9ea0','chartreuse':'#7fff00','chocolate':'#d2691e','coral':'#ff7f50','cornflowerblue':'#6495ed','cornsilk':'#fff8dc','crimson':'#dc143c','cyan':'#00ffff', 654 | 'darkblue':'#00008b','darkcyan':'#008b8b','darkgoldenrod':'#b8860b','darkgray':'#a9a9a9','darkgreen':'#006400','darkkhaki':'#bdb76b','darkmagenta':'#8b008b','darkolivegreen':'#556b2f','darkorange':'#ff8c00','darkorchid':'#9932cc','darkred':'#8b0000','darksalmon':'#e9967a','darkseagreen':'#8fbc8f','darkslateblue':'#483d8b','darkslategray':'#2f4f4f','darkturquoise':'#00ced1','darkviolet':'#9400d3','deeppink':'#ff1493','deepskyblue':'#00bfff','dimgray':'#696969','dodgerblue':'#1e90ff', 655 | 'firebrick':'#b22222','floralwhite':'#fffaf0','forestgreen':'#228b22','fuchsia':'#ff00ff', 656 | 'gainsboro':'#dcdcdc','ghostwhite':'#f8f8ff','gold':'#ffd700','goldenrod':'#daa520','gray':'#808080','green':'#008000','greenyellow':'#adff2f','grey':'#808080', 657 | 'honeydew':'#f0fff0','hotpink':'#ff69b4', 658 | 'indianred ':'#cd5c5c','indigo':'#4b0082','ivory':'#fffff0', 659 | 'khaki':'#f0e68c', 660 | 'lavender':'#e6e6fa','lavenderblush':'#fff0f5','lawngreen':'#7cfc00','lemonchiffon':'#fffacd','lightblue':'#add8e6','lightcoral':'#f08080','lightcyan':'#e0ffff','lightgoldenrodyellow':'#fafad2','lightgray':'#d3d3d3','lightgrey':'#d3d3d3','lightgreen':'#90ee90','lightpink':'#ffb6c1','lightsalmon':'#ffa07a','lightseagreen':'#20b2aa','lightskyblue':'#87cefa','lightslategray':'#778899','lightsteelblue':'#b0c4de','lightyellow':'#ffffe0','lime':'#00ff00','limegreen':'#32cd32','linen':'#faf0e6', 661 | 'magenta':'#ff00ff','maroon':'#800000','mediumaquamarine':'#66cdaa','mediumblue':'#0000cd','mediumorchid':'#ba55d3','mediumpurple':'#9370d8','mediumseagreen':'#3cb371','mediumslateblue':'#7b68ee','mediumspringgreen':'#00fa9a','mediumturquoise':'#48d1cc','mediumvioletred':'#c71585','midnightblue':'#191970','mintcream':'#f5fffa','mistyrose':'#ffe4e1','moccasin':'#ffe4b5', 662 | 'navajowhite':'#ffdead','navy':'#000080', 663 | 'oldlace':'#fdf5e6','olive':'#808000','olivedrab':'#6b8e23','orange':'#ffa500','orangered':'#ff4500','orchid':'#da70d6', 664 | 'palegoldenrod':'#eee8aa','palegreen':'#98fb98','paleturquoise':'#afeeee','palevioletred':'#d87093','papayawhip':'#ffefd5','peachpuff':'#ffdab9','peru':'#cd853f','pink':'#ffc0cb','plum':'#dda0dd','powderblue':'#b0e0e6','purple':'#800080', 665 | 'rebeccapurple':'#663399','red':'#ff0000','rosybrown':'#bc8f8f','royalblue':'#4169e1', 666 | 'saddlebrown':'#8b4513','salmon':'#fa8072','sandybrown':'#f4a460','seagreen':'#2e8b57','seashell':'#fff5ee','sienna':'#a0522d','silver':'#c0c0c0','skyblue':'#87ceeb','slateblue':'#6a5acd','slategray':'#708090','snow':'#fffafa','springgreen':'#00ff7f','steelblue':'#4682b4', 667 | 'tan':'#d2b48c','teal':'#008080','thistle':'#d8bfd8','tomato':'#ff6347','turquoise':'#40e0d0', 668 | 'violet':'#ee82ee', 669 | 'wheat':'#f5deb3','white':'#ffffff','whitesmoke':'#f5f5f5', 670 | 'yellow':'#ffff00','yellowgreen':'#9acd32' 671 | }; 672 | 673 | // --- 674 | 675 | // --- Color helpers --- 676 | 677 | // Returns string or null 678 | function makeColor(str, cache, f_rgba) { 679 | const cached = cache[str]; 680 | if(cached !== undefined) { 681 | // console.log('hit', str, cached); 682 | return cached; 683 | } else { 684 | const color = function() { 685 | if(str.substring(0, 4) == 'rgb(') { 686 | return makeColorFromRgb(str, f_rgba); 687 | } else if(str.substring(0, 4) == 'rgba') { 688 | return makeColorFromRgba(str, f_rgba); 689 | } else if(str.substring(0, 1) == '#') { 690 | return makeColorFromHex(str, f_rgba); 691 | } else if(HEX_FOR_NAMED_COLOR[str]) { 692 | return makeColorFromHex(HEX_FOR_NAMED_COLOR[str], f_rgba); 693 | } else if(str.substring(0, 4) == 'var(') { 694 | return makeColorFromVar(str, f_rgba); 695 | } else if(['none', 'transparent', 'initial', 'inherit', 'currentcolor'] 696 | .includes(str)) 697 | { 698 | return str; 699 | } else { 700 | console.error('Invalid color '+str); 701 | return null; 702 | } 703 | }(); 704 | cache[str] = color; 705 | // console.log('miss', str, color); 706 | return color; 707 | } 708 | } 709 | 710 | function makeColorFromRgb(str, f_rgba) { 711 | const tokens = str.slice(4, -1).split(', '); 712 | const c = f_rgba(parseInt(tokens[0]), parseInt(tokens[1]), 713 | parseInt(tokens[2]), 1); 714 | return 'rgb('+c.r+','+c.g+','+c.b+')'; 715 | } 716 | 717 | function makeColorFromRgba(str, f_rgba) { 718 | const tokens = str.slice(5, -1).split(', '); 719 | const c = f_rgba(parseInt(tokens[0]), parseInt(tokens[1]), 720 | parseInt(tokens[2]), parseFloat(tokens[3])); 721 | return 'rgba('+c.r+','+c.g+','+c.b+','+c.a+')'; 722 | } 723 | 724 | // https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 725 | function makeColorFromHex(str, f_rgba) { 726 | const c = function() { 727 | if(str.length == 4) { 728 | return f_rgba('0x'+str[1]+str[1]|0, '0x'+str[2]+str[2]|0, 729 | '0x'+str[3]+str[3]|0, 1); 730 | } else if(str.length == 7) { 731 | return f_rgba('0x'+str[1]+str[2]|0, '0x'+str[3]+str[4]|0, 732 | '0x'+str[5]+str[6]|0); 733 | } else { 734 | return null; 735 | } 736 | }(); 737 | return 'rgb('+c.r+','+c.g+','+c.b+')'; 738 | } 739 | 740 | // https://stackoverflow.com/questions/1573053/javascript-function-to-convert-color-names-to-hex-codes 741 | function makeColorFromVar(str, f_rgba) { 742 | const div = document.createElement('div'); 743 | document.head.appendChild(div); 744 | div.style.color = str; 745 | const _str = window.getComputedStyle(div).color; 746 | document.head.removeChild(div); 747 | const color = function() { 748 | if(_str.substring(0, 4) == 'rgb(') { 749 | return makeColorFromRgb(_str, f_rgba); 750 | } else { 751 | return makeColorFromRgba(_str, f_rgba); 752 | } 753 | }(); 754 | return color; 755 | } 756 | 757 | function saturation(r, g, b) { 758 | const max = Math.max(r, Math.max(g, b)); 759 | const min = Math.min(r, Math.min(g, b)); 760 | return (max - min) / max; 761 | } 762 | 763 | // Returns approximate ITU-R BT.601 luminance 764 | function luminance(r, g, b) { 765 | return ((r*3)+b+(g*4)) >> 3; 766 | } 767 | 768 | function shaded(percent, r, g, b, a) { 769 | const f = (percent >= 0) ? function(component) { 770 | return Math.round(component + (255-component)*(percent/100)); 771 | } : function(component) { 772 | return Math.round(component + component*(percent/100)); 773 | }; 774 | return { r: f(r), g: f(g), b: f(b), a: a }; 775 | } 776 | 777 | function inverted(r, g, b, a) { 778 | return { r: 255-r, g: 255-g, b: 255-b, a: a }; 779 | } 780 | 781 | function same(r, g, b, a) { 782 | return { r: r, g: g, b: b, a: a }; 783 | } 784 | 785 | function rgbToHex(str) { 786 | const tokens = str.slice(4, -1).split(','); 787 | const num = parseInt(tokens[2]) | (parseInt(tokens[1]) << 8) | 788 | (parseInt(tokens[0] << 16)); 789 | return '#'+(0x1000000+num).toString(16).slice(1); 790 | } 791 | 792 | // --- 793 | 794 | // --- Background image helpers --- 795 | 796 | const BACKGROUND_IMAGE_TYPE = { 797 | URL: 0, 798 | LINEAR_GRADIENT: 1, 799 | R_LINEAR_GRADIENT: 2, 800 | RADIAL_GRADIENT: 3, 801 | R_RADIAL_GRADIENT: 4 802 | }; 803 | const RADIAL_GRADIENT_KEYWORDS = new Set([ 804 | // shape 805 | 'circle', 'ellipse', 806 | // size 807 | 'closest-corner', 'closest-side', 'farthest-corner', 'farthest-side', 808 | // position 809 | 'at' 810 | ]); 811 | 812 | function makeBackgroundImage(str) { 813 | function makeGradientStr(type, prop) { 814 | return type+'('+(prop.meta != '' ? prop.meta+',' : '') + 815 | prop.colors.map(obj => { 816 | return makeDarkStyleColor(obj.color)+(obj.stop ? ' '+obj.stop : ''); 817 | }).join(',')+')'; 818 | } 819 | 820 | return parseBackgroundImage(str).map(prop => { 821 | switch(prop.type) { 822 | case BACKGROUND_IMAGE_TYPE.URL: 823 | return 'url('+prop.url+')'; 824 | case BACKGROUND_IMAGE_TYPE.LINEAR_GRADIENT: 825 | return makeGradientStr('linear-gradient', prop); 826 | case BACKGROUND_IMAGE_TYPE.R_LINEAR_GRADIENT: 827 | return makeGradientStr('repeating-linear-gradient', prop); 828 | case BACKGROUND_IMAGE_TYPE.RADIAL_GRADIENT: 829 | return makeGradientStr('radial-gradient', prop); 830 | case BACKGROUND_IMAGE_TYPE.R_RADIAL_GRADIENT: 831 | return makeGradientStr('repeating-radial-gradient', prop); 832 | } 833 | return str; 834 | }).join(','); 835 | } 836 | 837 | function parseBackgroundImage(str) { 838 | const STATE = { 839 | DONE: 0, 840 | TYPE: 1, 841 | URL: 2, 842 | LINEAR_GRADIENT: 3, 843 | RADIAL_GRADIENT: 4, 844 | UNKNOWN: 5 845 | }; 846 | const GRADIENT_STATE = { 847 | DONE: 0, 848 | META_OR_COLOR: 1, 849 | META: 2, 850 | COLOR: 3, 851 | COLOR_CLOSE_PAREN: 4, 852 | STOP: 5 853 | }; 854 | 855 | function makeGradientCtx() { 856 | return { first: '', second: '', state: GRADIENT_STATE.META_OR_COLOR }; 857 | } 858 | 859 | function makeGradientProp(type) { 860 | return { type: type, meta: '', colors: [] }; 861 | } 862 | 863 | function gradientStateDone(ch, ctx) { 864 | if(ch == ' ') { 865 | ctx.state = GRADIENT_STATE.COLOR; 866 | } 867 | } 868 | 869 | function gradientStateMeta(ch, ctx, prop) { 870 | if(ch == ',') { 871 | ctx.state = GRADIENT_STATE.DONE; 872 | prop.meta = ctx.first; 873 | ctx.first = ''; 874 | } else { 875 | ctx.first += ch; 876 | } 877 | } 878 | 879 | function gradientStateColor(ch, state, ctx, prop, props) { 880 | if(ch == ')') { 881 | state.value = STATE.DONE; 882 | prop.colors.push({ color: ctx.first }); 883 | props.push(prop); 884 | } else if(ch == ',') { 885 | ctx.state = GRADIENT_STATE.DONE; 886 | prop.colors.push({ color: ctx.first }); 887 | ctx.first = ''; 888 | } else if(ch == '(') { 889 | ctx.state = GRADIENT_STATE.COLOR_CLOSE_PAREN; 890 | ctx.first += '('; 891 | } else if(ch == ' ') { 892 | ctx.state = GRADIENT_STATE.STOP; 893 | } else { 894 | ctx.first += ch; 895 | } 896 | } 897 | 898 | function gradientStateColorCloseParen(ch, ctx) { 899 | if(ch == ')') { 900 | ctx.state = GRADIENT_STATE.COLOR; 901 | ctx.first += ')'; 902 | } else { 903 | ctx.first += ch; 904 | } 905 | } 906 | 907 | function gradientStateStop(ch, state, ctx, prop, props) { 908 | if(ch == ')') { 909 | state.value = STATE.DONE; 910 | prop.colors.push({ color: ctx.first, stop: ctx.second }); 911 | props.push(prop); 912 | } else if(ch == ',') { 913 | ctx.state = GRADIENT_STATE.DONE; 914 | prop.colors.push({ color: ctx.first, stop: ctx.second }); 915 | ctx.first = ''; 916 | ctx.second = ''; 917 | } else { 918 | ctx.second += ch; 919 | } 920 | } 921 | 922 | const state = { value: STATE.DONE }; 923 | let type = ''; 924 | let ctx = {}; 925 | let prop = {}; 926 | let props = []; 927 | 928 | for(const ch of str) { 929 | switch(state.value) { 930 | case STATE.DONE: 931 | if(ch == ',' || ch == ' ') { 932 | // Do nothing 933 | } else { 934 | state.value = STATE.TYPE; 935 | type = ch; 936 | } 937 | break; 938 | case STATE.TYPE: 939 | if(ch == '(') { 940 | if(type == 'url') { 941 | state.value = STATE.URL; 942 | ctx = {}; 943 | prop = { type: BACKGROUND_IMAGE_TYPE.URL, url: '' }; 944 | } else if(type == 'linear-gradient') { 945 | state.value = STATE.LINEAR_GRADIENT; 946 | ctx = makeGradientCtx(); 947 | prop = makeGradientProp(BACKGROUND_IMAGE_TYPE.LINEAR_GRADIENT); 948 | } else if(type == 'repeating-linear-gradient') { 949 | state.value = STATE.LINEAR_GRADIENT; 950 | ctx = makeGradientCtx(); 951 | prop = makeGradientProp(BACKGROUND_IMAGE_TYPE.R_LINEAR_GRADIENT); 952 | } else if(type == 'radial-gradient') { 953 | state.value = STATE.RADIAL_GRADIENT; 954 | ctx = makeGradientCtx(); 955 | prop = makeGradientProp(BACKGROUND_IMAGE_TYPE.RADIAL_GRADIENT); 956 | } else if(type == 'repeating-radial-gradient') { 957 | state.value = STATE.RADIAL_GRADIENT; 958 | ctx = makeGradientCtx(); 959 | prop = makeGradientProp(BACKGROUND_IMAGE_TYPE.R_RADIAL_GRADIENT); 960 | } else { 961 | state.value = STATE.UNKNOWN; 962 | } 963 | } else { 964 | type += ch; 965 | } 966 | break; 967 | case STATE.URL: 968 | if(ch == ')') { 969 | state.value = STATE.DONE; 970 | props.push(prop); 971 | } else { 972 | prop.url += ch; 973 | } 974 | break; 975 | case STATE.LINEAR_GRADIENT: 976 | switch(ctx.state) { 977 | case GRADIENT_STATE.DONE: 978 | gradientStateDone(ch, ctx); 979 | break; 980 | case GRADIENT_STATE.META_OR_COLOR: 981 | if(ch == '(') { 982 | ctx.state = GRADIENT_STATE.COLOR_CLOSE_PAREN; 983 | ctx.first += '('; 984 | } else if(ch == ' ') { 985 | if(ctx.first.substring(0, 1) == '#' || 986 | HEX_FOR_NAMED_COLOR[ctx.first]) { 987 | 988 | ctx.state = GRADIENT_STATE.STOP; 989 | } else { 990 | ctx.state = GRADIENT_STATE.META; 991 | ctx.first += ' '; 992 | } 993 | } else if(ch == ',') { 994 | ctx.state = GRADIENT_STATE.DONE; 995 | if(ctx.first.slice(-3) == 'deg') { 996 | prop.meta = ctx.first; 997 | } else { 998 | prop.colors.push({ color: ctx.first }); 999 | } 1000 | ctx.first = ''; 1001 | } else { 1002 | ctx.first += ch; 1003 | } 1004 | break; 1005 | case GRADIENT_STATE.META: 1006 | gradientStateMeta(ch, ctx, prop); 1007 | break; 1008 | case GRADIENT_STATE.COLOR: 1009 | gradientStateColor(ch, state, ctx, prop, props); 1010 | break; 1011 | case GRADIENT_STATE.COLOR_CLOSE_PAREN: 1012 | gradientStateColorCloseParen(ch, ctx); 1013 | break; 1014 | case GRADIENT_STATE.STOP: 1015 | gradientStateStop(ch, state, ctx, prop, props); 1016 | break; 1017 | } 1018 | break; 1019 | case STATE.RADIAL_GRADIENT: 1020 | switch(ctx.state) { 1021 | case GRADIENT_STATE.DONE: 1022 | gradientStateDone(ch, ctx); 1023 | break; 1024 | case GRADIENT_STATE.META_OR_COLOR: 1025 | if(ch == '(') { 1026 | ctx.state = GRADIENT_STATE.COLOR_CLOSE_PAREN; 1027 | ctx.first += '('; 1028 | } else if(ch == ' ') { 1029 | if(ctx.first.substring(0, 1) == '#' || 1030 | HEX_FOR_NAMED_COLOR[ctx.first]) { 1031 | 1032 | ctx.state = GRADIENT_STATE.STOP; 1033 | } else { 1034 | ctx.state = GRADIENT_STATE.META; 1035 | ctx.first += ' '; 1036 | } 1037 | } else if(ch == '-') { 1038 | ctx.state = GRADIENT_STATE.META; 1039 | ctx.first += '-'; 1040 | } else if(ch == '%') { 1041 | ctx.state = GRADIENT_STATE.META; 1042 | ctx.first += '%'; 1043 | } else if(ch == ',') { 1044 | ctx.state = GRADIENT_STATE.DONE; 1045 | if(RADIAL_GRADIENT_KEYWORDS.has(ctx.first)) { 1046 | prop.meta = ctx.first; 1047 | } else { 1048 | prop.colors.push({ color: ctx.first }); 1049 | } 1050 | ctx.first = ''; 1051 | } else { 1052 | ctx.first += ch; 1053 | } 1054 | break; 1055 | case GRADIENT_STATE.META: 1056 | gradientStateMeta(ch, ctx, prop); 1057 | break; 1058 | case GRADIENT_STATE.COLOR: 1059 | gradientStateColor(ch, state, ctx, prop, props); 1060 | break; 1061 | case GRADIENT_STATE.COLOR_CLOSE_PAREN: 1062 | gradientStateColorCloseParen(ch, ctx); 1063 | break; 1064 | case GRADIENT_STATE.STOP: 1065 | gradientStateStop(ch, state, ctx, prop, props); 1066 | break; 1067 | } 1068 | break; 1069 | case STATE.UNKNOWN: 1070 | if(ch == ')') { 1071 | state.value = STATE.DONE; 1072 | } 1073 | break; 1074 | } 1075 | } 1076 | return props; 1077 | } 1078 | 1079 | // --- 1080 | --------------------------------------------------------------------------------