diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 1342a0823df0..35300e710101 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -50,6 +50,15 @@ 0BF42D4F1A7CD09600889E28 /* TestFavicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D4E1A7CD09600889E28 /* TestFavicons.swift */; }; 0BF8F8DA1AEFF1C900E90BC2 /* noTitle.html in Resources */ = {isa = PBXBuildFile; fileRef = 0BF8F8D91AEFF1C900E90BC2 /* noTitle.html */; }; 19DE1F671EC13B6400428B8C /* LeanplumIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DE1F661EC13B6400428B8C /* LeanplumIntegration.swift */; }; + 1D06AE6624FEE4D5000B092B /* TopSitesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D06AE6524FEE4D5000B092B /* TopSitesProvider.swift */; }; + 1D06AE6A24FEE8D6000B092B /* TabProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D06AE6924FEE8D6000B092B /* TabProvider.swift */; }; + 1D0BA05C24F46A0400D731B5 /* TopSitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0BA05B24F46A0400D731B5 /* TopSitesManager.swift */; }; + 1D9E1FE524FEF56C006E561D /* TopSites in Resources */ = {isa = PBXBuildFile; fileRef = 3BC659481E5BA4AE006D560F /* TopSites */; }; + 1DA3CE5D24EEE73100422BB2 /* OpenTabsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA3CE5C24EEE73100422BB2 /* OpenTabsWidget.swift */; }; + 1DA3CE5F24EEE7C600422BB2 /* TabArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA3CE5E24EEE7C600422BB2 /* TabArchiver.swift */; }; + 1DA3CE6324EEE83200422BB2 /* SavedTab+ConfigureExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA3CE6224EEE83200422BB2 /* SavedTab+ConfigureExtension.swift */; }; + 1DA3CE6724EEE86C00422BB2 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075641E37F7AB006961AC /* AppInfo.swift */; }; + 1DDAD13E24F0651C007623C8 /* TopSitesWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDAD13C24F064F7007623C8 /* TopSitesWidget.swift */; }; 2386E4E624F8358E0072EF17 /* DefaultBrowserCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2386E4E524F8358E0072EF17 /* DefaultBrowserCard.swift */; }; 274A36C9239EB94000A21587 /* LibraryPanelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274A36C8239EB94000A21587 /* LibraryPanelButton.swift */; }; 274A36CC239EB99400A21587 /* LibraryPanelContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274A36CB239EB99400A21587 /* LibraryPanelContextMenu.swift */; }; @@ -241,9 +250,9 @@ 3D9CA9A81EF84D04002434DD /* NoImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9CA9A71EF84D04002434DD /* NoImageTests.swift */; }; 3D9CAA1C1EFCD655002434DD /* ClipBoardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9CAA1B1EFCD655002434DD /* ClipBoardTests.swift */; }; 3DEFED081F55EBE300F8620C /* TrackingProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEFED071F55EBE300F8620C /* TrackingProtectionTests.swift */; }; + 3E82980324BE4C31000A59FF /* FxATelemetryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E82980124BE4C31000A59FF /* FxATelemetryUtils.swift */; }; 4334145424C63779001541F3 /* IntroScreenSyncViewV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4334145324C63779001541F3 /* IntroScreenSyncViewV1.swift */; }; 4334145724C6378B001541F3 /* IntroScreenWelcomeViewV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4334145624C6378B001541F3 /* IntroScreenWelcomeViewV1.swift */; }; - 3E82980324BE4C31000A59FF /* FxATelemetryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E82980124BE4C31000A59FF /* FxATelemetryUtils.swift */; }; 43446CE7240D9F3000F5C643 /* ETP.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43446CE5240D9F3000F5C643 /* ETP.xcassets */; }; 43446CEA2412066500F5C643 /* UIViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43446CE92412066500F5C643 /* UIViewControllerExtension.swift */; }; 43446CF02412DDBE00F5C643 /* UpdateCoverSheetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43446CEF2412DDBE00F5C643 /* UpdateCoverSheetViewModelTests.swift */; }; @@ -262,6 +271,7 @@ 43A5643823CD1E1C00B6857D /* UpdateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A5643523CD1E1B00B6857D /* UpdateViewController.swift */; }; 43A5643923CD1E1C00B6857D /* Update.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43A5643623CD1E1B00B6857D /* Update.xcassets */; }; 43B137F223A181A200CB7FA0 /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B137F123A181A200CB7FA0 /* NSUserDefaultsPrefs.swift */; }; + 43BA967D2512D15900EB6134 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35210E01CB2F16600FC5DCB /* Strings.swift */; }; 43DDB96A240994370058A068 /* ETPCoverSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DDB969240994360058A068 /* ETPCoverSheetViewController.swift */; }; 43DDB96C240995A70058A068 /* ETPModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DDB96B240995A70058A068 /* ETPModel.swift */; }; 43DDB97624099F200058A068 /* ETPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DDB97524099F200058A068 /* ETPViewModel.swift */; }; @@ -633,7 +643,6 @@ DA9FD88624E213CD00168D1E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9FD88524E213CC00168D1E /* Helpers.swift */; }; DA9FD88824E213DD00168D1E /* QuickLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9FD88724E213DC00168D1E /* QuickLink.swift */; }; DA9FD88924E213E500168D1E /* QuickLinkSelection.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DA9FD88124E213B400168D1E /* QuickLinkSelection.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; - DA9FD88A24E2153A00168D1E /* libShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* libShared.a */; }; DABDA4AD20DA0FB900FBB0BD /* ObjcExceptionBridging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DABDA4AC20DA0FB800FBB0BD /* ObjcExceptionBridging.framework */; }; DACDE996225E537900C8F37F /* VersionSettingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DACDE995225E537900C8F37F /* VersionSettingTests.swift */; }; DADC62DA226E3297003AFF8B /* ReopenLastTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADC62D9226E3297003AFF8B /* ReopenLastTabTests.swift */; }; @@ -1256,6 +1265,13 @@ 0BF42D4E1A7CD09600889E28 /* TestFavicons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestFavicons.swift; sourceTree = ""; }; 0BF8F8D91AEFF1C900E90BC2 /* noTitle.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = noTitle.html; sourceTree = ""; }; 19DE1F661EC13B6400428B8C /* LeanplumIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeanplumIntegration.swift; sourceTree = ""; }; + 1D06AE6524FEE4D5000B092B /* TopSitesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSitesProvider.swift; sourceTree = ""; }; + 1D06AE6924FEE8D6000B092B /* TabProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabProvider.swift; sourceTree = ""; }; + 1D0BA05B24F46A0400D731B5 /* TopSitesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSitesManager.swift; sourceTree = ""; }; + 1DA3CE5C24EEE73100422BB2 /* OpenTabsWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenTabsWidget.swift; sourceTree = ""; }; + 1DA3CE5E24EEE7C600422BB2 /* TabArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabArchiver.swift; sourceTree = ""; }; + 1DA3CE6224EEE83200422BB2 /* SavedTab+ConfigureExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SavedTab+ConfigureExtension.swift"; sourceTree = ""; }; + 1DDAD13C24F064F7007623C8 /* TopSitesWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSitesWidget.swift; sourceTree = ""; }; 2386E4E524F8358E0072EF17 /* DefaultBrowserCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBrowserCard.swift; sourceTree = ""; }; 274A36C8239EB94000A21587 /* LibraryPanelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPanelButton.swift; sourceTree = ""; }; 274A36CB239EB99400A21587 /* LibraryPanelContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPanelContextMenu.swift; sourceTree = ""; }; @@ -1447,9 +1463,9 @@ 3D9CA9A71EF84D04002434DD /* NoImageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoImageTests.swift; sourceTree = ""; }; 3D9CAA1B1EFCD655002434DD /* ClipBoardTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClipBoardTests.swift; sourceTree = ""; }; 3DEFED071F55EBE300F8620C /* TrackingProtectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingProtectionTests.swift; sourceTree = ""; }; + 3E82980124BE4C31000A59FF /* FxATelemetryUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FxATelemetryUtils.swift; path = Account/FxATelemetryUtils.swift; sourceTree = ""; }; 4334145324C63779001541F3 /* IntroScreenSyncViewV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroScreenSyncViewV1.swift; sourceTree = ""; }; 4334145624C6378B001541F3 /* IntroScreenWelcomeViewV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroScreenWelcomeViewV1.swift; sourceTree = ""; }; - 3E82980124BE4C31000A59FF /* FxATelemetryUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FxATelemetryUtils.swift; path = Account/FxATelemetryUtils.swift; sourceTree = ""; }; 43446CE5240D9F3000F5C643 /* ETP.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = ETP.xcassets; sourceTree = ""; }; 43446CE92412066500F5C643 /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIViewControllerExtension.swift; path = Extensions/UIViewControllerExtension.swift; sourceTree = ""; }; 43446CEF2412DDBE00F5C643 /* UpdateCoverSheetViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCoverSheetViewModelTests.swift; sourceTree = ""; }; @@ -2101,7 +2117,6 @@ buildActionMask = 2147483647; files = ( 047F9B2924E1FE1C00CD7DF7 /* SwiftUI.framework in Frameworks */, - DA9FD88A24E2153A00168D1E /* libShared.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2296,6 +2311,8 @@ 047F9B2A24E1FE1C00CD7DF7 /* WidgetKit */ = { isa = PBXGroup; children = ( + 1DDAD13B24F064E6007623C8 /* TopSites */, + 1DA3CE5B24EEE71B00422BB2 /* OpenTabs */, DA9FD88724E213DC00168D1E /* QuickLink.swift */, DA9FD88024E213B400168D1E /* SearchQuickLinksSmall */, 047F9B3924E1FED200CD7DF7 /* SearchQuickLinksMedium */, @@ -2326,6 +2343,24 @@ path = ThirdParty/UIImageViewAligned; sourceTree = ""; }; + 1DA3CE5B24EEE71B00422BB2 /* OpenTabs */ = { + isa = PBXGroup; + children = ( + 1DA3CE5C24EEE73100422BB2 /* OpenTabsWidget.swift */, + 1D06AE6924FEE8D6000B092B /* TabProvider.swift */, + ); + path = OpenTabs; + sourceTree = ""; + }; + 1DDAD13B24F064E6007623C8 /* TopSites */ = { + isa = PBXGroup; + children = ( + 1DDAD13C24F064F7007623C8 /* TopSitesWidget.swift */, + 1D06AE6524FEE4D5000B092B /* TopSitesProvider.swift */, + ); + path = TopSites; + sourceTree = ""; + }; 274A36D1239EBAD600A21587 /* LibraryViewController */ = { isa = PBXGroup; children = ( @@ -3391,7 +3426,10 @@ 0BF0DB931A8545800039F300 /* URLBarView.swift */, D0FCF7F41FE45842004A7995 /* UserScriptManager.swift */, 63306D3821103EAE00F25400 /* SavedTab.swift */, + 1DA3CE6224EEE83200422BB2 /* SavedTab+ConfigureExtension.swift */, 63306D422110B3CD00F25400 /* TabManagerStore.swift */, + 1DA3CE5E24EEE7C600422BB2 /* TabArchiver.swift */, + 1D0BA05B24F46A0400D731B5 /* TopSitesManager.swift */, ); path = Browser; sourceTree = ""; @@ -4846,6 +4884,7 @@ buildActionMask = 2147483647; files = ( 047F9B2E24E1FE1F00CD7DF7 /* Assets.xcassets in Resources */, + 1D9E1FE524FEF56C006E561D /* TopSites in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5178,11 +5217,17 @@ files = ( 047F9B3E24E1FF4000CD7DF7 /* SearchQuickLinks.swift in Sources */, DA9FD88624E213CD00168D1E /* Helpers.swift in Sources */, + 1D06AE6A24FEE8D6000B092B /* TabProvider.swift in Sources */, DA9FD88424E213B500168D1E /* SmallQuickLink.swift in Sources */, + 1D06AE6624FEE4D5000B092B /* TopSitesProvider.swift in Sources */, + 1DDAD13E24F0651C007623C8 /* TopSitesWidget.swift in Sources */, + 43BA967D2512D15900EB6134 /* Strings.swift in Sources */, 047F9B4224E1FF4000CD7DF7 /* ImageButtonWithLabel.swift in Sources */, 047F9B2C24E1FE1C00CD7DF7 /* WidgetKit.swift in Sources */, + 1DA3CE6724EEE86C00422BB2 /* AppInfo.swift in Sources */, DA9FD88824E213DD00168D1E /* QuickLink.swift in Sources */, DA9FD88324E213B500168D1E /* QuickLinkSelection.intentdefinition in Sources */, + 1DA3CE5D24EEE73100422BB2 /* OpenTabsWidget.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5646,6 +5691,7 @@ 745DAB301CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift in Sources */, D0B9483D22A18B78002F4AA1 /* TextFieldTableViewCell.swift in Sources */, C45F44691D087DB600CB7EF0 /* TopTabsViewController.swift in Sources */, + 1D0BA05C24F46A0400D731B5 /* TopSitesManager.swift in Sources */, 0BF0DB941A8545800039F300 /* URLBarView.swift in Sources */, C8FB0C782321523D00031088 /* PageActionMenu.swift in Sources */, D01017F5219CB6BD009CBB5A /* DownloadContentScript.swift in Sources */, @@ -5707,6 +5753,7 @@ 0BB5B30B1AC0AD1F0052877D /* LoginsHelper.swift in Sources */, C86E4F712493BA8E0087BFD9 /* Metrics.swift in Sources */, E69E06C91C76198000D0F926 /* AuthenticationManagerConstants.swift in Sources */, + 1DA3CE6324EEE83200422BB2 /* SavedTab+ConfigureExtension.swift in Sources */, 392ED7E61D0AEFEF009D9B62 /* HomePageAccessors.swift in Sources */, 7BA8D1C71BA037F500C8AE9E /* OpenInHelper.swift in Sources */, CA90753824929B22005B794D /* NoLoginsView.swift in Sources */, @@ -5790,6 +5837,7 @@ 63306D3921103EAE00F25400 /* SavedTab.swift in Sources */, 2F44FCCB1A9E972E00FD20CC /* SearchEnginePicker.swift in Sources */, E68E7ACB1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift in Sources */, + 1DA3CE5F24EEE7C600422BB2 /* TabArchiver.swift in Sources */, D04D1B92209790B60074B35F /* Toast.swift in Sources */, D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */, 274A36CE239EB9EC00A21587 /* LibraryViewController+LibraryPanelDelegate.swift in Sources */, @@ -7037,6 +7085,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_Developer; + DEVELOPMENT_TEAM = 43AQ936H96; PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID)"; PRODUCT_MODULE_NAME = Client; PRODUCT_NAME = Client; diff --git a/Client/Application/AppDelegate.swift b/Client/Application/AppDelegate.swift index 18b74ef678f8..90886e8e5bb4 100644 --- a/Client/Application/AppDelegate.swift +++ b/Client/Application/AppDelegate.swift @@ -16,6 +16,7 @@ import Sync import CoreSpotlight import UserNotifications import Account +import WidgetKit #if canImport(BackgroundTasks) import BackgroundTasks @@ -378,6 +379,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati } else { syncOnDidEnterBackground(application: application) } + + tabManager.preserveTabs() + + WidgetCenter.shared.reloadAllTimelines() } fileprivate func syncOnDidEnterBackground(application: UIApplication) { diff --git a/Client/Frontend/Browser/FaviconHandler.swift b/Client/Frontend/Browser/FaviconHandler.swift index 6bc41e493d0a..9deeacbdbe39 100644 --- a/Client/Frontend/Browser/FaviconHandler.swift +++ b/Client/Frontend/Browser/FaviconHandler.swift @@ -8,8 +8,6 @@ import Storage import SDWebImage class FaviconHandler { - static let MaximumFaviconSize = 1 * 1024 * 1024 // 1 MiB file size limit - private let backgroundQueue = OperationQueue() init() { @@ -30,7 +28,7 @@ class FaviconHandler { var fetch: SDWebImageOperation? let onProgress: SDWebImageDownloaderProgressBlock = { (receivedSize, expectedSize, _) -> Void in - if receivedSize > FaviconHandler.MaximumFaviconSize || expectedSize > FaviconHandler.MaximumFaviconSize { + if receivedSize > FaviconFetcher.MaximumFaviconSize || expectedSize > FaviconFetcher.MaximumFaviconSize { fetch?.cancel() } } @@ -108,9 +106,3 @@ extension FaviconHandler: TabEventHandler { tab.favicons.removeAll(keepingCapacity: false) } } - -class FaviconError: MaybeErrorType { - internal var description: String { - return "No Image Loaded" - } -} diff --git a/Client/Frontend/Browser/SavedTab+ConfigureExtension.swift b/Client/Frontend/Browser/SavedTab+ConfigureExtension.swift new file mode 100644 index 000000000000..77ca0066889b --- /dev/null +++ b/Client/Frontend/Browser/SavedTab+ConfigureExtension.swift @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Foundation +import WebKit +import Storage +import Shared + +// This cannot be easily imported into extension targets, so we break it out here. +extension SavedTab { + convenience init?(tab: Tab, isSelected: Bool) { + assert(Thread.isMainThread) + + var sessionData = tab.sessionData + if sessionData == nil { + let currentItem: WKBackForwardListItem! = tab.webView?.backForwardList.currentItem + + // Freshly created web views won't have any history entries at all. + // If we have no history, abort. + if currentItem != nil { + // The back & forward list keep track of the users history within the session + let backList = tab.webView?.backForwardList.backList ?? [] + let forwardList = tab.webView?.backForwardList.forwardList ?? [] + let urls = (backList + [currentItem] + forwardList).map { $0.url } + let currentPage = -forwardList.count + sessionData = SessionData(currentPage: currentPage, urls: urls, lastUsedTime: tab.lastExecutedTime ?? Date.now()) + } + } + + self.init(screenshotUUID: tab.screenshotUUID, isSelected: isSelected, title: tab.title ?? tab.lastTitle, isPrivate: tab.isPrivate, faviconURL: tab.displayFavicon?.url, url: tab.url, sessionData: sessionData) + } + + func configureSavedTabUsing(_ tab: Tab, imageStore: DiskImageStore? = nil) -> Tab { + // Since this is a restored tab, reset the URL to be loaded as that will be handled by the SessionRestoreHandler + tab.url = nil + + if let faviconURL = faviconURL { + let icon = Favicon(url: faviconURL, date: Date()) + icon.width = 1 + tab.favicons.append(icon) + } + + if let screenshotUUID = screenshotUUID, + let imageStore = imageStore { + tab.screenshotUUID = screenshotUUID + imageStore.get(screenshotUUID.uuidString) >>== { screenshot in + if tab.screenshotUUID == screenshotUUID { + tab.setScreenshot(screenshot, revUUID: false) + } + } + } + + tab.sessionData = sessionData + tab.lastTitle = title + + return tab + } +} diff --git a/Client/Frontend/Browser/SavedTab.swift b/Client/Frontend/Browser/SavedTab.swift index ef3a0eb0c0b3..3b8d7b155590 100644 --- a/Client/Frontend/Browser/SavedTab.swift +++ b/Client/Frontend/Browser/SavedTab.swift @@ -8,9 +8,10 @@ import Storage import Shared class SavedTab: NSObject, NSCoding { - let isSelected: Bool - let title: String? - let isPrivate: Bool + var isSelected: Bool + var title: String? + var url: URL? + var isPrivate: Bool var sessionData: SessionData? var screenshotUUID: UUID? var faviconURL: String? @@ -25,7 +26,8 @@ class SavedTab: NSObject, NSCoding { "isPrivate": String(self.isPrivate) as AnyObject, "isSelected": String(self.isSelected) as AnyObject, "faviconURL": faviconURL as AnyObject, - "screenshotUUID": uuid as AnyObject + "screenshotUUID": uuid as AnyObject, + "url": url as AnyObject ] if let sessionDataInfo = self.sessionData?.jsonDictionary { @@ -35,33 +37,17 @@ class SavedTab: NSObject, NSCoding { return json } - init?(tab: Tab, isSelected: Bool) { - assert(Thread.isMainThread) - - self.screenshotUUID = tab.screenshotUUID as UUID? + init?(screenshotUUID: UUID?, isSelected: Bool, title: String?, isPrivate: Bool, faviconURL: String?, url: URL?, sessionData: SessionData?) { + + self.screenshotUUID = screenshotUUID self.isSelected = isSelected - self.title = tab.displayTitle - self.isPrivate = tab.isPrivate - self.faviconURL = tab.displayFavicon?.url - super.init() + self.title = title + self.isPrivate = isPrivate + self.faviconURL = faviconURL + self.url = url + self.sessionData = sessionData - if tab.sessionData == nil { - let currentItem: WKBackForwardListItem! = tab.webView?.backForwardList.currentItem - - // Freshly created web views won't have any history entries at all. - // If we have no history, abort. - if currentItem == nil { - return nil - } - - let backList = tab.webView?.backForwardList.backList ?? [] - let forwardList = tab.webView?.backForwardList.forwardList ?? [] - let urls = (backList + [currentItem] + forwardList).map { $0.url } - let currentPage = -forwardList.count - self.sessionData = SessionData(currentPage: currentPage, urls: urls, lastUsedTime: tab.lastExecutedTime ?? Date.now()) - } else { - self.sessionData = tab.sessionData - } + super.init() } required init?(coder: NSCoder) { @@ -71,6 +57,7 @@ class SavedTab: NSObject, NSCoding { self.title = coder.decodeObject(forKey: "title") as? String self.isPrivate = coder.decodeBool(forKey: "isPrivate") self.faviconURL = coder.decodeObject(forKey: "faviconURL") as? String + self.url = coder.decodeObject(forKey: "url") as? URL } func encode(with coder: NSCoder) { @@ -80,31 +67,6 @@ class SavedTab: NSObject, NSCoding { coder.encode(title, forKey: "title") coder.encode(isPrivate, forKey: "isPrivate") coder.encode(faviconURL, forKey: "faviconURL") - } - - func configureSavedTabUsing(_ tab: Tab, imageStore: DiskImageStore? = nil) -> Tab { - // Since this is a restored tab, reset the URL to be loaded as that will be handled by the SessionRestoreHandler - tab.url = nil - - if let faviconURL = faviconURL { - let icon = Favicon(url: faviconURL, date: Date()) - icon.width = 1 - tab.favicons.append(icon) - } - - if let screenshotUUID = screenshotUUID, - let imageStore = imageStore { - tab.screenshotUUID = screenshotUUID - imageStore.get(screenshotUUID.uuidString) >>== { screenshot in - if tab.screenshotUUID == screenshotUUID { - tab.setScreenshot(screenshot, revUUID: false) - } - } - } - - tab.sessionData = sessionData - tab.lastTitle = title - - return tab + coder.encode(url, forKey: "url") } } diff --git a/Client/Frontend/Browser/SessionData.swift b/Client/Frontend/Browser/SessionData.swift index ce4922e28f1c..b502ceadb027 100644 --- a/Client/Frontend/Browser/SessionData.swift +++ b/Client/Frontend/Browser/SessionData.swift @@ -6,33 +6,6 @@ import Foundation import Shared -private func migrate(urls: [URL]) -> [URL] { - return urls.compactMap { url in - var url = url - let port = AppInfo.webserverPort - [("http://localhost:\(port)/errors/error.html?url=", "\(InternalURL.baseUrl)/\(SessionRestoreHandler.path)?url=") - // TODO: handle reader pages ("http://localhost:6571/reader-mode/page?url=", "\(InternalScheme.url)/\(ReaderModeHandler.path)?url=") - ].forEach { - oldItem, newItem in - if url.absoluteString.hasPrefix(oldItem) { - var urlStr = url.absoluteString.replacingOccurrences(of: oldItem, with: newItem) - let comp = urlStr.components(separatedBy: newItem) - if comp.count > 2 { - // get the last instance of incorrectly nested urls - urlStr = newItem + (comp.last ?? "") - assertionFailure("SessionData urls have nested internal links, investigate: [\(url.absoluteString)]") - } - url = URL(string: urlStr) ?? url - } - } - - if let internalUrl = InternalURL(url), internalUrl.isAuthorized, let stripped = URL(string: internalUrl.stripAuthorization) { - return stripped - } - - return url - } -} class SessionData: NSObject, NSCoding { let currentPage: Int @@ -57,7 +30,7 @@ class SessionData: NSObject, NSCoding { **/ init(currentPage: Int, urls: [URL], lastUsedTime: Timestamp) { self.currentPage = currentPage - self.urls = migrate(urls: urls) + self.urls = urls self.lastUsedTime = lastUsedTime assert(urls.count > 0, "Session has at least one entry") @@ -66,13 +39,13 @@ class SessionData: NSObject, NSCoding { required init?(coder: NSCoder) { self.currentPage = coder.decodeAsInt(forKey: "currentPage") - self.urls = migrate(urls: coder.decodeObject(forKey: "urls") as? [URL] ?? [URL]()) + self.urls = (coder.decodeObject(forKey: "urls") as? [URL]) ?? [URL]() self.lastUsedTime = coder.decodeAsUInt64(forKey: "lastUsedTime") } func encode(with coder: NSCoder) { coder.encode(currentPage, forKey: "currentPage") - coder.encode(migrate(urls: urls), forKey: "urls") + coder.encode(urls, forKey: "urls") coder.encode(Int64(lastUsedTime), forKey: "lastUsedTime") } } diff --git a/Client/Frontend/Browser/TabArchiver.swift b/Client/Frontend/Browser/TabArchiver.swift new file mode 100644 index 000000000000..3c1f267ce1d9 --- /dev/null +++ b/Client/Frontend/Browser/TabArchiver.swift @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Foundation +import Sentry +import Shared + +struct TabArchiver { + static func tabsToRestore(tabsStateArchivePath: String?) -> [SavedTab] { + guard let tabStateArchivePath = tabsStateArchivePath, + FileManager.default.fileExists(atPath: tabStateArchivePath), + let tabData = try? Data(contentsOf: URL(fileURLWithPath: tabStateArchivePath)) else { + return [SavedTab]() + } + + let unarchiver = NSKeyedUnarchiver(forReadingWith: tabData) + unarchiver.setClass(SavedTab.self, forClassName: "Client.SavedTab") + unarchiver.setClass(SessionData.self, forClassName: "Client.SessionData") + + unarchiver.decodingFailurePolicy = .setErrorAndReturn + guard let tabs = unarchiver.decodeObject(forKey: "tabs") as? [SavedTab] else { + Sentry.shared.send( + message: "Failed to restore tabs", + tag: SentryTag.tabManager, + severity: .error, + description: "\(unarchiver.error ??? "nil")") + return [SavedTab]() + } + + return tabs + } +} diff --git a/Client/Frontend/Browser/TabManager.swift b/Client/Frontend/Browser/TabManager.swift index 55f08cafb0a6..af40443d70f0 100644 --- a/Client/Frontend/Browser/TabManager.swift +++ b/Client/Frontend/Browser/TabManager.swift @@ -33,6 +33,12 @@ class WeakTabManagerDelegate { } } +extension TabManager: TabEventHandler { + func tab(_ tab: Tab, didLoadFavicon favicon: Favicon?, with: Data?) { + store.preserveTabs(tabs, selectedTab: selectedTab) + } +} + // TabManager must extend NSObjectProtocol in order to implement WKNavigationDelegate class TabManager: NSObject { fileprivate var delegates = [WeakTabManagerDelegate]() @@ -113,6 +119,8 @@ class TabManager: NSObject { self.store = TabManagerStore(imageStore: imageStore) super.init() + register(self, forTabEvents: .didLoadFavicon) + addNavigationDelegate(self) NotificationCenter.default.addObserver(self, selector: #selector(prefsDidChange), name: UserDefaults.didChangeNotification, object: nil) @@ -210,6 +218,10 @@ class TabManager: NSObject { tab.applyTheme() } } + + func preserveTabs() { + store.preserveTabs(tabs, selectedTab: selectedTab) + } func shouldClearPrivateTabs() -> Bool { return profile.prefs.boolForKey("settings.closePrivateTabs") ?? false @@ -608,6 +620,7 @@ extension TabManager { } } } + guard count == 0, !AppConstants.IsRunningTest, !DebugSettingsBundleOptions.skipSessionRestore, store.hasTabsToRestoreAtStartup else { return } diff --git a/Client/Frontend/Browser/TabManagerStore.swift b/Client/Frontend/Browser/TabManagerStore.swift index f67ca473ffe6..9f8238e23fae 100644 --- a/Client/Frontend/Browser/TabManagerStore.swift +++ b/Client/Frontend/Browser/TabManagerStore.swift @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation -import WebKit import Storage import Shared import XCGLogger @@ -17,7 +16,9 @@ class TabManagerStore { fileprivate var writeOperation = DispatchWorkItem {} // Init this at startup with the tabs on disk, and then on each save, update the in-memory tab state. - fileprivate lazy var archivedStartupTabs = { return tabsToRestore() }() + fileprivate lazy var archivedStartupTabs = { + return TabArchiver.tabsToRestore(tabsStateArchivePath: tabsStateArchivePath()) + }() init(imageStore: DiskImageStore?, _ fileManager: FileManager = FileManager.default) { self.fileManager = fileManager @@ -43,36 +44,16 @@ class TabManagerStore { return URL(fileURLWithPath: path).appendingPathComponent("tabsState.archive").path } - fileprivate func tabsToRestore() -> [SavedTab] { - guard let tabStateArchivePath = tabsStateArchivePath(), - fileManager.fileExists(atPath: tabStateArchivePath), - let tabData = try? Data(contentsOf: URL(fileURLWithPath: tabStateArchivePath)) else { - return [SavedTab]() - } - - let unarchiver = NSKeyedUnarchiver(forReadingWith: tabData) - unarchiver.decodingFailurePolicy = .setErrorAndReturn - guard let tabs = unarchiver.decodeObject(forKey: "tabs") as? [SavedTab] else { - Sentry.shared.send( - message: "Failed to restore tabs", - tag: SentryTag.tabManager, - severity: .error, - description: "\(unarchiver.error ??? "nil")") - return [SavedTab]() - } - return tabs - } - fileprivate func prepareSavedTabs(fromTabs tabs: [Tab], selectedTab: Tab?) -> [SavedTab]? { var savedTabs = [SavedTab]() var savedUUIDs = Set() for tab in tabs { - if let savedTab = SavedTab(tab: tab, isSelected: tab === selectedTab) { + if let savedTab = SavedTab(tab: tab, isSelected: tab == selectedTab) { savedTabs.append(savedTab) - if let screenshot = tab.screenshot, - let screenshotUUID = tab.screenshotUUID { + let screenshotUUID = tab.screenshotUUID { savedUUIDs.insert(screenshotUUID.uuidString) + imageStore?.put(screenshotUUID.uuidString, image: screenshot) } } @@ -87,6 +68,7 @@ class TabManagerStore { // Write failures (i.e. due to read locks) are considered inconsequential, as preserveTabs will be called frequently. @discardableResult func preserveTabs(_ tabs: [Tab], selectedTab: Tab?) -> Success { assert(Thread.isMainThread) + print("preserve tabs!, existing tabs: \(tabs.count)") guard let savedTabs = prepareSavedTabs(fromTabs: tabs, selectedTab: selectedTab), let path = tabsStateArchivePath() else { clearArchive() @@ -97,13 +79,16 @@ class TabManagerStore { let tabStateData = NSMutableData() let archiver = NSKeyedArchiver(forWritingWith: tabStateData) + archiver.encode(savedTabs, forKey: "tabs") archiver.finishEncoding() let result = Success() writeOperation = DispatchWorkItem { let written = tabStateData.write(toFile: path, atomically: true) - log.debug("PreserveTabs write ok: \(written)") // Ignore write failure (could be restoring). + + // Ignore write failure (could be restoring). + log.debug("PreserveTabs write ok: \(written), bytes: \(tabStateData.length)") result.fill(Maybe(success: ())) } @@ -116,7 +101,6 @@ class TabManagerStore { func restoreStartupTabs(clearPrivateTabs: Bool, tabManager: TabManager) -> Tab? { let selectedTab = restoreTabs(savedTabs: archivedStartupTabs, clearPrivateTabs: clearPrivateTabs, tabManager: tabManager) - archivedStartupTabs.removeAll() return selectedTab } @@ -162,6 +146,6 @@ class TabManagerStore { extension TabManagerStore { func testTabCountOnDisk() -> Int { assert(AppConstants.IsRunningTest) - return tabsToRestore().count + return TabArchiver.tabsToRestore(tabsStateArchivePath: tabsStateArchivePath()).count } } diff --git a/Client/Frontend/Browser/TopSitesManager.swift b/Client/Frontend/Browser/TopSitesManager.swift new file mode 100644 index 000000000000..7a65ca557509 --- /dev/null +++ b/Client/Frontend/Browser/TopSitesManager.swift @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Foundation +import Shared +import UIKit +import Storage +import SyncTelemetry + +struct TopSitesHandler { + static func getTopSites(profile: Profile) -> Deferred<[Site]> { + let maxItems = UIDevice.current.userInterfaceIdiom == .pad ? 32 : 16 + return profile.history.getTopSitesWithLimit(maxItems).both(profile.history.getPinnedTopSites()).bindQueue(.main) { (topsites, pinnedSites) in + + let deferred = Deferred<[Site]>() + + guard let mySites = topsites.successValue?.asArray(), let pinned = pinnedSites.successValue?.asArray() else { + return deferred + } + + // How sites are merged together. We compare against the url's base domain. example m.youtube.com is compared against `youtube.com` + let unionOnURL = { (site: Site) -> String in + return URL(string: site.url)?.normalizedHost ?? "" + } + + // Fetch the default sites + let defaultSites = defaultTopSites(profile) + // create PinnedSite objects. used by the view layer to tell topsites apart + let pinnedSites: [Site] = pinned.map({ PinnedSite(site: $0) }) + + // Merge default topsites with a user's topsites. + let mergedSites = mySites.union(defaultSites, f: unionOnURL) + // Merge pinnedSites with sites from the previous step + let allSites = pinnedSites.union(mergedSites, f: unionOnURL) + + // Favour topsites from defaultSites as they have better favicons. But keep PinnedSites + let newSites = allSites.map { site -> Site in + if let _ = site as? PinnedSite { + return site + } + let domain = URL(string: site.url)?.shortDisplayString + return defaultSites.find { $0.title.lowercased() == domain } ?? site + } + + deferred.fill(newSites) + + return deferred + } + } + + static func defaultTopSites(_ profile: Profile) -> [Site] { + let suggested = SuggestedSites.asArray() + let deleted = profile.prefs.arrayForKey(DefaultSuggestedSitesKey) as? [String] ?? [] + return suggested.filter({deleted.firstIndex(of: $0.url) == .none}) + } + + static let DefaultSuggestedSitesKey = "topSites.deletedSuggestedSites" +} + +open class PinnedSite: Site { + let isPinnedSite = true + + init(site: Site) { + super.init(url: site.url, title: site.title, bookmarked: site.bookmarked) + self.icon = site.icon + self.metadata = site.metadata + } +} diff --git a/Client/Frontend/Home/FirefoxHomeViewController.swift b/Client/Frontend/Home/FirefoxHomeViewController.swift index a2c6002013c2..54076dd424e8 100644 --- a/Client/Frontend/Home/FirefoxHomeViewController.swift +++ b/Client/Frontend/Home/FirefoxHomeViewController.swift @@ -11,7 +11,6 @@ import SyncTelemetry import SnapKit private let log = Logger.browserLogger -private let DefaultSuggestedSitesKey = "topSites.deletedSuggestedSites" // MARK: - Lifecycle struct FirefoxHomeUX { @@ -572,9 +571,22 @@ extension FirefoxHomeViewController: DataObserverDelegate { // If the pocket stories are not availible for the Locale the PocketAPI will return nil // So it is okay if the default here is true - self.getTopSites().uponQueue(.main) { _ in + TopSitesHandler.getTopSites(profile: profile).uponQueue(.main) { result in // If there is no pending cache update and highlights are empty. Show the onboarding screen self.collectionView?.reloadData() + + self.topSitesManager.currentTraits = self.view.traitCollection + + let numRows = max(self.profile.prefs.intForKey(PrefsKeys.NumberOfTopSiteRows) ?? TopSitesRowCountSettingsController.defaultNumberOfRows, 1) + + let maxItems = Int(numRows) * self.topSitesManager.numberOfHorizontalItems() + + self.topSitesManager.content = Array(result.prefix(maxItems)) + + self.topSitesManager.urlPressedHandler = { [unowned self] url, indexPath in + self.longPressRecognizer.isEnabled = false + self.showSiteWithURLHandler(url as URL) + } self.getPocketSites().uponQueue(.main) { _ in if !self.pocketStories.isEmpty { @@ -603,59 +615,6 @@ extension FirefoxHomeViewController: DataObserverDelegate { showSiteWithURLHandler(Pocket.MoreStoriesURL) } - func getTopSites() -> Success { - let numRows = max(self.profile.prefs.intForKey(PrefsKeys.NumberOfTopSiteRows) ?? TopSitesRowCountSettingsController.defaultNumberOfRows, 1) - let maxItems = UIDevice.current.userInterfaceIdiom == .pad ? 32 : 16 - return self.profile.history.getTopSitesWithLimit(maxItems).both(self.profile.history.getPinnedTopSites()).bindQueue(.main) { (topsites, pinnedSites) in - guard let mySites = topsites.successValue?.asArray(), let pinned = pinnedSites.successValue?.asArray() else { - return succeed() - } - - // How sites are merged together. We compare against the url's base domain. example m.youtube.com is compared against `youtube.com` - let unionOnURL = { (site: Site) -> String in - return URL(string: site.url)?.normalizedHost ?? "" - } - - // Fetch the default sites - let defaultSites = self.defaultTopSites() - // create PinnedSite objects. used by the view layer to tell topsites apart - let pinnedSites: [Site] = pinned.map({ PinnedSite(site: $0) }) - - // Merge default topsites with a user's topsites. - let mergedSites = mySites.union(defaultSites, f: unionOnURL) - // Merge pinnedSites with sites from the previous step - let allSites = pinnedSites.union(mergedSites, f: unionOnURL) - - // Favour topsites from defaultSites as they have better favicons. But keep PinnedSites - let newSites = allSites.map { site -> Site in - if let _ = site as? PinnedSite { - return site - } - let domain = URL(string: site.url)?.shortDisplayString - return defaultSites.find { $0.title.lowercased() == domain } ?? site - } - - self.topSitesManager.currentTraits = self.view.traitCollection - let maxItems = Int(numRows) * self.topSitesManager.numberOfHorizontalItems() - if newSites.count > Int(ActivityStreamTopSiteCacheSize) { - self.topSitesManager.content = Array(newSites[0.. maxItems { - self.topSitesManager.content = Array(newSites[0.. [Site] { let suggested = SuggestedSites.asArray() - let deleted = profile.prefs.arrayForKey(DefaultSuggestedSitesKey) as? [String] ?? [] + let deleted = profile.prefs.arrayForKey(TopSitesHandler.DefaultSuggestedSitesKey) as? [String] ?? [] return suggested.filter({deleted.firstIndex(of: $0.url) == .none}) } @@ -1153,13 +1112,3 @@ class ASLibraryCell: UICollectionViewCell, Themeable { applyTheme() } } - -open class PinnedSite: Site { - let isPinnedSite = true - - init(site: Site) { - super.init(url: site.url, title: site.title, bookmarked: site.bookmarked) - self.icon = site.icon - self.metadata = site.metadata - } -} diff --git a/Client/Frontend/Strings.swift b/Client/Frontend/Strings.swift index e9531a68bee5..fe5ba018e38d 100644 --- a/Client/Frontend/Strings.swift +++ b/Client/Frontend/Strings.swift @@ -821,6 +821,10 @@ extension String { // Widget - Shared public static let QuickActionsGalleryTitle = NSLocalizedString("TodayWidget.QuickActionsGalleryTitle", tableName: "Today", value: "Quick Actions", comment: "Quick Actions title when widget enters edit mode") + + + // Quick View - Gallery View + public static let QuickViewGalleryTile = NSLocalizedString("TodayWidget.QuickViewGalleryTitle", tableName: "Today", value: "Quick View", comment: "Quick View title user is picking a widget to add.") // Quick Action - Medium Size Quick Action public static let QuickActionsSubLabel = NSLocalizedString("TodayWidget.QuickActionsSubLabel", tableName: "Today", value: "Firefox - Quick Actions", comment: "Sub label for medium size quick action widget") diff --git a/Client/Utils/FaviconFetcher.swift b/Client/Utils/FaviconFetcher.swift index f3c8082fc18b..55f7ef033606 100644 --- a/Client/Utils/FaviconFetcher.swift +++ b/Client/Utils/FaviconFetcher.swift @@ -24,6 +24,8 @@ class FaviconFetcherErrorType: MaybeErrorType { * If that fails, it will attempt to find a favicon.ico in the root host domain. */ open class FaviconFetcher: NSObject, XMLParserDelegate { + static let MaximumFaviconSize = 1 * 1024 * 1024 // 1 MiB file size limit + public static var userAgent: String = "" static let ExpirationTime = TimeInterval(60*60*24*7) // Only check for icons once a week fileprivate static var characterToFaviconCache = [String: UIImage]() @@ -166,7 +168,7 @@ open class FaviconFetcher: NSObject, XMLParserDelegate { fetch = manager.loadImage(with: url, options: .lowPriority, progress: { (receivedSize, expectedSize, _) in - if receivedSize > FaviconHandler.MaximumFaviconSize || expectedSize > FaviconHandler.MaximumFaviconSize { + if receivedSize > FaviconFetcher.MaximumFaviconSize || expectedSize > FaviconFetcher.MaximumFaviconSize { fetch?.cancel() } }, @@ -195,12 +197,14 @@ open class FaviconFetcher: NSObject, XMLParserDelegate { return deferred } - // Returns a single Favicon UIImage for a given URL + // Returns the largest Favicon UIImage for a given URL class func fetchFavImageForURL(forURL url: URL, profile: Profile) -> Deferred> { let deferred = Deferred>() FaviconFetcher.getForURL(url.domainURL, profile: profile).uponQueue(.main) { result in var iconURL: URL? - if let favicons = result.successValue, favicons.count > 0, let faviconImageURL = favicons.first?.url.asURL { + + if let favicons = result.successValue, favicons.count > 0, let faviconImageURL = + favicons.first?.url.asURL { iconURL = faviconImageURL } else { return deferred.fill(Maybe(failure: FaviconError())) @@ -274,7 +278,19 @@ open class FaviconFetcher: NSObject, XMLParserDelegate { // Default favicons and background colors provided via mozilla/tippy-top-sites private class func getBundledIcons() -> [String: BundledIconType] { - let filePath = Bundle.main.path(forResource: "top_sites", ofType: "json") + + // Alows us to access bundle from extensions + // Also found in `SentryIntegration`. Taken from: https://stackoverflow.com/questions/26189060/get-the-main-app-bundle-from-within-extension + var bundle = Bundle.main + if bundle.bundleURL.pathExtension == "appex" { + // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex + let url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent() + if let otherBundle = Bundle(url: url) { + bundle = otherBundle + } + } + + let filePath = bundle.path(forResource: "top_sites", ofType: "json") let file = try! Data(contentsOf: URL(fileURLWithPath: filePath!)) let decoder = JSONDecoder() var icons = [String: BundledIconType]() @@ -304,3 +320,9 @@ open class FaviconFetcher: NSObject, XMLParserDelegate { return icons } } + +class FaviconError: MaybeErrorType { + internal var description: String { + return "No Image Loaded" + } +} diff --git a/WidgetKit/Assets.xcassets/AccentColor.colorset/Contents.json b/WidgetKit/Assets.xcassets/AccentColor.colorset/Contents.json index eb8789700816..17bcc882be94 100644 --- a/WidgetKit/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/WidgetKit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.621", + "red" : "0.286" + } + }, "idiom" : "universal" } ], diff --git a/WidgetKit/Assets.xcassets/openFirefox.imageset/Contents.json b/WidgetKit/Assets.xcassets/openFirefox.imageset/Contents.json new file mode 100644 index 000000000000..f59b679ded3e --- /dev/null +++ b/WidgetKit/Assets.xcassets/openFirefox.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "View More Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/WidgetKit/Assets.xcassets/openFirefox.imageset/View More Icon.pdf b/WidgetKit/Assets.xcassets/openFirefox.imageset/View More Icon.pdf new file mode 100644 index 000000000000..a22411f40808 Binary files /dev/null and b/WidgetKit/Assets.xcassets/openFirefox.imageset/View More Icon.pdf differ diff --git a/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/Contents.json b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/Contents.json new file mode 100644 index 000000000000..398454d57173 --- /dev/null +++ b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "globe_1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "globe_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "globe_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_1x.png b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_1x.png new file mode 100644 index 000000000000..a90175fe176f Binary files /dev/null and b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_1x.png differ diff --git a/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_2x.png b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_2x.png new file mode 100644 index 000000000000..5c7d46093e4f Binary files /dev/null and b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_2x.png differ diff --git a/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_3x.png b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_3x.png new file mode 100644 index 000000000000..5d244b2578f7 Binary files /dev/null and b/WidgetKit/Assets.xcassets/placeholderFavicon.imageset/globe_3x.png differ diff --git a/WidgetKit/Helpers.swift b/WidgetKit/Helpers.swift index 314fa4fc253c..351bbdfc77cf 100644 --- a/WidgetKit/Helpers.swift +++ b/WidgetKit/Helpers.swift @@ -3,6 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation +import SwiftUI +import UIKit var scheme: String { guard let string = Bundle.main.object(forInfoDictionaryKey: "MozInternalURLScheme") as? String else { @@ -15,3 +17,35 @@ func linkToContainingApp(_ urlSuffix: String = "", query: String) -> URL { let urlString = "\(scheme)://\(query)\(urlSuffix)" return URL(string: urlString)! } + +func getImageForUrl(_ url: URL, completion: @escaping (Image?) -> Void) { + let queue = DispatchQueue.global() + + var fetchImageWork: DispatchWorkItem? + + fetchImageWork = DispatchWorkItem { + if let data = try? Data(contentsOf: url) { + if let image = UIImage(data: data) { + DispatchQueue.main.async { + if fetchImageWork?.isCancelled == true { return } + + completion(Image(uiImage: image)) + fetchImageWork = nil + } + } + } + } + + if let imageWork = fetchImageWork { + queue.async(execute: imageWork) + } + + // Timeout the favicon fetch request if it's taking too long + queue.asyncAfter(deadline: .now() + 2) { + // If we've already successfully called the completion block, early return + if fetchImageWork == nil { return } + + fetchImageWork?.cancel() + completion(nil) + } +} diff --git a/WidgetKit/ImageButtonWithLabel.swift b/WidgetKit/ImageButtonWithLabel.swift index 74c13d326443..c4dcff4c5039 100644 --- a/WidgetKit/ImageButtonWithLabel.swift +++ b/WidgetKit/ImageButtonWithLabel.swift @@ -62,8 +62,11 @@ struct ImageButtonWithLabel: View { var body: some View { Link(destination: link.url) { ZStack(alignment: .leading) { - ContainerRelativeShape() - .fill(LinearGradient(gradient: Gradient(colors: link.backgroundColors), startPoint: .bottomLeading, endPoint: .topTrailing)) + if !isSmall { + ContainerRelativeShape() + .fill(LinearGradient(gradient: Gradient(colors: link.backgroundColors), startPoint: .bottomLeading, endPoint: .topTrailing)) + } + VStack (alignment: .center, spacing: 50.0){ HStack(alignment: .top) { VStack(alignment: .leading){ diff --git a/WidgetKit/OpenTabs/OpenTabsWidget.swift b/WidgetKit/OpenTabs/OpenTabsWidget.swift new file mode 100644 index 000000000000..5830b85f7e4d --- /dev/null +++ b/WidgetKit/OpenTabs/OpenTabsWidget.swift @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Commenting it out until it is refactored + +/* + +import SwiftUI +import WidgetKit +import UIKit +import Combine + +struct OpenTabsWidget: Widget { + private let kind: String = "Quick View" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: TabProvider()) { entry in + OpenTabsView(entry: entry) + } + .supportedFamilies([.systemMedium, .systemLarge]) + .configurationDisplayName(String.QuickViewGalleryTitle) + .description(String.QuickViewGalleryDescription) + } +} + +struct OpenTabsView: View { + let entry: OpenTabsEntry + + @Environment(\.widgetFamily) var widgetFamily + + @ViewBuilder + func lineItemForTab(_ tab: SavedTab) -> some View { + let url = tab.sessionData!.urls.last! + + VStack(alignment: .leading) { + Link(destination: linkToContainingApp("?url=\(url)", query: "open-url")) { + HStack(alignment: .center, spacing: 15) { + if (entry.favicons[tab.title!] != nil) { + (entry.favicons[tab.title!])!.resizable().frame(width: 16, height: 16) + } else { + Image("placeholderFavicon") + .foregroundColor(Color.white) + .frame(width: 16, height: 16) + } + + Text(tab.title!) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .lineLimit(1) + .font(.system(size: 15, weight: .regular, design: .default)) + }.padding(.horizontal) + } + + Rectangle() + .fill(Color(UIColor(red: 0.85, green: 0.85, blue: 0.85, alpha: 0.3))) + .frame(height: 0.5) + .padding(.leading, 45) + } + } + + var openFirefoxButton: some View { + HStack(alignment: .center, spacing: 15) { + Image("openFirefox").foregroundColor(Color.white) + Text("Open Firefox").foregroundColor(Color.white).lineLimit(1).font(.system(size: 13, weight: .semibold, design: .default)) + Spacer() + }.padding([.horizontal]) + } + + var numberOfTabsToDisplay: Int { + if widgetFamily == .systemMedium { + return 3 + } else { + return 8 + } + } + + var body: some View { + Group { + if entry.tabs.isEmpty { + VStack { + Text(String.NoOpenTabsLabel) + HStack { + Spacer() + Image("openFirefox") + Text(String.OpenFirefoxLabel).foregroundColor(Color.white).lineLimit(1).font(.system(size: 13, weight: .semibold, design: .default)) + Spacer() + }.padding(10) + }.foregroundColor(Color.white) + } else { + VStack(spacing: 8) { + ForEach(entry.tabs.suffix(numberOfTabsToDisplay), id: \.self) { tab in + lineItemForTab(tab) + } + + if (entry.tabs.count > numberOfTabsToDisplay) { + HStack(alignment: .center, spacing: 15) { + Image("openFirefox").foregroundColor(Color.white).frame(width: 16, height: 16) + Text(String.localizedStringWithFormat(String.MoreTabsLabel, (entry.tabs.count - numberOfTabsToDisplay))) + .foregroundColor(Color.white).lineLimit(1).font(.system(size: 13, weight: .semibold, design: .default)) + Spacer() + }.padding([.horizontal]) + } else { + openFirefoxButton + } + + Spacer() + }.padding(.top, 14) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background((Color(UIColor(red: 0.11, green: 0.11, blue: 0.13, alpha: 1.00)))) + } + + private func linkToContainingApp(_ urlSuffix: String = "", query: String) -> URL { + let urlString = "\(scheme)://\(query)\(urlSuffix)" + return URL(string: urlString)! + } +} + +*/ diff --git a/WidgetKit/OpenTabs/TabProvider.swift b/WidgetKit/OpenTabs/TabProvider.swift new file mode 100644 index 000000000000..8055e02c2ed9 --- /dev/null +++ b/WidgetKit/OpenTabs/TabProvider.swift @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Commenting it out until it is refactored + +/* + +import SwiftUI +import WidgetKit +import UIKit +import Combine + +struct TabProvider: TimelineProvider { + public typealias Entry = OpenTabsEntry + + func placeholder(in context: Context) -> OpenTabsEntry { + OpenTabsEntry(date: Date(), favicons: [String: Image](), tabs: []) + } + + func getSnapshot(in context: Context, completion: @escaping (OpenTabsEntry) -> Void) { + let allOpenTabs = TabArchiver.tabsToRestore(tabsStateArchivePath: tabsStateArchivePath()) + let openTabs = allOpenTabs.filter { + !$0.isPrivate && + $0.sessionData != nil && + $0.url?.absoluteString.starts(with: "internal://") == false && + $0.title != nil + } + + let faviconFetchGroup = DispatchGroup() + + var tabFaviconDictionary = [String : Image]() + for tab in openTabs { + faviconFetchGroup.enter() + if let faviconURL = tab.faviconURL { + getImageForUrl(URL(string: faviconURL)!, completion: { image in + if image != nil { + tabFaviconDictionary[tab.title!] = image + } + + faviconFetchGroup.leave() + }) + } else { + faviconFetchGroup.leave() + } + } + + faviconFetchGroup.notify(queue: .main) { + let openTabsEntry = OpenTabsEntry(date: Date(), favicons: tabFaviconDictionary, tabs: openTabs) + completion(openTabsEntry) + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + getSnapshot(in: context, completion: { openTabsEntry in + let timeline = Timeline(entries: [openTabsEntry], policy: .atEnd) + completion(timeline) + }) + } + + fileprivate func tabsStateArchivePath() -> String? { + let profilePath: String? + profilePath = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: AppInfo.sharedContainerIdentifier)?.appendingPathComponent("profile.profile").path + guard let path = profilePath else { return nil } + return URL(fileURLWithPath: path).appendingPathComponent("tabsState.archive").path + } +} + +struct OpenTabsEntry: TimelineEntry { + let date: Date + let favicons: [String : Image] + let tabs: [SavedTab] +} + +*/ diff --git a/WidgetKit/QuickLink.swift b/WidgetKit/QuickLink.swift index 6b68fd770aa5..4cc955fada11 100644 --- a/WidgetKit/QuickLink.swift +++ b/WidgetKit/QuickLink.swift @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import SwiftUI -import Shared // Enum file that holds the different cases for the Quick Actions small widget with their configurations (string, backgrounds, images) as selected by the user in edit mode. It maps the values of IntentQuickLink enum in the QuickLinkSelectionIntent to the designated values of each case. enum QuickLink: Int { diff --git a/WidgetKit/SearchQuickLinksMedium/SearchQuickLinks.swift b/WidgetKit/SearchQuickLinksMedium/SearchQuickLinks.swift index d529da0493d3..09d721c4b937 100644 --- a/WidgetKit/SearchQuickLinksMedium/SearchQuickLinks.swift +++ b/WidgetKit/SearchQuickLinksMedium/SearchQuickLinks.swift @@ -5,21 +5,24 @@ #if canImport(WidgetKit) import WidgetKit import SwiftUI -import Shared struct Provider: TimelineProvider { - public typealias Entry = SimpleEntry - - public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) { + func placeholder(in context: Context) -> SimpleEntry { + return SimpleEntry(date: Date()) + } + + func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) { let entry = SimpleEntry(date: Date()) completion(entry) } - - public func timeline(with context: Context, completion: @escaping (Timeline) -> ()) { + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let entries = [SimpleEntry(date: Date())] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } + + public typealias Entry = SimpleEntry } struct SimpleEntry: TimelineEntry { @@ -44,11 +47,11 @@ struct SearchQuickLinksEntryView : View { } } -struct SearchQuickLinksWigdet: Widget { +struct SearchQuickLinksWidget: Widget { private let kind: String = "Quick Actions - Medium" public var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider(), placeholder: SearchQuickLinksEntryView()) { entry in + StaticConfiguration(kind: kind, provider: Provider()) { entry in SearchQuickLinksEntryView() } .supportedFamilies([.systemMedium]) diff --git a/WidgetKit/SearchQuickLinksSmall/SmallQuickLink.swift b/WidgetKit/SearchQuickLinksSmall/SmallQuickLink.swift index 961a3321c268..7ed440bf823c 100644 --- a/WidgetKit/SearchQuickLinksSmall/SmallQuickLink.swift +++ b/WidgetKit/SearchQuickLinksSmall/SmallQuickLink.swift @@ -7,20 +7,24 @@ import SwiftUI import WidgetKit struct IntentProvider: IntentTimelineProvider { - typealias Intent = QuickLinkSelectionIntent - public typealias Entry = QuickLinkEntry - - public func snapshot(for configuration: Intent, with context: Context, completion: @escaping (QuickLinkEntry) -> ()) { + func placeholder(in context: Context) -> QuickLinkEntry { + return QuickLinkEntry(date: Date(), link: .search) + } + + func getSnapshot(for configuration: QuickLinkSelectionIntent, in context: Context, completion: @escaping (QuickLinkEntry) -> Void) { let entry = QuickLinkEntry(date: Date(), link: QuickLink.from(configuration)) completion(entry) } - - public func timeline(for configuration: Intent, with context: Context, completion: @escaping (Timeline) -> ()) { + + func getTimeline(for configuration: QuickLinkSelectionIntent, in context: Context, completion: @escaping (Timeline) -> Void) { let link = QuickLink.from(configuration) let entries = [QuickLinkEntry(date: Date(), link: link)] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } + + typealias Intent = QuickLinkSelectionIntent + public typealias Entry = QuickLinkEntry } struct QuickLinkEntry: TimelineEntry { @@ -34,6 +38,8 @@ struct SmallQuickLinkView : View { @ViewBuilder var body: some View { ImageButtonWithLabel(isSmall: true, link: entry.link) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(LinearGradient(gradient: Gradient(colors: entry.link.backgroundColors), startPoint: .bottomLeading, endPoint: .topTrailing)).widgetURL(entry.link.url) } } @@ -41,7 +47,7 @@ struct SmallQuickLinkWidget: Widget { private let kind: String = "Quick Actions - Small" public var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: QuickLinkSelectionIntent.self, provider: IntentProvider(), placeholder: SmallQuickLinkView(entry: QuickLinkEntry(date: Date(), link: .search))) { entry in + IntentConfiguration(kind: kind, intent: QuickLinkSelectionIntent.self, provider: IntentProvider()) { entry in SmallQuickLinkView(entry: entry) } .supportedFamilies([.systemSmall]) diff --git a/WidgetKit/TopSites/TopSitesProvider.swift b/WidgetKit/TopSites/TopSitesProvider.swift new file mode 100644 index 000000000000..5deede7eece5 --- /dev/null +++ b/WidgetKit/TopSites/TopSitesProvider.swift @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Commenting it out until it is refactored + +/* + +import SwiftUI +import WidgetKit +import Shared +import Storage +import SyncTelemetry + +struct TopSitesProvider: TimelineProvider { + public typealias Entry = TopSitesEntry + + func placeholder(in context: Context) -> TopSitesEntry { + return TopSitesEntry(date: Date(), favicons: [String: Image](), sites: []) + } + + func getSnapshot(in context: Context, completion: @escaping (TopSitesEntry) -> Void) { + let profile = BrowserProfile(localName: "profile") + TopSitesHandler.getTopSites(profile: profile).uponQueue(.main) { topSites in + + let faviconFetchGroup = DispatchGroup() + var tabFaviconDictionary = [String : Image]() + + // Concurrently fetch each of the top sites icons + for site in topSites { + faviconFetchGroup.enter() + + // Get the bundled top site favicon, if available + if let siteURL = URL(string: site.url) { + if let bundled = FaviconFetcher.getBundledIcon(forUrl: siteURL), + let uiImage = UIImage(contentsOfFile: bundled.filePath) { + let color = bundled.bgcolor.components.alpha < 0.01 ? UIColor.white : bundled.bgcolor + + tabFaviconDictionary[site.url] = Image(uiImage: uiImage.withBackgroundAndPadding(color: color)) + faviconFetchGroup.leave() + } else { + // Fetch the favicon from the faviconURL if available + if let faviconPath = site.icon?.url, let faviconURL = URL(string: faviconPath) { + getImageForUrl(faviconURL, completion: { image in + if image != nil { + tabFaviconDictionary[site.url] = image + } + faviconFetchGroup.leave() + + }) + } else { + faviconFetchGroup.leave() + } + } + } else { + faviconFetchGroup.leave() + } + } + + faviconFetchGroup.notify(queue: .main) { + let topSitesEntry = TopSitesEntry(date: Date(), favicons: tabFaviconDictionary, sites: topSites) + + completion(topSitesEntry) + } + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + getSnapshot(in: context, completion: { topSitesEntry in + let timeline = Timeline(entries: [topSitesEntry], policy: .atEnd) + completion(timeline) + }) + } + + fileprivate func tabsStateArchivePath() -> String? { + let profilePath: String? + profilePath = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: AppInfo.sharedContainerIdentifier)?.appendingPathComponent("profile.profile").path + guard let path = profilePath else { return nil } + return URL(fileURLWithPath: path).appendingPathComponent("tabsState.archive").path + } +} + +struct TopSitesEntry: TimelineEntry { + let date: Date + let favicons: [String : Image] + let sites: [Site] +} + +fileprivate extension UIImage { + func withBackgroundAndPadding(color: UIColor, opaque: Bool = true) -> UIImage { + UIGraphicsBeginImageContextWithOptions(size, opaque, scale) + + guard let ctx = UIGraphicsGetCurrentContext(), let image = cgImage else { return self } + defer { UIGraphicsEndImageContext() } + + // Pad the image in a bit to make the favicons look better + let newSize = CGSize(width: size.width - 20, height: size.height - 20) + let rect = CGRect(origin: .zero, size: size) + let imageRect = CGRect(origin: CGPoint(x: 10, y: 10), size: newSize) + ctx.setFillColor(color.cgColor) + ctx.fill(rect) + ctx.concatenate(CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height)) + ctx.draw(image, in: imageRect) + + return UIGraphicsGetImageFromCurrentImageContext() ?? self + } +} + +fileprivate extension UIColor { + var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + return (r, g, b, a) + } +} + +*/ diff --git a/WidgetKit/TopSites/TopSitesWidget.swift b/WidgetKit/TopSites/TopSitesWidget.swift new file mode 100644 index 000000000000..3f6ee34e13d9 --- /dev/null +++ b/WidgetKit/TopSites/TopSitesWidget.swift @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Commenting it out until it is refactored + +/* + +import SwiftUI +import WidgetKit +import Combine +import Shared +import Storage +import SyncTelemetry + +struct TopSitesWidget: Widget { + private let kind: String = "Top Sites" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: TopSitesProvider()) { entry in + TopSitesView(entry: entry) + } + .supportedFamilies([.systemMedium]) + .configurationDisplayName(String.TopSitesGalleryTitle) + .description(String.TopSitesGalleryDescription) + } +} + +struct TopSitesView: View { + let entry: TopSitesEntry + + @ViewBuilder + func topSitesItem(_ site: Site) -> some View { + let url = site.url + + Link(destination: linkToContainingApp("?url=\(url)", query: "open-url")) { + if (entry.favicons[url] != nil) { + (entry.favicons[url])!.resizable().frame(width: 60, height: 60).mask(maskShape) + } else { + Rectangle() + .fill(Color(UIColor(red: 0.85, green: 0.85, blue: 0.85, alpha: 0.3))) + .frame(width: 60, height: 60) + } + } + } + + var maskShape: RoundedRectangle { + RoundedRectangle(cornerRadius: 5) + } + + var emptySquare: some View { + maskShape + .fill(Color(UIColor(red: 0.85, green: 0.85, blue: 0.85, alpha: 0.3))) + .frame(width: 60, height: 60) + .background(Color.clear).frame(maxWidth: .infinity) + } + + var body: some View { + VStack { + // TODO: Always fill with 16 squares, no matter what! + HStack { + if entry.sites.count > 3 { + ForEach(entry.sites.prefix(4), id: \.url) { tab in + topSitesItem(tab) + .background(Color.clear).frame(maxWidth: .infinity) + } + } else { + ForEach(entry.sites[0...entry.sites.count - 1], id: \.url) { tab in + topSitesItem(tab).frame(maxWidth: .infinity) + } + + ForEach(0..<(4 - entry.sites.count), id: \.self) { _ in + emptySquare + } + } + }.padding(.top) + Spacer() + HStack { + if entry.sites.count > 7 { + ForEach(entry.sites[4...7], id: \.url) { tab in + topSitesItem(tab).frame(maxWidth: .infinity) + } + } else { + // Ensure there is at least a single site in the second row + if entry.sites.count > 4 { + ForEach(entry.sites[4...entry.sites.count - 1], id: \.url) { tab in + topSitesItem(tab).frame(maxWidth: .infinity) + } + } + + ForEach(0..<(min(4, 8 - entry.sites.count)), id: \.self) { _ in + emptySquare + } + } + }.padding(.bottom) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background((Color(UIColor(red: 0.11, green: 0.11, blue: 0.13, alpha: 1.00)))) + } + + private func linkToContainingApp(_ urlSuffix: String = "", query: String) -> URL { + let urlString = "\(scheme)://\(query)\(urlSuffix)" + return URL(string: urlString)! + } +} + +*/ diff --git a/WidgetKit/WidgetKit.swift b/WidgetKit/WidgetKit.swift index 0cdf68f37f42..61fde1a9f9d5 100644 --- a/WidgetKit/WidgetKit.swift +++ b/WidgetKit/WidgetKit.swift @@ -11,7 +11,7 @@ struct FirefoxWidgets: WidgetBundle { @WidgetBundleBuilder var body: some Widget { SmallQuickLinkWidget() - SearchQuickLinksWigdet() + SearchQuickLinksWidget() } } #endif diff --git a/package-lock.json b/package-lock.json index 95123f387d1a..146576fc2af3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2815,28 +2815,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "resolved": false, "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "resolved": false, "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "resolved": false, "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, @@ -2847,14 +2847,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -2865,42 +2865,42 @@ }, "chownr": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "resolved": false, "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "resolved": false, "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "optional": true, @@ -2910,28 +2910,28 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "resolved": false, "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "resolved": false, "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "resolved": false, "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "resolved": false, "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "dev": true, "optional": true, @@ -2941,14 +2941,14 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "resolved": false, "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -2965,7 +2965,7 @@ }, "glob": { "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "resolved": false, "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "optional": true, @@ -2980,14 +2980,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "resolved": false, "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "resolved": false, "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, @@ -2997,7 +2997,7 @@ }, "ignore-walk": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "resolved": false, "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "dev": true, "optional": true, @@ -3007,7 +3007,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -3018,21 +3018,21 @@ }, "inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "resolved": false, "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "resolved": false, "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -3042,14 +3042,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -3059,14 +3059,14 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "minipass": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "resolved": false, "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "dev": true, "optional": true, @@ -3077,7 +3077,7 @@ }, "minizlib": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "resolved": false, "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "dev": true, "optional": true, @@ -3087,7 +3087,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -3097,14 +3097,14 @@ }, "ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "resolved": false, "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true, "optional": true }, "needle": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "resolved": false, "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "dev": true, "optional": true, @@ -3116,7 +3116,7 @@ }, "node-pre-gyp": { "version": "0.14.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", + "resolved": false, "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", "dev": true, "optional": true, @@ -3135,7 +3135,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -3146,7 +3146,7 @@ }, "npm-bundled": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "resolved": false, "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", "dev": true, "optional": true, @@ -3156,14 +3156,14 @@ }, "npm-normalize-package-bin": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "resolved": false, "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.7", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz", + "resolved": false, "integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==", "dev": true, "optional": true, @@ -3174,7 +3174,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "resolved": false, "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -3187,21 +3187,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -3211,21 +3211,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": false, "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": false, "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "resolved": false, "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -3236,21 +3236,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "resolved": false, "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "resolved": false, "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, @@ -3263,7 +3263,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -3272,7 +3272,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -3288,7 +3288,7 @@ }, "rimraf": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "resolved": false, "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "optional": true, @@ -3298,49 +3298,49 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "resolved": false, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "resolved": false, "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "resolved": false, "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "resolved": false, "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -3352,7 +3352,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -3362,7 +3362,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -3372,14 +3372,14 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "resolved": false, "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "resolved": false, "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "dev": true, "optional": true, @@ -3395,14 +3395,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "resolved": false, "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, @@ -3412,14 +3412,14 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true }, "yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "resolved": false, "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "optional": true