diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 1fb3875b7..238ab8f0d 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; + 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; }; + 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; + 6581C74020CED60100F4AD34 /* evergreen-subscribe-to-feed.js in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */; }; + 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */; }; + 6581C74620CED60100F4AD34 /* Subscribe to Feed.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D617E2029031C009BC708 /* AppDelegate.swift */; }; 840D61812029031C009BC708 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61802029031C009BC708 /* MasterViewController.swift */; }; 840D61832029031C009BC708 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61822029031C009BC708 /* DetailViewController.swift */; }; @@ -32,6 +38,8 @@ 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; }; 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; }; 843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843A3B5520311E7700BF76EC /* FeedListOutlineView.swift */; }; + 8440C8AD2129F9F5002353D1 /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D682106B3E100DD04E6 /* ArticlesDatabase.framework */; }; + 8440C8AE2129F9F5002353D1 /* ArticlesDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D682106B3E100DD04E6 /* ArticlesDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; }; 8444C8F21FED81840051386C /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B581FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift */; }; @@ -253,6 +261,13 @@ remoteGlobalIDString = 844BEE5A1F0AB3C8004AB7CD; remoteInfo = Articles; }; + 8440C8AF2129F9F5002353D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 841D4D5E2106B3E100DD04E6 /* ArticlesDatabase.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 844BEE361F0AB3AA004AB7CD; + remoteInfo = ArticlesDatabase; + }; 846E77391F6EF5D700A165E2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 846E77301F6EF5D600A165E2 /* Account.xcodeproj */; @@ -431,6 +446,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 6581C75720CED60100F4AD34 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 6581C74620CED60100F4AD34 /* Subscribe to Feed.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 84B06F681ED37B9000F0B54B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -445,6 +471,7 @@ 84C37FB620DD8DBB00CA8CF5 /* RSParser.framework in Embed Frameworks */, 84C37FA620DD8D8400CA8CF5 /* RSCore.framework in Embed Frameworks */, 846E773E1F6EF67A00A165E2 /* Account.framework in Embed Frameworks */, + 8440C8AE2129F9F5002353D1 /* ArticlesDatabase.framework in Embed Frameworks */, 841D4D6C2106B3ED00DD04E6 /* Articles.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -477,6 +504,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; + 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionViewController.swift; sourceTree = ""; }; + 6581C73C20CED60100F4AD34 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SafariExtensionViewController.xib; sourceTree = ""; }; + 6581C73E20CED60100F4AD34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "evergreen-subscribe-to-feed.js"; sourceTree = ""; }; + 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; }; + 6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Subscribe_to_Feed.entitlements; sourceTree = ""; }; 8403E75A201C4A79007F7246 /* FeedListKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListKeyboardDelegate.swift; sourceTree = ""; }; 840D617C2029031C009BC708 /* Evergreen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Evergreen.app; sourceTree = BUILT_PRODUCTS_DIR; }; 840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -672,6 +708,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 6581C73020CED60000F4AD34 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 840D61792029031C009BC708 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -705,6 +748,7 @@ 846E773D1F6EF67A00A165E2 /* Account.framework in Frameworks */, 84C37FA520DD8D8400CA8CF5 /* RSCore.framework in Frameworks */, 84FB9A2F1EDCD6C4003D53B9 /* Sparkle.framework in Frameworks */, + 8440C8AD2129F9F5002353D1 /* ArticlesDatabase.framework in Frameworks */, 841D4D6B2106B3ED00DD04E6 /* Articles.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -719,6 +763,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 6581C73620CED60100F4AD34 /* Safari Extension */ = { + isa = PBXGroup; + children = ( + 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */, + 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */, + 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */, + 6581C73E20CED60100F4AD34 /* Info.plist */, + 6581C73F20CED60100F4AD34 /* evergreen-subscribe-to-feed.js */, + 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */, + 6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */, + ); + path = "Safari Extension"; + sourceTree = ""; + }; 840D617D2029031C009BC708 /* Evergreen-iOS */ = { isa = PBXGroup; children = ( @@ -1090,6 +1148,7 @@ 840D617D2029031C009BC708 /* Evergreen-iOS */, 840D61942029031D009BC708 /* Evergreen-iOSTests */, 840D619F2029031E009BC708 /* Evergreen-iOSUITests */, + 6581C73620CED60100F4AD34 /* Safari Extension */, 84FB9A2C1EDCD6A4003D53B9 /* Frameworks */, 849C64741ED37A5D003D8FC0 /* EvergreenTests */, D5907CDA2002F084005947E5 /* xcconfig */, @@ -1114,6 +1173,7 @@ 840D617C2029031C009BC708 /* Evergreen.app */, 840D61912029031D009BC708 /* Evergreen-iOSTests.xctest */, 840D619C2029031D009BC708 /* Evergreen-iOSUITests.xctest */, + 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */, ); name = Products; sourceTree = ""; @@ -1278,6 +1338,7 @@ children = ( 847752FE2008879500D93690 /* CoreServices.framework */, 84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */, + 6581C73420CED60100F4AD34 /* Cocoa.framework */, ); name = Frameworks; path = Evergreen/Extensions; @@ -1358,6 +1419,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 6581C73220CED60000F4AD34 /* Subscribe to Feed */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6581C75620CED60100F4AD34 /* Build configuration list for PBXNativeTarget "Subscribe to Feed" */; + buildPhases = ( + 6581C72F20CED60000F4AD34 /* Sources */, + 6581C73020CED60000F4AD34 /* Frameworks */, + 6581C73120CED60000F4AD34 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Subscribe to Feed"; + productName = "Subscribe to Feed"; + productReference = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; + productType = "com.apple.product-type.app-extension"; + }; 840D617B2029031C009BC708 /* Evergreen-iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 840D61A32029031E009BC708 /* Build configuration list for PBXNativeTarget "Evergreen-iOS" */; @@ -1420,6 +1498,7 @@ 849C645E1ED37A5D003D8FC0 /* Resources */, 84C987A52000AC9E0066B150 /* ShellScript */, 84B06F681ED37B9000F0B54B /* Embed Frameworks */, + 6581C75720CED60100F4AD34 /* Embed App Extensions */, ); buildRules = ( ); @@ -1432,6 +1511,7 @@ 84C37FB820DD8DBB00CA8CF5 /* PBXTargetDependency */, 84C37FC820DD8E1D00CA8CF5 /* PBXTargetDependency */, 841D4D6E2106B3ED00DD04E6 /* PBXTargetDependency */, + 8440C8B02129F9F5002353D1 /* PBXTargetDependency */, ); name = Evergreen; productName = Evergreen; @@ -1467,6 +1547,10 @@ LastUpgradeCheck = 0930; ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { + 6581C73220CED60000F4AD34 = { + DevelopmentTeam = M8L2WTLA8W; + ProvisioningStyle = Manual; + }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; DevelopmentTeam = 9C84TZ7Q6Z; @@ -1553,6 +1637,7 @@ 840D617B2029031C009BC708 /* Evergreen-iOS */, 840D61902029031D009BC708 /* Evergreen-iOSTests */, 840D619B2029031D009BC708 /* Evergreen-iOSUITests */, + 6581C73220CED60000F4AD34 /* Subscribe to Feed */, ); }; /* End PBXProject section */ @@ -1708,6 +1793,16 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 6581C73120CED60000F4AD34 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */, + 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */, + 6581C74020CED60100F4AD34 /* evergreen-subscribe-to-feed.js in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 840D617A2029031C009BC708 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1780,11 +1875,20 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# See https://blog.curtisherbert.com/automated-build-numbers/\n\ngit=`sh /etc/profile; which git`\nbranch_name=`$git symbolic-ref HEAD | sed -e 's,.*/\\\\(.*\\\\),\\\\1,'`\ngit_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`\nsimple_branch_name=`$git rev-parse --abbrev-ref HEAD`\n\nbuild_number=\"$git_count\"\nif [ $CONFIGURATION != \"Release\" ]; then\nbuild_number+=\"-$simple_branch_name\"\nfi\n\nplist=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\ndsym_plist=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$plist\"\nif [ -f \"$DSYM_INFO_PLIST\" ] ; then\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$dsym_plist\"\nfi"; + shellScript = "# See https://blog.curtisherbert.com/automated-build-numbers/\n\ngit=`sh /etc/profile; which git`\nbranch_name=`$git symbolic-ref HEAD | sed -e 's,.*/\\\\(.*\\\\),\\\\1,'`\ngit_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`\nsimple_branch_name=`$git rev-parse --abbrev-ref HEAD`\n\nbuild_number=\"$git_count\"\nif [ $CONFIGURATION != \"Release\" ]; then\nbuild_number+=\"-$simple_branch_name\"\nfi\n\nplist=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\ndsym_plist=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$plist\"\nif [ -f \"$DSYM_INFO_PLIST\" ] ; then\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$dsym_plist\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 6581C72F20CED60000F4AD34 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */, + 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 840D61782029031C009BC708 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1976,6 +2080,11 @@ name = Articles; targetProxy = 841D4D6D2106B3ED00DD04E6 /* PBXContainerItemProxy */; }; + 8440C8B02129F9F5002353D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ArticlesDatabase; + targetProxy = 8440C8AF2129F9F5002353D1 /* PBXContainerItemProxy */; + }; 846E77401F6EF67A00A165E2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Account; @@ -2019,6 +2128,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 6581C73C20CED60100F4AD34 /* Base */, + ); + name = SafariExtensionViewController.xib; + sourceTree = ""; + }; 840D61842029031C009BC708 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2095,6 +2212,30 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 6581C74720CED60100F4AD34 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D5907CE02002F0FA005947E5 /* Evergreen_target.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Safari Extension/Subscribe_to_Feed.entitlements"; + INFOPLIST_FILE = "Safari Extension/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.Evergreen.Subscribe-to-Feed"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Debug; + }; + 6581C74820CED60100F4AD34 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D5907CE02002F0FA005947E5 /* Evergreen_target.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Safari Extension/Subscribe_to_Feed.entitlements"; + INFOPLIST_FILE = "Safari Extension/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.Evergreen.Subscribe-to-Feed"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Release; + }; 840D61A42029031E009BC708 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2545,6 +2686,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 6581C75620CED60100F4AD34 /* Build configuration list for PBXNativeTarget "Subscribe to Feed" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6581C74720CED60100F4AD34 /* Debug */, + 6581C74820CED60100F4AD34 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 840D61A32029031E009BC708 /* Build configuration list for PBXNativeTarget "Evergreen-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift b/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift index c139c8d10..990324b7d 100644 --- a/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/UnreadIndicatorView.swift @@ -38,7 +38,7 @@ class UnreadIndicatorView: NSView { override func draw(_ dirtyRect: NSRect) { if #available(OSX 10.14, *) { - let color = isEmphasized && isSelected ? NSColor.white : NSColor.controlAccent + let color = isEmphasized && isSelected ? NSColor.white : NSColor.controlAccentColor color.setFill() } else { let color = isEmphasized && isSelected ? NSColor.white : NSColor.systemBlue diff --git a/Frameworks/Articles/Articles.xcodeproj/project.pbxproj b/Frameworks/Articles/Articles.xcodeproj/project.pbxproj index e626d849b..8e79303ad 100644 --- a/Frameworks/Articles/Articles.xcodeproj/project.pbxproj +++ b/Frameworks/Articles/Articles.xcodeproj/project.pbxproj @@ -343,6 +343,7 @@ 844BEE701F0AB3C9004AB7CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.Articles; @@ -353,6 +354,7 @@ 844BEE711F0AB3C9004AB7CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.Articles; diff --git a/Safari Extension/Base.lproj/SafariExtensionViewController.xib b/Safari Extension/Base.lproj/SafariExtensionViewController.xib new file mode 100644 index 000000000..6c80db684 --- /dev/null +++ b/Safari Extension/Base.lproj/SafariExtensionViewController.xib @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Safari Extension/Info.plist b/Safari Extension/Info.plist new file mode 100644 index 000000000..4d68d1dd6 --- /dev/null +++ b/Safari Extension/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Subscribe to Feed + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSExtension + + NSExtensionPointIdentifier + com.apple.Safari.extension + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SafariExtensionHandler + SFSafariContentScript + + + Script + evergreen-subscribe-to-feed.js + + + SFSafariToolbarItem + + Action + Command + Identifier + Button + Image + ToolbarItemIcon.pdf + Label + Subscribe to Feed + + SFSafariWebsiteAccess + + Allowed Domains + + *.* + + Level + All + + + NSHumanReadableCopyright + Copyright © 2018 Ranchero Software. All rights reserved. + NSHumanReadableDescription + This extension adds a Safari toolbar button for easily subscribing to the syndication feed for the current page. + + diff --git a/Safari Extension/SafariExtensionHandler.swift b/Safari Extension/SafariExtensionHandler.swift new file mode 100644 index 000000000..f545ad188 --- /dev/null +++ b/Safari Extension/SafariExtensionHandler.swift @@ -0,0 +1,128 @@ +// +// SafariExtensionHandler.swift +// Subscribe to Feed +// +// Created by Daniel Jalkut on 6/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import SafariServices + +class SafariExtensionHandler: SFSafariExtensionHandler { + + // Safari App Extensions don't support any reasonable means of detecting whether a + // specific Safari page was loaded with the benefit of the extension's injected + // JavaScript. For this reason a condition can easily be reached where the toolbar + // icon is active for a page, but the expected supporting code is not loaded into + // the page. To detect this and disable our icon, we use a kind of "ping" trick + // to verify whether our code is installed. + + // I tried to use a NSMapTable from String to the closure directly, but Swift + // complains that the object has to be a class type. + typealias ValidationHandler = (Bool, String) -> Void + class ValidationWrapper { + let validationHandler: ValidationHandler + + init(validationHandler: @escaping ValidationHandler) { + self.validationHandler = validationHandler + } + } + + // Maps from UUID to a validation wrapper + static var gPingPongMap = Dictionary() + static var validationQueue = DispatchQueue(label: "Toolbar Validation") + + // Bottleneck for calling through to a validation handler we have saved, and removing it from the list. + static func callValidationHandler(forHandlerID handlerID: String, withShouldValidate shouldValidate: Bool) { + if let validationWrapper = gPingPongMap[handlerID] { + validationWrapper.validationHandler(shouldValidate, "") + gPingPongMap.removeValue(forKey: handlerID) + } + } + + override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) { + if (messageName == "subscribeToFeed") { + if let feedURLString = userInfo?["url"] as? String { + if let feedURL = URL(string: feedURLString) { + // We could do something more Evergreen-specific like invoke an app-specific scheme + // to subscribe in the app. For starters we just let NSWorkspace open the URL in the + // default "feed:" URL scheme handler. + NSWorkspace.shared.open(feedURL) + } + } + } + else if (messageName == "pong") { + if let validationIDString = userInfo?["validationID"] as? String { + // Should we validate the button? + let shouldValidate = userInfo?["shouldValidate"] as? Bool ?? false + SafariExtensionHandler.callValidationHandler(forHandlerID: validationIDString, withShouldValidate:shouldValidate) + } + } + } + + override func toolbarItemClicked(in window: SFSafariWindow) { + window.getActiveTab { (activeTab) in + activeTab?.getActivePage(completionHandler: { (activePage) in + activePage?.dispatchMessageToScript(withName: "toolbarButtonClicked", userInfo: nil) + }) + } + } + + override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) { + + let uniqueValidationID = NSUUID().uuidString + + SafariExtensionHandler.validationQueue.sync { + + // Save it right away to eliminate any doubt of whether the handler gets deallocated while + // we are waiting for a callback from the getActiveTab or getActivatePage methods below. + let validationWrapper = ValidationWrapper(validationHandler: validationHandler) + SafariExtensionHandler.gPingPongMap[uniqueValidationID] = validationWrapper + + // To avoid problems with validation handlers dispatched after we've, for example, + // switched to a new tab, we aggressively clear out the map of any pending validations, + // and focus only on the newest validation request we've been asked for. + for thisValidationID in SafariExtensionHandler.gPingPongMap.keys { + if thisValidationID != uniqueValidationID { + // Default to valid ... we'll know soon enough whether the latest state + // is actually still valid or not... + SafariExtensionHandler.callValidationHandler(forHandlerID: thisValidationID, withShouldValidate: true); + + } + } + + // See comments above where gPingPongMap is declared. Upon being asked to validate the + // toolbar icon for a specific page, we save the validationHandler and postpone calling + // it until we have either received a response from our installed JavaScript, or until + // a timeout period has elapsed + window.getActiveTab { (activeTab) in + guard let activeTab = activeTab else { + SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false); + return + } + + activeTab.getActivePage { (activePage) in + guard let activePage = activePage else { + SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false); + return + } + + activePage.getPropertiesWithCompletionHandler { (pageProperties) in + if let isActive = pageProperties?.isActive { + if isActive { + // Capture the uniqueValidationID to ensure it doesn't change out from under us on a future call + activePage.dispatchMessageToScript(withName: "ping", userInfo: ["validationID": uniqueValidationID]) + + let pongTimeoutInNanoseconds = Int(Double(NSEC_PER_SEC) * 0.5) + let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds) + DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in + SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate:false) + }) + } + } + } + } + } + } + } +} diff --git a/Safari Extension/SafariExtensionViewController.swift b/Safari Extension/SafariExtensionViewController.swift new file mode 100644 index 000000000..aac562ef0 --- /dev/null +++ b/Safari Extension/SafariExtensionViewController.swift @@ -0,0 +1,20 @@ +// +// SafariExtensionViewController.swift +// Subscribe to Feed +// +// Created by Daniel Jalkut on 6/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import SafariServices + +class SafariExtensionViewController: SFSafariExtensionViewController { + + // This would be the place to handle a popover that could, for example, list the possibly multiple feeds offered by a site. + static let shared: SafariExtensionViewController = { + let shared = SafariExtensionViewController() + shared.preferredContentSize = NSSize(width:320, height:240) + return shared + }() + +} diff --git a/Safari Extension/Subscribe_to_Feed.entitlements b/Safari Extension/Subscribe_to_Feed.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/Safari Extension/Subscribe_to_Feed.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Safari Extension/ToolbarItemIcon.pdf b/Safari Extension/ToolbarItemIcon.pdf new file mode 100644 index 000000000..07eb6c743 Binary files /dev/null and b/Safari Extension/ToolbarItemIcon.pdf differ diff --git a/Safari Extension/ToolbarItemIcon.sketch b/Safari Extension/ToolbarItemIcon.sketch new file mode 100644 index 000000000..aff4b6832 Binary files /dev/null and b/Safari Extension/ToolbarItemIcon.sketch differ diff --git a/Safari Extension/evergreen-subscribe-to-feed.js b/Safari Extension/evergreen-subscribe-to-feed.js new file mode 100644 index 000000000..576c4c49b --- /dev/null +++ b/Safari Extension/evergreen-subscribe-to-feed.js @@ -0,0 +1,118 @@ +var thisPageLinkObjects = null; + +// I convert the native "link" node into an object that I can pass out to the global page +function objectFromLink(theLink) { + var linkObject = new Object(); + + linkObject.href = theLink.href; + linkObject.type = theLink.type; + linkObject.title = theLink.title; + + return linkObject; +} + +// Some sites will list feeds with inappropriate or at least less-than-ideal information +// in the MIME type attribute. We cover some edge cases here that allow to be passed through, +// where they will successfully open as "feed://" URLs in the browser. +function isValidFeedLink(theLink) { + var isValid = false; + + switch (theLink.type) + { + case "application/atom+xml": + case "application/x.atom+xml": + case "application/rss+xml": + // These types do not require other criteria. + isValid = (theLink.href != null); + + case "text/xml": + case "application/rdf+xml": + // These types require a title that has "RSS" in it. + if (theLink.title && theLink.title.search(/RSS/i) != -1) + { + isValid = (theLink.href != null); + } + } + + return isValid; +} + +function scanForSyndicationFeeds() { + // In case we don't find any, we establish that we have at least tried by setting the + // variables to empty instead of null. + thisPageLinkObjects = [] + + thisPageLinks = document.getElementsByTagName("link"); + + for (thisLinkIndex = 0; thisLinkIndex < thisPageLinks.length; thisLinkIndex++) + { + var thisLink = thisPageLinks[thisLinkIndex]; + var thisLinkRel = thisLink.getAttribute("rel"); + if (thisLinkRel == "alternate") + { + if (isValidFeedLink(thisLink)) + { + thisPageLinkObjects.push(objectFromLink(thisLink)); + } + } + } +} + +function subscribeToFeed(theFeed) { + // Convert the URL to a feed:// scheme because Safari + // will refuse to load e.g. a feed that is listed merely + // as "text/xml". We do some preflighting of the link rel + // in the PageLoadEnd.js so we can be more confident it's a + // good feed: URL. + var feedURL = theFeed.href; + if (feedURL.match(/^http[s]?:\/\//)) + { + feedURL = feedURL.replace(/^http[s]?:\/\//, "feed://"); + } + else if (feedURL.match(/^feed:/) == false) + { + feedURL = "feed:" + feedURL; + } + + safari.extension.dispatchMessage("subscribeToFeed", { "url": feedURL }); +} + +function messageHandler(event) { + if (event.name === "toolbarButtonClicked") + { + // Workaround Radar #31182842, in which residual copies of our + // app extension may remain loaded in context of pages in Safari, + // causing multiple responses to broadcast message about toolbar + // button being clicked. In the case of the "extra" injections, + // the document location is null, so we can avoid doing on anything. + if ((document.location != null) && (thisPageLinkObjects.length > 0)) + { + feedToOpen = thisPageLinkObjects[0]; + subscribeToFeed(feedToOpen); + } + } + else if (event.name === "ping") + { + // Just a hack to get the toolbar icon validation to work as expected. + // If we don't pong back, the extension knows we are not loaded in a page. + + // There is a bug in Safari where the messageHandler is apparently held on to by Safari + // even after an extension is disabled. So an effort to "ping" an extension's scripts will + // succeed even if its been disabled and the page reloaded. Checking for the existance of + // document.location seems to ensure we have enough of a handle still on the document that + // we can do something useful with it. + var shouldValidate = (document.location != null) && (thisPageLinkObjects.length > 0); + + // Pass back the same validationID we were handed so they can look up the correlated validationHandler + safari.extension.dispatchMessage("pong", { "validationID": event.message.validationID, "shouldValidate": shouldValidate }); + } +} + +document.addEventListener("DOMContentLoaded", function(event) { + // Prevent injecting the JavaScript in IFRAMES, and from acting before Safari is ready... + if ((window.top === window) && (typeof safari != 'undefined') && (document.location != null)) + { + safari.self.addEventListener("message", messageHandler, false) + scanForSyndicationFeeds(); + } +}); diff --git a/Technotes/SubmoduleCheatSheet.md b/Technotes/SubmoduleCheatSheet.md index 6f1ce732b..e6c62a52d 100644 --- a/Technotes/SubmoduleCheatSheet.md +++ b/Technotes/SubmoduleCheatSheet.md @@ -2,6 +2,12 @@ Evergreen uses [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to include shared frameworks. At this writing (June 2018) they are DB5, RSCore, RSDatabase, RSWeb, RSTree, and RSParser. +After your first checkout: + + git submodule init + git submodule update + + To add a submodule: git submodule add https://github.com/username/path